Ver código fonte

feat(oa): CC designer multi-select and preview assigneeTypes support

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 meses atrás
pai
commit
f7d1004e6b

+ 1 - 0
web-admin/src/api/oaApproval.ts

@@ -251,6 +251,7 @@ export type OaFlowPreviewStep = {
   name: string
   signMode?: string
   assigneeType?: string
+  assigneeTypes?: string[]
   meta?: string
   people?: Array<{ id: string; nickName: string }>
   resolveError?: string

+ 26 - 5
web-admin/src/components/oa-approval/OaFlowPreview.vue

@@ -31,11 +31,12 @@
             <template v-if="isInitiatorSelect(s)">
               <span class="oa-flow-card__select">
                 <span class="oa-flow-card__people">
-                  <template v-if="localPeople(s).length">
-                    <span v-for="p in localPeople(s)" :key="p.id" class="oa-person-chip">
+                  <template v-if="displayPeople(s).length">
+                    <span v-for="p in displayPeople(s)" :key="p.id" class="oa-person-chip">
                       <span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
                       {{ p.nickName }}
                       <button
+                        v-if="isLocalSelected(s, p.id)"
                         type="button"
                         class="oa-person-chip__x"
                         title="移除"
@@ -218,10 +219,14 @@
     pickerNodeType.value === 'cc' ? '选择抄送人' : '选择审批人'
   )
 
+  function stepAssigneeTypes(s: OaFlowPreviewStep): string[] {
+    if (Array.isArray(s.assigneeTypes) && s.assigneeTypes.length) return s.assigneeTypes
+    return s.assigneeType ? [s.assigneeType] : []
+  }
+
   function isInitiatorSelect(s: OaFlowPreviewStep) {
-    return (
-      s.assigneeType === 'initiator_select' && (s.type === 'approval' || s.type === 'cc')
-    )
+    if (!(s.type === 'approval' || s.type === 'cc')) return false
+    return stepAssigneeTypes(s).includes('initiator_select')
   }
 
   function localPeople(s: OaFlowPreviewStep) {
@@ -233,6 +238,22 @@
     })
   }
 
+  function displayPeople(s: OaFlowPreviewStep) {
+    if (!isInitiatorSelect(s)) return s.people || []
+    const fromLocal = localPeople(s)
+    if (s.type !== 'cc') return fromLocal
+    const map = new Map<string, { id: string; nickName: string }>()
+    for (const p of s.people || []) {
+      if (p?.id) map.set(p.id, { id: p.id, nickName: p.nickName || p.id })
+    }
+    for (const p of fromLocal) map.set(p.id, p)
+    return [...map.values()]
+  }
+
+  function isLocalSelected(s: OaFlowPreviewStep, userId: string) {
+    return ((props.selectByNode || {})[s.id] || []).includes(userId)
+  }
+
   function avatarColor(id: string) {
     let hash = 0
     for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0

+ 2 - 1
web-admin/src/components/oa-approval/OaProgressChain.vue

@@ -232,7 +232,8 @@
       dept_leader: '部门负责人',
       direct_supervisor: '直属主管',
       supervisor_fallback_dept_leader: '部门负责人(主管回退)',
-      initiator_select: '发起人自选'
+      initiator_select: '发起人自选',
+      multi: '多种选人方式'
     }
     return map[src] || src
   }

+ 1 - 1
web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue

@@ -170,7 +170,7 @@
       const g = n.conditionGroups?.length || 0
       return `${g} 个分支`
     }
-    return summarizeApprover(n.approverConfig)
+    return summarizeApprover(n.approverConfig, n.type)
   }
 
   function onAdd(cmd: string) {

+ 68 - 10
web-admin/src/components/oa-approval/flow-tree/FlowDesigner.vue

@@ -69,15 +69,20 @@
                 :key="opt.value"
                 type="button"
                 class="assignee-seg__item"
-                :class="{ 'is-on': cfg.assigneeType === opt.value }"
-                @click="setAssigneeType(opt.value)"
+                :class="{
+                  'is-on':
+                    editing.type === 'cc'
+                      ? (cfg.assigneeTypes || []).includes(opt.value)
+                      : cfg.assigneeType === opt.value
+                }"
+                @click="onAssigneeOptionClick(opt.value)"
               >
                 {{ opt.label }}
               </button>
             </div>
           </el-form-item>
 
-          <el-form-item v-if="cfg.assigneeType === 'users'">
+          <el-form-item v-if="showUsersPicker">
             <template #label>
               指定成员
               <span class="form-label-tip">可多选,已选将作为本节点{{ editing.type === 'cc' ? '抄送' : '审批' }}人</span>
@@ -151,7 +156,7 @@
             </div>
           </el-form-item>
 
-          <el-form-item v-if="cfg.assigneeType === 'oa_roles'" label="工作流角色">
+          <el-form-item v-if="showRolesPicker" label="工作流角色">
             <el-select
               v-model="cfg.oaRoleIds"
               multiple
