408 lines
11 KiB
Vue
408 lines
11 KiB
Vue
<!-- 条件分支可视化规则:字段 + 运算符 + 值 -->
|
||
<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>
|