董国庆
2026-05-07 5e27d80ff458af7c2ecd973de458f712a8017ede
fix(运营工作台): 修复场站切换时城市未重置和任务单状态显示问题

修复场站切换时未重置城市和默认值的问题,确保城市状态正确同步。
调整任务单紧急程度三级的状态显示为警告色,统一多个页面的视觉标识。
修正车辆管理页面状态标签的颜色和显示方式,使用标签组件增强可读性。
优化围栏限制区域交互,增加只读模式和高亮显示,防止误操作。
修复场站绘制时的几何校验逻辑,避免与已有场站重叠或相交。
调整运营点保存逻辑,自动关联所在场站ID,提升数据准确性。
修正场站状态归一化逻辑,统一停用状态的判断条件。
修复地图围栏区域渲染样式,优化视觉层次和交互反馈。
8个文件已修改
223 ■■■■■ 已修改文件
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.payload.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.station-workflow.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/pendingReassignment/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue
@@ -257,7 +257,7 @@
      const urgencyMap: Record<number, { type: 'error' | 'warning' | 'info' | 'success', text: string }> = {
        0: { type: 'error', text: '一级' },
        1: { type: 'error', text: '二级' },
        2: { type: 'error', text: '三级' },
        2: { type: 'warning', text: '三级' },
        // 3: { type: 'success', text: '四级' },
      }
      const urgency = urgencyMap[row.taskUrgencyLevel]
src/views/assetManagement/vehicleManagement/operatingVehicle/index.vue
@@ -1,6 +1,6 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NEllipsis, NSpace } from 'naive-ui'
import { NButton, NEllipsis, NSpace, NTag } from 'naive-ui'
import ConfirmDialog from '@/components/ConfirmDialog/index.vue'
import { useRouter } from 'vue-router'
import { useBoolean } from '@/hooks'
@@ -161,10 +161,10 @@
})
const statusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  1: { type: 'success', text: '运营中' },
  2: { type: 'warning', text: '维修中' },
  3: { type: 'info', text: '已收车' },
  4: { type: 'error', text: '车辆异常' },
  1: { type: 'info', text: '运营中' },
  2: { type: 'error', text: '维修中' },
  3: { type: 'success', text: '已收车' },
  4: { type: 'warning', text: '车辆异常' },
}
function formatDateTime(value: any) {
@@ -258,7 +258,7 @@
    resizable: true,
    render: (row) => {
      const status = statusMap[row.operatingStatus] || { type: 'default', text: row.operatingStatus }
      return <span>{status.text}</span>
      return <NTag type={status.type} size="small" bordered={false}>{status.text}</NTag>
    },
  },
  {
src/views/businessOperations/operationWorkbench/list.vue
@@ -442,6 +442,8 @@
function handleTabChange(tab: WorkbenchTab) {
  if (tab === activeTab.value)
    return
  resetTabCity(tab)
  cityValue.value = DEFAULT_CITY_CODE
  activeTab.value = tab
}
src/views/businessOperations/operationWorkbench/map.vue
@@ -449,6 +449,7 @@
const fenceDraftPoints = ref<LngLatTuple[]>([]);
const fenceRestrictedAreas = ref<FenceRestrictedAreaDraft[]>([]);
const activeRestrictedAreaId = ref("");
const readonlyRestrictedAreaId = ref("");
const fenceInteractionMode = ref<FenceInteractionMode>("operation-preview");
const activeRestrictedEditorId = ref("");
const restrictedDrawSessionId = ref(0);
@@ -580,7 +581,11 @@
    (item) => item.id === selectedId.value
  );
  if (matchedStation) return matchedStation;
  if (String(stationDetailData.value?.id || "") === String(selectedId.value || ""))
  if (
    stationDetailData.value
    && selectedId.value
    && String(stationDetailData.value.id || "") === String(selectedId.value)
  )
    return mapStationApiItemToListItem(stationDetailData.value);
  return (
    currentList.value[0] ||
@@ -900,11 +905,11 @@
    showStopButton: verificationStatus === 5 && operationStatus === 2,
    showRestartButton:
      verificationStatus === 4 ||
      operationStatus === 3 ||
      operationStatus === 4,
      (verificationStatus === 5 &&
        (operationStatus === 3 || operationStatus === 4)),
    showNextStep:
      (verificationStatus === 1 && operationStatus === 1) ||
      (verificationStatus === 3 && operationStatus === 1) ||
      verificationStatus === 1 ||
      verificationStatus === 3 ||
      (verificationStatus === 5 && operationStatus === 1),
  };
});
@@ -1214,7 +1219,12 @@
    : [];
  return {
    operationAreas: areas.filter((item: any) => Number(item.areaType) === 1),
    restrictedAreas: areas.filter((item: any) => Number(item.areaType) === 2),
    restrictedAreas: areas
      .filter((item: any) => Number(item.areaType) === 2)
      .map((item: any) => ({
        ...item,
        isActive: readonlyRestrictedAreaId.value === String(item.id),
      })),
  };
});
@@ -1378,6 +1388,22 @@
    .filter((item) => item.points.length >= 3);
}
function getOtherStations() {
  const currentStationId =
    stationEditMode.value && !stationCreateMode.value
      ? String(stationDetailData.value?.id || selectedId.value || "")
      : "";
  return stationData.value
    .filter((item) => Array.isArray(item.polygonPath) && item.polygonPath.length >= 3)
    .filter((item) => String(item.id || "") !== currentStationId)
    .map((item) => ({
      id: String(item.id || ""),
      name: item.name || "未命名场站",
      points: clonePolygonPath(item.polygonPath),
    }))
    .filter((item) => item.points.length >= 3);
}
function getStationGeometryInvalidReason(points: LngLatTuple[]) {
  if (points.length < 3) return "请绘制至少3个点的场站";
  if (isFencePolygonSelfIntersect(points)) return "场站区域不允许自相交";
@@ -1386,6 +1412,25 @@
    isRingInRing(points, area.points)
  );
  if (!isInsideOperationArea) return "场站需完全处于运营区内";
  const otherStations = getOtherStations();
  const geometryUtil = (window as any).AMap?.GeometryUtil;
  if (geometryUtil) {
    const crossStation = otherStations.find((station) =>
      geometryUtil.doesRingRingIntersect(points, station.points)
    );
    if (crossStation)
      return `当前区域与${crossStation.name}相交,请避开已有场站后重新绘制`;
    const insideStation = otherStations.find((station) =>
      geometryUtil.isRingInRing(points, station.points)
    );
    if (insideStation)
      return `当前区域完全落在${insideStation.name}内,请在其他区域绘制`;
    const containStation = otherStations.find((station) =>
      geometryUtil.isRingInRing(station.points, points)
    );
    if (containStation)
      return `当前区域包含${containStation.name},请勿覆盖已有场站`;
  }
  const overlapRestrictedArea = getStationRestrictedAreas().find((area) =>
    doesPolygonOverlap(points, area.points)
  );
