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