From 53e0b9466d3fdd3e5caf7c42e476fffdb468bc2a Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 27 三月 2026 17:17:22 +0800
Subject: [PATCH] 1
---
src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue | 525 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 525 insertions(+), 0 deletions(-)
diff --git a/src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue b/src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue
new file mode 100644
index 0000000..8406af3
--- /dev/null
+++ b/src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue
@@ -0,0 +1,525 @@
+<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>
\ No newline at end of file
--
Gitblit v1.9.3