@@ -1584,6 +1629,7 @@
    item.isActive = false;
  });
  activeRestrictedAreaId.value = "";
  readonlyRestrictedAreaId.value = "";
  fenceDraftPoints.value = [];
  invalidFenceStationIds.value = [];
  updateFenceStationWarningStyles();
@@ -2019,9 +2065,9 @@
          ? "transparent"
          : "#EF938B",
        fillColor: isActive ? "#F14637" : "#EF938B",
        strokeWeight: isActive ? 2 : 1,
        strokeOpacity: 1,
        fillOpacity: isActive ? 0.18 : isOperationAreaActive ? 1 : 0.18,
        strokeWeight: 0,
        strokeOpacity: 0,
        fillOpacity: isActive ? 0.32 : isOperationAreaActive ? 1 : 0.18,
        strokeStyle: "solid",
        cursor: "pointer",
        zIndex: isActive ? 230 : 210,
@@ -2082,6 +2128,23 @@
}
function focusRestrictedArea(id: string) {
  if (!isFenceFormMode.value) {
    const nextId = String(id || "");
    readonlyRestrictedAreaId.value =
      readonlyRestrictedAreaId.value === nextId ? "" : nextId;
    renderMapEntities();
    if (!readonlyRestrictedAreaId.value) return;
    const polygon = fencePolygons.value.find((item: any) => {
      const extData = item.getExtData?.() || {};
      return (
        Number(extData.areaType) === 2 &&
        String(extData.areaId || "") === readonlyRestrictedAreaId.value
      );
    });
    if (polygon && map.value)
      map.value.setFitView([polygon], false, [120, 120, 120, 120]);
    return;
  }
  const area = fenceRestrictedAreas.value.find((item) => item.id === id);
  if (!area?.polygon) {
    startRestrictedAreaDraw(id);
@@ -2528,6 +2591,16 @@
  } catch {
    return false;
  }
}
function resolveStationByPoint(lon: number, lat: number) {
  if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
  const targetPoint: LngLatTuple = [lon, lat];
  return stationData.value.find((item) => {
    if (!Array.isArray(item.polygonPath) || item.polygonPath.length < 3)
      return false;
    return isPointInFencePath(targetPoint, item.polygonPath);
  }) || null;
}
function getFenceOriginalOperationPoints() {
@@ -3089,6 +3162,7 @@
    if (tab === "fence" && fenceCreateMode.value) return;
    if (tab === "station" && stationCreateMode.value) return;
    if (!id) {
      readonlyRestrictedAreaId.value = "";
      cleanupPointEditMapState();
      pointEditMode.value = false;
      pointCreateMode.value = false;
@@ -3107,6 +3181,7 @@
    }
    if (tab === "point") {
      readonlyRestrictedAreaId.value = "";
      fenceDetailData.value = null;
      stationDetailData.value = null;
      stationEditMode.value = false;
@@ -3132,6 +3207,7 @@
    pointCreateMode.value = false;
    pointCreateReady.value = false;
    pointDetailData.value = null;
    readonlyRestrictedAreaId.value = "";
    stationEditMode.value = false;
    stationCreateMode.value = false;
    stationEditSnapshot.value = "";
@@ -3428,6 +3504,18 @@
    return;
  }
  selectedStatuses.value = [...selectedStatuses.value, value];
}
function toggleCityDropdown() {
  const nextVisible = !cityDropdownVisible.value;
  statusDropdownVisible.value = false;
  cityDropdownVisible.value = nextVisible;
}
function toggleStatusDropdown() {
  const nextVisible = !statusDropdownVisible.value;
  cityDropdownVisible.value = false;
  statusDropdownVisible.value = nextVisible;
}
function handleSelectCity(value: string) {
@@ -4612,6 +4700,7 @@
  fenceDetailLoading.value = true;
  fenceEditMode.value = false;
  fenceCreateMode.value = false;
  readonlyRestrictedAreaId.value = "";
  try {
    const res = await getFenceDetail(id);
    if (res.code === 200 && res.data) {
@@ -4939,8 +5028,16 @@
  let nextV = v;
  let nextO = o;
  if (v === 1 && o === 1) nextV = 2;
  else if (v === 3 && o === 1) nextV = 5;
  // verificationStatus 非 5 时,按钮和流转都仅按 verificationStatus 判断;
  // 流转时顺带把 operationStatus 归一到正常链路,避免脏数据继续传递。
  if (v === 1) {
    nextV = 2;
    nextO = 1;
  }
  else if (v === 3) {
    nextV = 5;
    nextO = 1;
  }
  else if (v === 5 && o === 1) nextO = 2;
  else return;
@@ -5165,6 +5262,10 @@
  let electronicAreaId =
    pointDetailData.value?.electronicAreaId?.toString() || "";
  let fenceId = (pointDetailData.value as any)?.fenceId?.toString() || "";
  const containingStation = resolveStationByPoint(
    Number(pointForm.value.pointLon),
    Number(pointForm.value.pointLat)
  );
  if (operationArea) {
    electronicAreaId = operationArea.id?.toString() || "";
    fenceId = operationArea.electronicId?.toString() || "";
@@ -5195,6 +5296,7 @@
      supportOperationTime,
      electronicAreaId,
      fenceId,
      stationId: containingStation?.id,
    });
    const res = pointCreateMode.value
      ? await createPoint(basePayload as Api.Point.CreateData)
@@ -5782,6 +5884,7 @@
        return false;
      })
      .map((item) => {
        const areaType = Number(item.areaType);
        const points = String(item.fencePoints)
          .split(",")
          .map((pointStr: string) => {
@@ -5798,49 +5901,67 @@
        const isActive =
          activeTab.value === "fence" &&
          selectedId.value === String(item.electronicId);
        const isFenceReadonlyDimmed =
          isFenceFormMode.value && Number(item.areaType) === 1;
        const isReadonlyRestrictedActive =
          areaType === 2 &&
          activeTab.value === "fence" &&
          !isFenceFormMode.value &&
          isActive &&
          readonlyRestrictedAreaId.value === String(item.id);
        const isFenceReadonlyDimmed = areaType === 1 && !isActive;
        const isFenceDetailActive =
          Number(item.areaType) === 1 && isActive && !isFenceFormMode.value;
          areaType === 1 && isActive && !isFenceFormMode.value;
        const polygon = new (window as any).AMap.Polygon({
          path: points,
          fillColor:
            item.areaType === 2
              ? "#EF938B"
            areaType === 2
              ? isReadonlyRestrictedActive
                ? "#F14637"
                : "#EF938B"
              : isFenceDetailActive
              ? "#19B144"
              : isFenceReadonlyDimmed
              ? "#171F26"
              : "#171F26",
          strokeColor:
            areaType === 2
              ? isReadonlyRestrictedActive
                ? "#F14637"
                : "#EF938B"
              : isFenceDetailActive
              ? "#19B144"
              : "#0068FF",
          strokeColor:
            item.areaType === 2
              ? "#EF938B"
              : isFenceReadonlyDimmed
              ? "#AEB0B2"
              : isFenceDetailActive
              ? "#19B144"
              : "#0068FF",
          strokeWeight: isFenceDetailActive ? 2 : isActive ? 3 : 2,
              : "#AEB0B2",
          strokeWeight: areaType === 2
            ? 0
            : isFenceDetailActive
            ? 2
            : isActive
            ? 3
            : 2,
          strokeOpacity: areaType === 2 ? 0 : 1,
          fillOpacity:
            item.areaType === 2
              ? 0.4
              : isFenceReadonlyDimmed
              ? 0.08
            areaType === 2
              ? isReadonlyRestrictedActive
                ? 1
                : 1
              : isFenceDetailActive
              ? 0.1
              : isFenceReadonlyDimmed
              ? 0.08
              : isActive
              ? 0.24
              : 0.12,
              ? 0.06
              : 0.04,
          bubble: true,
          strokeStyle:
            Number(item.areaType) === 1 &&
            (isFenceReadonlyDimmed || isFenceDetailActive)
            areaType === 1 && isFenceReadonlyDimmed
              ? "dashed"
              : "solid",
          clickable:
            !isPointMapPickActive.value &&
            !isFenceFormMode.value &&
            !isStationFormMode.value,
          zIndex: isReadonlyRestrictedActive ? 230 : isActive ? 220 : 205,
        });
        polygon.on("click", () => {
          if (
@@ -5854,7 +5975,8 @@
        polygon.setExtData({
          id: String(item.electronicId),
          type: "fence",
          areaType: item.areaType,
          areaType,
          areaId: String(item.id || ""),
        });
        return polygon;
      })
@@ -6098,7 +6220,7 @@
              type="button"
              class="city-dropdown-trigger"
              :class="{ active: cityDropdownVisible }"
              @click.stop="cityDropdownVisible = !cityDropdownVisible"
              @click.stop="toggleCityDropdown"
            >
              <span class="city-dropdown-label">{{ currentCityLabel }}</span>
              <img
@@ -6182,7 +6304,7 @@
              <div
                class="filter-item"
                :class="{ active: selectedStatuses.length > 0 }"
                @click.stop="statusDropdownVisible = !statusDropdownVisible"
                @click.stop="toggleStatusDropdown"
              >
                <span class="filter-text">{{
                  selectedStatuses.length
src/views/businessOperations/operationWorkbench/workbench.payload.ts
@@ -60,6 +60,7 @@
  supportOperationTime: boolean;
  electronicAreaId: string;
  fenceId: string;
  stationId?: string;
}) {
  const {
    administrativeInfo,
@@ -72,6 +73,7 @@
    supportOperationTime,
    electronicAreaId,
    fenceId,
    stationId,
  } = args;
  // 自定义运营时段的接口入参。
@@ -107,6 +109,7 @@
        : undefined,
    electronicAreaId,
    fenceId,
    stationId,
  };
}
src/views/businessOperations/operationWorkbench/workbench.station-workflow.ts
@@ -27,13 +27,19 @@
// 场站详情转页面编辑表单结构。
export function buildStationWorkflowFormState(detail?: any | null) {
  const normalizedAreaStatus
    = Number(detail?.areaStatus) === 2
      || Number(detail?.status) === 3
      || String(detail?.status || detail?.statusText || "").trim() === "已停用"
      ? 2
      : 1;
  // 归一化后的场站表单。
  const form: StationFormState = {
    id: String(detail?.id || ""),
    stationName: detail?.stationName || detail?.name || "",
    stationNumber: detail?.stationNumber || "",
    description: detail?.description || detail?.desc || "",
    areaStatus: Number(detail?.areaStatus || 1),
    areaStatus: normalizedAreaStatus,
    operationType: String(detail?.operationType || "1") === "2" ? "2" : "1",
    effectivePeriod: Number(detail?.effectivePeriod || 1),
    parkingTimeLimit: detail?.parkingTimeLimit
src/views/operations/taskOrder/pendingReassignment/index.vue
@@ -72,7 +72,7 @@
const taskUrgencyLevelOptions = [
  { label: '一级', value: 0, type: 'error' },
  { label: '二级', value: 1, type: 'error' },
  { label: '三级', value: 2, type: 'error' },
  { label: '三级', value: 2, type: 'warning' },
  // { label: '四级', value: 3, type: 'warning' },
]
const statusOptions = [
src/views/operations/taskOrder/taskOrderCenter/index.vue
@@ -72,7 +72,7 @@
const taskUrgencyLevelOptions = [
  { label: '一级', value: 0, type: 'error' },
  { label: '二级', value: 1, type: 'error' },
  { label: '三级', value: 2, type: 'error' },
  { label: '三级', value: 2, type: 'warning' },
  // { label: '四级', value: 3, type: 'warning' },
]
const statusOptions = [