src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue
@@ -7,33 +7,55 @@
    @close="handleClose"
  >
    <el-row :gutter="20">
      <el-col :span="10">
        <div style="font-weight: 600; margin-bottom: 8px;">配置</div>
      <el-col :span="24">
        <div class="dialog-topbar">
          <div>
            <div style="font-weight: 600; margin-bottom: 8px;">配置</div>
            <div style="font-size: 12px; margin-bottom: 8px;">
              <span v-if="boundRouteName" style="color: #67c23a;">已绑定:{{ boundRouteName }}</span>
              <span v-else style="color: #e6a23c;">未绑定</span>
            </div>
          </div>
          <div class="export-toolbar">
            <el-date-picker
              v-model="exportDateRange"
              type="daterange"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              clearable
              style="width: 280px;"
            />
            <el-button type="success" plain @click="exportSelectedSteps">导出已勾选</el-button>
          </div>
        </div>
        <el-select
          v-model="selectedConfigId"
          v-model="selectedRouteId"
          filterable
          clearable
          placeholder="请选择工艺路线配置"
          placeholder="请选择工艺路线"
          style="width: 100%;"
          @change="handleConfigChange"
          @change="handleRouteChange"
        >
          <el-option
            v-for="cfg in configList"
            :key="cfg.configId"
            :label="cfg.configName"
            :value="cfg.configId"
            v-for="cfg in routeList"
            :key="cfg.routeId"
            :label="cfg.processRouteName"
            :value="cfg.routeId"
          />
        </el-select>
        <el-divider style="margin: 16px 0;" />
        <div style="font-weight: 600; margin-bottom: 8px;">步骤预览</div>
        <div style="font-size: 12px; color: #909399; margin-bottom: 6px;">
        <div style="font-size: 12px; color: #909399; margin-bottom: 10px;">
          根据所选配置展示流程图
        </div>
      </el-col>
      <el-col :span="14">
      <el-col :span="24">
        <div class="process-diagram">
          <div v-if="steps.length === 0" class="process-diagram-empty">暂无步骤</div>
          <div
@@ -42,13 +64,18 @@
            class="process-diagram-segment"
          >
            <div class="process-diagram-node">
              <el-checkbox
                v-model="step.checked"
                class="process-diagram-checkbox"
                @change="() => handleStepCheckedChange(step)"
              />
              <div class="process-diagram-index">{{ idx + 1 }}</div>
              <div class="process-diagram-name">{{ step.processName }}</div>
            </div>
            <div v-if="idx < steps.length - 1" class="process-diagram-arrow">→</div>
          </div>
        </div>
        <div v-if="selectedConfigId === null" style="margin-top: 10px; font-size: 12px; color: #909399;">
        <div v-if="selectedRouteId === null" style="margin-top: 10px; font-size: 12px; color: #909399;">
          请先选择一条已维护好的工艺路线
        </div>
      </el-col>
