zjk
2026-02-09 07005df3bfc187c52fc085cc5a59f80472ef49b8
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
16个文件已添加
1个文件已修改
1502 ■■■■■ 已修改文件
src/assets/point/active-coverage.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/add.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/car.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/checked.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/coverage.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/delete.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/location.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/more.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/notice.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/operation.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/point.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/reduce.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/test.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/uncheck.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/upload.png 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operationsManagement/pointManagement/index.scss 967 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operationsManagement/pointManagement/index.vue 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/point/active-coverage.png
src/assets/point/add.png
src/assets/point/car.png
src/assets/point/checked.png
src/assets/point/coverage.png
src/assets/point/delete.png
src/assets/point/location.png
src/assets/point/more.png
src/assets/point/notice.png
src/assets/point/operation.png
src/assets/point/point.png
src/assets/point/reduce.png
src/assets/point/test.png
src/assets/point/uncheck.png
src/assets/point/upload.png
src/views/operationsManagement/pointManagement/index.scss
New file
@@ -0,0 +1,967 @@
// 点位管理页面样式文件
.point-management-container {
  display: flex;
  width: 100%;
  height: 100%;
  position: relative;
  background: #f5f7fa;
}
.flex {
  display: flex;
}
.a-center {
  align-items: center;
}
.j-between {
  justify-content: space-between;
}
.relative {
  position: relative;
}
// ==================== 左侧面板 ====================
.left-panel {
  width: 280px;
  height: 100%;
  background: #ffffff;
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  z-index: 999;
}
// 面板头部
.panel-header {
  padding: 16px 12px 4px 12px;
}
.panel-title {
  /* 16/subtitle-Medium */
  font-family: PingFang SC;
  font-size: 16px;
  font-weight: 600;
  line-height: 24px;
  letter-spacing: normal;
  color: #3D3D3D;
}
// ==================== 搜索框 ====================
.search-box {
  padding: 0 12px 12px 12px;
  border-bottom: 1px solid rgba(23, 31, 38, 0.15);
}
.search-input-wrapper {
  position: relative;
  width: 100%;
}
.search-icon {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: #999;
  pointer-events: none;
}
.search-input {
  width: 100%;
  height: 32px;
  padding: 0 8px 0 8px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  color: #333;
  background: #ffffff;
  transition: all 0.3s;
  outline: none;
  &::placeholder {
    color: rgba(23, 31, 38, 0.3);
  }
  &:focus {
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  }
  &:hover {
    border-color: #b3b8c4;
  }
}
.clear-btn {
  position: absolute;
  right: 30px;
  top: 50%;
  transform: translateY(-50%);
  width: 12px;
  height: 12px;
  background: #c0c4cc;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 12px;
  transition: all 0.2s;
  &:hover {
    background: #909399;
  }
}
// ==================== 点位列表 ====================
.point-list {
  flex: 1;
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 0px;
  }
  &::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 0px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 0px;
    transition: background 0.3s;
    &:hover {
      background: #a8a8a8;
    }
  }
}
.point-item {
  padding: 7px 12px;
  background: #ffffff;
  border-bottom: 1px solid rgba(23, 31, 38, 0.15);
  cursor: pointer;
  position: relative;
  overflow: hidden;
  &:hover {
    background: #F4F6F8;
    box-sizing: border-box;
    border-width: 0px 0px 1px 0px;
    border-style: solid;
    border-color: rgba(23, 31, 38, 0.1);
    &::before {
      opacity: 1;
    }
  }
}
.point-id {
  font-family: PingFang SC;
  font-size: 13px;
  font-weight: normal;
  line-height: 18px;
  letter-spacing: normal;
  color: #171F26;
}
.point-type {
  padding: 2px 8px;
  border-radius: 4px;
  /* blue/1-浅色背景 */
  background: #E6F4FF;
  font-family: PingFang SC;
  font-size: 12px;
  font-weight: normal;
  line-height: 16px;
  text-align: center;
  letter-spacing: normal;
  /* blue/6-主题色 */
  color: #0068FF;
}
.type-dark {
  background: #E8E9E9;
  color: #171F26;
}
.point-status {
  padding: 2px 8px;
  font-family: PingFang SC;
  font-size: 12px;
  font-weight: normal;
  line-height: 16px;
  text-align: center;
  /* blue/6-主题色 */
  color: #0068FF;
  margin: 0 4px;
}
// 待测试
.wait-test {
  background: #FFF6EA;
  color: #FFAA2A;
}
// 测试中
.testing {
  background: #E6F4FF;
  color: #0068FF;
}
// 测试失败---运营失效
.test-fail {
  background: #FFF3F1;
  color: #E02716;
}
// 可运营
.test-pass {
  color: #0068FF;
  background: #E6F4FF;
}
// 待生效--待启用
.test-take-effect {
  background: #FFF6EA;
  color: #FFAA2A;
}
//启用中
.test-starting {
  background: #E9F9ED;
  color: #19B144;
}
// 已停用
.test-stop {
  background: #E8E9E9;
  color: #171F26;
}
// 失效
.test-invalid {
  background: #E8E9E9;
  color: #171F26;
}
.point-address {
  margin: 4px 0;
  font-family: PingFang SC;
  font-size: 13px;
  font-weight: normal;
  line-height: 18px;
  letter-spacing: normal;
  color: #171F26;
}
.point-time {
  font-family: PingFang SC;
  font-size: 13px;
  font-weight: normal;
  line-height: 18px;
  letter-spacing: normal;
  /* Text color/FONT Gy4-置灰信息 */
  color: rgba(23, 31, 38, 0.3);
}
// ==================== 底部按钮 ====================
.panel-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  border-top: 1px solid #e8eaed;
  background: #fff;
  padding: 12px;
}
.add-btn {
  /* 自动布局子元素 */
  width: 124px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background: #0068FF;
  color: #ffffff;
  cursor: pointer;
  font-family: PingFang SC;
  font-size: 13px;
  font-weight: normal;
  text-align: center;
  letter-spacing: normal;
  color: #FFFFFF;
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
  }
  &:active {
    transform: translateY(0);
    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
  }
  .icon {
    font-size: 18px;
  }
}
// ==================== 右侧地图区域 ====================
.map-container {
  flex: 1;
  height: 100%;
  position: relative;
  overflow: hidden;
}
.map-content {
  width: 100%;
  height: 100%;
}
// 地图加载提示
.map-loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: #666;
  font-size: 14px;
}
.map-error {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: #f56c6c;
  font-size: 14px;
  padding: 20px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
// ==================== 响应式设计 ====================
@media screen and (max-width: 1200px) {
  .left-panel {
    width: 320px;
  }
}
@media screen and (max-width: 768px) {
  .point-management-container {
    flex-direction: column;
  }
  .left-panel {
    width: 100%;
    height: 50%;
  }
  .map-container {
    height: 50%;
  }
}
// ==================== 空状态 ====================
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  color: #999;
  .empty-icon {
    font-size: 64px;
    margin-bottom: 16px;
    opacity: 0.5;
  }
  .empty-text {
    font-size: 14px;
    color: #999;
  }
}
//右侧信息
.right-panel {
  position: absolute;
  right: 0;
  top: 0;
  display: flex;
  height: 100%;
}
// 地图操作
.map-operate {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 8px;
  height: 100%;
}
.operate-item {
  display: flex;
  align-items: center;
  padding: 8px;
  gap: 2px;
  z-index: 0;
  border-radius: 4px;
  background: #FFFFFF;
  box-sizing: border-box;
  /* Gray color/Gray3-按钮-输入框描边 */
  border: 1px solid rgba(23, 31, 38, 0.15);
  cursor: pointer;
  position: relative;
  &:hover {
    border-radius: 4px;
    /* blue/1-浅色背景 */
    background: #E6F4FF;
    box-sizing: border-box;
    /* Gray color/Gray3-按钮-输入框描边 */
    border: 1px solid rgba(23, 31, 38, 0.15);
    .operate-text {
      color: #0068FF !important;
    }
  }
  &:active {
    border-radius: 4px;
    background: #A3D4FF;
    box-sizing: border-box;
    border: 1px solid rgba(23, 31, 38, 0.15);
    .operate-text {
      color: #0068FF !important;
    }
  }
  .operate-icon {
    /* 自动布局子元素 */
    width: 16px;
    height: 16px;
    z-index: 0;
  }
  .operate-text {
    font-family: PingFang SC;
    font-size: 13px;
    font-weight: normal;
    line-height: 15.21px;
    text-align: center;
    letter-spacing: normal;
    /* blue/6-主题色 */
    color: #171F26;
  }
}
.coverage {
  gap: 4px;
  margin-left: 8px;
}
.dot-detail {
  position: absolute;
  top: 34px;
  left: 0;
  display: flex;
  flex-direction: column;
  padding: 8px;
  gap: 4px;
  border-radius: 4px;
  /* white */
  background: #FFFFFF;
  /* 二级投影-悬浮弹框 */
  box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.1);
  .dot-item {
    display: flex;
    align-items: center;
    padding: 2px 4px;
    gap: 4px;
    border-radius: 4px;
    box-sizing: border-box;
    &:hover {
      background: #A3D4FF;
      border: 1px solid rgba(23, 31, 38, 0.15);
    }
    .dot-icon {
      /* 自动布局子元素 */
      width: 16px;
      height: 16px;
      z-index: 0;
    }
    .dot-tit {
      font-family: PingFang SC;
      font-size: 13px;
      font-weight: normal;
      line-height: 18px;
      letter-spacing: normal;
      color: #171F26;
    }
  }
  .active-dot {
    background: #A3D4FF;
    border: 1px solid rgba(23, 31, 38, 0.15);
  }
}
// 图层下拉
.coverage-detail {
  position: absolute;
  top: 34px;
  right: 0;
  /* 自动布局 */
  display: flex;
  flex-direction: column;
  padding: 0px 28px 10px 12px;
  gap: 8px;
  z-index: 1;
  border-radius: 4px;
  background: #FFFFFF;
  box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.1);
  .coverage-tit {
    /* 13/段落-Regular */
    font-family: PingFang SC;
    font-size: 13px;
    font-weight: normal;
    line-height: 20px;
    letter-spacing: normal;
    color: rgba(23, 31, 38, 0.6);
  }
  .coverage-icon {
    width: 16px;
    height: 16px;
    z-index: 0;
    margin-right: 8px;
  }
  .coverage-txt {
    font-family: PingFang SC;
    font-size: 13px;
    font-weight: normal;
    line-height: 18px;
    letter-spacing: normal;
    color: #171F26;
  }
}
.operate-bottom {
  display: flex;
  justify-content: flex-end;
  .operate-card {
    display: flex;
    flex-direction: column;
  }
  .location {
    margin-bottom: 8px;
  }
  .add {
    margin-bottom: -1px;
  }
  .location-icon {
    width: 32px;
    height: 32px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 0;
  }
}
// 点位详情
.point-detail {
  width: 280px;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: #fff;
  .point-info {
    padding: 16px 0px;
    margin-left: 12px;
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
  }
  .point-title {
    padding-bottom: 16px;
    border-bottom: 1px solid rgba(23, 31, 38, 0.15);
  }
  .point-detail-content {
    flex: 1;
    overflow-y: auto;
    &::-webkit-scrollbar {
      width: 0px;
    }
    &::-webkit-scrollbar-track {
      background: #f1f1f1;
      border-radius: 0px;
    }
    &::-webkit-scrollbar-thumb {
      background: #c1c1c1;
      border-radius: 0px;
      transition: background 0.3s;
      &:hover {
        background: #a8a8a8;
      }
    }
    .info-card {
      margin-top: 16px;
      display: flex;
      align-items: center;
      .info-icon {
        width: 3px;
        height: 14px;
        border-radius: 3px;
        background: #0068FF;
        margin-right: 8px;
      }
      .card-title {
        font-family: PingFang SC;
        font-size: 16px;
        font-weight: 600;
        line-height: 24px;
        letter-spacing: normal;
        color: #171F26;
      }
    }
    .info-form {
      margin-top: 8px;
      .label-require {
        font-family: PingFang SC;
        font-size: 13px;
        font-weight: normal;
        line-height: 18px;
        letter-spacing: normal;
        color: #E02716;
        margin-right: 4px;
      }
      .info-form-label {
        font-family: PingFang SC;
        font-size: 13px;
        font-weight: normal;
        line-height: 18px;
        letter-spacing: normal;
        color: #171F26;
      }
      .info-form-value {
        margin-top: 8px;
        padding-right: 12px;
      }
      .form-input {
        width: 100%;
        height: 32px;
        align-items: center;
        padding: 6px 8px;
        border-radius: 4px;
        background: #FFFFFF;
        box-sizing: border-box;
        border: 1px solid rgba(23, 31, 38, 0.15);
        font-family: PingFang SC;
        font-size: 13px;
        font-weight: normal;
        line-height: 20px;
        letter-spacing: normal;
        color: #171F26;
        background: #ffffff;
        transition: all 0.3s;
        outline: none;
        &::placeholder {
          color: rgba(23, 31, 38, 0.3);
        }
        &:focus {
          border-color: #667eea;
          box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
        }
        &:hover {
          border-color: #b3b8c4;
        }
      }
      .point-img {
        display: flex;
        flex-wrap: wrap;
        .img-item {
          position: relative;
          width: 83.33px;
          height: 56px;
          z-index: 1;
          border-radius: 4px;
          margin-right: 3px;
          margin-bottom: 3px;
          &:nth-child(3n+3) {
            margin-right: 0;
          }
          .point-img-item {
            width: 100%;
            height: 100%;
            object-fit: cover;
            border-radius: 4px;
            cursor: pointer;
            transition: transform 0.3s;
          }
          &:hover {
            z-index: 10;
          }
          &:hover .point-img-item {
            transform: scale(1.05);
          }
          .img-delete {
            position: absolute;
            top: -9px;
            right: -9px;
            width: 18px;
            height: 18px;
            cursor: pointer;
            z-index: 2;
            opacity: 0;
            transition: opacity 0.3s;
          }
          &:hover .img-delete {
            opacity: 1;
          }
        }
        .upload-item {
          cursor: pointer;
          border: 1px dashed rgba(23, 31, 38, 0.15);
          background: rgba(23, 31, 38, 0.02);
          transition: all 0.3s;
          display: flex;
          align-items: center;
          justify-content: center;
          overflow: visible;
          &:hover {
            border-color: #0068FF;
            background: rgba(0, 104, 255, 0.05);
          }
          .upload-icon {
            width: 83.33px;
            height: 56px;
            border-radius: 4px;
            object-fit: contain;
            opacity: 0.6;
            transition: opacity 0.3s;
          }
          &:hover .upload-icon {
            opacity: 1;
          }
        }
      }
      .notice-card {
        margin-top: 8px;
        display: flex;
        align-items: center;
        .notice-txt {
          font-family: PingFang SC;
          font-size: 12px;
          font-weight: normal;
          line-height: 16px;
          letter-spacing: normal;
          color: rgba(23, 31, 38, 0.6);
        }
        .img-notice {
          width: 12px;
          height: 12px;
          margin-right: 6px;
        }
      }
    }
  }
  .panel-footer {
    width: 280px;
    height: 48px;
    display: flex;
    align-items: center;
    padding: 8px 12px;
    gap: 8px;
    background: #FFFFFF;
    box-sizing: border-box;
    border-width: 1px 0px 0px 0px;
    border-style: solid;
    border-color: rgba(23, 31, 38, 0.1);
    .cancel-btn {
      flex: 1;
      height: 32px;
      border-radius: 4px;
      box-sizing: border-box;
      border: 1px solid rgba(23, 31, 38, 0.15);
      font-family: PingFang SC;
      font-size: 13px;
      font-weight: normal;
      line-height: 32px;
      text-align: center;
      letter-spacing: normal;
      color: #171F26;
    }
    .submit-btn {
      flex: 1;
      height: 32px;
      border-radius: 4px;
      background: #0068FF;
      font-family: PingFang SC;
      font-size: 13px;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      letter-spacing: normal;
      color: #FFFFFF;
    }
  }
}
// ==================== 图片预览弹窗 ====================
.image-preview-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  animation: fadeIn 0.3s ease;
  .preview-content {
    position: relative;
    max-width: 90vw;
    max-height: 90vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .preview-image {
    max-width: 100%;
    max-height: 90vh;
    object-fit: contain;
    border-radius: 8px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
    animation: zoomIn 0.3s ease;
  }
  .preview-close {
    position: absolute;
    top: -40px;
    right: -40px;
    width: 32px;
    height: 32px;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s;
    svg {
      width: 16px;
      height: 16px;
      color: #333;
    }
    &:hover {
      background: #fff;
      transform: rotate(90deg);
    }
  }
}
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes zoomIn {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
src/views/operationsManagement/pointManagement/index.vue
@@ -1,5 +1,5 @@
<script setup lang="tsx">
import { ref, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useScriptTag } from '@vueuse/core'
// 添加更好的错误处理和调试
@@ -8,6 +8,49 @@
const domRef = ref<HTMLDivElement>()
const map = ref(null)
const mapError = ref('')
const searchKeyword = ref('')
const activePointId = ref<number | null>(null)
const uploadInput = ref<HTMLInputElement>()
const previewVisible = ref(false)
const previewImage = ref('')
// 模拟已上传的图片列表
const uploadedImages = ref([
  { id: 1, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
  { id: 2, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
  { id: 3, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
  { id: 4, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
  { id: 5, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
  { id: 6, url: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },
])
// 模拟点位数据
const pointList = ref([
  { id: 1, name: '天河体育中心站点', address: '广州市天河区天河路299号', status: '待启用', type: '运营', isOpen: false },
  { id: 2, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '待测试', type: '测试', isOpen: true },
  { id: 3, name: '广州塔站点', address: '广州市海珠区阅江西路222号', status: '启用中', type: '运营', isOpen: true },
  { id: 4, name: '北京路站点', address: '广州市越秀区北京路', status: '已停用', type: '运营', isOpen: false },
  { id: 5, name: '白云机场站点', address: '广州市白云区机场路', status: '失效', type: '运营', isOpen: false },
  { id: 6, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '测试中', type: '测试', isOpen: true },
  { id: 7, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '测试不通过', type: '测试', isOpen: false },
  { id: 8, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '待生效', type: '测试', isOpen: false },
  { id: 9, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '可运营', type: '测试', isOpen: true },
  { id: 10, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '启用中', type: '测试', isOpen: true },
  { id: 11, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '已停用', type: '测试', isOpen: false },
  { id: 12, name: '珠江新城站点', address: '广州市天河区珠江新城花城广场', status: '失效', type: '测试', isOpen: false },
])
// 过滤后的点位列表
const filteredPointList = computed(() => {
  if (!searchKeyword.value.trim()) {
    return pointList.value
  }
  const keyword = searchKeyword.value.toLowerCase()
  return pointList.value.filter(point =>
    point.name.toLowerCase().includes(keyword)
    || point.address.toLowerCase().includes(keyword),
  )
})
async function initMap() {
  try {
@@ -20,15 +63,193 @@
      return
    }
    map.value = new AMap.Map(domRef.value, {
      zoom: 10,
      zoom: 11,
      center: [113.264499, 23.130061],
      mapStyle: 'amap://styles/whitesmoke', // 设置地图的显示样式
    })
    console.log('地图初始化成功:', map.value)
  } catch (err) {
    mapError.value = '地图初始化失败: ' + (err as Error).message
  }
  catch (err) {
    mapError.value = `地图初始化失败: ${(err as Error).message}`
    console.error('地图初始化错误:', err)
  }
}
// switch颜色
function railStyle({
  focused,
  checked,
}: {
  focused: boolean
  checked: boolean
}) {
  const style: CSSProperties = {}
  if (checked) {
    style.background = '#E5E7EB'
  }
  else {
    style.background = '#2080f0'
  }
  return style
}
// 搜索点位
function handleSearch() {
  console.log('搜索关键词:', searchKeyword.value)
}
// 清空搜索
function clearSearch() {
  searchKeyword.value = ''
}
// 新增点位
function handleAddPoint() {
  console.log('新增点位')
}
// 点击点位
function handlePointClick(point: any) {
  activePointId.value = point.id
  console.log('点击点位:', point)
}
// 切换点位开关状态
function handleSwitchChange(point: any, value: boolean) {
  console.log('切换点位开关:', point, value)
  point.isOpen = value
  // 这里可以调用 API 更新点位状态
}
// 获取状态类名
function getStatusClass(type: string, status: string) {
  if (type === '运营') {
    switch (status) {
      case '待启用':
        return 'test-take-effect'
      case '启用中':
        return 'test-starting'
      case '已停用':
        return 'test-stop'
      case '失效':
        return 'test-fail'
    }
  }
  else {
    switch (status) {
      case '待测试':
        return 'wait-test'
      case '测试中':
        return 'testing'
      case '测试不通过':
        return 'test-fail'
      case '可运营':
        return 'test-pass'
      case '待生效':
        return 'test-take-effect'
      case '启用中':
        return 'test-starting'
      case '已停用':
        return 'test-stop'
      case '失效':
        return 'test-invalid'
    }
  }
}
function getTypeClass(type: string) {
  if (type === '运营') {
    return 'point-type'
  }
  else {
    return 'type-dark'
  }
}
// 触发文件上传
function triggerUpload() {
  uploadInput.value?.click()
}
// 处理文件选择
function handleFileChange(event: Event) {
  const target = event.target as HTMLInputElement
  const files = target.files
  if (!files || files.length === 0) {
    return
  }
  // 检查图片数量限制
  const remainingSlots = 6 - uploadedImages.value.length
  if (remainingSlots <= 0) {
    console.error('最多只能上传6张图片')
    target.value = ''
    return
  }
  // 限制本次上传的文件数量
  const filesToUpload = Math.min(files.length, remainingSlots)
  // 验证文件
  for (let i = 0; i < filesToUpload; i++) {
    const file = files[i]
    // 验证文件类型
    if (!['image/jpeg', 'image/png'].includes(file.type)) {
      console.error(`文件 ${file.name} 格式不支持,仅支持 jpg/png`)
      continue
    }
    // 验证文件大小(2MB)
    if (file.size > 2 * 1024 * 1024) {
      console.error(`文件 ${file.name} 超过2MB限制`)
      continue
    }
    // 预览图片
    const reader = new FileReader()
    reader.onload = (e) => {
      const url = e.target?.result as string
      // 再次检查数量限制
      if (uploadedImages.value.length < 6) {
        uploadedImages.value.push({
          id: Date.now() + i,
          url,
        })
      }
    }
    reader.readAsDataURL(file)
    // TODO: 调用上传 API
    console.log('准备上传文件:', file.name)
  }
  // 提示用户如果有文件被忽略
  if (files.length > filesToUpload) {
    console.warn(`只能上传 ${filesToUpload} 张图片,已忽略其余 ${files.length - filesToUpload} 张`)
  }
  // 清空 input,允许重复选择同一文件
  target.value = ''
}
// 删除图片
function handleDeleteImage(id: number) {
  const index = uploadedImages.value.findIndex(img => img.id === id)
  if (index > -1) {
    uploadedImages.value.splice(index, 1)
  }
}
// 预览图片
function handlePreviewImage(url: string) {
  previewImage.value = url
  previewVisible.value = true
}
// 关闭预览
function closePreview() {
  previewVisible.value = false
  previewImage.value = ''
}
onMounted(() => {
@@ -37,12 +258,312 @@
</script>
<template>
  <!-- 地图区域 -->
  <div class="w-full h-full" id="mapBox">
    <div id="map" ref="domRef" class="w-full h-full" style="position: relative;" />
  <div class="point-management-container">
    <!-- 左侧点位管理面板 -->
    <div class="left-panel">
      <!-- 标题 -->
      <div class="panel-header">
        <h2 class="panel-title">
          点位管理
        </h2>
      </div>
      <!-- 搜索框 -->
      <div class="search-box">
        <div class="search-input-wrapper">
          <input
            v-model="searchKeyword" type="text" class="search-input" placeholder="搜索点位名称或地址"
            @keyup.enter="handleSearch"
          >
          <button v-if="searchKeyword" class="clear-btn" @click="clearSearch">
            ×
          </button>
          <svg class="search-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
            <path
              fill="currentColor"
              d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0 0 11.6 0l43.6-43.5a8.2 8.2 0 0 0 0-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
            />
          </svg>
        </div>
      </div>
      <!-- 点位列表 -->
      <div class="point-list">
        <div v-if="filteredPointList.length === 0" class="empty-state">
          <div class="empty-text">
            暂无点位数据
          </div>
        </div>
        <div
          v-for="point in filteredPointList" v-else :key="point.id" class="point-item"
          :class="{ active: activePointId === point.id }" @click="handlePointClick(point)"
        >
          <div class="flex a-center j-between">
            <div class="flex a-center">
              <div class="point-id">
                ID:{{ point.id }}
              </div>
              <div class="point-status" :class="getStatusClass(point.type, point.status)">
                {{ point.status }}
              </div>
              <div class="point-type" :class="getTypeClass(point.type)">
                {{ point.type }}
              </div>
            </div>
            <!-- switch 开关 -->
            <n-switch
              v-model:value="point.isOpen" size="small" :rail-style="railStyle"
              @update:value="(value) => handleSwitchChange(point, value)" @click.stop
            />
          </div>
          <div class="point-address">
            国贸大厦北门停车点
          </div>
          <div class="point-time">
            限停时长:60分钟
          </div>
        </div>
      </div>
      <!-- 底部新增按钮 -->
      <div class="panel-footer">
        <div class="add-btn" @click="handleAddPoint">
          <span class="icon">+</span>
          新增点位
        </div>
      </div>
    </div>
    <!-- 地图区域 -->
    <div class="map-container">
      <div v-if="mapError" class="map-error">
        {{ mapError }}
      </div>
      <div id="map" ref="domRef" class="map-content" />
    </div>
    <!-- 右侧运营详情和地图操作 -->
    <div class="right-panel">
      <div class="map-operate">
        <div class="flex a-center relative">
          <!-- 打点 -->
          <div class="operate-item">
            <img src="../../../assets//point/point.png" class="operate-icon">
            <div class="operate-text">
              打点
            </div>
          </div>
          <!-- 图层 -->
          <div class="operate-item coverage">
            <img src="../../../assets//point/coverage.png" class="operate-icon">
            <!-- <img src="../../../assets//point/active-coverage.png" class="operate-icon"> -->
            <div class="operate-text">
              图层
            </div>
            <img src="../../../assets//point/more.png" class="operate-icon">
          </div>
          <!-- 打点下拉 -->
          <div class="dot-detail">
            <div class="dot-item active-dot">
              <img src="../../../assets//point/test.png" class="dot-icon">
              <div class="dot-tit">
                测试点
              </div>
            </div>
            <div class="dot-item">
              <img src="../../../assets//point/operation.png" class="dot-icon">
              <div class="dot-tit">
                运营点
              </div>
            </div>
          </div>
          <!-- 图层下拉 -->
          <div class="coverage-detail">
            <div class="coverage-tit">
              区域
            </div>
            <div class="flex a-center">
              <img src="../../../assets//point/uncheck.png" class="coverage-icon">
              <div class="coverage-txt">
                运营区
              </div>
            </div>
            <div class="flex a-center">
              <img src="../../../assets//point/checked.png" class="coverage-icon">
              <div class="coverage-txt">
                禁行区
              </div>
            </div>
            <div class="coverage-tit">
              点位
            </div>
            <div class="flex a-center">
              <img src="../../../assets//point/checked.png" class="coverage-icon">
              <div class="coverage-txt">
                运营点
              </div>
            </div>
            <div class="flex a-center">
              <img src="../../../assets//point/checked.png" class="coverage-icon">
              <div class="coverage-txt">
                测试点
              </div>
            </div>
          </div>
        </div>
        <div class="operate-bottom">
          <div class="operate-card">
            <img src="../../../assets//point/location.png" class="location-icon location">
            <img src="../../../assets//point/add.png" class="location-icon add">
            <img src="../../../assets//point/reduce.png" class="location-icon">
          </div>
        </div>
      </div>
      <div class="point-detail">
        <div class="point-info">
          <div class="point-title">
            <h2 class="panel-title">
              点位详情
            </h2>
          </div>
          <!-- 详情内容 待完善 -->
          <div class="point-detail-content">
            <div class="info-card">
              <span class="info-icon" />
              <div class="card-title">
                基本信息
              </div>
            </div>
            <!-- 点位名称 -->
            <div class="info-form">
              <div class="info-form-label">
                <span class="label-require">*</span>点位名称
              </div>
              <div class="info-form-value">
                <input class="form-input" type="text" placeholder="请输入点位名称">
              </div>
            </div>
            <!-- POI坐标 -->
            <div class="info-form">
              <div class="info-form-label">
                POI
              </div>
              <div class="info-form-value">
                <input class="form-input" type="text" placeholder="请输入点位名称">
              </div>
            </div>
            <!-- 点位ID -->
            <div class="info-form">
              <div class="info-form-label">
                点位ID
              </div>
              <div class="info-form-value">
                <input class="form-input" type="text" placeholder="请输入点位名称">
              </div>
            </div>
            <!-- 备注 -->
            <div class="info-form">
              <div class="info-form-label">
                备注
              </div>
              <div class="info-form-value">
                <n-input type="textarea" maxlength="200" show-count />
              </div>
            </div>
            <!-- 点位图片 -->
            <div class="info-card">
              <span class="info-icon" />
              <div class="card-title">
                点位图片
              </div>
            </div>
            <div class="info-form">
              <div class="point-img">
                <div v-for="item in uploadedImages" :key="item.id" class="img-item">
                  <img :src="item.url" class="point-img-item" @click="handlePreviewImage(item.url)">
                  <img
                    class="img-delete" src="../../../assets/point/delete.png"
                    @click.stop="handleDeleteImage(item.id)"
                  >
                </div>
                <!-- 自定义上传区域 - 图片少于6张时显示 -->
                <div v-if="uploadedImages.length < 6" class="img-item upload-item" @click="triggerUpload">
                  <img src="../../../assets/point/upload.png" class="upload-icon">
                  <input
                    ref="uploadInput" type="file" accept="image/jpeg,image/png" multiple style="display: none"
                    @change="handleFileChange"
                  >
                </div>
              </div>
              <div class="notice-card">
                <img src="../../../assets/point/notice.png" class="img-notice">
                <div class="notice-txt">
                  Supported: jpg/png, 单个图片最大2M
                </div>
              </div>
            </div>
            <!-- 运营信息 -->
            <div class="info-card">
              <span class="info-icon" />
              <div class="card-title">
                运营信息
              </div>
            </div>
            <!-- 限停时长(分钟) -->
            <div class="info-form">
              <div class="info-form-label">
                限停时长(分钟)
              </div>
              <div class="info-form-value">
                <input class="form-input" type="text" placeholder="请输入点位名称">
              </div>
            </div>
            <!-- 运营时间段 -->
            <div class="info-form">
              <div class="info-form-label">
                <span class="label-require">*</span>运营时间段
              </div>
              <div class="info-form-value">
                <input class="form-input" type="text" placeholder="请输入点位名称">
              </div>
            </div>
          </div>
        </div>
        <!-- 底部保存 取消 -->
        <div class="panel-footer">
          <div class="cancel-btn">
            取消
          </div>
          <div class="submit-btn">
            保存
          </div>
        </div>
      </div>
    </div>
    <!-- 图片预览弹窗 -->
    <div v-if="previewVisible" class="image-preview-modal" @click="closePreview">
      <div class="preview-content">
        <img :src="previewImage" class="preview-image" @click.stop>
        <div class="preview-close" @click="closePreview">
          <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
            <path
              fill="currentColor"
              d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
            />
          </svg>
        </div>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
@import './index.scss';
</style>
<style lang="scss">
.robotaxi-content {
  padding: 0 !important;