| | |
| | | @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 |
| | |
| | | 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> |
| | |
| | | |
| | | <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(); |
| | |
| | | }, |
| | | }); |
| | | |
| | | 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) => { |
| | |
| | | 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( |
| | |
| | | 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?.("获取工艺路线配置失败"); |
| | | } |
| | |
| | | } |
| | | ); |
| | | |
| | | const handleConfigChange = async () => { |
| | | await fetchConfigDetail(selectedConfigId.value); |
| | | const handleRouteChange = async () => { |
| | | await fetchRouteSteps(selectedRouteId.value); |
| | | }; |
| | | |
| | | const handleStepCheckedChange = step => { |
| | | step.checked = Boolean(step.checked); |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | |
| | | |
| | | 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> |
| | | |
| | |
| | | padding: 10px 12px; |
| | | margin-right: 10px; |
| | | box-sizing: border-box; |
| | | position: relative; |
| | | } |
| | | |
| | | .process-diagram-checkbox { |
| | | position: absolute; |
| | | top: 8px; |
| | | right: 8px; |
| | | } |
| | | |
| | | .process-diagram-index { |
| | |
| | | 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> |