| src/components/operationWorkbench/PointNameConflictDialog.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/service/api/point.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/service/api/testOperationWorkbench.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/businessOperations/operationWorkbench/map.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/testDemo/operationWorkbench/components/PointPanel.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/testDemo/operationWorkbench/map.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/components/operationWorkbench/PointNameConflictDialog.vue
New file @@ -0,0 +1,257 @@ <script setup lang="ts"> import { ref, watch } from "vue"; type PointNameConflictAction = | "rename-current-point" | "rename-legacy-point" | "disable-legacy-point"; type PointNameConflictDialogResult = { resolveType: 1 | 2 | 3; newPointName?: string; }; type DuplicateNameCheckResult = { id?: string; pointName?: string; pointNumber?: string; } | null; const props = defineProps<{ validateNameConflict?: ( name: string ) => Promise<DuplicateNameCheckResult> | DuplicateNameCheckResult; }>(); const visible = ref(false); const loading = ref(false); const duplicateName = ref(""); const selectedAction = ref<PointNameConflictAction>("rename-current-point"); const currentPointName = ref(""); const legacyPointName = ref(""); const currentPointNameError = ref(""); const legacyPointNameError = ref(""); let dialogSettled = false; let dialogResolver: | ((value: PointNameConflictDialogResult | null) => void) | null = null; function resetDialogState() { loading.value = false; duplicateName.value = ""; selectedAction.value = "rename-current-point"; currentPointName.value = ""; legacyPointName.value = ""; currentPointNameError.value = ""; legacyPointNameError.value = ""; } function finalizeDialog(result: PointNameConflictDialogResult | null) { if (dialogSettled) return; dialogSettled = true; visible.value = false; loading.value = false; const resolver = dialogResolver; dialogResolver = null; resetDialogState(); resolver?.(result); } function handleCancel() { finalizeDialog(null); } function handleShowChange(show: boolean) { visible.value = show; if (!show) handleCancel(); } function clearFieldError(field: "current" | "legacy") { if (field === "current") currentPointNameError.value = ""; else legacyPointNameError.value = ""; } function setFieldError(field: "current" | "legacy", message: string) { if (field === "current") currentPointNameError.value = message; else legacyPointNameError.value = message; } async function validateInputNameConflict( field: "current" | "legacy", options?: { silent?: boolean } ) { if (!props.validateNameConflict) return false; const fieldName = field === "current" ? currentPointName.value : legacyPointName.value; const normalizedName = String(fieldName || "").trim(); if (!normalizedName) { clearFieldError(field); return false; } const duplicatePoint = await props.validateNameConflict(normalizedName); if (!duplicatePoint) { clearFieldError(field); return false; } setFieldError( field, field === "current" ? "新的点位名称仍与现有点位重复" : "旧点位的新名称仍与现有点位重复" ); return true; } async function handleInputBlur(field: "current" | "legacy") { await validateInputNameConflict(field); } async function buildConfirmResult(): | PointNameConflictDialogResult | null { if (selectedAction.value === "rename-current-point") { const nextName = String(currentPointName.value || "").trim(); if (!nextName) { setFieldError("current", "请输入新的点位名称"); return null; } clearFieldError("current"); if (await validateInputNameConflict("current", { silent: true })) return null; return { resolveType: 1, newPointName: nextName, }; } if (selectedAction.value === "rename-legacy-point") { const nextName = String(legacyPointName.value || "").trim(); if (!nextName) { setFieldError("legacy", "请输入旧点位的新名称"); return null; } clearFieldError("legacy"); if (await validateInputNameConflict("legacy", { silent: true })) return null; return { resolveType: 2, newPointName: nextName, }; } return { resolveType: 3, }; } async function handleConfirm() { const result = await buildConfirmResult(); if (!result) return; finalizeDialog(result); } watch(currentPointName, () => { clearFieldError("current"); }); watch(legacyPointName, () => { clearFieldError("legacy"); }); watch(selectedAction, (action) => { currentPointNameError.value = ""; legacyPointNameError.value = ""; if (action === "rename-current-point") currentPointName.value = ""; if (action === "rename-legacy-point") legacyPointName.value = ""; }); function open(options?: { duplicateName?: string }) { dialogSettled = false; duplicateName.value = String(options?.duplicateName || "").trim(); selectedAction.value = "rename-current-point"; currentPointName.value = ""; legacyPointName.value = ""; visible.value = true; return new Promise<PointNameConflictDialogResult | null>((resolve) => { dialogResolver = resolve; }); } defineExpose({ open, }); </script> <template> <n-modal v-model:show="visible" preset="card" title="提示" class="w-400px" :mask-closable="false" :closable="true" @update:show="handleShowChange" > <div> <div class="mb-16px text-14px text-[#171F26]"> 点位名称【{{ duplicateName }}】重复,请确认处理方式 </div> <n-radio-group v-model:value="selectedAction" name="point-name-conflict-action" class="w-full" > <n-space vertical :size="16" class="w-full"> <div class="w-full"> <n-radio value="rename-current-point">修改新点位名称</n-radio> <n-form-item class="mt-8px" :show-label="false" :show-feedback="!!currentPointNameError" :validation-status="currentPointNameError ? 'error' : undefined" :feedback="currentPointNameError" > <n-input v-model:value="currentPointName" placeholder="请输入" :disabled="selectedAction !== 'rename-current-point'" @blur="handleInputBlur('current')" /> </n-form-item> </div> <div class="w-full"> <n-radio value="rename-legacy-point">更改旧点位名称</n-radio> <n-form-item class="mt-8px" :show-label="false" :show-feedback="!!legacyPointNameError" :validation-status="legacyPointNameError ? 'error' : undefined" :feedback="legacyPointNameError" > <n-input v-model:value="legacyPointName" placeholder="请输入" :disabled="selectedAction !== 'rename-legacy-point'" @blur="handleInputBlur('legacy')" /> </n-form-item> </div> <n-radio value="disable-legacy-point"> 停用旧点位(不影响正在进行中的订单) </n-radio> </n-space> </n-radio-group> </div> <template #action> <n-space justify="end"> <n-button @click="handleCancel">取消</n-button> <n-button type="primary" :loading="loading" @click="handleConfirm"> 确定 </n-button> </n-space> </template> </n-modal> </template> src/service/api/point.ts
@@ -7,6 +7,12 @@ return request.Post<Service.ResponseResult<Entity.Point[]>>('/map/api/v1/point/list', params) } export function checkPointNameDuplicate(pointName: string) { return request.Get<Service.ResponseResult<any>>('/map/api/v1/point/checkNameDuplicate', { params: { pointName }, }) } export function createPoint(data: Api.Point.CreateData) { return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/add', data) } src/service/api/testOperationWorkbench.ts
@@ -42,31 +42,37 @@ /** 点位相关 */ export function fetchPointPageList(params: any) { return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/pageListDemo', params) return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/pageListDemo', params) } export function fetchPointList(params: Api.Point.ListParams) { return request.Post<Service.ResponseResult<Entity.Point[]>>('/map/api/v1/point/listDemo', params) } export function checkPointNameDuplicate(pointName: string) { return request.Get<Service.ResponseResult<any>>('/map/api/v1/point/checkNameDuplicate', { params: { pointName }, }) } export function createPoint(data: Api.Point.CreateData) { return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/addDemo', data) return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/add', data) } export function updatePoint(data: Api.Point.UpdateData) { return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/editDemo', data) return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/edit', data) } export function enableDisablePoint(data: Api.Point.EnableDisableData) { return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/enableDisableDemo', data) return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/enableDisable', data) } export function fetchPointDetail(id: number | string) { return request.Get<Service.ResponseResult<Entity.Point>>('/operation/api/v1/point/queryByIdDemo', { params: { id } }) return request.Get<Service.ResponseResult<Entity.Point>>('/map/api/v1/point/queryByIdDemo', { params: { id } }) } export function deletePoint(id: number | string) { return request.Get<Service.ResponseResult<any>>(`/operation/api/v1/point/deletePointDemo?ids=${id}`) return request.Get<Service.ResponseResult<any>>(`/map/api/v1/point/deletePoint?ids=${id}`) } export function fetchElectronicFenceAreaList(params: { cityCode?: string } = {}) { @@ -78,11 +84,11 @@ type: number name: string ids: number | string }>>>('/operation/api/v1/electronicFence/findListFenceAndStationDemo', { params }) }>>>('/map/api/v1/electronicFence/findListFenceAndStationDemo', { params }) } export function exportPointExcel(data: { pointIds: (number | string)[] }) { const method = request.Post<Blob>('/operation/api/v1/point/exportExcelDemo', data, { responseType: 'blob' }) const method = request.Post<Blob>('/map/api/v1/point/exportExcel', data, { responseType: 'blob' }) method.meta = { ...method.meta, isBlob: true } return method } src/views/businessOperations/operationWorkbench/map.vue
@@ -13,6 +13,7 @@ } from "vue"; import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router"; import { checkPointNameDuplicate, createPoint, deletePoint, fetchElectronicFenceAreaList, @@ -42,6 +43,7 @@ import PointPanel from "./components/PointPanel.vue"; import RestrictedAreaModal from "./components/RestrictedAreaModal.vue"; import StationPanel from "./components/StationPanel.vue"; import PointNameConflictDialog from "@/components/operationWorkbench/PointNameConflictDialog.vue"; import { buildStationListItem, clonePointTimeRanges, @@ -4242,184 +4244,114 @@ }); } type PointDuplicateHandleAction = | "rename-current-point" | "rename-legacy-point" | "disable-legacy-point"; type PointDuplicateConfirmResult = { confirmed: boolean; action: PointDuplicateHandleAction; currentPointName: string; legacyPointName: string; type PointNameConflictDialogResult = { resolveType: 1 | 2 | 3; newPointName?: string; }; // 点位重名弹窗改为页面内 n-modal 承载,保留 Promise 方式以兼容原保存流程。 const pointDuplicateModalVisible = ref(false); const pointDuplicateModalLoading = ref(false); const pointDuplicateName = ref(""); const pointDuplicateSelectedAction = ref<PointDuplicateHandleAction>( "rename-current-point" ); const pointDuplicateCurrentPointName = ref(""); const pointDuplicateLegacyPointName = ref(""); let pointDuplicateModalSettled = false; let pointDuplicateModalResolver: | ((value: PointDuplicateConfirmResult) => void) | null = null; type PointNameConflictDialogExposed = { open: (options?: { duplicateName?: string; }) => Promise<PointNameConflictDialogResult | null>; }; // 统一收口弹窗返回值,避免确认/取消两条链路重复组装数据。 function buildPointDuplicateConfirmResult( confirmed: boolean ): PointDuplicateConfirmResult { const pointNameConflictDialogRef = ref<PointNameConflictDialogExposed | null>(null); function normalizeDuplicatePointInfo(raw: any, fallbackPointName: string) { if (!raw && raw !== 0) return null; if (["string", "number"].includes(typeof raw)) { return { id: String(raw), pointName: fallbackPointName, pointNumber: "", }; } const pointId = String(raw?.pointId || raw?.id || "").trim(); if (!pointId) return null; return { confirmed, action: pointDuplicateSelectedAction.value, currentPointName: String(pointDuplicateCurrentPointName.value || "").trim(), legacyPointName: String(pointDuplicateLegacyPointName.value || "").trim(), id: pointId, pointName: String( raw?.pointName || raw?.name || raw?.duplicateName || fallbackPointName ).trim(), pointNumber: String(raw?.pointNumber || "").trim(), }; } // 每次弹窗结束后重置内部状态,避免上一次输入污染下一次编辑。 function resetPointDuplicateModalState() { pointDuplicateModalLoading.value = false; pointDuplicateName.value = ""; pointDuplicateSelectedAction.value = "rename-current-point"; pointDuplicateCurrentPointName.value = ""; pointDuplicateLegacyPointName.value = ""; } // modal 的确认、取消、右上角关闭都汇总到这里,保证 Promise 只 resolve 一次。 function finalizePointDuplicateModal(result: PointDuplicateConfirmResult) { if (pointDuplicateModalSettled) return; pointDuplicateModalSettled = true; pointDuplicateModalVisible.value = false; pointDuplicateModalLoading.value = false; const resolver = pointDuplicateModalResolver; pointDuplicateModalResolver = null; resetPointDuplicateModalState(); resolver?.(result); } function handleCancelPointDuplicateModal() { finalizePointDuplicateModal(buildPointDuplicateConfirmResult(false)); } function handlePointDuplicateModalShowChange(show: boolean) { pointDuplicateModalVisible.value = show; if (!show) handleCancelPointDuplicateModal(); } // 正式工作台编辑点位时优先走接口兜底判重;接口异常时,再回退到当前列表数据本地判重。 async function findDuplicatePointByName(pointName: string) { const normalizedName = String(pointName || "").trim(); if (!normalizedName) return null; const currentPointId = String(pointDetailData.value?.id || ""); const currentPointNumber = String(pointDetailData.value?.pointNumber || ""); try { const pointResult = await fetchPointList({ cityCode: cityValue.value || undefined, queryParams: normalizedName, } as any); if (pointResult.code === 200 && Array.isArray(pointResult.data)) { const duplicateItem = pointResult.data.find((item: any) => { const itemName = String(item?.pointName || item?.name || "").trim(); const itemId = String(item?.id || item?.pointId || ""); const itemPointNumber = String(item?.pointNumber || ""); const isSelf = (currentPointId && itemId === currentPointId) || (currentPointNumber && itemPointNumber === currentPointNumber); return itemName === normalizedName && !isSelf; }); if (duplicateItem) { return { id: String(duplicateItem?.id || duplicateItem?.pointId || ""), pointName: String( duplicateItem?.pointName || duplicateItem?.name || normalizedName ), pointNumber: String(duplicateItem?.pointNumber || ""), }; } } const res = await checkPointNameDuplicate(normalizedName); if (res.code !== 200) return null; return normalizeDuplicatePointInfo(res.data, normalizedName); } catch (error) { console.error("点位重名兜底校验失败:", error); console.error("点位重名校验失败:", error); window.$message?.error("点位名称校验失败,请稍后重试"); return null; } } function buildPendingPointConflictPayload( resolvePayload: PointNameConflictDialogResult, duplicatePointId: string ) { if (resolvePayload.resolveType === 1) { return { pointName: String(resolvePayload.newPointName || "").trim(), conflictResolveType: 1, conflictOldPointId: duplicatePointId, }; } const duplicateListItem = pointData.value.find((item) => { const itemName = String(item.raw?.pointName || item.name || "").trim(); const itemId = String(item.raw?.id || item.id || ""); const itemPointNumber = String(item.raw?.pointNumber || item.id || ""); const isSelf = (currentPointId && itemId === currentPointId) || (currentPointNumber && itemPointNumber === currentPointNumber); return itemName === normalizedName && !isSelf; }); if (!duplicateListItem) return null; if (resolvePayload.resolveType === 2) { return { conflictResolveType: 2, conflictOldPointId: duplicatePointId, conflictOldPointName: String(resolvePayload.newPointName || "").trim(), }; } return { id: String(duplicateListItem.raw?.id || duplicateListItem.id || ""), pointName: String( duplicateListItem.raw?.pointName || duplicateListItem.name || normalizedName ), pointNumber: String(duplicateListItem.raw?.pointNumber || ""), conflictResolveType: 3, conflictOldPointId: duplicatePointId, }; } // 弹窗内仅做当前前端可落地的校验,确认后再继续走原编辑保存逻辑。 async function handleConfirmPointDuplicateModal() { pointDuplicateModalLoading.value = true; try { if (pointDuplicateSelectedAction.value === "rename-current-point") { const normalizedCurrentPointName = String( pointDuplicateCurrentPointName.value || "" ).trim(); if (!normalizedCurrentPointName) { window.$message?.warning("请输入新的点位名称"); return; } const duplicateItem = await findDuplicatePointByName( normalizedCurrentPointName ); if (duplicateItem) { window.$message?.warning("新的点位名称仍与现有点位重复"); return; } } if (pointDuplicateSelectedAction.value === "rename-legacy-point") { const normalizedLegacyPointName = String( pointDuplicateLegacyPointName.value || "" ).trim(); if (!normalizedLegacyPointName) { window.$message?.warning("请输入旧点位的新名称"); return; } } finalizePointDuplicateModal(buildPointDuplicateConfirmResult(true)); } finally { if (!pointDuplicateModalSettled) pointDuplicateModalLoading.value = false; async function preparePointSavePayloadForConflict<T extends Record<string, any>>( payload: T ) { const duplicatePoint = await findDuplicatePointByName(payload.pointName || ""); if (!duplicatePoint) { return { shouldContinue: true, payload, }; } } // 打开重名弹窗时仍返回 Promise,方便保存流程按“等待用户处理后再继续”串行执行。 function openPointDuplicateConfirmDialog(duplicateName: string) { pointDuplicateModalSettled = false; pointDuplicateName.value = duplicateName; pointDuplicateSelectedAction.value = "rename-current-point"; pointDuplicateCurrentPointName.value = ""; pointDuplicateLegacyPointName.value = ""; pointDuplicateModalVisible.value = true; return new Promise<PointDuplicateConfirmResult>((resolve) => { pointDuplicateModalResolver = resolve; const resolvePayload = await pointNameConflictDialogRef.value?.open({ duplicateName: duplicatePoint.pointName, }); if (!resolvePayload) { return { shouldContinue: false, payload, }; } const submitPatch = buildPendingPointConflictPayload( resolvePayload, duplicatePoint.id ); return { shouldContinue: true, payload: { ...payload, ...submitPatch, } as T, }; } function openPointSaveLeaveDialog() { @@ -5811,6 +5743,35 @@ return result.valid; } async function finalizeBusinessPointSaveSuccess() { workbenchStore.markListNeedRefresh(); window.$message?.success("保存成功"); pointEditMode.value = false; pointCreateMode.value = false; pointCreateReady.value = false; pointEditSnapshot.value = ""; selectedCreateTool.value = ""; cleanupPointEditMapState(); await loadPointResources(); // 正式工作台保存后统一关闭右侧详情,避免继续展示可能已失效的详情数据。 const workflowState = buildPointClosePanelState({ isCreateMode: false, detail: pointDetailData.value, fallbackPointPictures: selectedItem.value?.raw?.pointPictures, defaultPointImage: DEFAULT_POINT_IMAGE, }); pointPanelClosed.value = workflowState.pointPanelClosed; selectedId.value = workflowState.selectedId; pointDetailData.value = workflowState.pointDetailData; pointForm.value = workflowState.form; pointCreateStatusState.value = workflowState.pointCreateStatus; lastAutoFilledPointName.value = workflowState.lastAutoFilledPointName; pointAddressQuery.value = workflowState.addressQuery; pointAddressOptions.value = workflowState.addressOptions; uploadedImages.value = workflowState.uploadedImages; return true; } // 点位保存入口。 // 主要作用: // - 校验基础字段、坐标、状态、运营时间和图片上传状态; @@ -5874,24 +5835,6 @@ window.$message?.warning("图片上传中,请等待上传完成后再保存"); return false; } if (!pointCreateMode.value) { const duplicatePoint = await findDuplicatePointByName( pointForm.value.pointName ); if (duplicatePoint) { const duplicateDecision = await openPointDuplicateConfirmDialog( duplicatePoint.pointName ); if (!duplicateDecision.confirmed) return false; if ( duplicateDecision.action === "rename-current-point" && duplicateDecision.currentPointName ) { pointForm.value.pointName = duplicateDecision.currentPointName; } // “更改旧点位名称 / 停用旧点位”待后续接口支持后接入真实处理,当前先保留确认交互并继续保存当前点位。 } } const operationArea = resolveOperationArea( fenceAreaData.value, Number(pointForm.value.pointLon), @@ -5923,7 +5866,6 @@ pointForm.value.pointOnOff || pointDetailData.value?.pointOnOff || "3" ); pointForm.value.pointOnOff = pointOnOff; pointSaving.value = true; try { const basePayload = buildPointSavePayload({ administrativeInfo: normalizedAdministrativeInfo, @@ -5942,40 +5884,23 @@ fenceId, stationId: containingStation?.id, }); const res = pointCreateMode.value ? await createPoint(basePayload as Api.Point.CreateData) : await updatePoint({ const initialPayload = pointCreateMode.value ? (basePayload as Api.Point.CreateData) : ({ ...(basePayload as Api.Point.UpdateData), id: String(pointDetailData.value!.id), pointNumber: pointDetailData.value!.pointNumber, }); } as Api.Point.UpdateData); const prepareResult = await preparePointSavePayloadForConflict(initialPayload); if (!prepareResult.shouldContinue) return false; const submitPayload = prepareResult.payload; pointSaving.value = true; const res = pointCreateMode.value ? await createPoint(submitPayload as Api.Point.CreateData) : await updatePoint(submitPayload as Api.Point.UpdateData); if (res.code === 200) { workbenchStore.markListNeedRefresh(); window.$message?.success("保存成功"); pointEditMode.value = false; pointCreateMode.value = false; pointCreateReady.value = false; pointEditSnapshot.value = ""; selectedCreateTool.value = ""; cleanupPointEditMapState(); await loadPointResources(); // 正式工作台保存后统一关闭右侧详情,避免继续展示可能已失效的详情数据。 const workflowState = buildPointClosePanelState({ isCreateMode: false, detail: pointDetailData.value, fallbackPointPictures: selectedItem.value?.raw?.pointPictures, defaultPointImage: DEFAULT_POINT_IMAGE, }); pointPanelClosed.value = workflowState.pointPanelClosed; selectedId.value = workflowState.selectedId; pointDetailData.value = workflowState.pointDetailData; pointForm.value = workflowState.form; pointCreateStatusState.value = workflowState.pointCreateStatus; lastAutoFilledPointName.value = workflowState.lastAutoFilledPointName; pointAddressQuery.value = workflowState.addressQuery; pointAddressOptions.value = workflowState.addressOptions; uploadedImages.value = workflowState.uploadedImages; return true; return await finalizeBusinessPointSaveSuccess(); } return false; // window.$message?.error(res.msg || "保存失败"); @@ -7642,71 +7567,10 @@ @confirm="handleConfirmRestrictedArea" /> <n-modal v-model:show="pointDuplicateModalVisible" preset="card" title="提示" class="w-400px" :auto-focus="false" :mask-closable="false" :closable="true" :segmented="{ content: true, action: true, }" @update:show="handlePointDuplicateModalShowChange" > <!-- 重名处理弹窗:按 UI 图保留单选项下直接接输入框的布局。 --> <div > <div class="mb-16px text-14px text-[#171F26]"> 点位名称【{{ pointDuplicateName }}】重复,请确认处理方式 </div> <n-radio-group v-model:value="pointDuplicateSelectedAction" name="point-duplicate-action" class="w-full" > <n-space vertical :size="16" class="w-full"> <div class="w-full"> <!-- 仅当前选中时允许输入,其余输入框保留灰态以还原设计稿。 --> <n-radio value="rename-current-point">修改新点位名称</n-radio> <n-input v-model:value="pointDuplicateCurrentPointName" class="mt-8px" placeholder="请输入" :disabled="pointDuplicateSelectedAction !== 'rename-current-point'" /> </div> <div class="w-full"> <n-radio value="rename-legacy-point">更改旧点位名称</n-radio> <n-input v-model:value="pointDuplicateLegacyPointName" class="mt-8px" placeholder="请输入" :disabled="pointDuplicateSelectedAction !== 'rename-legacy-point'" /> </div> <n-radio value="disable-legacy-point">停用旧点位(不影响正在进行中的订单)</n-radio> </n-space> </n-radio-group> </div> <template #action> <n-space justify="end"> <n-button @click="handleCancelPointDuplicateModal">取消</n-button> <n-button type="primary" :loading="pointDuplicateModalLoading" @click="handleConfirmPointDuplicateModal" > 确定 </n-button> </n-space> </template> </n-modal> <PointNameConflictDialog ref="pointNameConflictDialogRef" :validate-name-conflict="findDuplicatePointByName" /> <div v-if="previewVisible" class="preview-mask" @click="closePreview"> <div class="preview-content" @click.stop> src/views/testDemo/operationWorkbench/components/PointPanel.vue
@@ -31,6 +31,7 @@ showStopButton: boolean showRestartButton: boolean showNextStep: boolean showDetailActions: boolean } }>() @@ -453,7 +454,7 @@ </div> </div> <div class="panel-footer"> <div v-if="buttonConfig.showDetailActions" class="panel-footer"> <button v-if="showDeleteButton" class="cancel-btn delete-btn" src/views/testDemo/operationWorkbench/map.vue
@@ -13,6 +13,7 @@ } from "vue"; import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router"; import { checkPointNameDuplicate, createPoint, deletePoint, fetchElectronicFenceAreaList, @@ -37,6 +38,7 @@ import PointPanel from "./components/PointPanel.vue"; import RestrictedAreaModal from "./components/RestrictedAreaModal.vue"; import StationPanel from "./components/StationPanel.vue"; import PointNameConflictDialog from "@/components/operationWorkbench/PointNameConflictDialog.vue"; import { buildStationListItem, clonePointTimeRanges, @@ -1095,12 +1097,20 @@ showStopButton: false, showRestartButton: false, showNextStep: false, showDetailActions: false, }; const showDetailActions = pointCreateMode.value || pointEditMode.value ? false : ![3, 5].includes(verificationStatus); return { showVerifyButtons: verificationStatus === 2, showStopButton: false, showRestartButton: verificationStatus === 4, showNextStep: verificationStatus === 1, showDetailActions, }; }); @@ -5633,6 +5643,159 @@ return result.valid; } type PointNameConflictDialogResult = { resolveType: 1 | 2 | 3; newPointName?: string; }; type PointNameConflictDialogExposed = { open: (options?: { duplicateName?: string; }) => Promise<PointNameConflictDialogResult | null>; }; const pointNameConflictDialogRef = ref<PointNameConflictDialogExposed | null>(null); function normalizeDuplicatePointInfo(raw: any, fallbackPointName: string) { if (!raw && raw !== 0) return null; if (["string", "number"].includes(typeof raw)) { return { id: String(raw), pointName: fallbackPointName, pointNumber: "", }; } const pointId = String(raw?.pointId || raw?.id || "").trim(); if (!pointId) return null; return { id: pointId, pointName: String( raw?.pointName || raw?.name || raw?.duplicateName || fallbackPointName ).trim(), pointNumber: String(raw?.pointNumber || "").trim(), }; } async function findDuplicatePointByName(pointName: string) { const normalizedName = String(pointName || "").trim(); if (!normalizedName) return null; try { const res = await checkPointNameDuplicate(normalizedName); if (res.code !== 200) return null; return normalizeDuplicatePointInfo(res.data, normalizedName); } catch (error) { console.error("点位重名校验失败:", error); window.$message?.error("点位名称校验失败,请稍后重试"); return null; } } function buildPendingPointConflictPayload( resolvePayload: PointNameConflictDialogResult, duplicatePointId: string ) { if (resolvePayload.resolveType === 1) { return { pointName: String(resolvePayload.newPointName || "").trim(), conflictResolveType: 1, conflictOldPointId: duplicatePointId, }; } if (resolvePayload.resolveType === 2) { return { conflictResolveType: 2, conflictOldPointId: duplicatePointId, conflictOldPointName: String(resolvePayload.newPointName || "").trim(), }; } return { conflictResolveType: 3, conflictOldPointId: duplicatePointId, }; } async function preparePointSavePayloadForConflict<T extends Record<string, any>>( payload: T ) { const duplicatePoint = await findDuplicatePointByName(payload.pointName || ""); if (!duplicatePoint) { return { shouldContinue: true, payload, }; } const resolvePayload = await pointNameConflictDialogRef.value?.open({ duplicateName: duplicatePoint.pointName, }); if (!resolvePayload) { return { shouldContinue: false, payload, }; } const submitPatch = buildPendingPointConflictPayload( resolvePayload, duplicatePoint.id ); return { shouldContinue: true, payload: { ...payload, ...submitPatch, } as T, }; } async function finalizeTestPointSaveSuccess( response: Service.ResponseResult<any>, isCreatingPoint: boolean ) { workbenchStore.markListNeedRefresh(); window.$message?.success("保存成功"); const savedPointId = String( response?.data?.pointId || response?.data?.id || (!isCreatingPoint ? pointDetailData.value?.id || selectedId.value : "") || "" ); const createdPointNumber = isCreatingPoint ? String(response?.data?.pointNumber || (response as any)?.pointNumber || "") : ""; pointEditMode.value = false; pointCreateMode.value = false; pointCreateReady.value = false; pointEditSnapshot.value = ""; selectedCreateTool.value = ""; cleanupPointEditMapState(); await loadPointResources(); if (savedPointId) { pointPanelClosed.value = false; selectedId.value = savedPointId; await loadPointDetailData(savedPointId); return true; } if (createdPointNumber) { const createdItem = pointData.value.find( (item) => String(item.raw?.pointNumber || "") === createdPointNumber ); if (createdItem) { pointPanelClosed.value = false; selectedId.value = createdItem.id; await loadPointDetailData(String(createdItem.id)); return true; } } if (selectedId.value) await loadPointDetailData(String(selectedId.value)); return true; } async function handleSavePointEdit() { if (!isPointFormMode.value) return false; if (!pointForm.value.pointName.trim()) { @@ -5698,7 +5861,6 @@ pointForm.value.pointOnOff || pointDetailData.value?.pointOnOff || "3" ); pointForm.value.pointOnOff = pointOnOff; pointSaving.value = true; try { const isCreatingPoint = pointCreateMode.value; const basePayload = buildPointSavePayload({ @@ -5719,40 +5881,23 @@ fenceId, stationId: containingStation?.id, }); const res = pointCreateMode.value ? await createPoint(basePayload as Api.Point.CreateData) : await updatePoint({ const initialPayload = pointCreateMode.value ? (basePayload as Api.Point.CreateData) : ({ ...(basePayload as Api.Point.UpdateData), id: String(pointDetailData.value!.id), pointNumber: pointDetailData.value!.pointNumber, }); } as Api.Point.UpdateData); const prepareResult = await preparePointSavePayloadForConflict(initialPayload); if (!prepareResult.shouldContinue) return false; const submitPayload = prepareResult.payload; pointSaving.value = true; const res = pointCreateMode.value ? await createPoint(submitPayload as Api.Point.CreateData) : await updatePoint(submitPayload as Api.Point.UpdateData); if (res.code === 200) { workbenchStore.markListNeedRefresh(); window.$message?.success("保存成功"); const createdPointNumber = isCreatingPoint ? String(res?.data?.pointNumber || (res as any)?.pointNumber || "") : ""; pointEditMode.value = false; pointCreateMode.value = false; pointCreateReady.value = false; pointEditSnapshot.value = ""; selectedCreateTool.value = ""; cleanupPointEditMapState(); await loadPointResources(); if (createdPointNumber) { const createdItem = pointData.value.find( (item) => String(item.raw?.pointNumber || "") === createdPointNumber ); if (createdItem) { pointPanelClosed.value = false; selectedId.value = createdItem.id; await loadPointDetailData(String(createdItem.id)); return true; } } if (selectedId.value) await loadPointDetailData(String(selectedId.value)); return true; return await finalizeTestPointSaveSuccess(res, isCreatingPoint); } return false; // window.$message?.error(res.msg || "保存失败"); @@ -7426,6 +7571,11 @@ @confirm="handleConfirmRestrictedArea" /> <PointNameConflictDialog ref="pointNameConflictDialogRef" :validate-name-conflict="findDuplicatePointByName" /> <div v-if="previewVisible" class="preview-mask" @click="closePreview"> <div class="preview-content" @click.stop> <img