Files
zkxw/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
T

408 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 条件分支可视化规则:字段 + 运算符 + 值 -->
<template>
<div class="cond-rule-editor">
<el-alert
v-if="legacyWhen && !(modelValue && modelValue.length)"
type="warning"
:closable="false"
show-icon
class="cond-rule-editor__legacy"
title="检测到旧版条件公式,请用下方可视化重新配置"
/>
<div v-if="!candidates.length" class="cond-rule-editor__empty">
请先到「2 表单设计」勾选参与条件分支的字段
</div>
<template v-else>
<div
v-for="(rule, idx) in localRules"
:key="idx"
class="cond-rule-editor__row-wrap"
>
<div class="cond-rule-editor__row">
<el-select
:model-value="rule.field"
placeholder="字段"
:disabled="disabled"
@update:model-value="(v) => onFieldChange(idx, String(v))"
>
<el-option
v-for="c in candidates"
:key="c.field"
:label="c.title"
:value="c.field"
/>
</el-select>
<el-select
:model-value="rule.op"
placeholder="条件"
:disabled="disabled"
@update:model-value="(v) => onOpChange(idx, v)"
>
<el-option
v-for="o in opsForRow(rule)"
:key="o.value"
:label="o.label"
:value="o.value"
/>
</el-select>
<el-select
v-if="isPersonLikeField(rule.field)"
:model-value="personValueIds(rule.value)"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="选择人员(任一命中)"
:disabled="disabled"
@update:model-value="(v) => onValueChange(idx, Array.isArray(v) ? v.map(String) : [])"
>
<el-option
v-for="u in userOptions"
:key="u.id"
:label="u.label"
:value="u.id"
/>
</el-select>
<el-select
v-else-if="isPostField(rule.field)"
:model-value="personValueIds(rule.value)"
multiple
filterable
placeholder="选择职位(任一命中)"
:disabled="disabled"
class="cond-rule-editor__post-select"
@update:model-value="(v) => onValueChange(idx, Array.isArray(v) ? v.map(String) : [])"
>
<el-option
v-for="p in postOptions"
:key="p.id"
:label="p.label"
:value="p.id"
/>
</el-select>
<el-select
v-else-if="optionsFor(rule.field)?.length"
:model-value="rule.value"
placeholder="值"
:disabled="disabled"
@update:model-value="(v) => onValueChange(idx, v)"
>
<el-option
v-for="opt in optionsFor(rule.field)"
:key="String(opt.value)"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-else
:model-value="valueText(rule.value)"
placeholder="比较值"
:disabled="disabled"
@update:model-value="(v) => onValueChange(idx, coerceValue(rule.field, v))"
/>
<el-button
class="cond-rule-editor__del"
text
type="danger"
:disabled="disabled || localRules.length <= 1"
title="删除条件"
@click="removeRule(idx)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
<div v-if="idx < localRules.length - 1" class="cond-rule-editor__and">且(AND)</div>
</div>
<el-button
class="btn-ghost"
:disabled="disabled || !candidates.length"
@click="addRule"
>
<el-icon><Plus /></el-icon>
且条件
</el-button>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { Delete, Plus } from '@element-plus/icons-vue'
import { fetchPostList, fetchUserList } from '@/api/admin'
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
import {
opsForFieldType,
type ConditionCandidate,
type ConditionOp
} from '@/components/oa-approval/oa-condition-fields'
import type { ConditionRule } from './types'
defineOptions({ name: 'OaConditionRuleEditor' })
const props = withDefaults(
defineProps<{
modelValue?: ConditionRule[]
disabled?: boolean
candidates: ConditionCandidate[]
legacyWhen?: string
}>(),
{
modelValue: () => [],
disabled: false,
candidates: () => [],
legacyWhen: ''
}
)
const emit = defineEmits<{
'update:modelValue': [value: ConditionRule[]]
}>()
const localRules = computed(() => props.modelValue || [])
const userOptions = ref<Array<{ id: string; label: string }>>([])
const postOptions = ref<Array<{ id: string; label: string }>>([])
function meta(field: string) {
return props.candidates.find((c) => c.field === field)
}
function isPersonLikeField(field: string) {
const t = meta(field)?.type
return t === 'person' || t === 'ownerPerson' || t === 'initiator'
}
function isPostField(field: string) {
return meta(field)?.type === 'post'
}
function personValueIds(v: unknown): string[] {
if (Array.isArray(v)) return v.map(String)
if (v == null || v === '') return []
return [String(v)]
}
function defaultValueForType(type: string, opts?: ConditionCandidate['options']) {
if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') {
return [] as string[]
}
return opts?.[0]?.value ?? ''
}
function opsForRow(rule: ConditionRule) {
const t = meta(rule.field)?.type || 'number'
return opsForFieldType(t)
}
function optionsFor(field: string) {
return meta(field)?.options
}
function valueText(v: unknown) {
if (Array.isArray(v)) return v.join('、')
return v == null ? '' : String(v)
}
function coerceValue(field: string, raw: string): string | number {
const t = meta(field)?.type || ''
if (t === 'inputNumber' || t === 'number' || t === 'money') {
if (raw === '' || raw == null) return ''
const n = Number(raw)
return Number.isFinite(n) ? n : raw
}
return raw
}
function emitRules(next: ConditionRule[]) {
emit('update:modelValue', next)
}
function ensureSeed() {
if (props.disabled) return
if (!props.candidates.length) return
if ((props.modelValue || []).length) return
const first = props.candidates[0]
emitRules([
{
field: first.field,
op: opsForFieldType(first.type)[0].value,
value: defaultValueForType(first.type, first.options)
}
])
}
watch(
() => [props.disabled, props.candidates.length, (props.modelValue || []).length] as const,
() => {
if (!props.disabled) ensureSeed()
},
{ immediate: true }
)
function onFieldChange(idx: number, field: string) {
const next = [...localRules.value]
const t = meta(field)?.type || 'number'
const ops = opsForFieldType(t)
next[idx] = {
field,
op: ops[0].value,
value: defaultValueForType(t, optionsFor(field))
}
emitRules(next)
}
function onOpChange(idx: number, op: unknown) {
const next = [...localRules.value]
next[idx] = { ...next[idx], op: op as ConditionOp }
emitRules(next)
}
function onValueChange(idx: number, value: unknown) {
const next = [...localRules.value]
next[idx] = { ...next[idx], value: value as string | number | string[] }
emitRules(next)
}
function addRule() {
if (!props.candidates.length) return
const first = props.candidates[0]
emitRules([
...localRules.value,
{
field: first.field,
op: opsForFieldType(first.type)[0].value,
value: defaultValueForType(first.type, first.options)
}
])
}
function removeRule(idx: number) {
if (localRules.value.length <= 1) return
const next = localRules.value.filter((_, i) => i !== idx)
emitRules(next)
}
async function loadUsers() {
try {
const pageSize = MAX_ADMIN_LIST_PAGE_SIZE
const raw: any[] = []
let page = 1
for (;;) {
const res = await fetchUserList({ page, pageSize, status: '1' })
const list = res?.list || []
raw.push(...list)
if (list.length < pageSize) break
if (typeof res?.total === 'number' && raw.length >= res.total) break
page += 1
}
userOptions.value = raw.map((u) => {
const id = String(u.id || '')
const nick = String(u.nickName || '').trim()
const userNo = String(u.userNo || '').trim()
return { id, label: nick || userNo || id }
}).filter((u) => u.id)
} catch {
userOptions.value = []
}
}
async function loadPosts() {
try {
const pageSize = MAX_ADMIN_LIST_PAGE_SIZE
const raw: any[] = []
let page = 1
for (;;) {
const res = await fetchPostList({ page, pageSize })
const list = res?.list || []
raw.push(...list)
if (list.length < pageSize) break
if (typeof res?.total === 'number' && raw.length >= res.total) break
page += 1
}
postOptions.value = raw
.filter((r) => String(r.status ?? '1') === '1')
.map((r) => {
const id = String(r.id || '')
const name = String(r.postName || '').trim()
const code = String(r.postCode || '').trim()
return { id, label: name || code || id }
})
.filter((p) => p.id)
} catch {
postOptions.value = []
}
}
onMounted(() => {
if (props.candidates.some((c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator')) {
loadUsers()
}
if (props.candidates.some((c) => c.type === 'post')) {
loadPosts()
}
})
watch(
() =>
props.candidates.some(
(c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator'
),
(need) => {
if (need && !userOptions.value.length) loadUsers()
}
)
watch(
() => props.candidates.some((c) => c.type === 'post'),
(need) => {
if (need && !postOptions.value.length) loadPosts()
}
)
</script>
<style scoped lang="scss">
.cond-rule-editor {
margin-bottom: 12px;
}
.cond-rule-editor__legacy {
margin-bottom: 10px;
}
.cond-rule-editor__empty {
padding: 10px 12px;
margin-bottom: 8px;
color: #d97706;
font-size: 12px;
background: #fffbeb;
border: 1px dashed #fde68a;
border-radius: 8px;
}
.cond-rule-editor__row {
display: grid;
grid-template-columns: 1fr auto 1fr auto;
gap: 6px;
align-items: center;
margin-bottom: 6px;
}
.cond-rule-editor__del {
padding: 4px;
}
.cond-rule-editor__and {
margin: 0 0 6px;
color: var(--mint-text-muted, #94a3b8);
font-size: 11px;
text-align: center;
}
.cond-rule-editor__post-select :deep(.el-select__selection) {
flex-wrap: wrap;
}
</style>