董国庆
2026-05-28 a79c6af0e6c8ea8e031d623f109722d00a03c8e7
refactor(运营工作台): 重命名testStatus为flowStatus并更新状态逻辑

统一将围栏表单中的testStatus字段重命名为flowStatus,同步更新所有引用该字段的代码。
重构围栏状态文本展示规则,新增工作台专属的状态渲染与样式适配逻辑。
更新测试版API接口路径,统一添加Demo后缀。
新增围栏边界变更检测逻辑,修改边界后弹出确认提示并自动重置流程状态。
优化围栏编辑面板交互规则,运营态围栏禁止编辑与相关操作。
同步更新正式版运营工作台的围栏保存逻辑。
9个文件已修改
474 ■■■■■ 已修改文件
src/service/api/testOperationWorkbench.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/components/FencePanel.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/map.definition.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/map.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/status.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/workbench.constants.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/workbench.fence-workflow.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/workbench.payload.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/testOperationWorkbench.ts
@@ -10,19 +10,19 @@
}
export function getFenceList(params: any) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/electronicFence/pageList', params)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/electronicFence/pageListDemo', params)
}
export function addFence(params: any) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/electronicFence/add', params)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/electronicFence/addDemo', params)
}
export function copyFence(id: any) {
  return request.Post<Service.ResponseResult<any>>(`/map/api/v1/electronicFence/copy?id=${id}`)
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/electronicFence/copyDemo?id=${id}`)
}
export function editFence(params: any) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/electronicFence/edit', params)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/electronicFence/editDemo', params)
}
export function validateFenceOperationStrategyShorten(params: {
@@ -30,47 +30,47 @@
  operationStrategy: string
}) {
  return request.Post<Service.ResponseResult<any>>(
    '/map/api/v1/electronicFence/validateOperationStrategyShorten',
    '/operation/api/v1/electronicFence/validateOperationStrategyShortenDemo',
    params,
  )
}
export function getFenceDetail(id: any) {
  return request.Post<Service.ResponseResult<any>>(`/map/api/v1/electronicFence/queryById?id=${id}`)
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/electronicFence/queryByIdDemo?id=${id}`)
}
/** 点位相关 */
export function fetchPointPageList(params: any) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/pageList', params)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/pageListDemo', params)
}
export function fetchPointList(params: Api.Point.ListParams) {
  return request.Post<Service.ResponseResult<Entity.Point[]>>('/map/api/v1/point/list', params)
  return request.Post<Service.ResponseResult<Entity.Point[]>>('/map/api/v1/point/listDemo', params)
}
export function createPoint(data: Api.Point.CreateData) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/add', data)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/addDemo', data)
}
export function updatePoint(data: Api.Point.UpdateData) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/edit', data)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/editDemo', data)
}
export function enableDisablePoint(data: Api.Point.EnableDisableData) {
  return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/enableDisable', data)
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/enableDisableDemo', data)
}
export function fetchPointDetail(id: number | string) {
  return request.Get<Service.ResponseResult<Entity.Point>>('/map/api/v1/point/queryById', { params: { id } })
  return request.Get<Service.ResponseResult<Entity.Point>>('/operation/api/v1/point/queryByIdDemo', { params: { id } })
}
export function deletePoint(id: number | string) {
  return request.Get<Service.ResponseResult<any>>(`/map/api/v1/point/deletePoint?ids=${id}`)
  return request.Get<Service.ResponseResult<any>>(`/operation/api/v1/point/deletePointDemo?ids=${id}`)
}
export function fetchElectronicFenceAreaList(params: { cityCode?: string } = {}) {
  return request.Post<Service.ResponseResult<Entity.ElectronicFenceArea[]>>('/map/api/v1/point/listElectronicFenceArea', params)
  return request.Post<Service.ResponseResult<Entity.ElectronicFenceArea[]>>('/operation/api/v1/point/listElectronicFenceAreaDemo', params)
}
export function fetchFenceAndStationAreaList(params: { cityCode?: string } = {}) {
@@ -78,11 +78,11 @@
    type: number
    name: string
    ids: number | string
  }>>>('/map/api/v1/electronicFence/findListFenceAndStation', { params })
  }>>>('/operation/api/v1/electronicFence/findListFenceAndStationDemo', { params })
}
export function exportPointExcel(data: { pointIds: (number | string)[] }) {
  const method = request.Post<Blob>('/map/api/v1/point/exportExcel', data, { responseType: 'blob' })
  const method = request.Post<Blob>('/operation/api/v1/point/exportExcelDemo', data, { responseType: 'blob' })
  method.meta = { ...method.meta, isBlob: true }
  return method
}
src/views/businessOperations/operationWorkbench/map.vue
@@ -5648,7 +5648,6 @@
  pointForm.value.pointOnOff = pointOnOff;
  pointSaving.value = true;
  try {
    const isCreatingPoint = pointCreateMode.value;
    const basePayload = buildPointSavePayload({
      administrativeInfo: normalizedAdministrativeInfo,
      pointForm: pointForm.value,
@@ -5676,9 +5675,6 @@
    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;
@@ -5686,19 +5682,22 @@
      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));
      // 正式工作台保存后统一关闭右侧详情,避免继续展示可能已失效的详情数据。
      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 false;
@@ -5764,9 +5763,10 @@
    window.$message?.warning("未找到运营区信息,暂无法编辑");
    return;
  }
  const hasGeometryChanged =
    !fenceCreateMode.value && hasFenceGeometryChanged();
  if (
    !fenceCreateMode.value &&
    hasFenceGeometryChanged() &&
    hasGeometryChanged &&
    !(await openConfirmDialog(
      "提示",
      "已修改围栏边界或禁行区边界,保存后围栏将重回测试状态,确定保存吗?",
@@ -5784,6 +5784,7 @@
    const isCreatingFence = fenceCreateMode.value;
    const shouldConfirmStatusChange =
      saveType === 1 &&
      !hasGeometryChanged &&
      !fenceCreateMode.value &&
      fenceForm.value.areaStatus !== getFenceOriginalAreaStatus();
    if (shouldConfirmStatusChange) {
@@ -5799,6 +5800,7 @@
      fenceStrategyList.value
    );
    if (
      !hasGeometryChanged &&
      !fenceCreateMode.value &&
      !(await confirmFenceOperationStrategyShortenEdit(
        String(fenceDetailData.value.id),
@@ -5809,13 +5811,15 @@
      return;
    }
    const fencePointRelationPayload =
      saveType === 1
      // 围栏边界变更后的状态和关联影响交由编辑接口统一处理;
      // 前端只保留一次边界变更确认,确认后直接继续保存。
      saveType === 1 && !hasGeometryChanged
        ? await resolveFencePointRelationPayload()
        : { addPointIdList: [], delPointIdList: [], disablePointIdList: [] };
    const disabledPointCount =
      fencePointRelationPayload.disablePointIdList.length;
    const fenceSubmitConfirmMessage =
      saveType === 1
      saveType === 1 && !hasGeometryChanged
        ? buildFenceSubmitConfirmMessage(disabledPointCount)
        : "";
    if (fenceSubmitConfirmMessage) {
@@ -5851,24 +5855,19 @@
    if (res.code === 200) {
      workbenchStore.markListNeedRefresh();
      window.$message?.success(
        saveType === 0
          ? "保存草稿成功"
          : isCreatingFence
          ? "添加成功"
          : "编辑成功"
        saveType === 0 ? "保存草稿成功" : "编辑成功"
      );
      const currentFenceId = String(fenceDetailData.value?.id || "");
      fenceEditMode.value = false;
      fenceCreateMode.value = false;
      fenceEditSnapshot.value = "";
      selectedCreateTool.value = "";
      cleanupFenceEditorState();
      await Promise.all([
        loadFenceData(),
        !fenceCreateMode.value && currentFenceId
          ? loadFenceDetailData(currentFenceId)
          : Promise.resolve(),
      ]);
      await loadFenceData();
      // 正式工作台保存后统一关闭右侧详情,避免继续展示可能已失效的详情数据。
      const workflowState = buildFenceClosePanelState();
      fencePanelClosed.value = workflowState.fencePanelClosed;
      selectedId.value = "";
      fenceDetailData.value = null;
      renderMapEntities();
      return;
    }
src/views/testDemo/operationWorkbench/components/FencePanel.vue
@@ -8,7 +8,6 @@
  NInput,
  NSelect,
  NSwitch,
  NTag,
  NTimePicker,
} from "naive-ui";
import type { FormInst, FormItemRule } from "naive-ui";
@@ -23,7 +22,7 @@
import delIcon from "@/assets/geofence/del.png";
import noEntryIcon from "@/assets/geofence/noEntry.png";
import resetPointIcon from "@/assets/point/reset-point.png";
import { getStatusClass } from "../status";
import { getFenceFlowStatusText, getStatusClass } from "../status";
const props = defineProps<{
  editMode: boolean;
@@ -34,9 +33,9 @@
  strategyList: Array<Record<string, any>>;
  detailRows: Array<{ label: string; value: string }>;
  fenceDetailData: Record<string, any> | null;
  currentFenceTestStatusText: string;
  currentFenceFlowStatusText: string;
  statusMeta: {
    testStatus: number;
    flowStatus: number;
  };
  buttonConfig: {
    showVerifyButtons: boolean;
@@ -98,18 +97,29 @@
  { label: "星期日", value: 7 },
];
// 测试工作台里 flowStatus=5 代表围栏已进入运营态,此时面板仅保留详情展示。
const isLocked = computed(
  () => !props.createMode && Number(props.fenceDetailData?.status) === 2
);
const isAreaStatusSwitchDisabled = computed(
  () =>
    !props.createMode && [0, 1].includes(Number(props.fenceDetailData?.status))
    !props.createMode &&
    Number(
      props.fenceDetailData?.flowStatus
        || props.statusMeta?.flowStatus
        || props.form?.flowStatus
        || 1
    ) === 5
);
const showDraftButton = computed(
  () => !!props.createMode || Number(props.fenceDetailData?.status) === 0
);
const currentFenceTestStatusClass = computed(() =>
  getStatusClass(props.currentFenceTestStatusText)
  getStatusClass(props.currentFenceFlowStatusText)
);
const currentFenceStepStatusText = computed(() =>
  getFenceFlowStatusText(
    props.statusMeta?.flowStatus ||
      props.fenceDetailData?.flowStatus ||
      props.form?.flowStatus
  )
);
const operationArea = computed(
  () => props.areaSummary.operationAreas?.[0] || null
@@ -241,7 +251,15 @@
        return true;
      if (value == null) return new Error("请选择生效时间");
      if (typeof value === "number") {
        if (!props.createMode && Number(props.fenceDetailData?.status) === 2)
        if (
          !props.createMode &&
          Number(
            props.fenceDetailData?.flowStatus
              || props.statusMeta?.flowStatus
              || props.form?.flowStatus
              || 1
          ) === 5
        )
          return true;
        const now = new Date();
        const minAllowedDateTime = new Date(
@@ -286,20 +304,6 @@
    },
  } as FormItemRule,
};
function getFenceStatusText(status: number) {
  if (Number(status) === 0) return "草稿";
  if (Number(status) === 1) return "待生效";
  if (Number(status) === 2) return "启用中";
  if (Number(status) === 3) return "已停用";
  return "-";
}
function getFenceStatusTagType(status: number) {
  if (Number(status) === 2) return "success";
  if (Number(status) === 1) return "warning";
  return "default";
}
function getStrategyTypeText(item: Record<string, any>) {
  if (Number(item?.type) === 3) {
@@ -486,14 +490,6 @@
        <div class="main-head" :class="{ 'sticky-top': showDrawerHead }">
          <div v-if="createMode" class="flex items-center gap-4px mb-16px">
            <div class="title">围栏详情</div>
            <n-switch
              v-model:value="form.areaStatus"
              :checked-value="1"
              :unchecked-value="2"
            >
              <template #checked> 启用 </template>
              <template #unchecked> 停用 </template>
            </n-switch>
            <img
              :src="closeIcon"
              class="w-24px h-24px ml-auto cursor-pointer"
@@ -508,26 +504,9 @@
                form.electronicName ||
                "围栏详情"
              }}
              <NTag
                style="font-weight: 500"
                :type="
                  getFenceStatusTagType(Number(fenceDetailData?.status || 0))
                "
                :bordered="false"
                class="h-24px mx-4px"
              >
                {{ getFenceStatusText(Number(fenceDetailData?.status || 0)) }}
              </NTag>
              <n-switch
                v-model:value="form.areaStatus"
                :checked-value="1"
                :unchecked-value="2"
                :disabled="isAreaStatusSwitchDisabled"
                class="text-13px"
              >
                <template #checked> 启用 </template>
                <template #unchecked> 停用 </template>
              </n-switch>
              <span class="fence-test-status-tag mx-4px" :class="currentFenceTestStatusClass">
                {{ currentFenceFlowStatusText || "-" }}
              </span>
            </div>
            <img
              :src="closeIcon"
@@ -593,7 +572,7 @@
              <div class="box-title mb-8px">围栏测试状态</div>
              <div class="fence-test-status-wrapper">
                <PointStatusSteps
                  :point-status="currentFenceTestStatusText || '待测试'"
                  :point-status="currentFenceStepStatusText || '待测试'"
                  :custom-steps="['待测试', '测试中', '可运营']"
                  error-step-label="测试不通过"
                />
@@ -631,7 +610,7 @@
                    </div>
                    <div
                      class="verify-btn pass"
                      @click="emit('verify-status-update', 3)"
                      @click="emit('verify-status-update', 5)"
                    >
                      测试通过
                    </div>
@@ -1019,16 +998,9 @@
          <div class="flex items-center gap-4px mb-4px">
            <div v-if="fenceDetailData" class="title items-center">
              {{ fenceDetailData.electronicName || "" }}
              <NTag
                style="font-weight: 500"
                :type="
                  getFenceStatusTagType(Number(fenceDetailData.status || 0))
                "
                :bordered="false"
                class="h-24px mx-4px"
              >
                {{ getFenceStatusText(Number(fenceDetailData.status || 0)) }}
              </NTag>
              <span class="fence-test-status-tag mx-4px" :class="currentFenceTestStatusClass">
                {{ currentFenceFlowStatusText || "-" }}
              </span>
            </div>
            <img
              :src="closeIcon"
@@ -1073,17 +1045,6 @@
          <div class="box flex flex-col">
            <div class="box-title mb-8px">生命周期</div>
            <div class="box-item mb-8px">
              <div class="label">围栏测试状态</div>
              <div class="value">
                <span
                  class="fence-test-status-tag"
                  :class="currentFenceTestStatusClass"
                >
                  {{ currentFenceTestStatusText || "-" }}
                </span>
              </div>
            </div>
            <div class="box-item mb-8px">
              <div class="label">生效时间</div>
              <div class="value">
@@ -1234,7 +1195,8 @@
          </div>
        </div>
      </div>
      <div class="drawer-footer">
      <!-- 运营围栏在测试工作台只允许查看详情,不展示底部编辑入口。 -->
      <div v-if="!isLocked" class="drawer-footer">
        <NButton type="primary" class="w-full" @click="emit('edit-current')">
          编辑
        </NButton>
@@ -1610,6 +1572,7 @@
  font-size: 12px;
  line-height: 16px;
  color: #0068ff;
  font-weight: 400;
  background: #e6f4ff;
  &.wait-test {
@@ -1631,6 +1594,11 @@
    background: #e6f4ff;
    color: #0068ff;
  }
  &.test-stop {
    background: #e8e9e9;
    color: #171f26;
  }
}
.fence-inner-stats {
src/views/testDemo/operationWorkbench/map.definition.ts
@@ -79,7 +79,7 @@
  id: string;
  electronicName: string;
  areaDescription: string;
  testStatus: number;
  flowStatus: number;
  areaType: number;
  areaStatus: number;
  effectType: number;
src/views/testDemo/operationWorkbench/map.vue
@@ -42,7 +42,6 @@
  clonePointTimeRanges,
  formatFenceListDesc,
  formatStationListDesc,
  getFenceStatusText,
  mapListItemToStationPoint,
  mapPointApiItemToListItem,
  mapStationApiItemToListItem,
@@ -126,7 +125,8 @@
} from "./workbench.station-workflow";
import {
  buildPointStatusMetricSummary,
  getFenceTestStatusText,
  getFenceFlowStatusText,
  getFenceWorkbenchStatusText,
  buildStationStatusMetricSummary,
  getPointStatusText,
  getStatusClass,
@@ -417,7 +417,6 @@
  (document.activeElement as HTMLElement | null)?.blur?.();
}
const pointCreateStatusState = ref(createDefaultPointCreateStatus());
const fenceLocalTestStatusMap = ref<Record<string, number>>({});
// 点位编辑弹窗与地址搜索相关状态,供地图临时点位和右侧表单共用。
const pointAddressQuery = ref("");
@@ -445,6 +444,7 @@
const fenceDraftPolygon = shallowRef<any>(null);
const fenceDraftRestrictedPolygons = shallowRef<any[]>([]);
const fenceDraftPoints = ref<LngLatTuple[]>([]);
const lastValidFenceDrawingPoints = ref<LngLatTuple[]>([]);
const restrictedDrawingVertexCount = ref(0);
const fenceRestrictedAreas = ref<FenceRestrictedAreaDraft[]>([]);
const activeRestrictedAreaId = ref("");
@@ -455,6 +455,7 @@
const restrictedAreaEditorClosed = ref(false);
const suppressRestrictedEditorEnd = ref(false);
let restrictedEditorRollbacking = false;
let fenceEditorRollbacking = false;
const restrictedAreaModalVisible = ref(false);
const fencePanelRef = ref<any>(null);
const restrictedAreaModalRef = ref<any>(null);
@@ -744,26 +745,25 @@
  );
});
function normalizeFenceTestStatus(value?: number | string | null) {
function normalizeFenceFlowStatus(value?: number | string | null) {
  const status = Number(value);
  return [1, 2, 3, 4].includes(status) ? status : 1;
  return [1, 2, 3, 4, 5].includes(status) ? status : 1;
}
function resolveFenceLocalTestStatus(detail?: any) {
  const fenceId = String(detail?.id || selectedId.value || "");
  if (fenceId) {
    const localStatus = fenceLocalTestStatusMap.value[fenceId];
    if ([1, 2, 3, 4].includes(Number(localStatus)))
      return Number(localStatus);
  }
  return normalizeFenceTestStatus(detail?.testStatus);
function resolveFenceLocalFlowStatus(detail?: any) {
  return normalizeFenceFlowStatus(detail?.flowStatus);
}
const currentFenceTestStatusText = computed(() => {
const currentFenceFlowStatusText = computed(() => {
  const currentStatus = fenceDetailData.value?.status;
  if (fenceEditMode.value || fenceCreateMode.value)
    return getFenceTestStatusText(fenceForm.value.testStatus);
  if (activeTab.value !== "fence") return getFenceTestStatusText();
  return getFenceTestStatusText(resolveFenceLocalTestStatus(fenceDetailData.value));
    return getFenceWorkbenchStatusText(fenceForm.value.flowStatus, currentStatus);
  if (activeTab.value !== "fence")
    return getFenceWorkbenchStatusText(undefined, currentStatus);
  return getFenceWorkbenchStatusText(
    resolveFenceLocalFlowStatus(fenceDetailData.value),
    currentStatus
  );
});
// 测试运营工作台的点位编辑仅保留测试流程,不再按运营态额外锁定位置编辑。
@@ -841,10 +841,12 @@
const isFenceEditWorkflow = computed(
  () => fenceEditMode.value && !fenceCreateMode.value
);
// 测试工作台不继承“启用中不可操作”的旧规则;这里只保留运营围栏的兜底锁,
// 防止异常情况下进入编辑态后还能继续拖拽围栏几何。
const isFenceGeometryLocked = computed(
  () =>
    isFenceEditWorkflow.value &&
    Number(fenceForm.value.areaStatus || fenceDetailData.value?.areaStatus || 1) === 1
    resolveFenceLocalFlowStatus(fenceDetailData.value) === 5
);
const isStationFormMode = computed(
  () =>
@@ -1103,10 +1105,10 @@
});
const fenceButtonConfig = computed(() => {
  const testStatus =
  const flowStatus =
    fenceCreateMode.value || fenceEditMode.value
      ? normalizeFenceTestStatus(fenceForm.value.testStatus)
      : resolveFenceLocalTestStatus(fenceDetailData.value);
      ? normalizeFenceFlowStatus(fenceForm.value.flowStatus)
      : resolveFenceLocalFlowStatus(fenceDetailData.value);
  if (!fenceCreateMode.value && !fenceDetailData.value)
    return {
      showVerifyButtons: false,
@@ -1114,9 +1116,9 @@
      showNextStep: false,
    };
  return {
    showVerifyButtons: testStatus === 2,
    showRestartButton: testStatus === 4,
    showNextStep: testStatus === 1,
    showVerifyButtons: flowStatus === 2,
    showRestartButton: flowStatus === 4,
    showNextStep: flowStatus === 1,
  };
});
@@ -1158,7 +1160,7 @@
  return JSON.stringify({
    electronicName: fenceForm.value.electronicName.trim(),
    areaDescription: fenceForm.value.areaDescription.trim(),
    testStatus: normalizeFenceTestStatus(fenceForm.value.testStatus),
    flowStatus: normalizeFenceFlowStatus(fenceForm.value.flowStatus),
    areaStatus: fenceForm.value.areaStatus,
    effectType: fenceForm.value.effectType,
    effectTime: fenceForm.value.effectTime,
@@ -1195,7 +1197,9 @@
});
const isLockedFenceStatusEdit = computed(
  () => !fenceCreateMode.value && Number(fenceDetailData.value?.status) === 2
  () =>
    !fenceCreateMode.value &&
    resolveFenceLocalFlowStatus(fenceDetailData.value) === 5
);
const fenceSaveBlockReason = computed(() => {
@@ -1804,6 +1808,7 @@
  activeRestrictedAreaId.value = "";
  readonlyRestrictedAreaId.value = "";
  fenceDraftPoints.value = [];
  lastValidFenceDrawingPoints.value = [];
  restrictedDrawingVertexCount.value = 0;
  invalidFenceStationIds.value = [];
  updateFenceStationWarningStyles();
@@ -1819,10 +1824,55 @@
  restrictedDrawingVertexCount.value = getRestrictedDrawingVertexCount();
}
function applyRestrictedPolygonPath(polygon: any, points: LngLatTuple[]) {
function applyPolygonPath(polygon: any, points: LngLatTuple[]) {
  polygon?.setPath?.(
    points.map(([lng, lat]) => new (window as any).AMap.LngLat(lng, lat))
  );
}
// 围栏新建打点时,MouseTool 临时路径的最后一个点是跟随鼠标移动的预览点,
// 这里裁掉尾点,仅保留用户已经确认的顶点做实时几何校验。
function getFenceDrawingConfirmedPoints() {
  const path = fenceMouseTool.value?.polygonTemp?.obj?.getPath?.();
  if (!Array.isArray(path) || !path.length) return [];
  const confirmedPath = path.length > 1 ? path.slice(0, -1) : path;
  return confirmedPath.map(
    (point: any) => [point.lng, point.lat] as LngLatTuple
  );
}
function applyFenceTempPolygonPath(
  polygon: any,
  points: LngLatTuple[],
  trailingPoint?: any
) {
  const nextPath = points.map(
    ([lng, lat]) => new (window as any).AMap.LngLat(lng, lat)
  );
  if (trailingPoint) nextPath.push(trailingPoint);
  polygon?.setPath?.(nextPath);
}
// 围栏打点阶段一旦形成自相交,立即回退到上一个合法点集,而不是等到双击闭合后再报错。
function enforceFenceDrawingSelfIntersection() {
  const tempPolygon = fenceMouseTool.value?.polygonTemp?.obj;
  const path = tempPolygon?.getPath?.();
  if (!Array.isArray(path) || !path.length) return false;
  const nextPoints = getFenceDrawingConfirmedPoints();
  if (nextPoints.length < 4) {
    lastValidFenceDrawingPoints.value = clonePolygonPath(nextPoints);
    return false;
  }
  if (!isFencePolygonSelfIntersect(nextPoints)) {
    lastValidFenceDrawingPoints.value = clonePolygonPath(nextPoints);
    return false;
  }
  const fallbackPoints = lastValidFenceDrawingPoints.value.length
    ? lastValidFenceDrawingPoints.value
    : nextPoints.slice(0, -1);
  applyFenceTempPolygonPath(tempPolygon, fallbackPoints, path[path.length - 1]);
  window.$message?.warning("运营区域不允许自相交");
  return true;
}
function enforceRestrictedDrawingVertexLimit() {
@@ -1956,16 +2006,38 @@
    }
  );
  fencePolyEditor.value.open();
  const syncFencePoints = () => {
    fenceDraftPoints.value = fenceDraftPolygon.value
      .getPath()
      .map((point: any) => [point.lng, point.lat] as LngLatTuple);
  let lastValidPoints = clonePolygonPath(fenceDraftPoints.value);
  const syncFenceDraftPointState = (points: LngLatTuple[]) => {
    fenceDraftPoints.value = clonePolygonPath(points);
    syncFenceStationWarningState(fenceDraftPoints.value);
    renderFenceEditGuide();
    logPolygonMatchedPoints(
      "fence-preview",
      getPointItemsWithinPolygon(fenceDraftPoints.value, pointSpatialData.value)
    );
  };
  const syncFencePoints = () => {
    const nextPoints = fenceDraftPolygon.value
      .getPath()
      .map((point: any) => [point.lng, point.lat] as LngLatTuple);
    // 创建完成后的拖点、加点、删点同样实时校验自相交,非法变更立即回滚。
    if (
      nextPoints.length >= 4 &&
      lastValidPoints.length >= 3 &&
      isFencePolygonSelfIntersect(nextPoints) &&
      !fenceEditorRollbacking
    ) {
      fenceEditorRollbacking = true;
      window.$message?.warning("运营区域不允许自相交");
      applyPolygonPath(fenceDraftPolygon.value, lastValidPoints);
      syncFenceDraftPointState(lastValidPoints);
      setTimeout(() => {
        fenceEditorRollbacking = false;
      }, 0);
      return;
    }
    syncFenceDraftPointState(nextPoints);
    if (nextPoints.length >= 3) lastValidPoints = clonePolygonPath(nextPoints);
  };
  fencePolyEditor.value.on("end", syncFencePoints);
  fencePolyEditor.value.on("adjust", syncFencePoints);
@@ -1977,6 +2049,7 @@
function startFencePolygonDraw() {
  if (!map.value || typeof (window as any).AMap === "undefined") return;
  cleanupFenceEditorState();
  lastValidFenceDrawingPoints.value = [];
  fenceMouseTool.value = new (window as any).AMap.MouseTool(map.value);
  fenceMouseTool.value.polygon({
    strokeColor: "#F19216",
@@ -2431,7 +2504,7 @@
        window.$message?.warning(
          `禁行区最多支持设置 ${restrictedAreaVertexLimit} 个点位`
        );
        applyRestrictedPolygonPath(polygon, lastValidPoints);
        applyPolygonPath(polygon, lastValidPoints);
        restrictedDrawingVertexCount.value = lastValidPoints.length;
        setTimeout(() => {
          restrictedEditorRollbacking = false;
@@ -4869,6 +4942,16 @@
      enforceRestrictedDrawingVertexLimit();
    }, 0);
  }
  if (
    activeTab.value === "fence" &&
    isFenceFormMode.value &&
    fenceMouseTool.value &&
    fenceInteractionMode.value !== "restricted-draw"
  ) {
    setTimeout(() => {
      enforceFenceDrawingSelfIntersection();
    }, 0);
  }
  if (suppressFenceEditGuideDismissOnce.value) return;
  dismissFenceEditGuide();
  if (
@@ -4903,7 +4986,7 @@
// ===== 围栏详情态与编辑态切换 =====
function resetFenceForm(detail?: any) {
  const workflowState = buildFenceWorkflowFormState(detail);
  workflowState.form.testStatus = resolveFenceLocalTestStatus(detail);
  workflowState.form.flowStatus = resolveFenceLocalFlowStatus(detail);
  fenceForm.value = workflowState.form;
  fenceStrategyList.value = workflowState.strategyList;
  if (workflowState.restrictedAreas) fenceRestrictedAreas.value = workflowState.restrictedAreas;
@@ -4955,7 +5038,7 @@
  fenceCreateMode.value = workflowState.fenceCreateMode;
  fenceEditMode.value = workflowState.fenceEditMode;
  fenceForm.value = workflowState.fenceForm;
  fenceForm.value.testStatus = 1;
  fenceForm.value.flowStatus = 1;
  fenceStrategyList.value = workflowState.fenceStrategyList;
  restrictedAreaForm.value = workflowState.restrictedAreaForm;
  fenceRestrictedAreas.value = workflowState.fenceRestrictedAreas;
@@ -5150,11 +5233,16 @@
function handleEnterFenceEdit() {
  if (!fenceDetailData.value) return;
  // 运营围栏在测试工作台仅允许查看详情;即使列表接口异常返回,也在前端兜底拦截。
  if (resolveFenceLocalFlowStatus(fenceDetailData.value) === 5) {
    window.$message?.warning("运营围栏不可编辑");
    return;
  }
  const workflowState = buildFenceEditModeState(fenceDetailData.value);
  fenceCreateMode.value = workflowState.fenceCreateMode;
  armFenceEditGuide();
  fenceForm.value = workflowState.fenceForm;
  fenceForm.value.testStatus = resolveFenceLocalTestStatus(fenceDetailData.value);
  fenceForm.value.flowStatus = resolveFenceLocalFlowStatus(fenceDetailData.value);
  fenceStrategyList.value = workflowState.fenceStrategyList;
  fenceEditMode.value = workflowState.fenceEditMode;
  enterFenceDraftByDetail(fenceDetailData.value);
@@ -5395,19 +5483,11 @@
  }
}
function applyLocalFenceTestStatus(testStatus: number) {
  const normalizedStatus = normalizeFenceTestStatus(testStatus);
  fenceForm.value.testStatus = normalizedStatus;
  if (fenceCreateMode.value) return;
  const fenceId = String(fenceDetailData.value?.id || selectedId.value || "");
  if (!fenceId) return;
  fenceLocalTestStatusMap.value = {
    ...fenceLocalTestStatusMap.value,
    [fenceId]: normalizedStatus,
  };
  if (fenceDetailData.value) fenceDetailData.value.testStatus = normalizedStatus;
  const listItem = fenceData.value.find((item) => item.id === fenceId);
  if (listItem) listItem.raw = { ...(listItem.raw || {}), testStatus: normalizedStatus };
function applyLocalFenceFlowStatus(flowStatus: number) {
  const normalizedStatus = normalizeFenceFlowStatus(flowStatus);
  fenceForm.value.flowStatus = normalizedStatus;
  // 围栏测试流转在编辑阶段只更新表单草稿;
  // 左右侧的最终状态展示仍以后端保存成功后的返回结果为准。
}
function handleFenceVerifyStatusUpdate(status: number) {
@@ -5418,11 +5498,11 @@
      "测试不通过后,当前围栏将保持不可运营状态,确定吗?"
    ).then((ok) => {
      if (!ok) return;
      applyLocalFenceTestStatus(status);
      applyLocalFenceFlowStatus(status);
    });
    return;
  }
  if (status === 3) {
  if (status === 5 || status === 3) {
    const overlapResult = findPolygonOverlapArea(
      fenceDraftPoints.value,
      getOtherOperationAreas()
@@ -5435,13 +5515,15 @@
      return;
    }
  }
  applyLocalFenceTestStatus(status);
  applyLocalFenceFlowStatus(status);
}
function handleFenceNextStep() {
  if (!fenceCreateMode.value && !fenceDetailData.value) return;
  if (normalizeFenceTestStatus(fenceForm.value.testStatus) !== 1) return;
  applyLocalFenceTestStatus(2);
  const currentStatus = normalizeFenceFlowStatus(fenceForm.value.flowStatus);
  if (currentStatus === 1) {
    applyLocalFenceFlowStatus(2);
  }
}
async function handleRestartFenceTest() {
@@ -5453,7 +5535,7 @@
    ))
  )
    return;
  applyLocalFenceTestStatus(1);
  applyLocalFenceFlowStatus(1);
}
async function handleCancelFenceEdit() {
@@ -5464,7 +5546,7 @@
  }
  const workflowState = buildFenceCancelEditState(fenceDetailData.value);
  fenceForm.value = workflowState.fenceForm;
  fenceForm.value.testStatus = resolveFenceLocalTestStatus(fenceDetailData.value);
  fenceForm.value.flowStatus = resolveFenceLocalFlowStatus(fenceDetailData.value);
  fenceStrategyList.value = workflowState.fenceStrategyList;
  fenceEditMode.value = workflowState.fenceEditMode;
  fenceEditSnapshot.value = workflowState.fenceEditSnapshot;
@@ -5735,9 +5817,9 @@
    window.$message?.warning("未找到运营区信息,暂无法编辑");
    return;
  }
  const hasGeometryChanged = !fenceCreateMode.value && hasFenceGeometryChanged();
  if (
    !fenceCreateMode.value &&
    hasFenceGeometryChanged() &&
    hasGeometryChanged &&
    !(await openConfirmDialog(
      "提示",
      "已修改围栏边界或禁行区边界,保存后围栏将重回测试状态,确定保存吗?",
@@ -5804,6 +5886,7 @@
      fenceCreateMode: fenceCreateMode.value,
      fenceId: String(fenceDetailData.value?.id || ""),
      currentStatus: fenceDetailData.value?.status,
      hasGeometryChanged,
      cityValue: cityValue.value,
      fenceDetailCityCode: fenceDetailData.value?.cityCode,
      saveType,
@@ -6044,16 +6127,17 @@
    if (fenceRes.code === 200 && fenceRes.data?.records) {
      fenceData.value = fenceRes.data.records.map((item: any) => {
        const localTestStatus = resolveFenceLocalTestStatus(item);
        const localFlowStatus = resolveFenceLocalFlowStatus(item);
        return {
          id: String(item.id),
          name: item.electronicName || "-",
          status: getFenceStatusText(item.status),
          // 测试工作台左侧围栏状态优先看 flowStatus,只有 flowStatus=5 时才沿用旧主状态。
          status: getFenceWorkbenchStatusText(localFlowStatus, item.status),
          desc: formatFenceListDesc(item),
          type: "fence",
          raw: {
            ...item,
            testStatus: localTestStatus,
            flowStatus: localFlowStatus,
          },
        };
      });
@@ -7262,9 +7346,9 @@
          :strategy-list="fenceStrategyList"
          :detail-rows="detailRows"
          :fence-detail-data="fenceDetailData"
          :current-fence-test-status-text="currentFenceTestStatusText"
          :current-fence-flow-status-text="currentFenceFlowStatusText"
          :status-meta="{
            testStatus: normalizeFenceTestStatus(fenceForm.testStatus),
            flowStatus: normalizeFenceFlowStatus(fenceForm.flowStatus),
          }"
          :button-config="fenceButtonConfig"
          :strategy-summary="fenceStrategySummary"
src/views/testDemo/operationWorkbench/status.ts
@@ -19,13 +19,33 @@
  return "待测试";
}
export function getFenceTestStatusText(testStatus?: number | string) {
  const status = Number(testStatus);
export function getFenceFlowStatusText(flowStatus?: number | string) {
  const status = Number(flowStatus);
  if (status === 2) return "测试中";
  if (status === 3) return "可运营";
  if (status === 3 || status === 5) return "可运营";
  if (status === 4) return "测试不通过";
  return "待测试";
}
function getFenceOperationStatusText(status?: number | string) {
  const normalizedStatus = Number(status);
  if (normalizedStatus === 1) return "待生效";
  if (normalizedStatus === 2) return "启用中";
  if (normalizedStatus === 3) return "已停用";
  return "草稿";
}
// 测试工作台左侧围栏状态展示规则:
// flowStatus=5 说明围栏已经进入运营态,此时沿用服务端主状态;
// 其余情况统一展示测试流状态,并复用点位的状态颜色样式。
export function getFenceWorkbenchStatusText(
  flowStatus?: number | string,
  status?: number | string
) {
  return Number(flowStatus) === 5
    ? getFenceOperationStatusText(status)
    : getFenceFlowStatusText(flowStatus);
}
export interface PointStatusMetricItem {
@@ -167,7 +187,7 @@
  if (["待测试"].includes(String(status || ""))) return "wait-test";
  if (["测试中"].includes(String(status || ""))) return "testing";
  if (["测试不通过"].includes(String(status || ""))) return "test-fail";
  if (["可运营"].includes(String(status || ""))) return "test-pass";
  if (["测试通过", "可运营"].includes(String(status || ""))) return "test-pass";
  if (["启用中", "运营中"].includes(String(status || ""))) return "test-starting";
  if (["待生效", "待配置"].includes(String(status || ""))) return "test-take-effect";
  if (["草稿", "已停用"].includes(String(status || ""))) return "test-stop";
src/views/testDemo/operationWorkbench/workbench.constants.ts
@@ -74,6 +74,10 @@
// 三类对象在左侧筛选区共用的状态筛选项。
export const statusOptionsMap: Record<WorkbenchTab, OptionItem[]> = {
  fence: [
    { label: "待测试", value: "wait-test" },
    { label: "测试中", value: "testing" },
    { label: "测试不通过", value: "test-fail" },
    { label: "可运营", value: "operable" },
    { label: "草稿", value: "draft" },
    { label: "待生效", value: "pending" },
    { label: "启用中", value: "enabled" },
src/views/testDemo/operationWorkbench/workbench.fence-workflow.ts
@@ -15,7 +15,7 @@
    id: "",
    electronicName: "",
    areaDescription: "",
    testStatus: 1,
    flowStatus: 1,
    areaType: 1,
    areaStatus: 1,
    effectType: 2,
@@ -119,7 +119,7 @@
    id: String(detail?.id || ""),
    electronicName: detail?.electronicName || "",
    areaDescription: operationArea?.areaDescription || "",
    testStatus: Number(detail?.testStatus) || 1,
    flowStatus: Number(detail?.flowStatus) || 1,
    areaType: 1,
    areaStatus: Number(operationArea?.areaStatus) || 1,
    effectType: detail ? Number(detail?.effectType) || 2 : 2,
src/views/testDemo/operationWorkbench/workbench.payload.ts
@@ -181,6 +181,7 @@
  fenceCreateMode: boolean;
  fenceId?: string;
  currentStatus?: number;
  hasGeometryChanged?: boolean;
  cityValue: string;
  fenceDetailCityCode?: string | number;
  saveType: number;
@@ -198,6 +199,7 @@
    fenceCreateMode,
    fenceId,
    currentStatus,
    hasGeometryChanged,
    cityValue,
    fenceDetailCityCode,
    saveType,
@@ -238,6 +240,15 @@
  return {
    ...(fenceCreateMode ? {} : { id: fenceId }),
    status,
    flowStatus:
      // 围栏运营区或禁行区边界一旦变更,保存后需要回到待测试状态。
      hasGeometryChanged
        ? 1
        : Number(fenceForm.flowStatus) === 3
        ? 5
        : [1, 2, 3, 4, 5].includes(Number(fenceForm.flowStatus))
        ? Number(fenceForm.flowStatus)
        : 1,
    cityCode: fenceCreateMode
      ? cityValue || fenceDetailCityCode || 0
      : fenceDetailCityCode || cityValue || 0,