@@ -260,6 +265,7 @@
     createNode,
     ensureFlowRoot,
     findParent,
+    normalizeCcAssigneeTypes,
     type ApproverConfig,
     type AssigneeType,
     type ConditionRule,
@@ -312,6 +318,7 @@
   const editing = ref<FlowNode | null>(null)
   const cfg = reactive<ApproverConfig>({
     assigneeType: 'dept_leader',
+    assigneeTypes: [],
     userIds: [],
     oaRoleIds: [],
     signMode: 'or'
@@ -325,6 +332,15 @@
     { label: '发起人自选', value: 'initiator_select' }
   ]
 
+  const showUsersPicker = computed(() => {
+    if (editing.value?.type === 'cc') return (cfg.assigneeTypes || []).includes('users')
+    return cfg.assigneeType === 'users'
+  })
+  const showRolesPicker = computed(() => {
+    if (editing.value?.type === 'cc') return (cfg.assigneeTypes || []).includes('oa_roles')
+    return cfg.assigneeType === 'oa_roles'
+  })
+
   const drawerTitle = computed(() => {
     const t = editing.value?.type
     if (t === 'start') return '发起人'
@@ -337,7 +353,7 @@
   const drawerSubtitle = computed(() => {
     const t = editing.value?.type
     if (t === 'approval') return '配置本节点审批人与多人会签规则'
-    if (t === 'cc') return '配置本节点抄送人'
+    if (t === 'cc') return '可同时选择多种方式,抄送时合并去重'
     if (t === 'condition') return '配置条件分支名称与可视化规则'
     if (t === 'start') return '流程起点节点'
     return '配置流程节点'
@@ -410,6 +426,21 @@
     await onAssigneeTypeChange()
   }
 
+  async function onAssigneeOptionClick(type: AssigneeType) {
+    if (editing.value?.type === 'cc') {
+      const cur = [...(cfg.assigneeTypes || [])]
+      const i = cur.indexOf(type)
+      if (i >= 0) cur.splice(i, 1)
+      else cur.push(type)
+      cfg.assigneeTypes = cur
+      if (cur[0]) cfg.assigneeType = cur[0]
+      if (cur.includes('users')) await loadUsers()
+      if (cur.includes('oa_roles')) await loadRoles()
+      return
+    }
+    await setAssigneeType(type)
+  }
+
   async function loadRoles() {
     roleLoading.value = true
     try {
@@ -537,13 +568,20 @@
       }))
     }
     const ac = editing.value.approverConfig || { assigneeType: 'dept_leader' as const }
-    cfg.assigneeType = ac.assigneeType || 'dept_leader'
+    if (editing.value.type === 'cc') {
+      const types = normalizeCcAssigneeTypes(ac)
+      cfg.assigneeTypes = types.length ? types : ['initiator_select']
+      cfg.assigneeType = cfg.assigneeTypes[0]
+    } else {
+      cfg.assigneeTypes = []
+      cfg.assigneeType = ac.assigneeType || 'dept_leader'
+    }
     cfg.userIds = [...(ac.userIds || [])]
     cfg.oaRoleIds = [...(ac.oaRoleIds || [])]
     cfg.signMode = ac.signMode || 'or'
     memberPickBuffer.value = ''
-    if (cfg.assigneeType === 'users') await loadUsers()
-    if (cfg.assigneeType === 'oa_roles') await loadRoles()
+    if (showUsersPicker.value) await loadUsers()
+    if (showRolesPicker.value) await loadRoles()
     drawerVisible.value = true
   }
 
@@ -594,12 +632,32 @@
       return
     }
     live.name = editing.value.name