@@ -67,13 +94,16 @@
<script setup>
import { computed, getCurrentInstance, ref, watch } from "vue";
import { salesProcessFlowConfigList, salesProcessFlowConfigGetById } from "@/api/salesManagement/salesProcessFlowConfig.js";
import { salesProcessFlowConfigList, salesProcessFlowConfigItemList } from "@/api/salesManagement/salesProcessFlowConfig.js";
const emit = defineEmits(["update:visible", "confirm"]);
const props = defineProps({
  visible: { type: Boolean, default: false },
  defaultConfigId: { type: [Number, String, null], default: null },
  // 打开弹窗时的回显:若业务已绑定工艺路线则传入该 routeId;否则默认展示列表第一条
  defaultRouteId: { type: [Number, String, null], default: null },
  // 页面提示:订单已绑定的工艺路线名称
  boundRouteName: { type: String, default: "" },
});
const { proxy } = getCurrentInstance();
@@ -87,9 +117,10 @@
  },
});
const configList = ref([]);
const selectedConfigId = ref(null);
const routeList = ref([]);
const selectedRouteId = ref(null);
const steps = ref([]);
const exportDateRange = ref([]);
const saving = ref(false);
const normalizeStepsFromApi = (list) => {
@@ -99,29 +130,35 @@
    processId: s.processId ?? s.process_id ?? s.id ?? null,
    processName: s.processName ?? s.process_name ?? s.name ?? "",
    sortNo: s.sortNo ?? idx + 1,
    isCompleted: Number(s.isCompleted ?? s.completed ?? 0),
    checked: Boolean(s.checked ?? false),
  }));
};
const fetchConfigList = async () => {
  const res = await salesProcessFlowConfigList();
  const list = res?.data ?? res?.records ?? res ?? [];
  configList.value = Array.isArray(list) ? list : [];
const normalizeRouteList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((r) => ({
    routeId: r.routeId ?? r.id ?? null,
    processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "",
    isDefault: Boolean(r.isDefault),
  }));
};
const fetchConfigDetail = async (id) => {
  if (!id) {
const fetchRouteList = async () => {
  // 选择弹窗:尽量一次性拉全,避免分页影响选择体验
  const res = await salesProcessFlowConfigList({ current: 1, size: 1000 });
  const records = res?.records ?? res?.data?.records ?? res?.data ?? res ?? [];
  routeList.value = normalizeRouteList(records).filter((r) => r.routeId !== null && r.routeId !== undefined && r.routeId !== "");
};
const fetchRouteSteps = async (routeId) => {
  if (!routeId) {
    steps.value = [];
    return;
  }
  const res = await salesProcessFlowConfigGetById(id);
  const detail = res?.data ?? res ?? {};
  steps.value = normalizeStepsFromApi(detail?.steps ?? []);
};
const initDefault = async () => {
  await fetchConfigList();
  selectedConfigId.value = props.defaultConfigId ?? null;
  await fetchConfigDetail(selectedConfigId.value);
  const res = await salesProcessFlowConfigItemList(routeId);
  const raw = res?.data ?? res ?? [];
  steps.value = normalizeStepsFromApi(raw);
};
watch(
@@ -129,7 +166,18 @@
  async (v) => {
    if (v) {
      try {
        await initDefault();
        await fetchRouteList();
        // 回显绑定:
        // 1. 若传入 defaultRouteId,则优先使用它
        // 2. 否则优先选中标记为默认(isDefault=true)的工艺路线
        // 3. 若都没有,则回退为第一条
        const first = routeList.value?.[0] ?? null;
        const defaultRoute =
          routeList.value.find((r) => r.isDefault) ?? first;
        const desired = props.defaultRouteId ?? (defaultRoute ? defaultRoute.routeId : null);
        selectedRouteId.value = desired ?? null;
        await fetchRouteSteps(selectedRouteId.value);
      } catch {
        proxy?.$modal?.msgError?.("获取工艺路线配置失败");
      }
@@ -137,8 +185,12 @@
  }
);
const handleConfigChange = async () => {
  await fetchConfigDetail(selectedConfigId.value);
const handleRouteChange = async () => {
  await fetchRouteSteps(selectedRouteId.value);
};
const handleStepCheckedChange = step => {
  step.checked = Boolean(step.checked);
};
const handleClose = () => {
@@ -148,19 +200,50 @@
const confirmSelect = async () => {
  if (saving.value) return;
  if (selectedConfigId.value === null || selectedConfigId.value === undefined || selectedConfigId.value === "") {
    proxy?.$modal?.msgWarning?.("请选择工艺路线配置");
  if (selectedRouteId.value === null || selectedRouteId.value === undefined || selectedRouteId.value === "") {
    proxy?.$modal?.msgWarning?.("请选择工艺路线");
    return;
  }
  saving.value = true;
  try {
    emit("confirm", selectedConfigId.value);
    handleClose();
    emit("confirm", selectedRouteId.value);
  } catch (e) {
    proxy?.$modal?.msgError?.("确认失败,请稍后重试");
  } finally {
    saving.value = false;
  }
};
const exportSelectedSteps = () => {
  const selectedSteps = steps.value.filter(step => step.checked);
  if (selectedSteps.length === 0) {
    proxy?.$modal?.msgWarning?.("请先勾选要导出的工序");
    return;
  }
  const payload = {
    exportDateRange: Array.isArray(exportDateRange.value) ? exportDateRange.value : [],
    routeId: selectedRouteId.value,
    routeName: routeList.value.find(item => String(item.routeId) === String(selectedRouteId.value))?.processRouteName || "",
    steps: selectedSteps.map(step => ({
      processId: step.processId,
      processName: step.processName,
      sortNo: step.sortNo,
      isCompleted: Number(step.isCompleted ?? 0),
    })),
  };
  const blob = new Blob([JSON.stringify(payload, null, 2)], {
    type: "application/json;charset=utf-8",
  });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  const dateText =
    Array.isArray(exportDateRange.value) && exportDateRange.value.length === 2
      ? `${exportDateRange.value[0]}_${exportDateRange.value[1]}`
      : "all";
  a.href = url;
  a.download = `工艺路线导出_${dateText}.json`;
  a.click();
  URL.revokeObjectURL(url);
};
</script>
@@ -192,6 +275,13 @@
  padding: 10px 12px;
  margin-right: 10px;
  box-sizing: border-box;
  position: relative;
}
.process-diagram-checkbox {
  position: absolute;
  top: 8px;
  right: 8px;
}
.process-diagram-index {
@@ -230,5 +320,18 @@
  justify-content: flex-end;
  gap: 10px;
}
</style>
.dialog-topbar {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
}
.export-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
</style>