董国庆
2026-06-02 b26da67546bcb85ab7bc5f790efa1c09bfcc45d6
点位demo联调
1个文件已添加
5个文件已修改
904 ■■■■■ 已修改文件
src/components/operationWorkbench/PointNameConflictDialog.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/point.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/testOperationWorkbench.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.vue 404 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/components/PointPanel.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/map.vue 212 ●●●●● 补丁 | 查看 | 原始文档 | 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