From 19f2e3bdbe04e7ea79c6a0bdc8c7318d4837b189 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 28 五月 2026 17:36:45 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_pro_山西_晋和园

---
 src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue |  399 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 399 insertions(+), 0 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
new file mode 100644
index 0000000..78304ea
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
@@ -0,0 +1,399 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆鑺傜偣鏁帮紝姣忚妭鐐瑰浜� + 浼氱/鎴栫 -->
+<template>
+  <div class="tfe">
+    <div v-if="innerList.length" class="tfe-flow">
+      <div v-for="(item, index) in innerList" :key="item._uid" class="tfe-flow-item">
+        <div class="tfe-card" :class="{ 'tfe-card--empty': !item.approvers?.length }">
+          <div class="tfe-badge">{{ index + 1 }}</div>
+          <div class="tfe-head">
+            <span class="tfe-level">{{ levelText(index) }}</span>
+            <el-radio-group
+              v-if="!readonly"
+              v-model="item.signMode"
+              size="small"
+              @change="emitOut"
+            >
+              <el-radio-button value="countersign">浼氱</el-radio-button>
+              <el-radio-button value="or_sign">鎴栫</el-radio-button>
+            </el-radio-group>
+            <el-tag v-else size="small" type="info" effect="plain">
+              {{ signModeLabel(item.signMode) }}
+            </el-tag>
+          </div>
+          <p class="tfe-mode-tip">{{ signModeTip(item.signMode) }}</p>
+          <div v-if="!readonly" class="tfe-select">
+            <el-select
+              v-model="item.approverIds"
+              multiple
+              collapse-tags
+              collapse-tags-tooltip
+              :max-collapse-tags="2"
+              filterable
+              placeholder="璇烽�夋嫨瀹℃壒浜猴紙鍙閫夛級"
+              style="width: 100%"
+              @change="(ids) => onApproversChange(ids, 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 v-if="item.approvers?.length" class="tfe-chips" :class="{ 'tfe-chips--readonly': readonly }">
+            <el-tag
+              v-for="a in item.approvers"
+              :key="String(a.approverId)"
+              size="small"
+              type="info"
+              effect="plain"
+            >
+              {{ a.approverName || "鈥�" }}
+            </el-tag>
+          </div>
+          <div v-if="!readonly" class="tfe-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>
+          <p v-else-if="!item.approvers?.length" class="tfe-empty-approver">鏆傛棤瀹℃壒浜�</p>
+        </div>
+        <div v-if="index < innerList.length - 1" class="tfe-conn">
+          <div class="tfe-conn-line"></div>
+          <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon>
+        </div>
+      </div>
+
+      <div v-if="!readonly" class="tfe-add-wrap">
+        <div v-if="innerList.length" class="tfe-conn">
+          <div class="tfe-conn-line"></div>
+          <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon>
+        </div>
+        <div class="tfe-add-card" @click="addNode">
+          <div class="tfe-add-icon"><el-icon :size="26"><Plus /></el-icon></div>
+          <span>鏂板鑺傜偣</span>
+        </div>
+      </div>
+    </div>
+
+    <div v-else class="tfe-empty">
+      <el-icon :size="44" color="#c0c4cc"><User /></el-icon>
+      <p>鏆傛棤瀹℃壒鑺傜偣</p>
+      <el-button v-if="!readonly" 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";
+import { NODE_SIGN_MODE_OPTIONS, normalizeFlowNodes } from "../approveTemplateConstants.js";
+
+const props = defineProps({
+  modelValue: { type: Array, default: () => [] },
+  userOptions: { type: Array, default: () => [] },
+  /** 閫夋嫨妯℃澘鍚庣敵璇峰満鏅細浠呭睍绀猴紝涓嶅彲鏀瑰鎵逛汉/鑺傜偣 */
+  readonly: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const innerList = ref([]);
+
+function signModeTip(mode) {
+  return NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.desc || "";
+}
+
+function signModeLabel(mode) {
+  return (
+    NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.label ||
+    (mode === "or_sign" ? "鎴栫" : "浼氱")
+  );
+}
+
+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) {
+  const normalized = normalizeFlowNodes(rows);
+  return normalized.map((n) => ({
+    _uid: newUid(),
+    id: n.id,
+    templateId: n.templateId,
+    nodeOrder: n.nodeOrder,
+    signMode: n.signMode,
+    approverIds: n.approvers.map((a) => a.approverId),
+    approvers: [...n.approvers],
+  }));
+}
+
+function publicShape(rows) {
+  return normalizeFlowNodes(
+    (rows || []).map((r) => ({
+      id: r.id,
+      templateId: r.templateId,
+      nodeOrder: r.nodeOrder,
+      signMode: r.signMode,
+      approvers: r.approvers || [],
+    }))
+  );
+}
+
+function emitOut() {
+  emit("update:modelValue", publicShape(innerList.value));
+}
+
+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 onApproversChange(ids, row) {
+  const idList = Array.isArray(ids) ? ids : [];
+  const prevById = new Map((row.approvers || []).map((a) => [String(a.approverId), a]));
+  row.approverIds = idList;
+  row.approvers = idList.map((id) => {
+    const prev = prevById.get(String(id));
+    const u = findUser(id);
+    const item = {
+      approverId: id,
+      approverName: u ? u.nickName || u.userName || "" : prev?.approverName || "",
+    };
+    if (prev?.id != null) item.id = prev.id;
+    if (prev?.nodeId != null) item.nodeId = prev.nodeId;
+    else if (row.id != null) item.nodeId = row.id;
+    if (prev?.templateId != null) item.templateId = prev.templateId;
+    else if (row.templateId != null) item.templateId = row.templateId;
+    return item;
+  });
+  emitOut();
+}
+
+function addNode() {
+  if (props.readonly) return;
+  innerList.value.push({
+    _uid: newUid(),
+    nodeOrder: innerList.value.length + 1,
+    signMode: "countersign",
+    approverIds: [],
+    approvers: [],
+  });
+  emitOut();
+}
+
+function remove(index) {
+  if (props.readonly) return;
+  innerList.value.splice(index, 1);
+  emitOut();
+}
+
+function moveLeft(index) {
+  if (props.readonly) return;
+  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 (props.readonly) return;
+  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>
+.tfe {
+  width: 100%;
+}
+.tfe-flow {
+  display: flex;
+  align-items: flex-start;
+  flex-wrap: nowrap;
+  overflow-x: auto;
+  padding: 6px 0 10px;
+}
+.tfe-flow-item {
+  display: flex;
+  align-items: center;
+}
+.tfe-card {
+  width: 248px;
+  flex-shrink: 0;
+  border: 2px solid var(--el-border-color);
+  border-radius: 12px;
+  padding: 14px 12px 12px;
+  position: relative;
+  background: var(--el-bg-color);
+}
+.tfe-card--empty {
+  border-style: dashed;
+  background: var(--el-fill-color-lighter);
+}
+.tfe-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;
+}
+.tfe-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  margin: 8px 0 4px;
+}
+.tfe-level {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+.tfe-mode-tip {
+  font-size: 11px;
+  color: var(--el-text-color-secondary);
+  margin: 0 0 10px;
+  line-height: 1.4;
+  min-height: 30px;
+}
+.tfe-select {
+  margin-bottom: 8px;
+}
+.tfe-chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-bottom: 8px;
+  min-height: 24px;
+}
+.tfe-chips--readonly {
+  margin-top: 4px;
+  margin-bottom: 0;
+}
+.tfe-empty-approver {
+  font-size: 12px;
+  color: var(--el-text-color-placeholder);
+  margin: 4px 0 0;
+}
+.tfe-actions {
+  display: flex;
+  justify-content: center;
+  gap: 8px;
+  padding-top: 10px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+.tfe-conn {
+  display: flex;
+  align-items: center;
+  width: 40px;
+  flex-shrink: 0;
+  align-self: center;
+}
+.tfe-conn-line {
+  flex: 1;
+  height: 2px;
+  background: var(--el-border-color);
+}
+.tfe-conn-icon {
+  font-size: 14px;
+  color: var(--el-text-color-placeholder);
+  margin-left: -2px;
+}
+.tfe-add-wrap {
+  display: flex;
+  align-items: center;
+}
+.tfe-add-card {
+  width: 120px;
+  min-height: 200px;
+  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;
+}
+.tfe-add-card:hover {
+  border-color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  color: var(--el-color-primary);
+}
+.tfe-add-icon {
+  width: 44px;
+  height: 44px;
+  border-radius: 50%;
+  background: var(--el-color-primary);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.tfe-empty {
+  text-align: center;
+  padding: 28px 16px;
+  border: 1px dashed var(--el-border-color);
+  border-radius: 12px;
+  background: var(--el-fill-color-lighter);
+}
+.tfe-empty p {
+  margin: 10px 0 14px;
+  color: var(--el-text-color-secondary);
+  font-size: 14px;
+}
+</style>

--
Gitblit v1.9.3