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}个用户发放优惠券`)