fix(oa): align initiator-select picker with flow designer member chips
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4641d333e6
commit
91b8dd1aaa
1 file changed
+347
-25
@@ -76,23 +76,72 @@
|
||||
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>
|
||||
<div class="member-picker">
|
||||
<el-select
|
||||
v-model="memberPickBuffer"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="搜索姓名 / 工号,点击添加"
|
||||
style="width: 100%"
|
||||
popper-class="oa-flow-member-select-popper"
|
||||
@change="onMemberPick"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="u.id"
|
||||
:label="memberOptionLabel(u)"
|
||||
:value="u.id"
|
||||
:disabled="pickerIds.includes(u.id)"
|
||||
>
|
||||
<div class="member-option">
|
||||
<span class="member-avatar" :style="{ background: avatarColor(u.id) }">
|
||||
{{ avatarInitial(u.nickName) }}
|
||||
</span>
|
||||
<span class="member-option__meta">
|
||||
<span class="member-option__name">{{ u.nickName || '未命名' }}</span>
|
||||
<span v-if="memberOptionSub(u)" class="member-option__sub">
|
||||
{{ memberOptionSub(u) }}
|
||||
</span>
|
||||
</span>
|
||||
<el-tag
|
||||
v-if="pickerIds.includes(u.id)"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="member-option__tag"
|
||||
>
|
||||
已选
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<div class="member-chips" :class="{ 'is-empty': !pickerIds.length }">
|
||||
<template v-if="pickerIds.length">
|
||||
<span v-for="id in pickerIds" :key="id" class="member-chip">
|
||||
<span
|
||||
class="member-avatar member-avatar--sm"
|
||||
:style="{ background: avatarColor(id) }"
|
||||
>
|
||||
{{ avatarInitial(resolveUser(id)?.nickName) }}
|
||||
</span>
|
||||
<span class="member-chip__name" :title="chipTitle(id)">
|
||||
{{ resolveUser(id)?.nickName || id }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="member-chip__rm"
|
||||
:aria-label="`移除 ${resolveUser(id)?.nickName || id}`"
|
||||
@click="removePickerMember(id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>暂未选择成员,请从上方搜索添加</template>
|
||||
</div>
|
||||
<p class="member-picked-count">已选 {{ pickerIds.length }} 人</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="pickerVisible = false">
|
||||
@@ -110,7 +159,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { Check, Close, Plus } from '@element-plus/icons-vue'
|
||||
import type { OaFlowPreviewStep } from '@/api/oaApproval'
|
||||
|
||||
@@ -118,6 +167,17 @@
|
||||
|
||||
type UserOption = { id: string; nickName: string; userNo?: string }
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'#059669',
|
||||
'#0ea5e9',
|
||||
'#8b5cf6',
|
||||
'#d97706',
|
||||
'#ef4444',
|
||||
'#14b8a6',
|
||||
'#6366f1',
|
||||
'#ec4899'
|
||||
]
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
steps?: OaFlowPreviewStep[]
|
||||
@@ -152,6 +212,7 @@
|
||||
const pickerNodeId = ref('')
|
||||
const pickerNodeType = ref('')
|
||||
const pickerIds = ref<string[]>([])
|
||||
const memberPickBuffer = ref('')
|
||||
|
||||
const pickerTitle = computed(() =>
|
||||
pickerNodeType.value === 'cc' ? '选择抄送人' : '选择审批人'
|
||||
@@ -172,19 +233,62 @@
|
||||
})
|
||||
}
|
||||
|
||||
function userOptionLabel(u: UserOption) {
|
||||
const name = u.nickName || u.id
|
||||
return u.userNo ? `${name}(${u.userNo})` : name
|
||||
function avatarColor(id: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0
|
||||
return AVATAR_COLORS[hash % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
function avatarInitial(name?: string) {
|
||||
const t = (name || '?').trim()
|
||||
return t ? t.slice(0, 1) : '?'
|
||||
}
|
||||
|
||||
function resolveUser(id: string) {
|
||||
return userOptions.value.find((u) => u.id === id)
|
||||
}
|
||||
|
||||
function memberOptionLabel(u: UserOption) {
|
||||
const name = (u.nickName || '').trim() || '未命名'
|
||||
const no = (u.userNo || '').trim()
|
||||
if (no && no !== name) return `${name}(${no})`
|
||||
return name
|
||||
}
|
||||
|
||||
function memberOptionSub(u: UserOption) {
|
||||
const name = (u.nickName || '').trim()
|
||||
const no = (u.userNo || '').trim()
|
||||
if (!no || no === name) return ''
|
||||
return `账号 ${no}`
|
||||
}
|
||||
|
||||
function chipTitle(id: string) {
|
||||
const u = resolveUser(id)
|
||||
return u ? memberOptionLabel(u) : id
|
||||
}
|
||||
|
||||
function onMemberPick(id: string) {
|
||||
if (!id) return
|
||||
if (!pickerIds.value.includes(id)) {
|
||||
pickerIds.value = [...pickerIds.value, id]
|
||||
}
|
||||
nextTick(() => {
|
||||
memberPickBuffer.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
function removePickerMember(id: string) {
|
||||
pickerIds.value = pickerIds.value.filter((x) => x !== id)
|
||||
}
|
||||
|
||||
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)
|
||||
for (const x of ids) {
|
||||
if (!x || seen.has(x)) continue
|
||||
seen.add(x)
|
||||
uniq.push(x)
|
||||
}
|
||||
if (uniq.length) next[nodeId] = uniq
|
||||
else delete next[nodeId]
|
||||
@@ -204,6 +308,7 @@
|
||||
pickerNodeId.value = s.id
|
||||
pickerNodeType.value = s.type || ''
|
||||
pickerIds.value = [...((props.selectByNode || {})[s.id] || [])]
|
||||
memberPickBuffer.value = ''
|
||||
pickerVisible.value = true
|
||||
}
|
||||
|
||||
@@ -218,6 +323,7 @@
|
||||
pickerNodeId.value = ''
|
||||
pickerNodeType.value = ''
|
||||
pickerIds.value = []
|
||||
memberPickBuffer.value = ''
|
||||
}
|
||||
|
||||
function signBadge(s: OaFlowPreviewStep) {
|
||||
@@ -510,4 +616,220 @@
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-option {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.member-option__meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.member-option__name {
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-option__sub {
|
||||
overflow: hidden;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-option__tag {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&--sm {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
|
||||
&.is-empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 4px 8px 4px 4px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.member-chip__name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-chip__rm {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
background: #f1f5f9;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
}
|
||||
}
|
||||
|
||||
.member-picked-count {
|
||||
margin: 8px 0 0;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 下拉挂 body,须非 scoped -->
|
||||
<style lang="scss">
|
||||
.oa-flow-member-select-popper {
|
||||
.el-select-dropdown__list {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item {
|
||||
height: auto !important;
|
||||
min-height: 56px;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px 12px !important;
|
||||
line-height: normal !important;
|
||||
border-radius: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&.hover,
|
||||
&:hover {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
}
|
||||
}
|
||||
|
||||
.member-option {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.member-option__meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-option__name {
|
||||
overflow: hidden;
|
||||
color: var(--mint-text, #0f172a);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-option__sub {
|
||||
overflow: hidden;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-option__tag {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user