From dacc95761cf7090c628fc37a5d4f8bb825ccbbb0 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 16 五月 2026 15:41:45 +0800
Subject: [PATCH] 企业新闻和通知公告

---
 src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue |  201 ++++++++++++++++++++++++--------------------------
 1 files changed, 97 insertions(+), 104 deletions(-)

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