feat(oa): allow picking initiator-select users inside flow preview

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-23 16:14:13 +08:00
1 parent 624ac87476
commit eafd3c7d9e
1 file changed
+231 -3
@@ -28,7 +28,33 @@
<span v-if="s.resolveError" class="oa-flow-card__err" :title="s.resolveError">
{{ s.resolveError }}
</span>
<span v-if="(s.people || []).length" class="oa-flow-card__people">
<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">
<span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
{{ p.nickName }}
<button
type="button"
class="oa-person-chip__x"
title="移除"
@click="removeUser(s.id, p.id)"
>
×
</button>
</span>
</template>
<span v-else class="oa-flow-card__placeholder">
{{ s.type === 'cc' ? '请选择抄送人' : '请选择审批人' }}
</span>
</span>
<button type="button" class="oa-flow-add-btn" title="添加" @click="openPicker(s)">
<el-icon><Plus /></el-icon>
</button>
</span>
</template>
<span v-else-if="(s.people || []).length" class="oa-flow-card__people">
<span v-for="p in s.people" :key="p.id" class="oa-person-chip">
<span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
{{ p.nickName || p.id }}
@@ -38,15 +64,60 @@
</div>
</div>
</div>
<el-dialog
v-model="pickerVisible"
:title="pickerTitle"
width="520px"
align-center
append-to-body
destroy-on-close
class="dialog-form-layout"
modal-class="protocol-dialog-mask"
@closed="onPickerClosed"
>
<el-select
v-model="pickerIds"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
placeholder="请选择人员"
style="width: 100%"
>
<el-option
v-for="u in userOptions"
:key="u.id"
:label="userOptionLabel(u)"
:value="u.id"
/>
</el-select>
<template #footer>
<div class="dialog-footer">
<el-button @click="pickerVisible = false">
<el-icon><Close /></el-icon>
<span>取消</span>
</el-button>
<el-button type="primary" @click="confirmPicker">
<el-icon><Check /></el-icon>
<span>确定</span>
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { Check, Close, Plus } from '@element-plus/icons-vue'
import type { OaFlowPreviewStep } from '@/api/oaApproval'
defineOptions({ name: 'OaFlowPreview' })
type UserOption = { id: string; nickName: string; userNo?: string }
const props = withDefaults(
defineProps<{
steps?: OaFlowPreviewStep[]
@@ -54,18 +125,100 @@
hint?: string
resolved?: boolean
pendingMessage?: string
selectByNode?: Record<string, string[]>
userOptions?: UserOption[]
}>(),
{
steps: () => [],
loading: false,
hint: '按当前发起人解析审批人',
resolved: true,
pendingMessage: '必填信息填写后,流程将自动显示'
pendingMessage: '必填信息填写后,流程将自动显示',
selectByNode: () => ({}),
userOptions: () => []
}
)
const emit = defineEmits<{
'update:selectByNode': [Record<string, string[]>]
'select-change': []
}>()
const steps = computed(() => props.steps || [])
const resolved = computed(() => props.resolved !== false)
const userOptions = computed(() => props.userOptions || [])
const pickerVisible = ref(false)
const pickerNodeId = ref('')
const pickerNodeType = ref('')
const pickerIds = ref<string[]>([])
const pickerTitle = computed(() =>
pickerNodeType.value === 'cc' ? '选择抄送人' : '选择审批人'
)
function isInitiatorSelect(s: OaFlowPreviewStep) {
return (
s.assigneeType === 'initiator_select' && (s.type === 'approval' || s.type === 'cc')
)
}
function localPeople(s: OaFlowPreviewStep) {
const ids = (props.selectByNode || {})[s.id] || []
const byId = new Map(userOptions.value.map((u) => [u.id, u]))
return ids.map((id) => {
const u = byId.get(id)
return { id, nickName: u?.nickName || id }
})
}
function userOptionLabel(u: UserOption) {
const name = u.nickName || u.id
return u.userNo ? `${name}(${u.userNo})` : name
}
function updateSelect(nodeId: string, ids: string[]) {
const next: Record<string, string[]> = { ...(props.selectByNode || {}) }
const uniq: string[] = []
const seen = new Set<string>()
for (const id of ids) {
if (!id || seen.has(id)) continue
seen.add(id)
uniq.push(id)
}
if (uniq.length) next[nodeId] = uniq
else delete next[nodeId]
emit('update:selectByNode', next)
emit('select-change')
}
function removeUser(nodeId: string, userId: string) {
const cur = (props.selectByNode || {})[nodeId] || []
updateSelect(
nodeId,
cur.filter((id) => id !== userId)
)
}
function openPicker(s: OaFlowPreviewStep) {
pickerNodeId.value = s.id
pickerNodeType.value = s.type || ''
pickerIds.value = [...((props.selectByNode || {})[s.id] || [])]
pickerVisible.value = true
}
function confirmPicker() {
if (pickerNodeId.value) {
updateSelect(pickerNodeId.value, [...pickerIds.value])
}
pickerVisible.value = false
}
function onPickerClosed() {
pickerNodeId.value = ''
pickerNodeType.value = ''
pickerIds.value = []
}
function signBadge(s: OaFlowPreviewStep) {
if (s.type === 'cc' || s.signMode === 'cc') return 'cc'
@@ -222,6 +375,17 @@
white-space: nowrap;
}
.oa-flow-card__select {
display: inline-flex;
flex: 1 1 140px;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: flex-end;
min-width: 0;
margin-left: auto;
}
.oa-flow-card__people {
display: inline-flex;
flex: 1 1 140px;
@@ -233,6 +397,45 @@
margin-left: auto;
}
.oa-flow-card__select .oa-flow-card__people {
flex: 0 1 auto;
margin-left: 0;
}
.oa-flow-card__placeholder {
color: var(--mint-text-muted, #94a3b8);
font-size: 12px;
line-height: 1.2;
}
.oa-flow-add-btn {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin-left: 0;
padding: 0;
color: var(--mint-text, #334155);
font-size: 14px;
line-height: 1;
cursor: pointer;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 6px;
&:hover {
color: var(--primary, #10b981);
border-color: #a7f3d0;
background: var(--primary-bg, #ecfdf5);
}
}
.oa-flow-card__select .oa-flow-add-btn {
margin-left: 0;
}
.oa-person-chip {
display: inline-flex;
gap: 3px;
@@ -258,6 +461,21 @@
border-radius: 50%;
}
.oa-person-chip__x {
margin: 0;
padding: 0 2px;
color: var(--mint-text-muted, #94a3b8);
font-size: 12px;
line-height: 1;
cursor: pointer;
background: transparent;
border: none;
&:hover {
color: #dc2626;
}
}
.oa-flow-badge {
flex-shrink: 0;
padding: 0 5px;
@@ -282,4 +500,14 @@
background: #ecfeff;
}
}
.dialog-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
.el-button .el-icon {
margin-right: 6px;
}
}
</style>