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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 14:30:08 +08:00
1 parent 835c1c0836
commit f7d1004e6b
7 files changed
+166 -25

No files matched your search

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