<template>
|
<div style="display: inline-block; margin-left: 8px;">
|
<el-button type="primary" plain @click="openDialog">工艺流程</el-button>
|
<el-dialog
|
v-model="visible"
|
title="工艺路线与工序维护"
|
width="1280px"
|
class="process-route-dialog"
|
:close-on-click-modal="false"
|
@close="closeDialog"
|
>
|
<el-row :gutter="16" class="dialog-main">
|
<el-col :span="10">
|
<div class="left-panel">
|
<div style="font-weight: 600; margin-bottom: 10px;">工艺路线</div>
|
<div class="route-toolbar route-toolbar-left">
|
<el-input
|
v-model="routeKeyword"
|
placeholder="按工艺路线名称查询"
|
clearable
|
@keyup.enter="handleRouteQuery"
|
/>
|
<el-button type="primary" @click="handleRouteQuery">查询</el-button>
|
<el-input
|
v-model="routeNameDraft"
|
placeholder="输入名称后新增"
|
clearable
|
/>
|
<el-button type="primary" plain @click="createRoute">新增</el-button>
|
</div>
|
|
<div class="left-table-wrap">
|
<el-table
|
:data="routeList"
|
border
|
row-key="routeId"
|
highlight-current-row
|
height="100%"
|
table-layout="fixed"
|
@current-change="handleRouteSelect"
|
>
|
<el-table-column label="序号" width="56" align="center">
|
<template #default="scope">{{ scope.$index + 1 }}</template>
|
</el-table-column>
|
<el-table-column label="工艺路线名称" min-width="150" prop="processRouteName" show-overflow-tooltip />
|
<el-table-column label="默认" width="56" align="center">
|
<template #default="scope">
|
<el-tag v-if="scope.row.isDefault" type="success" size="small">是</el-tag>
|
<span v-else>-</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="160" align="center">
|
<template #default="scope">
|
<el-button link type="primary" size="small" @click="editRoute(scope.row)">改名</el-button>
|
<el-button link type="primary" size="small" @click="setDefaultRoute(scope.row)">默认</el-button>
|
<el-button link type="danger" size="small" @click="deleteRoute(scope.row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<pagination
|
v-show="routeTotal > 0"
|
:total="routeTotal"
|
layout="total, sizes, prev, pager, next, jumper"
|
:page="routePage.current"
|
:limit="routePage.size"
|
@pagination="handleRoutePaginationChange"
|
/>
|
</div>
|
</el-col>
|
|
<el-col :span="14">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
<div style="font-weight: 600;">工序维护</div>
|
<el-tag v-if="selectedRouteId" type="success" size="small">当前路线:{{ selectedRouteName }}</el-tag>
|
<el-tag v-else type="info" size="small">请先选择工艺路线</el-tag>
|
</div>
|
|
<div class="route-toolbar">
|
<el-input
|
v-model="processNameDraft"
|
placeholder="输入工序名称后新增"
|
style="max-width: 260px;"
|
clearable
|
:disabled="!selectedRouteId"
|
/>
|
<el-button type="primary" plain :disabled="!selectedRouteId" @click="createProcessItem">新增工序</el-button>
|
</div>
|
|
<div class="process-diagram">
|
<div v-if="processItems.length === 0" class="process-diagram-empty">暂无工序</div>
|
<div
|
v-for="(step, idx) in processItems"
|
:key="String(step.itemId) + '_' + idx"
|
class="process-diagram-segment"
|
>
|
<div class="process-diagram-node">
|
<div class="process-diagram-index">{{ idx + 1 }}</div>
|
<div class="process-diagram-name">{{ step.processName }}</div>
|
</div>
|
<div v-if="idx < processItems.length - 1" class="process-diagram-arrow">→</div>
|
</div>
|
</div>
|
|
<el-table ref="processTableRef" :data="processItems" border row-key="itemId" height="420px" size="small">
|
<el-table-column label="序号" width="80" align="center">
|
<template #default="scope">{{ scope.$index + 1 }}</template>
|
</el-table-column>
|
<el-table-column label="工序名称" min-width="220" prop="processName" show-overflow-tooltip />
|
<el-table-column label="操作" width="300" align="center">
|
<template #default="scope">
|
<el-button link type="primary" size="small" @click="editProcessItem(scope.row)">编辑</el-button>
|
<el-button link type="danger" size="small" @click="deleteProcessItem(scope.row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-col>
|
</el-row>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { getCurrentInstance, ref, watch, onBeforeUnmount, nextTick } from "vue";
|
import { ElMessageBox } from "element-plus";
|
import Sortable from "sortablejs";
|
import {
|
salesProcessFlowConfigList,
|
salesProcessFlowConfigUpsert,
|
salesProcessFlowConfigDelete,
|
salesProcessFlowConfigSetDefault,
|
salesProcessFlowConfigItemList,
|
salesProcessFlowConfigItemUpsert,
|
salesProcessFlowConfigItemSort,
|
salesProcessFlowConfigItemDelete,
|
} from "@/api/salesManagement/salesProcessFlowConfig.js";
|
|
const { proxy } = getCurrentInstance();
|
const visible = ref(false);
|
let prevBodyOverflow = "";
|
let prevBodyOverflowY = "";
|
|
const lockBodyScroll = () => {
|
// 兜底处理:有些场景下 Element Plus 不会完全禁止背景页面滚动
|
prevBodyOverflow = document.body.style.overflow || "";
|
prevBodyOverflowY = document.body.style.overflowY || "";
|
document.body.style.overflow = "hidden";
|
document.body.style.overflowY = "hidden";
|
};
|
|
const unlockBodyScroll = () => {
|
document.body.style.overflow = prevBodyOverflow;
|
document.body.style.overflowY = prevBodyOverflowY;
|
};
|
|
watch(visible, (v) => {
|
if (v) lockBodyScroll();
|
else unlockBodyScroll();
|
});
|
|
onBeforeUnmount(() => {
|
unlockBodyScroll();
|
});
|
|
const routeKeyword = ref("");
|
const routeNameDraft = ref("");
|
const processNameDraft = ref("");
|
const routeList = ref([]);
|
const routeTotal = ref(0);
|
const routePage = ref({
|
current: 1,
|
size: 10,
|
});
|
const selectedRouteId = ref(null);
|
const selectedRouteName = ref("");
|
const processItems = ref([]);
|
const processTableRef = ref(null);
|
let processStepsSortable = null;
|
let isProcessingDrag = false;
|
|
const destroyProcessSortable = () => {
|
if (processStepsSortable) {
|
processStepsSortable.destroy();
|
processStepsSortable = null;
|
}
|
};
|
|
const initProcessSortable = () => {
|
destroyProcessSortable();
|
if (!processTableRef.value) return;
|
const tbody = processTableRef.value?.$el?.querySelector(".el-table__body tbody")
|
|| processTableRef.value?.$el?.querySelector(".el-table__body-wrapper > table > tbody");
|
if (!tbody) return;
|
|
processStepsSortable = new Sortable(tbody, {
|
animation: 150,
|
ghostClass: "sortable-ghost",
|
draggable: ".el-table__row",
|
handle: ".el-table__row",
|
filter: ".el-button, .el-input, .el-select",
|
preventOnFilter: true,
|
onEnd: async (evt) => {
|
if (isProcessingDrag) return;
|
const { oldIndex, newIndex } = evt;
|
if (oldIndex === newIndex) return;
|
if (!selectedRouteId.value) return;
|
if (!processItems.value[oldIndex]) return;
|
|
isProcessingDrag = true;
|
try {
|
const arr = [...processItems.value];
|
const moving = arr.splice(oldIndex, 1)[0];
|
arr.splice(newIndex, 0, moving);
|
processItems.value = arr;
|
ensureSortNo();
|
|
if (!moving?.itemId) {
|
proxy?.$modal?.msgError?.("当前工序缺少ID,无法排序");
|
await fetchProcessItems(selectedRouteId.value);
|
return;
|
}
|
|
// 使用专用排序接口,避免 upsert 造成重复新增
|
await salesProcessFlowConfigItemSort({
|
id: moving.itemId,
|
dragSort: newIndex + 1,
|
});
|
proxy?.$modal?.msgSuccess?.("顺序调整成功");
|
await fetchProcessItems(selectedRouteId.value);
|
} finally {
|
isProcessingDrag = false;
|
}
|
},
|
});
|
};
|
|
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 normalizeItemList = (list) => {
|
if (!Array.isArray(list)) return [];
|
return list.map((i, idx) => ({
|
itemId: i.itemId ?? i.id ?? null,
|
routeId: i.routeId ?? i.processRouteId ?? selectedRouteId.value,
|
processName: i.processName ?? i.name ?? "",
|
sortNo: i.sortNo ?? idx + 1,
|
}));
|
};
|
|
const fetchRouteList = async () => {
|
const res = await salesProcessFlowConfigList({
|
current: routePage.value.current,
|
size: routePage.value.size,
|
processRouteName: routeKeyword.value || undefined,
|
});
|
const records = res?.records ?? res?.data?.records ?? [];
|
const total = res?.total ?? res?.data?.total ?? 0;
|
routeTotal.value = Number(total) || 0;
|
routeList.value = normalizeRouteList(records);
|
};
|
|
const handleRouteQuery = async () => {
|
routePage.value.current = 1;
|
await fetchRouteList();
|
};
|
|
const handleRoutePaginationChange = async (obj) => {
|
routePage.value.current = obj.page;
|
routePage.value.size = obj.limit;
|
await fetchRouteList();
|
};
|
|
const fetchProcessItems = async (routeId) => {
|
if (!routeId) {
|
processItems.value = [];
|
destroyProcessSortable();
|
return;
|
}
|
const res = await salesProcessFlowConfigItemList(routeId);
|
const raw = res?.data ?? res ?? [];
|
processItems.value = normalizeItemList(raw);
|
ensureSortNo();
|
await nextTick();
|
initProcessSortable();
|
};
|
|
const openDialog = async () => {
|
visible.value = true;
|
selectedRouteId.value = null;
|
selectedRouteName.value = "";
|
processItems.value = [];
|
routePage.value.current = 1;
|
await fetchRouteList();
|
};
|
|
const closeDialog = () => {
|
visible.value = false;
|
selectedRouteId.value = null;
|
selectedRouteName.value = "";
|
processItems.value = [];
|
routeNameDraft.value = "";
|
processNameDraft.value = "";
|
routeTotal.value = 0;
|
destroyProcessSortable();
|
};
|
|
const handleRouteSelect = async (row) => {
|
if (!row?.routeId) return;
|
selectedRouteId.value = row.routeId;
|
selectedRouteName.value = row.processRouteName;
|
await fetchProcessItems(selectedRouteId.value);
|
};
|
|
const createRoute = async () => {
|
if (!routeNameDraft.value) {
|
proxy?.$modal?.msgWarning("请先输入工艺路线名称");
|
return;
|
}
|
const payload = { processRouteName: routeNameDraft.value };
|
await salesProcessFlowConfigUpsert(payload);
|
proxy?.$modal?.msgSuccess("工艺路线新增成功");
|
routeNameDraft.value = "";
|
await handleRouteQuery();
|
};
|
|
const editRoute = async (row) => {
|
const oldName = row?.processRouteName ?? "";
|
const { value } = await ElMessageBox.prompt("请输入新的工艺路线名称", "修改工艺路线", {
|
inputValue: oldName,
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
});
|
await salesProcessFlowConfigUpsert({
|
routeId: row.routeId,
|
processRouteName: value,
|
});
|
proxy?.$modal?.msgSuccess("工艺路线修改成功");
|
await fetchRouteList();
|
if (selectedRouteId.value === row.routeId) selectedRouteName.value = value;
|
};
|
|
const deleteRoute = async (row) => {
|
await ElMessageBox.confirm("确认删除该工艺路线?", "删除", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
});
|
await salesProcessFlowConfigDelete(row.routeId);
|
proxy?.$modal?.msgSuccess("删除成功");
|
if (selectedRouteId.value === row.routeId) {
|
selectedRouteId.value = null;
|
selectedRouteName.value = "";
|
processItems.value = [];
|
}
|
// 删除后若当前页被清空,回退一页
|
if (routeList.value.length <= 1 && routePage.value.current > 1) {
|
routePage.value.current = routePage.value.current - 1;
|
}
|
await fetchRouteList();
|
};
|
|
const setDefaultRoute = async (row) => {
|
await salesProcessFlowConfigSetDefault(row.routeId);
|
proxy?.$modal?.msgSuccess("默认工艺路线设置成功");
|
await fetchRouteList();
|
};
|
|
const ensureSortNo = () => {
|
processItems.value = processItems.value.map((i, idx) => ({ ...i, sortNo: idx + 1 }));
|
};
|
|
const createProcessItem = async () => {
|
if (!selectedRouteId.value) {
|
proxy?.$modal?.msgWarning("请先选择工艺路线");
|
return;
|
}
|
if (!processNameDraft.value) {
|
proxy?.$modal?.msgWarning("请先输入工序名称");
|
return;
|
}
|
await salesProcessFlowConfigItemUpsert({
|
routeId: selectedRouteId.value,
|
processName: processNameDraft.value,
|
sortNo: processItems.value.length + 1,
|
});
|
proxy?.$modal?.msgSuccess("工序新增成功");
|
processNameDraft.value = "";
|
await fetchProcessItems(selectedRouteId.value);
|
};
|
|
const editProcessItem = async (row) => {
|
const { value } = await ElMessageBox.prompt("请输入新的工序名称", "修改工序", {
|
inputValue: row.processName,
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
});
|
await salesProcessFlowConfigItemUpsert({
|
id: row.itemId,
|
routeId: selectedRouteId.value,
|
processName: value,
|
sortNo: row.sortNo,
|
});
|
proxy?.$modal?.msgSuccess("工序修改成功");
|
await fetchProcessItems(selectedRouteId.value);
|
};
|
|
const deleteProcessItem = async (row) => {
|
await ElMessageBox.confirm("确认删除该工序?", "删除", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
});
|
await salesProcessFlowConfigItemDelete(row.itemId);
|
proxy?.$modal?.msgSuccess("工序删除成功");
|
await fetchProcessItems(selectedRouteId.value);
|
};
|
</script>
|
|
<style scoped>
|
.route-toolbar {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.route-toolbar-left {
|
flex-wrap: nowrap;
|
}
|
|
.route-toolbar-left :deep(.el-input) {
|
width: 190px;
|
}
|
|
.process-route-dialog :deep(.el-dialog__body) {
|
height: 760px;
|
overflow: hidden;
|
overflow-y: hidden;
|
}
|
|
.dialog-main {
|
height: 100%;
|
}
|
|
.left-panel {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.left-table-wrap {
|
flex: 1;
|
min-height: 0;
|
overflow: hidden;
|
}
|
|
.process-diagram {
|
display: flex;
|
align-items: center;
|
gap: 0;
|
flex-wrap: nowrap;
|
overflow-x: auto;
|
padding: 10px 0;
|
}
|
|
.process-diagram-segment {
|
display: flex;
|
align-items: center;
|
}
|
|
.process-diagram-node {
|
width: 160px;
|
min-width: 160px;
|
height: 78px;
|
border: 1px solid #ebeef5;
|
border-radius: 10px;
|
background: #fff;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
padding: 10px 12px;
|
margin-right: 10px;
|
box-sizing: border-box;
|
}
|
|
.process-diagram-index {
|
font-size: 12px;
|
color: #909399;
|
margin-bottom: 4px;
|
}
|
|
.process-diagram-name {
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.process-diagram-arrow {
|
font-size: 18px;
|
color: #909399;
|
margin-right: 14px;
|
margin-left: -6px;
|
}
|
|
.process-diagram-empty {
|
width: 100%;
|
text-align: center;
|
padding: 24px 0;
|
color: #909399;
|
border: 1px dashed #ebeef5;
|
border-radius: 8px;
|
}
|
|
</style>
|