From 04b1a9cfde4049be9a38b9832d5289d4a192c883 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 15 五月 2026 16:29:33 +0800
Subject: [PATCH] 加班申请模块和审批流程公共组件

---
 src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue                            |    2 
 src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue |  360 ++++++++++++++++++++++++++++++++++++
 src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue                         |  201 +++++++++----------
 3 files changed, 458 insertions(+), 105 deletions(-)

diff --git a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
index f52d57d..7c3849b 100644
--- a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氳鍋囩敵璇凤紙瀛楁涓哄墠绔崰浣嶏紝鍚庢湡涓庡悗绔帴鍙e榻愶級-->
+<!--OA妯″潡锛氳鍋囩敵璇�-->
 <template>
   <div class="app-container">
     <div class="search_form mb20">
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue
new file mode 100644
index 0000000..9e3ada5
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue
@@ -0,0 +1,360 @@
+<!-- 鍔犵彮鐢宠妯″潡鍐咃細鍙鍒犲鎵硅妭鐐癸紝姣忚妭鐐瑰繀閫� 1 浜� -->
+<template>
+  <div class="afe">
+    <div v-if="innerList.length" class="afe-flow">
+      <div v-for="(item, index) in innerList" :key="item._uid" class="afe-flow-item">
+        <div class="afe-card" :class="{ 'afe-card--empty': !item.approverId }">
+          <div class="afe-badge">{{ index + 1 }}</div>
+          <div class="afe-avatar-wrap">
+            <div
+              class="afe-avatar"
+              :class="{ 'afe-avatar--on': item.approverId }"
+              :style="item.approverId ? { backgroundColor: avatarColor(item.approverName) } : {}"
+            >
+              <span v-if="item.approverId">{{ (item.approverName || '?').charAt(0) }}</span>
+              <el-icon v-else :size="22"><User /></el-icon>
+            </div>
+            <div class="afe-level">{{ levelText(index) }}</div>
+          </div>
+          <div class="afe-select">
+            <el-select
+              v-model="item.approverId"
+              placeholder="璇烽�夋嫨瀹℃壒浜�"
+              filterable
+              clearable
+              style="width: 100%"
+              @change="(v) => onPick(v, item)"
+            >
+              <el-option
+                v-for="u in userOptions"
+                :key="String(u.userId ?? u.id)"
+                :label="optionLabel(u)"
+                :value="u.userId ?? u.id"
+              />
+            </el-select>
+          </div>
+          <div class="afe-actions">
+            <el-button type="primary" circle size="small" :disabled="index === 0" title="鍓嶇Щ" @click="moveLeft(index)">
+              <el-icon><ArrowLeft /></el-icon>
+            </el-button>
+            <el-button
+              type="primary"
+              circle
+              size="small"
+              :disabled="index === innerList.length - 1"
+              title="鍚庣Щ"
+              @click="moveRight(index)"
+            >
+              <el-icon><ArrowRight /></el-icon>
+            </el-button>
+            <el-button type="danger" circle size="small" title="鍒犻櫎鑺傜偣" @click="remove(index)">
+              <el-icon><Delete /></el-icon>
+            </el-button>
+          </div>
+        </div>
+        <div v-if="index < innerList.length - 1" class="afe-conn">
+          <div class="afe-conn-line"></div>
+          <el-icon class="afe-conn-icon"><ArrowRight /></el-icon>
+        </div>
+      </div>
+
+      <div class="afe-add-wrap">
+        <div class="afe-conn" v-if="innerList.length">
+          <div class="afe-conn-line"></div>
+          <el-icon class="afe-conn-icon"><ArrowRight /></el-icon>
+        </div>
+        <div class="afe-add-card" @click="addNode">
+          <div class="afe-add-icon"><el-icon :size="26"><Plus /></el-icon></div>
+          <span>鏂板鑺傜偣</span>
+        </div>
+      </div>
+    </div>
+
+    <div v-else class="afe-empty">
+      <el-icon :size="44" color="#c0c4cc"><User /></el-icon>
+      <p>鏆傛棤瀹℃壒鑺傜偣</p>
+      <el-button type="primary" @click="addNode">娣诲姞绗竴涓妭鐐�</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ArrowLeft, ArrowRight, Delete, Plus, User } from "@element-plus/icons-vue";
+import { ref, watch } from "vue";
+
+const props = defineProps({
+  modelValue: { type: Array, default: () => [] },
+  /** 涓庣埗椤� userList 缁撴瀯涓�鑷达細userId / id銆乶ickName銆乽serName */
+  userOptions: { type: Array, default: () => [] },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const innerList = ref([]);
+
+const palette = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#9B59B6", "#1ABC9C"];
+
+function avatarColor(name) {
+  if (!name) return "#c0c4cc";
+  let h = 0;
+  for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
+  return palette[Math.abs(h) % palette.length];
+}
+
+function levelText(i) {
+  const t = ["绗竴绾�", "绗簩绾�", "绗笁绾�", "绗洓绾�", "绗簲绾�", "绗叚绾�", "绗竷绾�", "绗叓绾�"];
+  return t[i] || `绗�${i + 1}绾;
+}
+
+function optionLabel(u) {
+  const nick = u.nickName || "";
+  const un = u.userName || "";
+  if (nick && un && nick !== un) return `${nick}锛�${un}锛塦;
+  return nick || un || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+}
+
+function newUid() {
+  return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+}
+
+function mapIn(rows) {
+  if (!Array.isArray(rows)) return [];
+  return rows.map((r, i) => ({
+    _uid: newUid(),
+    approverId: r.approverId ?? r.approver_id ?? null,
+    approverName: r.approverName ?? r.approver_name ?? "",
+    sortOrder: r.sortOrder ?? r.nodeOrder ?? i + 1,
+    nodeOrder: r.nodeOrder ?? r.sortOrder ?? i + 1,
+    roleName: r.roleName ?? "",
+    roleCode: r.roleCode ?? "",
+  }));
+}
+
+function publicShape(rows) {
+  const arr = Array.isArray(rows) ? rows : [];
+  return arr.map((r, i) => ({
+    approverId: r.approverId ?? null,
+    approverName: r.approverName ?? "",
+    roleName: r.roleName ?? "",
+    roleCode: r.roleCode ?? "",
+    sortOrder: i + 1,
+  }));
+}
+
+function emitOut() {
+  const out = innerList.value.map((r, i) => ({
+    approverId: r.approverId ?? null,
+    approverName: r.approverName ?? "",
+    sortOrder: i + 1,
+    nodeOrder: i + 1,
+    roleName: r.roleName ?? "",
+    roleCode: r.roleCode ?? "",
+  }));
+  emit("update:modelValue", out);
+}
+
+watch(
+  () => props.modelValue,
+  (v) => {
+    const next = publicShape(v || []);
+    if (JSON.stringify(next) === JSON.stringify(publicShape(innerList.value))) return;
+    innerList.value = mapIn(v || []);
+  },
+  { deep: true, immediate: true }
+);
+
+function findUser(id) {
+  if (id == null || id === "") return null;
+  return props.userOptions.find((u) => String(u.userId ?? u.id) === String(id)) ?? null;
+}
+
+function onPick(userId, row) {
+  if (!userId) {
+    row.approverName = "";
+    emitOut();
+    return;
+  }
+  const u = findUser(userId);
+  row.approverName = u ? u.nickName || u.userName || "" : "";
+  emitOut();
+}
+
+function addNode() {
+  innerList.value.push({
+    _uid: newUid(),
+    approverId: null,
+    approverName: "",
+    roleName: "",
+    roleCode: "",
+  });
+  emitOut();
+}
+
+function remove(index) {
+  innerList.value.splice(index, 1);
+  emitOut();
+}
+
+function moveLeft(index) {
+  if (index < 1) return;
+  const t = innerList.value[index];
+  innerList.value[index] = innerList.value[index - 1];
+  innerList.value[index - 1] = t;
+  emitOut();
+}
+
+function moveRight(index) {
+  if (index >= innerList.value.length - 1) return;
+  const t = innerList.value[index];
+  innerList.value[index] = innerList.value[index + 1];
+  innerList.value[index + 1] = t;
+  emitOut();
+}
+</script>
+
+<style scoped>
+.afe {
+  width: 100%;
+}
+.afe-flow {
+  display: flex;
+  align-items: flex-start;
+  flex-wrap: nowrap;
+  overflow-x: auto;
+  padding: 6px 0 10px;
+  gap: 0;
+}
+.afe-flow-item {
+  display: flex;
+  align-items: center;
+}
+.afe-card {
+  width: 200px;
+  flex-shrink: 0;
+  border: 2px solid var(--el-border-color);
+  border-radius: 12px;
+  padding: 14px 12px 12px;
+  position: relative;
+  background: var(--el-bg-color);
+}
+.afe-card--empty {
+  border-style: dashed;
+  background: var(--el-fill-color-lighter);
+}
+.afe-badge {
+  position: absolute;
+  top: -8px;
+  left: 12px;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: var(--el-color-primary);
+  color: #fff;
+  font-size: 12px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.afe-avatar-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 6px 0 10px;
+}
+.afe-avatar {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background: var(--el-fill-color);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--el-text-color-placeholder);
+  margin-bottom: 6px;
+  font-size: 18px;
+  font-weight: 600;
+}
+.afe-avatar--on {
+  color: #fff;
+}
+.afe-level {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+.afe-select {
+  margin-bottom: 10px;
+}
+.afe-actions {
+  display: flex;
+  justify-content: center;
+  gap: 8px;
+  padding-top: 10px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+.afe-conn {
+  display: flex;
+  align-items: center;
+  width: 40px;
+  flex-shrink: 0;
+  align-self: center;
+}
+.afe-conn-line {
+  flex: 1;
+  height: 2px;
+  background: var(--el-border-color);
+}
+.afe-conn-icon {
+  font-size: 14px;
+  color: var(--el-text-color-placeholder);
+  margin-left: -2px;
+}
+.afe-add-wrap {
+  display: flex;
+  align-items: center;
+}
+.afe-add-card {
+  width: 120px;
+  min-height: 168px;
+  flex-shrink: 0;
+  border: 2px dashed var(--el-border-color);
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  cursor: pointer;
+  color: var(--el-text-color-regular);
+  font-size: 13px;
+  background: var(--el-fill-color-lighter);
+  transition: border-color 0.2s, background 0.2s;
+}
+.afe-add-card:hover {
+  border-color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  color: var(--el-color-primary);
+}
+.afe-add-icon {
+  width: 44px;
+  height: 44px;
+  border-radius: 50%;
+  background: var(--el-color-primary);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.afe-empty {
+  text-align: center;
+  padding: 28px 16px;
+  border: 1px dashed var(--el-border-color);
+  border-radius: 12px;
+  background: var(--el-fill-color-lighter);
+}
+.afe-empty p {
+  margin: 10px 0 14px;
+  color: var(--el-text-color-secondary);
+  font-size: 14px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
index 477bc06..0bdd83f 100644
--- a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氬姞鐝敵璇凤紙瀛楁涓哄墠绔崰浣嶏紝鍚庢湡涓庡悗绔帴鍙e榻愶級-->
+<!--OA妯″潡锛氬姞鐝敵璇�-->
 <template>
   <div class="app-container">
     <div class="search_form mb20">
@@ -44,7 +44,7 @@
     <el-dialog
       v-model="formDialog.visible"
       :title="formDialog.title"
-      width="960px"
+      width="1040px"
       append-to-body
       destroy-on-close
       class="overtime-apply-form-dialog"
@@ -136,23 +136,13 @@
         </el-row>
         <el-row :gutter="24">
           <el-col :span="24">
-            <el-form-item label="棰勮瀹℃壒娴�">
-              <div class="approval-flow-preview">
-                <div
-                  v-for="(node, index) in PRESET_APPROVAL_FLOW_NODES"
-                  :key="node.roleCode"
-                  class="flow-node-wrap"
-                >
-                  <div class="flow-node">
-                    <span class="flow-node-order">{{ index + 1 }}</span>
-                    <span class="flow-node-name">{{ node.roleName }}</span>
-                  </div>
-                  <el-icon v-if="index < PRESET_APPROVAL_FLOW_NODES.length - 1" class="flow-arrow">
-                    <ArrowRight />
-                  </el-icon>
-                </div>
-              </div>
-              <p class="flow-tip">鎸夐『搴忛�愮骇瀹℃壒锛屽悇鑺傜偣瀹℃壒浜虹敱绯荤粺鏍规嵁缁勭粐鏋舵瀯鑷姩鍖归厤</p>
+            <el-form-item label="瀹℃壒娴佺▼" prop="approvalFlowNodes">
+              <ApprovalFlowEditor
+                v-model="form.approvalFlowNodes"
+                :user-options="flowUserOptions"
+                @update:model-value="onApprovalFlowChange"
+              />
+              <p class="flow-tip">鑷冲皯淇濈暀涓�涓妭鐐癸紱姣忎釜鑺傜偣閫夋嫨涓�鍚嶅鎵逛汉锛涘彲鏂板銆佸垹闄ゆ垨璋冩暣椤哄簭銆�</p>
             </el-form-item>
           </el-col>
         </el-row>
@@ -199,22 +189,16 @@
         <el-descriptions-item label="鍔犵彮缁撴潫鏃ユ湡">{{ detailRow.overtimeEndTime || "鈥�" }}</el-descriptions-item>
         <el-descriptions-item label="鍔犵彮鏃堕暱">{{ formatHours(detailRow.overtimeHours) }}</el-descriptions-item>
         <el-descriptions-item label="鍔犵彮浜嬬敱">{{ detailRow.overtimeReason }}</el-descriptions-item>
-        <el-descriptions-item label="棰勮瀹℃壒娴�">
-          <div class="approval-flow-preview approval-flow-detail">
-            <div
-              v-for="(node, index) in detailApprovalFlowNodes"
-              :key="node.roleCode"
-              class="flow-node-wrap"
-            >
-              <div class="flow-node flow-node--compact">
-                <span class="flow-node-order">{{ index + 1 }}</span>
-                <span class="flow-node-name">{{ node.roleName }}</span>
-              </div>
-              <el-icon v-if="index < detailApprovalFlowNodes.length - 1" class="flow-arrow">
-                <ArrowRight />
-              </el-icon>
+        <el-descriptions-item label="瀹℃壒娴佺▼">
+          <template v-if="sortedApprovalNodes(detailRow).length">
+            <div class="detail-flow-chain">
+              <template v-for="(n, i) in sortedApprovalNodes(detailRow)" :key="i">
+                <span class="detail-flow-step">{{ i + 1 }}. {{ approvalNodeLabel(n) }}</span>
+                <span v-if="i < sortedApprovalNodes(detailRow).length - 1" class="detail-flow-sep">鈫�</span>
+              </template>
             </div>
-          </div>
+          </template>
+          <span v-else>鈥�</span>
         </el-descriptions-item>
         <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
         <el-descriptions-item label="鍒涘缓鏃堕棿">{{ detailRow.createTime || "鈥�" }}</el-descriptions-item>
@@ -256,9 +240,10 @@
 </template>
 
 <script setup>
-import { ArrowRight, Search } from "@element-plus/icons-vue";
+import { Search } from "@element-plus/icons-vue";
 import dayjs from "dayjs";
 import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ApprovalFlowEditor from "./components/ApprovalFlowEditor.vue";
 import { userListNoPageByTenantId } from "@/api/system/user.js";
 import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from "vue";
 
@@ -269,22 +254,24 @@
   { label: "娉曞畾鑺傚亣鏃ュ姞鐝�", value: "holiday" },
 ];
 
-/** 棰勮瀹℃壒娴佽妭鐐癸紙涓庢祦绋嬪紩鎿庨厤缃榻愬崰浣嶏級 */
-const PRESET_APPROVAL_FLOW_NODES = [
-  { roleCode: "direct_leader", roleName: "鐩村睘涓婄骇", sortOrder: 1 },
-  { roleCode: "dept_leader", roleName: "閮ㄩ棬璐熻矗浜�", sortOrder: 2 },
-];
-
-function resolveApprovalFlowNodes(row) {
-  const nodes = row?.approvalFlowNodes;
-  if (Array.isArray(nodes) && nodes.length) {
-    return [...nodes].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
-  }
-  return PRESET_APPROVAL_FLOW_NODES;
+/** 鏈湴婕旂ず锛氫袱鏉$┖鑺傜偣锛屾彁浜ゅ墠椤讳负姣忚妭鐐归�夋嫨瀹℃壒浜� */
+function demoApprovalFlowNodes() {
+  return [
+    { approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1, roleName: "", roleCode: "" },
+    { approverId: null, approverName: "", sortOrder: 2, nodeOrder: 2, roleName: "", roleCode: "" },
+  ];
 }
 
-function cloneApprovalFlowNodes() {
-  return PRESET_APPROVAL_FLOW_NODES.map((n) => ({ ...n }));
+function sortedApprovalNodes(row) {
+  const list = row?.approvalFlowNodes;
+  if (!Array.isArray(list) || !list.length) return [];
+  return [...list].sort((a, b) => (a.sortOrder ?? a.nodeOrder ?? 0) - (b.sortOrder ?? b.nodeOrder ?? 0));
+}
+
+function approvalNodeLabel(n) {
+  const name = (n.approverName || "").trim();
+  if (name) return name;
+  return "鏈�夋嫨瀹℃壒浜�";
 }
 
 function overtimeTypeLabel(v) {
@@ -303,6 +290,9 @@
   overtimeEndTime: "",
   overtimeReason: "",
   attachmentList: [],
+  approvalFlowNodes: [
+    { approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1, roleName: "", roleCode: "" },
+  ],
 });
 
 const { proxy } = getCurrentInstance();
@@ -426,7 +416,7 @@
     overtimeEndTime: "2026-05-10 21:30:00",
     overtimeHours: 3.5,
     overtimeReason: "椤圭洰涓婄嚎淇濋殰銆�",
-    approvalFlowNodes: cloneApprovalFlowNodes(),
+    approvalFlowNodes: demoApprovalFlowNodes(),
     approvalResult: "pending",
     attachmentList: [{ name: "浠诲姟鍗�.pdf" }],
     createTime: "2026-05-09 10:20:00",
@@ -442,7 +432,7 @@
     overtimeEndTime: "2026-05-11 12:15:00",
     overtimeHours: 3.25,
     overtimeReason: "瀹㈡埛鐜板満鏀寔銆�",
-    approvalFlowNodes: cloneApprovalFlowNodes(),
+    approvalFlowNodes: demoApprovalFlowNodes(),
     approvalResult: "approved",
     attachmentList: [],
     createTime: "2026-05-10 16:00:00",
@@ -555,6 +545,8 @@
 const formRef = ref();
 const form = reactive(createEmptyForm());
 
+const flowUserOptions = computed(() => allUsersCache.value.filter((u) => isActiveUser(u)));
+
 const overtimeHoursDisplay = computed(() => {
   const h = computeOvertimeHours(form.overtimeStartTime, form.overtimeEndTime);
   return h == null ? "" : String(h);
@@ -564,6 +556,10 @@
   nextTick(() => {
     formRef.value?.validateField?.("overtimeEndTime");
   });
+}
+
+function onApprovalFlowChange() {
+  nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
 }
 
 const formRules = {
@@ -590,11 +586,32 @@
     },
   ],
   overtimeReason: [{ required: true, message: "璇峰~鍐欏姞鐝簨鐢�", trigger: "blur" }],
+  approvalFlowNodes: [
+    {
+      validator: (_rule, _val, callback) => {
+        const nodes = form.approvalFlowNodes || [];
+        if (!nodes.length) {
+          callback(new Error("璇疯嚦灏戜繚鐣欎竴涓鎵硅妭鐐�"));
+          return;
+        }
+        if (nodes.some((n) => n.approverId == null || n.approverId === "")) {
+          callback(new Error("姣忎釜瀹℃壒鑺傜偣蹇呴』閫夋嫨涓�鍚嶅鎵逛汉"));
+          return;
+        }
+        const ids = nodes.map((n) => String(n.approverId));
+        if (new Set(ids).size !== ids.length) {
+          callback(new Error("鍚屼竴瀹℃壒浜轰笉鑳介噸澶嶅嚭鐜板湪澶氫釜鑺傜偣"));
+          return;
+        }
+        callback();
+      },
+      trigger: "change",
+    },
+  ],
 };
 
 const detailDialog = reactive({ visible: false });
 const detailRow = ref({});
-const detailApprovalFlowNodes = computed(() => resolveApprovalFlowNodes(detailRow.value));
 
 const filesDialog = reactive({ visible: false, row: null });
 
@@ -673,7 +690,7 @@
     overtimeReason: raw.overtimeReason ?? "",
     approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length
       ? raw.approvalFlowNodes.map((n) => ({ ...n }))
-      : cloneApprovalFlowNodes(),
+      : [],
     approvalResult: raw.approvalResult && ["pending", "approved", "rejected", "cancelled"].includes(raw.approvalResult)
       ? raw.approvalResult
       : "pending",
@@ -730,6 +747,9 @@
       overtimeEndTime: row.overtimeEndTime,
       overtimeReason: row.overtimeReason,
       attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
+      approvalFlowNodes: row.approvalFlowNodes?.length
+        ? JSON.parse(JSON.stringify(row.approvalFlowNodes))
+        : [],
     });
     const u = userById(row.applicantId);
     if (u) {
@@ -775,7 +795,15 @@
     overtimeEndTime: form.overtimeEndTime,
     overtimeHours: hours,
     overtimeReason: form.overtimeReason,
-    approvalFlowNodes: cloneApprovalFlowNodes(),
+    approvalFlowNodes: (form.approvalFlowNodes || []).map((n, i) => ({
+      approverId: n.approverId,
+      approverName:
+        n.approverName || userById(n.approverId)?.nickName || userById(n.approverId)?.userName || "",
+      sortOrder: i + 1,
+      nodeOrder: i + 1,
+      roleName: n.roleName || "",
+      roleCode: n.roleCode || "",
+    })),
     attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
   };
   if (formDialog.mode === "add") {
@@ -857,60 +885,25 @@
 .overtime-apply-form-dialog :deep(.el-dialog__body) {
   padding-top: 12px;
 }
-.approval-flow-preview {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 8px;
-  width: 100%;
-}
-.approval-flow-detail {
-  padding: 4px 0;
-}
-.flow-node-wrap {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-.flow-node {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  min-width: 140px;
-  padding: 10px 16px;
-  background: var(--el-fill-color-light);
-  border: 1px solid var(--el-border-color-lighter);
-  border-radius: 8px;
-}
-.flow-node--compact {
-  min-width: 120px;
-  padding: 8px 12px;
-}
-.flow-node-order {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  width: 22px;
-  height: 22px;
-  font-size: 12px;
-  font-weight: 600;
-  color: #fff;
-  background: var(--el-color-primary);
-  border-radius: 50%;
-  flex-shrink: 0;
-}
-.flow-node-name {
-  font-size: 14px;
-  color: var(--el-text-color-primary);
-}
-.flow-arrow {
-  font-size: 18px;
-  color: var(--el-text-color-secondary);
-}
 .flow-tip {
   margin: 10px 0 0;
   font-size: 12px;
   line-height: 1.5;
   color: var(--el-text-color-secondary);
 }
+.detail-flow-chain {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 6px 8px;
+  line-height: 1.6;
+}
+.detail-flow-step {
+  font-size: 14px;
+  color: var(--el-text-color-primary);
+}
+.detail-flow-sep {
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
 </style>

--
Gitblit v1.9.3