|
|
@@ -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>
|