Files
zkxw/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
T
zlwuandCursor 638c4184d2 feat(oa): visual condition rules driven by form field flags
Mark eligible form fields for condition branches with required lock,
and replace formula inputs with a field/op/value editor in the flow designer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 17:09:23 +08:00

245 lines
6.2 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="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))"
/>
</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, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
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 || [])
function meta(field: string) {
return props.candidates.find((c) => c.field === field)
}
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) {
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: first.options?.[0]?.value ?? ''
}
])
}
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)
const opts = optionsFor(field)
next[idx] = {
field,
op: ops[0].value,
value: opts?.[0]?.value ?? ''
}
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 }
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: first.options?.[0]?.value ?? ''
}
])
}
</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;
gap: 6px;
margin-bottom: 6px;
}
.cond-rule-editor__and {
margin: 0 0 6px;
color: var(--mint-text-muted, #94a3b8);
font-size: 11px;
text-align: center;
}
</style>