-    if (live.type === 'approval' || live.type === 'cc') {
+    if (live.type === 'cc') {
+      const types = [...new Set(cfg.assigneeTypes || [])].filter(Boolean) as AssigneeType[]
+      if (!types.length) {
+        ElMessage.warning('请至少选择一种选人方式')
+        return
+      }
+      if (types.includes('users') && !(cfg.userIds || []).length) {
+        ElMessage.warning('已勾选指定成员,请选择至少一名成员')
+        return
+      }
+      if (types.includes('oa_roles') && !(cfg.oaRoleIds || []).length) {
+        ElMessage.warning('已勾选工作流角色,请选择至少一个角色')
+        return
+      }
+      live.approverConfig = {
+        assigneeTypes: types,
+        assigneeType: types[0],
+        userIds: [...(cfg.userIds || [])],
+        oaRoleIds: [...(cfg.oaRoleIds || [])]
+      }
+    } else if (live.type === 'approval') {
       live.approverConfig = {
         assigneeType: cfg.assigneeType,
         userIds: cfg.assigneeType === 'users' ? [...(cfg.userIds || [])] : undefined,
         oaRoleIds: cfg.assigneeType === 'oa_roles' ? [...(cfg.oaRoleIds || [])] : undefined,
-        signMode: live.type === 'approval' ? cfg.signMode || 'or' : undefined
+        signMode: cfg.signMode || 'or'
       }
     }
     if (live.type === 'condition' && editing.value.conditionGroups) {

+ 55 - 3
web-admin/src/components/oa-approval/flow-tree/types.ts

@@ -8,7 +8,10 @@ export type AssigneeType =
   | 'initiator_select'
 
 export interface ApproverConfig {
-  assigneeType: AssigneeType
+  /** 审批人:单选。抄送人:兼容旧数据 / 双写首项 */
+  assigneeType?: AssigneeType
+  /** 抄送人:多选选人方式(优先)。审批人:不使用 */
+  assigneeTypes?: AssigneeType[]
   userIds?: string[]
   oaRoleIds?: string[]
   oaRoleCodes?: string[]
@@ -78,7 +81,10 @@ export function createNode(type: 'approval' | 'cc' | 'condition'): FlowNode {
       id: newId('cc'),
       type: 'cc',
       name: '抄送人',
-      approverConfig: { assigneeType: 'initiator_select' },
+      approverConfig: {
+        assigneeTypes: ['initiator_select'],
+        assigneeType: 'initiator_select'
+      },
       child: null
     }
   }
@@ -118,6 +124,14 @@ export function ensureFlowRoot(raw: unknown): FlowNode {
   return createDefaultFlow()
 }
 
+const VALID_ASSIGNEE: AssigneeType[] = [
+  'users',
+  'oa_roles',
+  'dept_leader',
+  'direct_supervisor',
+  'initiator_select'
+]
+
 const ASSIGNEE_LABEL: Record<AssigneeType, string> = {
   users: '指定成员',
   oa_roles: '工作流角色',
@@ -126,7 +140,45 @@ const ASSIGNEE_LABEL: Record<AssigneeType, string> = {
   initiator_select: '发起人自选'
 }
 
-export function summarizeApprover(cfg?: ApproverConfig): string {
+export function normalizeCcAssigneeTypes(cfg?: ApproverConfig | null): AssigneeType[] {
+  if (!cfg) return []
+  const raw = cfg.assigneeTypes
+  if (Array.isArray(raw) && raw.length) {
+    const out: AssigneeType[] = []
+    const seen = new Set<string>()
+    for (const item of raw) {
+      if (VALID_ASSIGNEE.includes(item as AssigneeType) && !seen.has(item)) {
+        seen.add(item)
+        out.push(item as AssigneeType)
+      }
+    }
+    return out
+  }
+  if (cfg.assigneeType && VALID_ASSIGNEE.includes(cfg.assigneeType)) {
+    return [cfg.assigneeType]
+  }
+  return []
+}
+
+export function summarizeApprover(cfg?: ApproverConfig, nodeType?: FlowNodeType): string {
+  if (nodeType === 'cc') {
+    const types = normalizeCcAssigneeTypes(cfg)
+    if (!types.length) return '未配置抄送人'
+    const parts = types.map((t) => {
+      const base = ASSIGNEE_LABEL[t] || t
+      if (t === 'users') {
+        const n = cfg?.userIds?.length || 0
+        return n ? `${base}(${n}人)` : `${base}(未选)`
+      }
+      if (t === 'oa_roles') {
+        const n = (cfg?.oaRoleIds?.length || 0) + (cfg?.oaRoleCodes?.length || 0)
+        return n ? `${base}(已选${n})` : `${base}(未选)`
+      }
+      return base
+    })
+    const text = parts.join(' + ')
+    return text.length > 36 ? `${text.slice(0, 34)}…` : text
+  }
   if (!cfg?.assigneeType) return '未配置审批人'
   const base = ASSIGNEE_LABEL[cfg.assigneeType] || cfg.assigneeType
   if (cfg.assigneeType === 'users') {

+ 13 - 5
web-admin/src/views/oa-approval/start/index.vue

@@ -348,11 +348,19 @@
   async function submit() {
     if (!picked.value?.id) return
     for (const s of flowSteps.value) {
-      if (s.type === 'approval' && s.assigneeType === 'initiator_select') {
-        const ids = initiatorSelectByNode.value[s.id] || []
-        if (!ids.length) {
-          ElMessage.warning(`请为「${s.name || '审批人'}」选择审批人`)
-          return
+      if (s.type === 'approval') {
+        const types =
+          Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
+            ? s.assigneeTypes
+            : s.assigneeType
+              ? [s.assigneeType]
+              : []
+        if (types.includes('initiator_select')) {
+          const ids = initiatorSelectByNode.value[s.id] || []
+          if (!ids.length) {
+            ElMessage.warning(`请为「${s.name || '审批人'}」选择审批人`)
+            return
+          }
         }
       }
     }