zjk
2026-06-01 cc5ff3e32ab90098500ce9c3f5cd3f148f497b96
Merge branch 'deploy-1.0' into deploy-2.1
19个文件已修改
733 ■■■■■ 已修改文件
src/service/api/coupon.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/list.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/status.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.adapters.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.entry.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.fence-workflow.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.form.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.geometry.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.payload.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.point-workflow.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.station-workflow.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/cruiseCheckInDetails/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/cruiseCheckInRule/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/components/AddFormModal.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/accountPermission/accountManagement/components/AccountModal.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/components/ActivityOrderList.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/coupon/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/coupon.ts
@@ -45,7 +45,7 @@
 */
export function sendCoupon(data: {
  couponId: number | string
  phone?: string[] // 手机号集合 不传为全部
  ids?: number[] // 用户ID集合 不传为全部
}) {
  return request.Post<Service.ResponseResult<any>>('/account/api/v1/appUserCoupon/send', data)
}
src/views/businessOperations/operationWorkbench/list.vue
@@ -1,6 +1,6 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NDataTable, NDatePicker, NInput, NSelect, NSpace, NTag, NTooltip } from 'naive-ui'
import { NButton, NDataTable, NDatePicker, NInput, NSelect, NSpace, NTag, NTimePicker, NTooltip } from 'naive-ui'
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
@@ -78,6 +78,13 @@
  { label: '20以上禁行区域', value: '20' },
]
const fenceStrategyTypeOptions = [
  { label: '工作日', value: 0 },
  { label: '法定节假日', value: 1 },
  { label: '非工作日', value: 2 },
  { label: '自定义时间', value: 3 },
]
const stationStatusOptions = [
  { label: '草稿', value: 0 },
  { label: '待生效', value: 1 },
@@ -100,6 +107,10 @@
  electronicNumberOrName: '',
  electronicAreaId: null as string | null,
  statusList: null as number[] | null,
  type: null as number | null,
  lifecycleRange: null as [number, number] | null,
  startTime: '',
  endTime: '',
  updateBy: '',
  updateTime: null as [number, number] | null,
  createBy: '',
@@ -412,6 +423,18 @@
  return value ? dayjs(value[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss') : undefined
}
function parseTimePickerValue(value?: string | null) {
  const normalizedValue = String(value || '').trim()
  if (!normalizedValue)
    return null
  const parsed = dayjs(`2000-01-01 ${normalizedValue}`)
  return parsed.isValid() ? parsed.valueOf() : null
}
function formatTimePickerValue(value: number | null) {
  return typeof value === 'number' ? dayjs(value).format('HH:mm') : ''
}
function getCurrentTabModel() {
  if (activeTab.value === 'fence')
    return fenceModel.value
@@ -623,6 +646,11 @@
    pageSize: pagination.pageSize,
    electronicNumberOrName: fenceModel.value.electronicNumberOrName,
    statusList: fenceModel.value.statusList,
    type: fenceModel.value.type,
    effectTime: fenceModel.value.lifecycleRange ? dayjs(fenceModel.value.lifecycleRange[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss') : null,
    failureTime: fenceModel.value.lifecycleRange ? dayjs(fenceModel.value.lifecycleRange[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss') : null,
    startTime: fenceModel.value.startTime || null,
    endTime: fenceModel.value.endTime || null,
    createBy: fenceModel.value.createBy,
    updateBy: fenceModel.value.updateBy,
    updateStartTime: fenceModel.value.updateTime ? dayjs(fenceModel.value.updateTime[0]).format('YYYY-MM-DD HH:mm:ss') : null,
@@ -1483,6 +1511,52 @@
        ),
      },
      {
        label: '运营策略',
        path: 'type',
        render: () => (
          <NSelect
            v-model:value={fenceModel.value.type}
            options={fenceStrategyTypeOptions}
            placeholder="请选择"
            clearable
          />
        ),
      },
      {
        label: '生命周期',
        path: 'lifecycleRange',
        render: () => (
          <NDatePicker v-model:value={fenceModel.value.lifecycleRange} type="daterange" clearable actions={[]} close-on-select={true} format="yyyy/MM/dd" />
        ),
      },
      {
        label: '运营时间',
        path: 'operationTime',
        render: () => (
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', width: '100%' }}>
            <NTimePicker
              value={parseTimePickerValue(fenceModel.value.startTime)}
              format="HH:mm"
              placeholder="开始时间"
              clearable
              actions={['now']}
              style={{ width: 'calc(50% - 12px)' }}
              onUpdate:value={value => fenceModel.value.startTime = formatTimePickerValue(value)}
            />
            <span>至</span>
            <NTimePicker
              value={parseTimePickerValue(fenceModel.value.endTime)}
              format="HH:mm"
              placeholder="结束时间"
              clearable
              actions={['now']}
              style={{ width: 'calc(50% - 12px)' }}
              onUpdate:value={value => fenceModel.value.endTime = formatTimePickerValue(value)}
            />
          </div>
        ),
      },
      {
        label: '更新人',
        path: 'updateBy',
        render: () => <NInput v-model:value={fenceModel.value.updateBy} />,
@@ -2248,6 +2322,11 @@
})
const cardClassName = computed(() => activeTab.value === 'point' ? 'point-management-list' : '')
const showBatchExportButton = computed(() => ['point', 'station'].includes(activeTab.value))
const batchExportButtonKey = computed(() => `batch-export-${activeTab.value}`)
const batchExportButtonId = computed(() => activeTab.value === 'station' ? 'exportStationExcel' : 'exportPointExcel')
const batchExportPermissionCode = computed(() => activeTab.value === 'station' ? 202642934 : 202642928)
const batchExportButtonText = computed(() => activeTab.value === 'station' ? '批量导出场站' : '批量导出点位')
const currentOperationLogBusinessType = computed(() => {
  if (activeTab.value === 'fence')
    return 22
@@ -2258,6 +2337,14 @@
function handleOpenOperationLog() {
  showOperationLogModal.value = true
}
function handleBatchExport() {
  if (activeTab.value === 'station') {
    handleStationBatchExport()
    return
  }
  handlePointBatchExport()
}
watch(
@@ -2390,28 +2477,18 @@
            <NButton v-permission="2026051805" @click="handleOpenOperationLog">
              操作日志
            </NButton>
            <template v-if="activeTab === 'point'">
            <template v-if="showBatchExportButton">
              <span v-if="checkedRowKeys.length > 0" class="selected-text">
                已选择{{ checkedRowKeys.length }}条记录
              </span>
              <NButton
                v-permission="202642928"
                :key="batchExportButtonKey"
                :id="batchExportButtonId"
                v-permission="batchExportPermissionCode"
                :disabled="checkedRowKeys.length === 0"
                @click="handlePointBatchExport"
                @click="handleBatchExport"
              >
                批量导出
              </NButton>
            </template>
            <template v-else-if="activeTab === 'station'">
              <span v-if="checkedRowKeys.length > 0" class="selected-text">
                已选择 {{ checkedRowKeys.length }} 条记录
              </span>
              <NButton
                v-permission="202642934"
                :disabled="checkedRowKeys.length === 0"
                @click="handleStationBatchExport"
              >
                批量导出
                {{ batchExportButtonText }}
              </NButton>
            </template>
          </n-flex>
src/views/businessOperations/operationWorkbench/map.vue
@@ -173,13 +173,20 @@
// 三类实体的列表数据由同一张地图承载,左侧列表只是按当前 tab 切换展示。
const pointData = ref<ListItem[]>([]);
// 用于围栏/场站空间关系计算的点位基准数据。
// 和 pointData 大体同源,但语义上强调“给地图几何判断使用”。
const pointSpatialData = ref<ListItem[]>([]);
// 点位关键词搜索时的即时结果集,包含业务点位和高德联想结果。
const pointSearchList = ref<ListItem[]>([]);
// 标记当前左侧点位列表是否处于关键词搜索结果模式。
const pointKeywordSearchActive = ref(false);
const fenceData = ref<ListItem[]>([]);
// 围栏区域明细,包含运营区和禁行区,供地图 polygon 渲染使用。
const fenceAreaData = ref<any[]>([]);
const stationData = ref<ListItem[]>([]);
// 创建场站表单的默认初始值。
// 使用场景:页面初始化、关闭场站面板、退出创建态、保存成功后的状态重置。
function createEmptyStationForm(): StationFormState {
  return {
    id: "",
@@ -331,6 +338,8 @@
  });
}
// 使用详情数据重置场站表单,并同步默认选中的场站内点位。
// 主要用于场站详情加载完成后,从“接口结构”回灌为“右侧面板表单结构”。
function resetStationForm(detail?: any | null) {
  const workflowState = buildStationWorkflowFormState(detail);
  stationForm.value = workflowState.form;
@@ -382,23 +391,35 @@
// 页面级筛选与面板展示状态。
const activeTab = ref<WorkbenchTab>(initialEntryContext.tab);
// 当前城市编码,既影响接口拉数,也影响地图默认视口和创建归属。
const cityValue = ref<string>(initialEntryContext.cityCode);
// 左侧关键词搜索框内容。
const keyword = ref("");
// 左侧列表区域是否折叠。
const leftExpanded = ref(true);
// 三类实体右侧面板的显隐控制。
const pointPanelClosed = ref(false);
const fencePanelClosed = ref(false);
const stationPanelClosed = ref(false);
// 顶部创建菜单和筛选下拉显隐。
const showCreateMenu = ref(false);
const cityDropdownVisible = ref(false);
const statusDropdownVisible = ref(false);
// 当前 tab 选中的状态筛选值。
const selectedStatuses = ref<string[]>([]);
// 当前选中实体 id,是详情加载、地图聚焦和列表高亮的主键。
const selectedId = ref<string>(initialEntryContext.id || "");
// 入口指定的展示类型:detail / edit / create。
// 数据加载完成前先暂存,后续由入口恢复逻辑消费。
const pendingWorkbenchType = ref<string>(initialEntryContext.type || "");
// 标识三类实体基础数据是否已经完成首轮装载。
const workbenchDataReady = ref(false);
const showLayerMenu = ref(false);
const mapError = ref("");
const mapLoading = ref(true);
// 点位保存中状态,主要用于右侧面板按钮 loading。
const pointSaving = ref(false);
// 点位工作流状态:编辑、新建、是否完成必要准备、地图选点模式。
const pointEditMode = ref(false);
const pointCreateMode = ref(false);
const pointCreateReady = ref(false);
@@ -415,10 +436,12 @@
  );
});
// 统一收口各个下拉弹层的展开状态,避免在模板中散落过多单独事件处理。
function handleSelectShowUpdate(key: string, show: boolean) {
  selectShowState[key] = show;
}
// 主动让当前激活的时间选择器失焦,避免弹层残留。
function closeTimePicker() {
  (document.activeElement as HTMLElement | null)?.blur?.();
}
@@ -436,15 +459,19 @@
const fenceStrategyList = ref<FenceStrategyFormItem[]>([]);
// AMap 实例与覆盖物引用,便于重复渲染时做增删和清理。
// 地图容器 DOM 和核心 AMap 实例。
const domRef = ref<HTMLDivElement | null>(null);
const map = shallowRef<any>(null);
const geocoderRef = shallowRef<any>(null);
// 点位编辑中的临时 marker / 信息窗。
const pointEditMarker = shallowRef<any>(null);
const pointEditInfoWindow = shallowRef<any>(null);
const fenceEditGuideMarker = shallowRef<any>(null);
// 正式态地图覆盖物集合,重绘前会统一清理。
const pointMarkers = shallowRef<any[]>([]);
const stationPolygons = shallowRef<any[]>([]);
const fencePolygons = shallowRef<any[]>([]);
// 围栏编辑相关的鼠标工具、编辑器与草稿数据。
const fenceMouseTool = shallowRef<any>(null);
const fencePolyEditor = shallowRef<any>(null);
const fenceDraftPolygon = shallowRef<any>(null);
@@ -461,6 +488,7 @@
const restrictedAreaModalVisible = ref(false);
const fencePanelRef = ref<any>(null);
const restrictedAreaModalRef = ref<any>(null);
// 场站编辑相关的鼠标工具、编辑器与草稿数据。
const stationMouseTool = shallowRef<any>(null);
const stationPolyEditor = shallowRef<any>(null);
const stationDraftPolygon = shallowRef<any>(null);
@@ -495,6 +523,8 @@
  showPoint: true,
  showStation: true,
});
// 三类实体详情/编辑表单状态与快照。
// detailData 负责承接接口详情,Form 负责右侧编辑态,Snapshot 用于未保存变更判断。
const pointDetailData = ref<Entity.Point | null>(null);
const pointEditSnapshot = ref<string>("");
const fenceEditSnapshot = ref<string>("");
@@ -515,10 +545,13 @@
  () => statusOptionsMap[activeTab.value] || []
);
// 将筛选值反查为当前 tab 对应的展示文案,供状态筛选与统计口径复用。
function getStatusLabelByValue(tab: WorkbenchTab, value: string) {
  return statusOptionsMap[tab].find((option) => option.value === value)?.label || "";
}
// 统一归一化不同接口返回的状态文案,避免筛选时因同义状态导致匹配失败。
// 当前主要对场站口径做兼容,例如“运营中/启用中”“草稿/待配置”等。
function normalizeStatusFilterLabel(tab: WorkbenchTab, status: string) {
  const statusText = String(status || "").trim();
  if (tab !== "station") return statusText;
@@ -545,6 +578,10 @@
  return "搜索点位名称、地址或ID";
});
// 左侧面板当前真正参与展示的数据源。
// 处理逻辑:
// 1. 点位 tab 优先使用关键词搜索结果,并叠加状态筛选;
// 2. 围栏/场站 tab 使用对应实体列表,再同时叠加关键词和状态筛选。
const currentList = computed(() => {
  if (activeTab.value === "point")
    return pointDisplayList.value.filter((item) => {
@@ -611,6 +648,9 @@
  return mapPointApiItemToListItem(pointDetailData.value);
});
// 当前 tab 的选中实体。
// 该计算属性是右侧详情面板、地图聚焦、列表高亮的统一来源,
// 会根据创建态、面板关闭态和详情兜底态自动切换数据源。
const selectedItem = computed(() => {
  if (activeTab.value === "point") {
    if (pointCreateMode.value) return null;
@@ -681,6 +721,8 @@
  cityValue.value = entryContext.cityCode;
}
// 按当前城市配置同步地图默认视口。
// 优先使用本地预设的中心点和缩放级别,缺失时再回退到城市名定位。
function applyCityMapViewport() {
  if (!map.value) return;
  const cityMeta = cityCenterMap[cityValue.value];
@@ -883,6 +925,7 @@
  if (stationDraftPoints.value.length < 3) return "请先在运营区内绘制场站区域";
  return "【紫色场站】可调整区域点位,请保持完全处于运营区内且避开禁行区";
});
// 地图顶部提示文案,按当前编辑中的实体类型在围栏/场站提示之间切换。
const mapTipText = computed(() => fenceMapTipText.value || stationMapTipText.value);
const hasRightPanel = computed(() => {
@@ -1294,8 +1337,17 @@
  if (!isStationFormMode.value) return "";
  if (!stationForm.value.stationName.trim()) return "请输入场站名称";
  if (stationDraftPoints.value.length < 3) return "请先在运营区内绘制场站";
  const geometryError = getStationGeometryInvalidReason(stationDraftPoints.value);
  const geometryError = getStationGeometryInvalidReason(stationDraftPoints.value, {
    allowOutsideOperationArea: isStationEditWorkflow.value,
  });
  if (geometryError) return geometryError;
  if (
    isStationEditWorkflow.value &&
    !resolveStationOperationAreaByPolygon(stationDraftPoints.value) &&
    Number(stationForm.value.areaStatus || 1) === 1
  ) {
    return "当前场站不在围栏区域内,不允许启用";
  }
  if (!stationForm.value.parkingTimeLimit) return "请输入限停时长";
  return getStationCustomTimeErrorMessage();
});
@@ -1544,14 +1596,25 @@
    .filter((item) => item.points.length >= 3);
}
function getStationGeometryInvalidReason(points: LngLatTuple[]) {
function resolveStationOperationAreaByPolygon(points: LngLatTuple[]) {
  if (points.length < 3) return null;
  return (
    getStationOperationAreas().find((area) => isRingInRing(points, area.points))
    || null
  );
}
function getStationGeometryInvalidReason(
  points: LngLatTuple[],
  options: {
    allowOutsideOperationArea?: boolean;
  } = {}
) {
  if (points.length < 3) return "请绘制至少3个点的场站";
  if (isFencePolygonSelfIntersect(points)) return "场站区域不允许自相交";
  const operationAreas = getStationOperationAreas();
  const isInsideOperationArea = operationAreas.some((area) =>
    isRingInRing(points, area.points)
  );
  if (!isInsideOperationArea) return "场站需完全处于运营区内";
  const matchedOperationArea = resolveStationOperationAreaByPolygon(points);
  if (!matchedOperationArea && !options.allowOutsideOperationArea)
    return "场站需完全处于运营区内";
  const otherStations = getOtherStations();
  const geometryUtil = (window as any).AMap?.GeometryUtil;
  if (geometryUtil) {
@@ -1579,7 +1642,9 @@
}
function validateStationGeometry(points: LngLatTuple[]) {
  const errorMessage = getStationGeometryInvalidReason(points);
  const errorMessage = getStationGeometryInvalidReason(points, {
    allowOutsideOperationArea: isStationEditWorkflow.value,
  });
  if (errorMessage) {
    window.$message?.warning(errorMessage);
    return false;
@@ -1615,6 +1680,9 @@
  }
}
// 清理场站地图编辑器相关状态。
// 使用场景:退出场站编辑/创建、取消编辑、关闭面板、重新开始绘制前。
// removePolygon=false 时仅关闭编辑器,不移除当前草稿 polygon。
function cleanupStationEditorState(removePolygon = true) {
  stationMouseTool.value?.close?.();
  stationMouseTool.value = null;
@@ -1747,6 +1815,9 @@
  stationPolyEditor.value.open();
}
// 开始绘制场站区域。
// 使用场景:新建场站进入绘制态时调用。
// 绘制完成后会先做几何校验,再将结果落入场站草稿并开启可编辑态。
function startStationPolygonDraw() {
  if (!map.value || typeof (window as any).AMap === "undefined") return;
  cleanupStationEditorState();
@@ -1776,6 +1847,8 @@
  });
}
// 根据场站详情回灌地图草稿区,用于编辑已有场站。
// 会复用详情中的 polygonPath,并直接进入可拖拽调整的编辑态。
function enterStationDraftByDetail(detail: any) {
  const polygonPath = clonePolygonPath(detail?.polygonPath);
  if (polygonPath.length < 3 || !map.value) return;
@@ -1784,6 +1857,8 @@
  enableStationDraftEdit();
}
// 清理围栏地图编辑器、禁行区编辑器和相关草稿状态。
// 使用场景:退出围栏编辑/创建、取消编辑、关闭面板、重新开始绘制前。
function cleanupFenceEditorState(removePolygon = true) {
  fenceMouseTool.value?.close?.();
  fenceMouseTool.value = null;
@@ -1946,6 +2021,9 @@
  syncFencePoints();
}
// 开始绘制围栏主运营区。
// 使用场景:新建围栏进入绘制态时调用。
// 绘制完成后会校验几何合法性,成功后生成围栏草稿并进入编辑态。
function startFencePolygonDraw() {
  if (!map.value || typeof (window as any).AMap === "undefined") return;
  cleanupFenceEditorState();
@@ -1981,6 +2059,11 @@
  });
}
// 根据围栏详情回灌地图草稿区,用于编辑已有围栏。
// 主要作用:
// - 解析主运营区 polygon;
// - 恢复禁行区草稿数据;
// - 根据锁定状态决定是否开启主区域可编辑态。
function enterFenceDraftByDetail(detail: any) {
  const operationArea = Array.isArray(detail?.electronicFenceAreas)
    ? detail.electronicFenceAreas.find(
@@ -2713,23 +2796,14 @@
function resolveStationOperationArea() {
  if (!fenceAreaData.value.length) return null;
  const draftPolygonPath = clonePolygonPath(stationDraftPoints.value);
  const draftCenter = getPolygonCenter(draftPolygonPath);
  const centerLon = Number.isFinite(Number(draftCenter?.[0]))
    ? Number(draftCenter?.[0])
    : selectedItem.value?.type === "station"
    ? Number(selectedItem.value.lng)
    : NaN;
  const centerLat = Number.isFinite(Number(draftCenter?.[1]))
    ? Number(draftCenter?.[1])
    : selectedItem.value?.type === "station"
    ? Number(selectedItem.value.lat)
    : NaN;
  if (!Number.isFinite(centerLon) || !Number.isFinite(centerLat)) return null;
  return resolveOperationArea(
    fenceAreaData.value,
    centerLon,
    centerLat
  if (draftPolygonPath.length >= 3)
    return resolveStationOperationAreaByPolygon(draftPolygonPath);
  const detailPolygonPath = clonePolygonPath(
    stationDetailData.value?.polygonPath || stationForm.value.polygonPath
  );
  if (detailPolygonPath.length >= 3)
    return resolveStationOperationAreaByPolygon(detailPolygonPath);
  return null;
}
const stationLifecycleCache = ref<Record<string, Record<string, any> | null>>({});
@@ -2783,6 +2857,7 @@
}
const stationOperationLifecycle = computed(() => {
  if (!stationOperationArea.value) return null;
  const detailLifecycle = normalizeStationLifecycleSource(stationDetailData.value);
  if (detailLifecycle) return detailLifecycle;
  return stationOperationLifecycleFallback.value;
@@ -2834,6 +2909,45 @@
      )
    : null;
  return Number(operationArea?.areaStatus || 1);
}
// 获取场站进入编辑前的原始启停状态。
// 使用场景:
// 1. 场站编辑保存前判断本次是否发生了“启用 -> 停用”的状态切换;
// 2. 仅在真实发生停用时,才进一步询问是否联动停用内部点位。
function getStationOriginalAreaStatus() {
  const detail = stationDetailData.value;
  return Number(detail?.areaStatus) === 2 ||
    Number(detail?.status) === 3 ||
    String(detail?.status || detail?.statusText || "").trim() === "已停用"
    ? 2
    : 1;
}
// 打开场站停用时的联动确认弹窗。
// 使用场景:编辑已有场站并发布执行,且场站从启用切换为停用、内部存在点位时触发。
// 返回值约定:
// - true: 用户确认同时停用场站内点位
// - false: 用户仅停用场站,不联动停用点位
// - null: 用户关闭弹窗,本次保存流程终止
function openStationDisablePointStopDialog(pointCount: number) {
  return new Promise<boolean | null>((resolve) => {
    if (!window.$dialog) {
      resolve(null);
      return;
    }
    window.$dialog.warning({
      title: "提示",
      content: `当前场站包含${pointCount}个点位,确认一起停用?`,
      positiveText: "是",
      negativeText: "否",
      maskClosable: false,
      autoFocus: false,
      onPositiveClick: () => resolve(true),
      onNegativeClick: () => resolve(false),
      onClose: () => resolve(null),
    });
  });
}
async function resolveFencePointRelationPayload() {
@@ -3014,6 +3128,12 @@
  });
}
// 进入场站新建流程。
// 使用场景:点击“新建场站”或通过工作台入口直接进入场站创建。
// 主要作用:
// - 先处理与点位/围栏/场站现有编辑态的冲突;
// - 初始化场站创建表单与地图草稿状态;
// - 打开地图绘制工具,开始绘制场站区域。
async function startStationCreate() {
  if (
    activeTab.value === "point" &&
@@ -3070,6 +3190,8 @@
  startStationPolygonDraw();
}
// 关闭场站右侧面板。
// 若当前处于编辑/创建态,会先确认是否放弃未保存内容。
async function handleCloseStationPanel() {
  if (
    isStationFormMode.value &&
@@ -3104,6 +3226,10 @@
  renderMapEntities();
}
// 场站保存成功后统一收口右侧面板状态,并立即重绘地图实体。
// 使用场景:
// 1. 新建/编辑场站保存成功后关闭右侧面板;
// 2. 将最新的选中 id 回写给列表/地图,用于后续高亮或重新定位。
function closeStationDetailPanelAfterSave(nextSelectedId = "") {
  const workflowState = buildStationCloseAfterSaveState(nextSelectedId);
  stationDetailData.value = workflowState.stationDetailData;
@@ -3119,6 +3245,8 @@
  renderMapEntities();
}
// 进入场站编辑态。
// 会优先拉取最新场站详情,再将详情数据回灌到表单和地图草稿中。
async function handleEnterStationEdit() {
  if (!selectedItem.value || activeTab.value !== "station") return;
  const detail = await loadStationDetailData(
@@ -3128,6 +3256,8 @@
  applyStationEditByDetail(detail);
}
// 取消场站编辑。
// 编辑已有场站时回退到详情态;新建场站时直接退出创建流程。
async function handleCancelStationEdit() {
  if (
    !(await confirmDiscardStationEdit(
@@ -3159,16 +3289,45 @@
  }
}
// 场站保存入口。
// 使用场景:
// 1. 保存草稿时传入 0;
// 2. 发布执行时传入 1。
// 主要作用:
// - 校验当前表单是否允许保存;
// - 在“停用场站且存在点位”时确认是否联动停用点位;
// - 组装编辑/新增 payload 并提交;
// - 保存成功后刷新场站与必要的点位数据,再关闭面板并重绘地图。
async function handleSaveStationEdit(saveType: 0 | 1) {
  if (activeTab.value !== "station") return;
  if (saveType === 1 && stationSaveBlockReason.value) {
    window.$message?.warning(stationSaveBlockReason.value);
    return;
  }
  // 是否为场站新增流程;仅编辑已有场站时才需要判断 pointStop。
  const isCreatingStation = stationCreateMode.value;
  // 当前表单里的目标启停状态:1 启用,2 停用。
  const areaStatus = Number(stationForm.value.areaStatus || 1);
  // 当前场站内部点位数量,用于决定是否需要弹出联动停用确认。
  const pointCount = Array.isArray(stationForm.value.pointList)
    ? stationForm.value.pointList.length
    : 0;
  // 本次保存是否需要联动停用场站内部点位。
  // 仅在编辑已有场站并执行停用时由确认弹窗写入。
  let pointStop: boolean | undefined;
  if (
    !isCreatingStation &&
    saveType === 1 &&
    areaStatus === 2 &&
    getStationOriginalAreaStatus() !== 2 &&
    pointCount > 0
  ) {
    const confirmResult = await openStationDisablePointStopDialog(pointCount);
    if (confirmResult === null) return;
    pointStop = confirmResult;
  }
  stationSaving.value = true;
  try {
    const areaStatus = Number(stationForm.value.areaStatus || 1);
    const nextStatus =
      saveType === 0 ? "草稿" : areaStatus === 2 ? "已停用" : "启用中";
    const pointList = stationForm.value.pointList.map((item) => ({
@@ -3187,14 +3346,7 @@
      ? Number(polygonCenter?.[1])
      : undefined;
    const currentSelectedStation = selectedItem.value;
    const operationArea =
      polygonCenter && fenceAreaData.value.length
        ? resolveOperationArea(
            fenceAreaData.value,
            Number(polygonCenter[0]),
            Number(polygonCenter[1])
          )
        : null;
    const operationArea = resolveStationOperationAreaByPolygon(polygonPath);
    const stationArea = Number(calcFenceArea(polygonPath).toFixed(2));
    const detail = {
      id:
@@ -3222,6 +3374,7 @@
      centerLon,
      saveType,
      areaStatus,
      pointStop,
      operationArea,
      polygonPath,
      operationType: stationForm.value.operationType,
@@ -3263,6 +3416,10 @@
      raw: detail,
    };
    await loadStationData();
    // 当接口已执行“停用场站并联动停用点位”时,需重拉点位数据。
    // 这样后续 renderMapEntities 才能基于最新状态刷新地图点位颜色、筛选和选中态。
    if (pointStop)
      await loadPointResources();
    const refreshedStation = stationData.value.find(
      (item) =>
        String(item.id) === String(detail.id) ||
@@ -3294,6 +3451,15 @@
  }
}
watch(
  [isStationEditWorkflow, stationOperationArea, () => stationForm.value.areaStatus],
  ([editing, operationArea, areaStatus]) => {
    if (!editing || operationArea || Number(areaStatus) !== 1) return;
    stationForm.value.areaStatus = 2;
  }
);
// tab 切换时重置左侧筛选与选中态,并按目标 tab 刷新对应资源。
watch(activeTab, async (value) => {
  keyword.value = "";
  selectedStatuses.value = [];
@@ -3316,6 +3482,7 @@
  await loadStationData();
});
// 当前列表变化时兜底修正 selectedId,避免选中项被过滤或删除后仍残留旧选中态。
watch(currentList, (list) => {
  if (activeTab.value === "point") {
    if (pointCreateMode.value) return;
@@ -3387,6 +3554,15 @@
    selectedId.value = "";
});
// 按入口上下文恢复指定实体的详情或编辑展示。
// 使用场景:
// 1. 路由 query 带 id/type 进入工作台;
// 2. tab 切换后重新消费入口上下文;
// 3. 数据加载完成后自动定位到目标实体。
// 主要作用:
// - 根据 tab 调用对应详情加载函数;
// - 清理其他实体的编辑残留状态;
// - 在 edit 场景下自动切换到对应编辑模式。
async function handleEntrySelection(
  tab: WorkbenchTab,
  id: string,
@@ -3514,6 +3690,7 @@
  { immediate: true }
);
// 城市切换时统一处理“未保存内容确认 -> 退出创建态 -> 重拉工作台数据”。
watch(cityValue, async (newValue, oldValue) => {
  if (isPointFormMode.value && !(await confirmDiscardPointEdit())) {
    cityValue.value = oldValue || cityOptions.value[0]?.value || DEFAULT_CITY_OPTIONS[0].value;
@@ -3637,11 +3814,18 @@
  }
);
// 城市变化后同步调整地图视口,但不参与数据拉取。
watch(cityValue, () => {
  if (!map.value) return;
  applyCityMapViewport();
});
// tab 切换入口。
// 使用场景:用户点击顶部 tab 时触发。
// 主要作用:
// - 先确认当前 tab 是否存在未保存内容;
// - 再清理不兼容的创建/编辑态;
// - 最后切换 activeTab 并刷新对应数据或视图状态。
async function handleTabChange(tab: WorkbenchTab) {
  if (
    activeTab.value === "point" &&
@@ -3809,6 +3993,8 @@
  });
}
// 关闭点位右侧面板。
// 如果当前仍处于点位编辑/创建流程,会先确认是否丢弃未保存内容。
async function handleClosePointPanel() {
  if (isPointFormMode.value && !(await confirmDiscardPointEdit())) return;
  const workflowState = buildPointClosePanelState({
@@ -4069,6 +4255,7 @@
}
// 统一把点位详情数据转换成右侧表单结构,避免详情态/编辑态字段口径不一致。
// 同时会同步上传图片列表、地址输入框与候选地址状态。
function resetPointForm(detail?: Entity.Point | null) {
  const workflowState = buildPointWorkflowFormState({
    detail,
@@ -4146,6 +4333,14 @@
  cleanupPointEditMapState();
}
// 进入点位新建流程。
// 使用场景:
// 1. 用户主动点击新建点位;
// 2. 通过高德搜索结果“打点”进入并带入预填信息。
// 主要作用:
// - 处理与其他实体编辑态的互斥关系;
// - 初始化点位表单、创建状态与地图选点模式;
// - 可选地带入名称、地址、经纬度预填值。
async function startPointCreate(prefill?: {
  name?: string;
  address?: string;
@@ -4818,6 +5013,7 @@
}
// ===== 围栏详情态与编辑态切换 =====
// 使用围栏详情数据重置围栏表单、策略组与禁行区草稿数据。
function resetFenceForm(detail?: any) {
  const workflowState = buildFenceWorkflowFormState(detail);
  fenceForm.value = workflowState.form;
@@ -4841,6 +5037,12 @@
  renderMapEntities();
}
// 进入围栏新建流程。
// 使用场景:点击“新建围栏”或工作台入口恢复到围栏创建态。
// 主要作用:
// - 清理其他实体创建/编辑残留;
// - 初始化围栏主运营区、策略组和禁行区草稿状态;
// - 打开地图绘制工具,开始绘制围栏主运营区。
async function startFenceCreate() {
  if (
    activeTab.value === "point" &&
@@ -4948,6 +5150,11 @@
  return true;
}
// 拉取围栏详情并切回详情态。
// 主要作用:
// - 关闭围栏编辑/创建状态;
// - 请求围栏详情接口;
// - 用详情数据重置右侧围栏表单。
async function loadFenceDetailData(id: string) {
  fenceDetailLoading.value = true;
  fenceEditMode.value = false;
@@ -4969,6 +5176,8 @@
  }
}
// 拉取场站详情并切回详情态。
// 成功后会同步刷新地图渲染与聚焦,确保场站 polygon 与右侧面板一致。
async function loadStationDetailData(id: string) {
  stationEditMode.value = false;
  stationCreateMode.value = false;
@@ -4993,6 +5202,8 @@
  return null;
}
// 拉取点位详情并切回详情态。
// 若详情接口失败,会回退到当前选中项原始数据兜底,避免右侧面板空白。
async function loadPointDetailData(id: string) {
  pointDetailLoading.value = true;
  pointEditMode.value = false;
@@ -5023,6 +5234,7 @@
}
// 进入点位编辑时先重新渲染地图,再创建临时点位,避免和原始 marker 重叠。
// 进入后右侧表单、临时 marker、地址候选与上传图片状态会一并切到编辑态。
async function handleEnterPointEdit() {
  if (activeTab.value !== "point") return;
  const targetId = String(selectedId.value || selectedItem.value?.id || "");
@@ -5088,6 +5300,8 @@
  await startStationCreate();
}
// 取消点位编辑。
// 编辑已有点位时回退到详情态;新建点位时退出创建并清理临时点位状态。
async function handleCancelPointEdit() {
  if (pointCreateMode.value && !(await confirmDiscardPointEdit())) return;
  const workflowState = buildPointCancelEditState({
@@ -5348,6 +5562,8 @@
  }
}
// 取消围栏编辑。
// 编辑已有围栏时回退到详情态;新建围栏时退出创建流程并清理草稿区。
async function handleCancelFenceEdit() {
  if (!(await confirmDiscardFenceEdit())) return;
  if (fenceCreateMode.value) {
@@ -5363,6 +5579,8 @@
  renderMapEntities();
}
// 关闭围栏右侧面板。
// 若仍处于围栏编辑态,则先走取消编辑逻辑,保证地图草稿和右侧状态一起收口。
async function handleCloseFencePanel() {
  if (isFenceFormMode.value) {
    await handleCancelFenceEdit();
@@ -5442,6 +5660,12 @@
  return result.valid;
}
// 点位保存入口。
// 主要作用:
// - 校验基础字段、坐标、状态、运营时间和图片上传状态;
// - 解析行政区、所属运营区与所属场站;
// - 组装新增/编辑 payload 并提交;
// - 保存成功后刷新点位资源、详情态与地图展示。
async function handleSavePointEdit() {
  if (!isPointFormMode.value) return false;
  if (!pointForm.value.pointName.trim()) {
@@ -5596,6 +5820,15 @@
  }
}
// 围栏保存入口。
// 使用场景:
// 1. saveType=0 保存草稿;
// 2. saveType=1 发布执行。
// 主要作用:
// - 校验围栏主运营区、禁行区、生命周期和运营策略;
// - 编辑发布时确认启停变更与运营策略缩短影响;
// - 计算围栏变更对点位关系的影响并提示确认;
// - 提交新增/编辑接口,成功后刷新围栏资源与地图展示。
async function handleSaveFenceEdit(saveType = 1) {
  if (!fenceCreateMode.value && !fenceDetailData.value) return;
  if (!isLockedFenceStatusEdit.value) {
@@ -5843,10 +6076,20 @@
  });
}
// 清空点位关键词搜索框,并回退到普通点位列表模式。
function clearPointKeyword() {
  keyword.value = "";
}
// 重新拉取地图页点位主数据。
// 使用场景:
// 1. 页面初始化;
// 2. 城市切换;
// 3. 点位新增/编辑/删除后;
// 4. 场站停用并联动停用点位后,刷新地图点位状态。
// 主要作用:
// - 更新左侧点位列表基准数据 pointData;
// - 同步更新空间计算使用的数据 pointSpatialData。
async function loadPointData() {
  try {
    const pointResult = await fetchPointList({
@@ -5869,10 +6112,14 @@
  }
}
// 点位资源刷新入口。
// 当前先只收口点位主数据加载,后续如点位相关资源扩展,可继续在这里统一串联。
async function loadPointResources() {
  await loadPointData();
}
// 执行点位关键词搜索。
// 同时请求业务点位接口和高德联想结果,最后合并为左侧点位搜索列表。
async function loadPointKeywordSearch() {
  const keywordText = keyword.value.trim();
  if (!keywordText) {
@@ -5927,6 +6174,8 @@
  }
}
// 拉取围栏列表与围栏区域明细。
// 前者用于左侧列表与实体状态展示,后者用于地图绘制运营区/禁行区 polygon。
async function loadFenceData() {
  try {
    const [fenceRes, areaRes] = await Promise.all([
@@ -5958,6 +6207,8 @@
  }
}
// 拉取当前城市下的场站列表,并适配为地图与列表共用的 ListItem。
// 这里主要加载启用中的场站,用于地图上的正式态展示。
async function loadStationData() {
  try {
    const res = await fetchEnabledStationList({
@@ -5995,6 +6246,15 @@
  }
}
// 工作台总加载入口。
// 使用场景:
// 1. 页面初始化;
// 2. 城市切换;
// 3. 需要整体重置三类实体数据时。
// 主要作用:
// - 并行拉取点位、围栏、场站三类资源;
// - 恢复入口 query 指定的详情/编辑展示;
// - 数据就绪后统一触发地图重绘与自动聚焦。
async function loadWorkbenchData() {
  workbenchDataReady.value = false;
  await Promise.all([loadPointResources(), loadFenceData(), loadStationData()]);
@@ -6042,6 +6302,13 @@
  map.value.zoomOut();
}
// 初始化高德地图实例,并绑定页面级地图事件。
// 使用场景:页面首次挂载时调用一次。
// 主要作用:
// - 加载 AMap SDK;
// - 创建 map / geocoder 实例;
// - 绑定点击、缩放等事件;
// - 完成首次地图实体渲染和聚焦。
async function initMap() {
  try {
    if (!domRef.value) return;
@@ -6089,6 +6356,8 @@
  }
}
// 清空当前地图上由工作台托管的所有实体覆盖物。
// 每次 renderMapEntities 前都会先执行,确保不会出现重复 marker / polygon。
function clearMapEntities() {
  if (!map.value) return;
  if (pointMarkers.value.length) map.value.remove(pointMarkers.value);
@@ -6105,6 +6374,15 @@
  return MARK_ICON_RED;
}
// 地图主渲染入口。
// 使用场景:
// - 初始化完成后首次绘制;
// - tab、筛选、城市、编辑态、数据源变化后重绘;
// - 保存/取消/关闭面板后同步刷新地图覆盖物。
// 主要作用:
// - 根据当前图层开关、tab、编辑态决定绘制哪些围栏/场站/点位;
// - 跳过当前正在编辑的草稿实体,避免与正式态覆盖物重叠;
// - 为 marker / polygon 绑定点击和高亮所需的 extData 与事件。
function renderMapEntities() {
  if (!map.value) return;
  clearMapEntities();
@@ -6395,6 +6673,12 @@
  return true;
}
// 将地图视角聚焦到当前编辑实体或选中实体。
// 优先级:
// 1. 围栏/场站编辑草稿;
// 2. 场站内选中的点位;
// 3. 当前列表选中实体。
// fitView=true 时会尽量完整展示实体范围,false 时更偏向保留当前视角。
function focusSelectedEntity(fitView = true) {
  if (!map.value) return;
src/views/businessOperations/operationWorkbench/status.ts
@@ -1,3 +1,5 @@
// 根据点位的运营状态和测试状态,统一推导工作台展示文案。
// 这是点位列表、地图 marker、场站内点位状态展示的基础口径。
export function getPointStatusText(
  operationStatus?: number | string,
  verificationStatus?: number | string,
@@ -24,6 +26,8 @@
  value: number;
}
// 点位状态统计所需的最小数据结构。
// 既支持直接传 status 文案,也支持传 operationStatus + verificationStatus 组合。
export interface PointStatusMetricSource {
  status?: string;
  operationStatus?: number | string;
@@ -46,6 +50,7 @@
  "失效",
] as const;
// 归一化点位状态文案,避免统计卡片因为同义状态拆散计数。
function normalizePointMetricStatus(status?: string) {
  const rawStatus = String(status || "").trim();
  if (!rawStatus) return "待测试";
@@ -55,6 +60,8 @@
  return rawStatus;
}
// 汇总点位状态统计,输出“可运营/不可运营”两组指标和总量。
// 主要用于地图页顶部或侧边的状态统计展示。
export function buildPointStatusMetricSummary(
  pointList: PointStatusMetricSource[]
): PointStatusMetricSummary {
@@ -95,6 +102,8 @@
  value: number;
}
// 场站状态统计所需的最小数据结构。
// 支持直接传 status 文案,也支持 areaStatus 作为兜底。
export interface StationStatusMetricSource {
  status?: string;
  areaStatus?: number | string;
@@ -110,6 +119,7 @@
const AVAILABLE_STATION_STATUS_LABELS = ["启用中", "待生效"] as const;
const UNAVAILABLE_STATION_STATUS_LABELS = ["已停用", "已失效"] as const;
// 归一化场站状态文案,兼容“运营中/待启用/失效”等历史文案。
function normalizeStationMetricStatus(
  status?: string,
  areaStatus?: number | string
@@ -122,6 +132,8 @@
  return rawStatus;
}
// 汇总场站状态统计,输出启用侧和停用侧两组指标。
// 常用于工作台左侧或概览区域的状态汇总展示。
export function buildStationStatusMetricSummary(
  stationList: StationStatusMetricSource[]
): StationStatusMetricSummary {
@@ -154,6 +166,8 @@
  };
}
// 将状态文案映射为 UI 样式 class。
// 供列表、卡片、面板中的状态标签统一复用同一套颜色口径。
export function getStatusClass(status?: string) {
  if (["待测试"].includes(String(status || ""))) return "wait-test";
  if (["测试中"].includes(String(status || ""))) return "testing";
src/views/businessOperations/operationWorkbench/workbench.adapters.ts
@@ -14,6 +14,7 @@
// 统一收口工作台里的数据适配、展示文案拼装和本地结构转换逻辑。
// 复制时间段数组并补齐默认字段,避免直接修改原始引用。
// 使用场景:场站/点位表单回显、详情转编辑态、取消编辑回滚。
export function clonePointTimeRanges(list?: PointTimeRangeItem[]) {
  return Array.isArray(list)
    ? list.map((item, index) => ({
@@ -25,6 +26,7 @@
}
// 将工作台列表项转换为场站内部使用的点位结构。
// 使用场景:场站编辑时把地图或左侧列表中的点位加入场站 pointList。
export function mapListItemToStationPoint(item: ListItem): StationPointItem {
  return {
    pointId: String(item.id),
@@ -47,6 +49,7 @@
}
// 将点位接口数据转换为左侧列表和地图渲染共用的结构。
// 这样点位列表、地图 marker、右侧兜底详情都能复用同一份 ListItem。
export function mapPointApiItemToListItem(point: any): ListItem {
  return {
    id: String(point.id),
@@ -65,6 +68,7 @@
}
// 生成场站列表摘要文案。
// 主要用于前端本地构造的场站列表项描述,例如新建/编辑中的临时场站。
export function formatStationListDesc(pointList: StationPointItem[]) {
  // 统计场站内处于运营态的点位数量。
  const runningCount = pointList.filter((item) =>
@@ -74,6 +78,7 @@
}
// 从多个可能的字段名中提取第一个有效数值指标。
// 用于兼容后端在不同接口或历史版本中的统计字段差异。
export function getListMetricValue(source: any, keys: string[]) {
  for (const key of keys) {
    // 当前字段尝试转为数字后的结果。
@@ -84,6 +89,7 @@
}
// 基于后端统计字段生成场站列表摘要文案。
// 与 formatStationListDesc 的区别是:这里优先消费接口直接返回的聚合指标。
export function formatStationListDescByMetrics(item: any) {
  // 场站内点位总数。
  const pointCount = getListMetricValue(item, ["pointCount", "pointNum"]);
@@ -97,6 +103,7 @@
}
// 统一解析场站状态文案,兼容数值状态和后端直接返回的名称。
// 供场站列表展示、详情归一化和地图状态展示共用。
export function getStationStatusText(item: any) {
  if (item?.statusName) return String(item.statusName);
  if (Number(item?.status) === 0) return "草稿";
@@ -108,6 +115,7 @@
}
// 解析场站自定义时段,兼容数组、对象、JSON 字符串和旧格式字符串。
// 返回值始终为右侧时间段编辑组件可直接消费的结构。
export function parseStationEffectiveTimePeriod(raw?: unknown) {
  if (!raw) return clonePointTimeRanges();
@@ -173,6 +181,10 @@
}
// 将场站接口数据转换为工作台列表结构,同时补齐右侧详情需要的 raw 字段。
// 主要作用:
// - 把接口中的场站主数据、点位列表和 polygonPath 归一化;
// - 生成地图定位所需的中心点;
// - 为 map.vue 的详情态/编辑态提供统一 raw 数据源。
export function mapStationApiItemToListItem(item: any): ListItem {
  // 场站围栏点串解析后的多边形路径。
  const polygonPath = parseFencePoints(item.fencePoints);
@@ -253,11 +265,15 @@
}
// 将场站详情归一化为页面编辑态直接可消费的 raw 结构。
// 本质上是复用 mapStationApiItemToListItem 的 raw,避免重复维护详情字段转换逻辑。
export function normalizeStationDetailData(detail: any) {
  return mapStationApiItemToListItem(detail).raw;
}
// 根据场站点位分布推导一个可展示的多边形区域。
// 使用场景:
// - 场站缺少显式 polygonPath 时的展示兜底;
// - 基于内部点位快速推导可视化区域。
export function buildStationPolygonPath(
  seed: {
    lng: number;
@@ -306,6 +322,7 @@
}
// 将场站种子数据和点位列表组装为工作台展示用列表项。
// 主要用于前端本地临时构造场站数据,例如创建态或未落库前的地图展示。
export function buildStationListItem(
  seed: {
    id: string;
@@ -356,6 +373,7 @@
}
// 将围栏数值状态转换为工作台展示文案。
// 供围栏列表、地图渲染和右侧详情的状态展示复用。
export function getFenceStatusText(status: number) {
  if (Number(status) === 0) return "草稿";
  if (Number(status) === 1) return "待生效";
@@ -365,6 +383,7 @@
}
// 生成围栏列表的摘要文案,兼容后端多种统计字段名。
// 用于左侧围栏列表的 desc 展示。
export function formatFenceListDesc(item: any) {
  // 围栏下禁行区数量。
  const prohibitedCount = getListMetricValue(item, [
src/views/businessOperations/operationWorkbench/workbench.entry.ts
@@ -9,17 +9,20 @@
// 统一处理工作台页面的入口 query 解析、tab 校验和入口去重 key 生成。
// 将路由 query 中的值规范为单个字符串,兼容数组和空值场景。
// 供后续所有入口 query 解析函数统一复用,避免每处手写类型兼容。
export function getQueryStringValue(value: unknown) {
  if (Array.isArray(value)) return String(value[0] || "");
  return typeof value === "string" ? value : "";
}
// 判断字符串是否属于工作台支持的 tab。
// 用于对外部 query 里的 tab 参数做白名单校验。
export function isWorkbenchTab(value: string): value is WorkbenchTab {
  return tabOptions.some((item) => item.value === value);
}
// 判断当前 query 是否携带了工作台入口上下文。
// map.vue 会据此决定是否需要消费一次外部跳转传入的上下文。
export function hasWorkbenchEntryQuery(query: Record<string, unknown>) {
  return [
    query.tab,
@@ -34,6 +37,10 @@
}
// 将外部入口 query 解析为工作台内部统一使用的上下文结构。
// 主要作用:
// - 兼容新旧字段名,例如 cityCode/cityId、id/selectedId、type/mode;
// - 校验 tab 和城市是否合法;
// - 输出 map.vue 可直接消费的统一入口对象。
export function resolveWorkbenchEntryContext(
  query: Record<string, unknown>,
  options: OptionItem[] = DEFAULT_CITY_OPTIONS
@@ -68,6 +75,7 @@
}
// 生成入口 query 的唯一 key,用于避免重复消费同一批上下文参数。
// 使用场景:watch route.query 时判断当前这批入口参数是否已经处理过。
export function buildWorkbenchEntryQueryKey(query: Record<string, unknown>) {
  if (!hasWorkbenchEntryQuery(query)) return "";
src/views/businessOperations/operationWorkbench/workbench.fence-workflow.ts
@@ -10,6 +10,7 @@
// 统一收口围栏工作流里的默认表单、策略组构造和详情转表单逻辑。
// 围栏表单的默认初始值。
// 使用场景:新建围栏初始化、退出围栏流程、保存完成后的表单清空。
export function createEmptyFenceForm(): FenceFormState {
  return {
    id: "",
@@ -26,6 +27,7 @@
}
// 禁行区弹窗表单的默认初始值。
// 对应围栏右侧“禁行区编辑弹窗”的基础表单结构。
export function createEmptyRestrictedAreaForm(): RestrictedAreaFormState {
  return {
    id: "",
@@ -37,6 +39,9 @@
}
// 围栏策略字符串解析为页面表单结构。
// 主要作用:
// - 兼容接口返回的字符串 JSON 和对象数组两种结构;
// - 将接口里的 time/typeStr 转为前端可编辑的策略表单项。
export function parseFenceStrategies(raw: any): FenceStrategyFormItem[] {
  if (!raw) return [];
@@ -73,6 +78,7 @@
}
// 默认运营区策略组。
// 用于新建围栏时初始化主运营区策略配置。
export function createDefaultFenceStrategy(index = 1): FenceStrategyFormItem {
  return {
    id: Date.now() + index,
@@ -86,6 +92,7 @@
}
// 默认禁行区策略组。
// 禁行区允许空时段,因此默认 time 为空数组。
export function createDefaultRestrictedStrategy(index = 1): FenceStrategyFormItem {
  return {
    id: Date.now() + index,
@@ -99,12 +106,17 @@
}
// 禁行区策略组解析后,如果为空则补一个默认策略组。
// 这样禁行区弹窗始终至少有一组可编辑策略,避免空态下无法操作。
export function parseRestrictedAreaStrategies(raw: any): FenceStrategyFormItem[] {
  const parsed = parseFenceStrategies(raw);
  return parsed.length ? parsed : [createDefaultRestrictedStrategy(1)];
}
// 围栏详情转页面表单结构。
// 主要作用:
// - 从详情里抽取主运营区作为右侧围栏表单主体;
// - 解析生命周期和运营策略;
// - 在新建场景下补出空禁行区列表。
export function buildFenceWorkflowFormState(detail?: any) {
  // 只提取运营区作为右侧围栏表单主体。
  const operationArea = Array.isArray(detail?.electronicFenceAreas)
@@ -146,6 +158,7 @@
}
// 进入围栏新建流程时需要批量切换的页面状态。
// map.vue 会一次性消费这组状态,用于打开围栏面板、切换 tab 并初始化编辑态。
export function buildFenceCreateModeState() {
  // 新建围栏默认使用空表单和空禁行区列表。
  const workflowState = buildFenceWorkflowFormState();
@@ -167,6 +180,7 @@
}
// 退出围栏新建流程时需要恢复的页面状态。
// 会同步清理禁行区弹窗、草稿策略和右侧表单状态。
export function buildFenceExitCreateModeState() {
  // 恢复为空围栏表单与默认弹窗状态。
  return {
@@ -184,6 +198,7 @@
}
// 进入围栏编辑流程时需要的页面表单状态。
// 使用场景:已存在围栏从详情态切到编辑态。
export function buildFenceEditModeState(detail?: any) {
  // 详情先统一转换为围栏表单。
  const workflowState = buildFenceWorkflowFormState(detail);
@@ -197,6 +212,7 @@
}
// 取消围栏编辑后恢复为详情态的表单状态。
// 只恢复右侧表单相关状态;地图草稿区的回滚由 map.vue 负责。
export function buildFenceCancelEditState(detail?: any) {
  // 取消编辑后回到详情态表单。
  const workflowState = buildFenceWorkflowFormState(detail);
@@ -210,6 +226,7 @@
}
// 关闭围栏右侧面板时需要恢复的页面状态。
// 该函数只负责“面板显隐”这一层,避免和取消编辑逻辑耦合。
export function buildFenceClosePanelState() {
  return {
    fencePanelClosed: true,
src/views/businessOperations/operationWorkbench/workbench.form.ts
@@ -8,6 +8,7 @@
// 统一收口工作台里的表单归一化、时间段校验、图片解析和保存辅助逻辑。
// 规范点位上下车属性,未命中时统一回退为“上下车均可”。
// 使用场景:详情回显、表单初始化、保存前归一化。
export function normalizePointOnOff(
  value?: string | number | null
): "1" | "2" | "3" {
@@ -19,6 +20,7 @@
}
// 当点位进入运营相关状态时,自动兜底限停时长。
// 避免历史数据或新建表单在进入运营链路时遗漏该必填字段。
export function ensurePointParkingTimeLimitByStatus(
  currentValue: string,
  statusText?: string
@@ -33,6 +35,7 @@
}
// 将场站表单中的自定义时段格式化为接口需要的 JSON 字符串。
// 仅在 operationType 为自定义时间时返回有效值。
export function formatStationEffectiveTimePeriod(
  operationType: "1" | "2",
  list: PointTimeRangeItem[]
@@ -50,12 +53,14 @@
}
// 根据保存类型和区域状态推导场站状态码。
// 场站接口用 status 表示草稿/启用/停用,因此这里统一封装 map.vue 中的状态映射。
export function getStationStatusCode(saveType: 0 | 1, areaStatus?: number) {
  if (saveType === 0) return 0;
  return Number(areaStatus) === 2 ? 3 : 2;
}
// 过滤 payload 中的 undefined 字段,避免把无效值提交给接口。
// 常用于 payload 组装阶段,减少后端对空字段的兼容成本。
export function omitUndefinedFields<T extends Record<string, any>>(payload: T) {
  return Object.fromEntries(
    Object.entries(payload).filter(([, value]) => value !== undefined)
@@ -63,6 +68,7 @@
}
// 解析点位自定义时段,兼容 JSON 数组和旧版逗号分隔字符串。
// 返回值始终保持为页面时间段组件可直接消费的数组结构。
export function parsePointTimeRanges(raw?: string) {
  if (!raw) return [{ id: 1, startTime: null, endTime: null }];
@@ -92,6 +98,7 @@
}
// 将点位图片字段解析为前端上传列表结构。
// 主要作用:兼容后端历史多种图片字段格式,统一适配为上传组件的 fileList。
export function normalizePointPictures(
  raw?: string,
  defaultPointImage = ""
@@ -162,6 +169,7 @@
}
// 校验点位自定义时段,返回首个错误文案,空字符串表示通过。
// 校验内容包括:至少一段、边界完整、开始早于结束、时段之间不重叠。
export function getPointTimeRangeValidationMessage(
  list: PointTimeRangeItem[]
) {
@@ -201,6 +209,7 @@
}
// 校验围栏策略组,返回校验结果和首个全局错误信息。
// 该函数同时会把每个策略项的局部错误直接回写到 strategy.errorMsg,便于表单逐项提示。
export function validateFenceStrategies(
  strategyList: FenceStrategyFormItem[]
) {
src/views/businessOperations/operationWorkbench/workbench.geometry.ts
@@ -4,6 +4,7 @@
// 统一收口工作台地图相关的纯几何处理逻辑,避免这些工具函数散落在页面文件中。
// 复制并清洗多边形路径,确保经纬度都是有效数字。
// 使用场景:接口详情回灌、草稿区复制、payload 组装前的路径标准化。
export function clonePolygonPath(list?: LngLatTuple[]) {
  return Array.isArray(list)
    ? list
@@ -13,6 +14,7 @@
}
// 将多边形路径转换为后端围栏接口需要的 `lng_lat` 字符串格式。
// 围栏、禁行区、场站保存时都会复用这一口径。
export function stringifyFencePoints(list?: LngLatTuple[]) {
  return clonePolygonPath(list)
    .map((point) => `${point[0]}_${point[1]}`)
@@ -20,6 +22,7 @@
}
// 计算多边形的几何中心点,用于场站或区域的默认定位展示。
// 这是简单的顶点平均值算法,主要满足地图视角与默认中心点展示需求。
export function getPolygonCenter(path: LngLatTuple[]) {
  if (!path.length) return null;
@@ -40,6 +43,7 @@
}
// 将后端返回的围栏点串解析为前端可操作的经纬度数组。
// 使用场景:围栏/禁行区/场站详情进入地图草稿编辑态时的路径回灌。
export function parseFencePoints(raw?: string): LngLatTuple[] {
  if (!raw) return [];
@@ -53,12 +57,14 @@
}
// 调用高德几何工具计算围栏面积,结果统一换算为平方公里。
// 供围栏和场站保存、展示时统一复用。
export function calcFenceArea(points: LngLatTuple[]) {
  if (!points.length || typeof (window as any).AMap === "undefined") return 0;
  return Math.abs((window as any).AMap.GeometryUtil.ringArea(points)) / 1000000;
}
// 判断一个多边形是否完全包含在另一个多边形内。
// 常用于校验场站是否完全位于运营区内,或禁行区是否位于围栏主区域内。
export function isRingInRing(inner: LngLatTuple[], outer: LngLatTuple[]) {
  if (
    !inner.length ||
@@ -71,6 +77,7 @@
}
// 判断两条线段是否发生相交,用于多边形自相交检测。
// 这是几何校验的底层工具,不直接暴露给页面业务层使用。
function isSegmentsIntersect(
  p1: LngLatTuple,
  p2: LngLatTuple,
@@ -101,6 +108,7 @@
}
// 判断多边形是否存在边与边交叉的非法情况。
// 用于阻止用户绘制出自相交的非法围栏/场站区域。
export function isFencePolygonSelfIntersect(points: LngLatTuple[]) {
  if (points.length < 4) return false;
@@ -123,6 +131,7 @@
}
// 判断两个多边形是否存在相交、包含或被包含关系。
// 常用于校验禁行区与主区域、场站与禁行区之间是否发生几何冲突。
export function doesPolygonOverlap(source: LngLatTuple[], target: LngLatTuple[]) {
  if (
    source.length < 3 ||
src/views/businessOperations/operationWorkbench/workbench.payload.ts
@@ -20,6 +20,7 @@
// 统一收口工作台保存时的 payload 组装逻辑,避免 map.vue 内部堆积大量参数拼装代码。
// 点位图片上传列表转换为接口需要的字符串结构。
// 使用场景:点位新增/编辑保存前,将上传组件里的列表压缩为后端可接收的 JSON 字符串。
export function buildPointPicturesPayload(uploadedImages: UploadImageItem[]) {
  if (!uploadedImages.length) return undefined;
@@ -36,6 +37,7 @@
}
// 点位自定义时段转换为接口需要的字符串数组 JSON。
// 仅当点位运营策略为“自定义时间”时才会真正生效。
export function buildPointEffectiveTimePeriod(
  operationType: "1" | "2",
  customTimeRanges: PointTimeRangeItem[]
@@ -49,6 +51,7 @@
}
// 组装点位保存的基础 payload,创建和编辑都复用这一段。
// 主要作用:把 map.vue 中零散的表单值、归属信息、图片、运营策略统一压缩为接口结构。
export function buildPointSavePayload(args: {
  administrativeInfo: Record<string, any>;
  pointForm: PointFormState;
@@ -114,6 +117,7 @@
}
// 围栏运营策略转换为接口和确认逻辑共用的数据结构。
// 既用于最终保存 payload,也用于发布前的影响确认逻辑。
export function buildFenceOperationStrategyPayload(
  strategyList: FenceStrategyFormItem[]
) {
@@ -126,6 +130,7 @@
}
// 组装围栏主运营区 payload。
// 使用场景:围栏保存时,把右侧表单和地图草稿区的主运营区合并成接口字段。
export function buildFenceOperationAreaPayload(args: {
  operationArea: any;
  fenceForm: FenceFormState;
@@ -145,6 +150,7 @@
}
// 组装禁行区 payload 列表。
// 其中临时禁行区 id 会被转成 null,表示本次是新增禁行区。
export function buildFenceRestrictedAreaPayloadList(
  restrictedAreas: FenceRestrictedAreaDraft[]
) {
@@ -163,6 +169,10 @@
}
// 计算围栏保存时的状态值,兼容创建、草稿、启停状态切换。
// 规则:
// - 新建围栏直接使用 saveType;
// - 已经进入运营链路的围栏,状态切换由 areaStatus 决定;
// - 草稿/待生效阶段仍沿用 saveType。
export function resolveFenceSaveStatus(args: {
  fenceCreateMode: boolean;
  saveType: number;
@@ -177,6 +187,10 @@
}
// 组装围栏保存 payload。
// 主要作用:
// - 统一拼装围栏主运营区、禁行区、生命周期和运营策略;
// - 在编辑发布场景下附带点位关系变更结果;
// - 隔离 map.vue 中对创建/编辑、草稿/发布的分支判断。
export function buildFenceSavePayload(args: {
  fenceCreateMode: boolean;
  fenceId?: string;
@@ -260,6 +274,7 @@
}
// 场站点位列表转换为接口 xpPointList 结构。
// stationSortNo 会按当前列表顺序自动重排,保证保存时顺序稳定。
export function buildStationPointPayload(pointList: StationPointItem[]) {
  return pointList.map((item, index) => ({
    id: item.pointId,
@@ -274,6 +289,10 @@
}
// 组装场站保存 payload。
// 主要作用:
// - 合并场站基础信息、区域几何、内部点位、运营时间;
// - 在编辑停用场景下透传 pointStop;
// - 统一创建/编辑两条保存链路的提交口径。
export function buildStationSavePayload(args: {
  isCreatingStation: boolean;
  detailId: string;
@@ -281,6 +300,7 @@
  centerLon?: number;
  saveType: 0 | 1;
  areaStatus: number;
  pointStop?: boolean;
  operationArea: any;
  polygonPath: LngLatTuple[];
  operationType: "1" | "2";
@@ -298,6 +318,7 @@
    centerLon,
    saveType,
    areaStatus,
    pointStop,
    operationArea,
    polygonPath,
    operationType,
@@ -316,6 +337,7 @@
  );
  // 场站保存状态码。
  const statusCode = getStationStatusCode(saveType, areaStatus);
  const shouldClearOperationArea = !isCreatingStation && !operationArea;
  return omitUndefinedFields({
    ...(isCreatingStation ? {} : { id: Number(detailId) }),
@@ -326,15 +348,20 @@
    electronicAreaId:
      operationArea?.id !== undefined && operationArea?.id !== null
        ? Number(operationArea.id)
        : shouldClearOperationArea
        ? null
        : undefined,
    electronicId:
      operationArea?.electronicId !== undefined &&
      operationArea?.electronicId !== null
        ? Number(operationArea.electronicId)
        : shouldClearOperationArea
        ? null
        : undefined,
    fencePoints: stringifyFencePoints(polygonPath) || undefined,
    operationType: Number(operationType || 1),
    parkingTimeLimit: Number(parkingTimeLimit || 0),
    pointStop,
    pointCount: pointList.length,
    remark: description.trim() || undefined,
    stationArea,
src/views/businessOperations/operationWorkbench/workbench.point-workflow.ts
@@ -10,6 +10,11 @@
  parsePointTimeRanges,
} from "./workbench.form";
// 从点位详情中提取右侧表单需要的运营策略字段。
// 使用场景:
// 1. 点位详情进入详情态;
// 2. 点位详情进入编辑态;
// 3. 点位存在所属场站时,优先继承场站运营策略字段。
function resolvePointOperationStrategy(detail?: any | null) {
  const inheritFromStation = !!detail?.stationId;
  const operationType = inheritFromStation
@@ -36,6 +41,7 @@
// 统一收口点位工作流里的表单初始化、详情转表单和新建预填逻辑。
// 点位创建流程默认的运营状态。
// 当前工作台约定:新建点位默认进入“待测试 + 待运营”的初始组合。
export function createDefaultPointCreateStatus() {
  return {
    verificationStatus: 1,
@@ -44,6 +50,7 @@
}
// 点位表单的默认初始值。
// 使用场景:页面首次初始化、退出点位流程、保存成功后的表单清空。
export function createEmptyPointForm(): PointFormState {
  return {
    id: "",
@@ -63,6 +70,7 @@
}
// 点位详情转表单时使用的地址选项结构。
// 主要作用:把详情里的当前地址转换成地址下拉组件可直接消费的 option。
export function buildPointAddressOptions(detail?: any | null): AddressOptionItem[] {
  if (!detail?.pointAddress) return [];
@@ -81,6 +89,10 @@
}
// 将点位详情归一化为页面右侧表单所需结构。
// 主要作用:
// - 统一详情态和编辑态的字段口径;
// - 同步产出表单、地址输入框、地址候选项、图片上传列表;
// - 屏蔽接口字段差异,避免 map.vue 内重复做兼容判断。
export function buildPointWorkflowFormState(args: {
  detail?: any | null;
  fallbackPointPictures?: unknown;
@@ -131,6 +143,7 @@
}
// 点位新建流程的表单和地址预填状态。
// 使用场景:纯手动新建点位,或从地图搜索结果带经纬度/地址预填进入。
export function buildPointCreateWorkflowState(
  prefill?: Partial<
    Pick<
@@ -176,6 +189,7 @@
}
// 进入点位新建流程时需要批量切换的页面状态。
// 主要作用:集中生成 map.vue 进入点位创建态时需要覆盖的一组状态片段。
export function buildPointCreateModeState(
  prefill?: Partial<
    Pick<
@@ -201,6 +215,7 @@
}
// 退出点位新建流程后需要恢复的页面状态。
// 使用场景:取消新建、关闭右侧面板、保存成功后收口创建态。
export function buildPointExitCreateModeState() {
  return {
    pointCreateMode: false,
@@ -213,6 +228,7 @@
}
// 进入点位编辑流程时需要的表单和位置快照。
// 位置快照用于后续判断地址/坐标是否被修改,以及取消编辑时的回滚参考。
export function buildPointEditModeState(args: {
  detail?: any | null;
  fallbackPointPictures?: unknown;
@@ -235,6 +251,7 @@
}
// 退出点位编辑流程时需要恢复的表单与状态。
// 使用场景:取消编辑、关闭面板、重新回到详情态时统一复用。
export function buildPointExitEditModeState(args: {
  detail?: any | null;
  fallbackPointPictures?: unknown;
@@ -251,6 +268,7 @@
}
// 关闭点位右侧面板时需要恢复的页面状态。
// 关闭面板后会清空当前选中点位,避免左侧列表和地图残留“面板已关闭但仍高亮详情”的状态。
export function buildPointClosePanelState(args: {
  isCreateMode: boolean;
  detail?: any | null;
@@ -277,6 +295,9 @@
}
// 点位取消编辑时需要恢复的页面状态。
// 规则:
// - 新建点位取消时,直接退出创建流程并关闭面板;
// - 编辑已有点位取消时,回退为详情态表单。
export function buildPointCancelEditState(args: {
  isCreateMode: boolean;
  detail?: any | null;
src/views/businessOperations/operationWorkbench/workbench.station-workflow.ts
@@ -8,6 +8,7 @@
// 统一收口场站工作流里的默认表单和详情转表单逻辑。
// 场站表单的默认初始值。
// 使用场景:新建场站初始化、关闭场站面板、保存成功后的状态清空。
export function createEmptyStationForm(): StationFormState {
  return {
    id: "",
@@ -26,6 +27,10 @@
}
// 场站详情转页面编辑表单结构。
// 主要作用:
// - 统一详情态和编辑态的字段口径;
// - 兼容 areaStatus/status/statusText 的多种来源;
// - 同步归一化场站内点位列表与 polygonPath。
export function buildStationWorkflowFormState(detail?: any | null) {
  const normalizedAreaStatus
    = Number(detail?.areaStatus) === 2
@@ -92,6 +97,7 @@
}
// 进入场站新建流程时需要批量切换的页面状态。
// 这里通过构造一个临时 detail,再复用详情转表单逻辑,避免新建/编辑维护两套字段映射。
export function buildStationCreateModeState() {
  // 临时详情数据用于兼容当前页面的新建态初始化流程。
  const tempDetail = {
@@ -122,6 +128,7 @@
}
// 退出场站新建流程时需要恢复的页面状态。
// 使用场景:新建取消、关闭面板、保存成功后退出场站创建态。
export function buildStationExitCreateModeState() {
  return {
    stationCreateMode: false,
@@ -135,6 +142,7 @@
}
// 进入场站编辑流程时需要的页面表单状态。
// 使用场景:已存在场站从详情态切换到编辑态。
export function buildStationEditModeState(detail?: any | null) {
  // 详情先统一转换为场站表单。
  const workflowState = buildStationWorkflowFormState(detail);
@@ -148,6 +156,7 @@
}
// 场站保存成功后关闭右侧详情面板时需要恢复的状态。
// nextSelectedId 用于把新建/编辑后的目标实体重新写回选中态,便于地图高亮与后续定位。
export function buildStationCloseAfterSaveState(nextSelectedId = "") {
  return {
    stationDetailData: null,
@@ -163,6 +172,7 @@
}
// 关闭场站右侧面板时需要恢复的页面状态。
// 与保存成功后的关闭不同,这里会直接清空 selectedId。
export function buildStationClosePanelState() {
  return {
    stationPanelClosed: true,
@@ -178,6 +188,9 @@
}
// 场站取消编辑时需要恢复的页面状态。
// 规则:
// - 新建场站取消时,退出创建并关闭面板;
// - 编辑已有场站取消时,回退到详情态表单。
export function buildStationCancelEditState(args: {
  isCreateMode: boolean;
  detail?: any | null;
src/views/operations/cruiseCheckInDetails/index.vue
@@ -79,7 +79,7 @@
  },
  {
    title: '打卡点位',
    key: 'ruleName',
    key: 'locationAddress',
    width: 240,
    resizable: true,
  },
src/views/operations/cruiseCheckInRule/index.vue
@@ -66,23 +66,23 @@
    resizable: true,
  },
  {
    title: '执行人员',
    key: 'userName',
    title: '执行人数',
    key: 'userNumber',
    width: 150,
    resizable: true,
  },
  {
    title: '手机号',
    key: 'phone',
    width: 150,
    resizable: true,
  },
  {
    title: '人员角色',
    key: 'roleName',
    width: 150,
    resizable: true,
  },
  // {
  //   title: '手机号',
  //   key: 'phone',
  //   width: 150,
  //   resizable: true,
  // },
  // {
  //   title: '人员角色',
  //   key: 'roleName',
  //   width: 150,
  //   resizable: true,
  // },
  {
    title: '操作',
    key: 'actions',
src/views/operations/taskOrder/taskOrderCenter/components/AddFormModal.vue
@@ -292,6 +292,7 @@
async function handleConfirm() {
  try {
    console.log(formModel.value)
    await formRef.value?.validate()
    emit('confirm', formModel.value)
    show.value = false
src/views/operations/taskOrder/taskOrderCenter/index.vue
@@ -380,7 +380,7 @@
  })
  const params = {
    ...data,
    taskTypeId: addParentIds(taskErrorIdOptions.value, data.taskTypeId).join(','),
    taskTypeId: addParentIds(taskErrorIdOptions.value, [data.taskTypeId]).join(','),
    taskImage: JSON.stringify(taskImage),
  }
  createTaskAccident(params).then((res) => {
src/views/system/accountPermission/accountManagement/components/AccountModal.vue
@@ -130,6 +130,7 @@
  resetForm()
  if ((props.mode === 'edit' || props.mode === 'view') && props.rowData) {
    Object.assign(formModel.value, props.rowData)
    formModel.value.phoneNumber = props.rowData.phoneNumber_ || formModel.value.phoneNumber
  }
}
src/views/userContent/active/activityManagement/components/ActivityOrderList.vue
@@ -354,17 +354,88 @@
])
const columns = computed<DataTableColumns<any>>(() => [
  { type: 'selection', width: 50, resizable: true },
  { title: '订单编号', key: 'orderNo', width: 100, resizable: true },
  { title: '下单时间', key: 'orderTime', width: 150, resizable: true },
  { title: '订单来源', key: 'source', width: 120, resizable: true },
  { title: '乘车时间', key: 'rideTime', width: 150, resizable: true },
  { title: '下单用户昵称', key: 'userName', width: 120, resizable: true },
  { title: '下单用户手机', key: 'userPhone', width: 120, resizable: true },
  { title: '起点', key: 'startPoint', width: 150, resizable: true, ellipsis: true },
  { title: '终点', key: 'endPoint', width: 150, resizable: true, ellipsis: true },
  { title: '订单金额', key: 'orderAmount', width: 100, resizable: true },
  { title: '优惠金额', key: 'discountAmount', width: 100, resizable: true },
  {
    type: 'selection',
    width: 40,
    fixed: 'left',
  },
  {
    title: '订单编号',
    key: 'orderNumber',
    width: 150,
    resizable: true,
    fixed: 'left',
  },
  {
    title: '下单时间',
    key: 'createTime',
    width: 160,
    resizable: true,
    render: row => <span>{row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-'}</span>,
  },
  {
    title: '订单来源',
    key: 'orderSource',
    width: 100,
    resizable: true,
    render(row) {
      return row.orderSource === 1 ? '小鹏小程序' : '-'
    },
  },
  {
    title: '乘车时间',
    key: 'rideTime',
    width: 160,
    resizable: true,
  },
  {
    title: '下单用户昵称',
    key: 'nickName',
    width: 120,
    resizable: true,
  },
  {
    title: '下单用户手机',
    key: 'phoneNumber',
    width: 120,
    resizable: true,
  },
  {
    title: '起点',
    key: 'startPointName',
    width: 160,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
  },
  {
    title: '终点',
    key: 'endPointName',
    width: 160,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
  },
  {
    title: '订单金额',
    key: 'orderMoney',
    width: 120,
    resizable: true,
    render(row) {
      return row.orderMoney ? `¥${row.orderMoney.toFixed(2)}` : '-'
    },
  },
  {
    title: '优惠金额',
    key: 'discountAmount',
    width: 120,
    resizable: true,
    render(row) {
      return row.discountAmount ? `¥${row.discountAmount.toFixed(2)}` : '-'
    },
  },
  {
    title: '操作',
    key: 'actions',
@@ -528,7 +599,7 @@
          :data="list"
          :loading="loading"
          :bordered="false"
          :scroll-x="1400"
          :scroll-x="1500"
          :row-key="(row) => row.id"
          flex-height
          @update:checked-row-keys="handleCheck"
src/views/userContent/active/coupon/index.vue
@@ -196,7 +196,7 @@
      try {
        const { code } = await sendCoupon({
          couponId: currentCouponId.value!,
          phone: [],
          ids: [],
        })
        if (code === 200) {
          message.success(`成功向${payload.userCount}个用户发放优惠券`)
@@ -227,7 +227,7 @@
      try {
        const { code } = await sendCoupon({
          couponId: currentCouponId.value!,
          phone: rows.map(r => r.account), // Assuming 'account' is the phone number field
          ids: rows.map(r => r.id), // Assuming 'account' is the phone number field
        })
        if (code === 200) {
          message.success(`成功向${rows.length}个用户发放优惠券`)