| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å çç³è¯·æ¨¡åå
ï¼å¯å¢å 审æ¹èç¹ï¼æ¯èç¹å¿
é 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ãnickNameãuserName */ |
| | | 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> |