From 638c4184d2d90f3e4142869d5cfa72179c45b94e Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 15 Jul 2026 17:09:23 +0800 Subject: [PATCH] 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 --- .../oa-approval/OaFormDesignerEmbed.vue | 27 +- .../flow-tree/ConditionRuleEditor.vue | 244 ++++++++++++++++++ .../oa-approval/flow-tree/FlowBranch.vue | 21 +- .../oa-approval/flow-tree/FlowDesigner.vue | 67 +++-- .../components/oa-approval/flow-tree/types.ts | 10 +- web-admin/src/oa-fc-host/App.vue | 160 +++++++++++- .../src/views/oa-approval/designer/index.vue | 37 ++- 7 files changed, 529 insertions(+), 37 deletions(-) create mode 100644 web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue diff --git a/web-admin/src/components/oa-approval/OaFormDesignerEmbed.vue b/web-admin/src/components/oa-approval/OaFormDesignerEmbed.vue index a13f04ae..fb6f6681 100644 --- a/web-admin/src/components/oa-approval/OaFormDesignerEmbed.vue +++ b/web-admin/src/components/oa-approval/OaFormDesignerEmbed.vue @@ -22,6 +22,8 @@ const props = defineProps<{ /** { rule, option } */ modelValue?: { rule?: unknown; option?: unknown } | null + /** 已被流程条件引用的 field 列表(用于拦截取消勾选) */ + flowConditionFields?: string[] }>() const frameRef = ref(null) @@ -36,7 +38,8 @@ READY: 'oa-fc:ready', SET: 'oa-fc:set-schema', GET: 'oa-fc:get-schema', - SCHEMA: 'oa-fc:schema' + SCHEMA: 'oa-fc:schema', + FLOW_REFS: 'oa-fc:set-flow-refs' } as const const hostSrc = computed(() => { @@ -75,8 +78,19 @@ }) } + function pushFlowRefs() { + if (!frameReady.value) return + postToFrame({ + type: MSG.FLOW_REFS, + fields: Array.isArray(props.flowConditionFields) ? props.flowConditionFields : [] + }) + } + function onFrameLoad() { - if (frameReady.value) pushSchemaToFrame() + if (frameReady.value) { + pushSchemaToFrame() + pushFlowRefs() + } } function onMessage(ev: MessageEvent) { @@ -85,6 +99,7 @@ if (data.type === MSG.READY) { frameReady.value = true pushSchemaToFrame() + pushFlowRefs() // 宿主刚就绪时再补推一次,避免 setRule 早于内部 dragForm 初始化 window.setTimeout(() => pushSchemaToFrame(), 50) window.setTimeout(() => pushSchemaToFrame(), 200) @@ -129,6 +144,14 @@ { deep: true } ) + watch( + () => props.flowConditionFields, + () => { + pushFlowRefs() + }, + { deep: true } + ) + onMounted(() => { window.addEventListener('message', onMessage) }) diff --git a/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue b/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue new file mode 100644 index 00000000..e6aa956c --- /dev/null +++ b/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue @@ -0,0 +1,244 @@ + + + + + + diff --git a/web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue b/web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue index ad4aed24..c934d497 100644 --- a/web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue +++ b/web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue @@ -34,13 +34,13 @@ class="flow-cond-col" >
- {{ g.name || (g.isDefault ? '默认' : `条件${idx + 1}`) }} - {{ g.when }} + {{ branchTitle(g, idx) }}
import { Delete } from '@element-plus/icons-vue' - import type { FlowNode } from './types' + import { + formatConditionPreview, + type ConditionCandidate + } from '@/components/oa-approval/oa-condition-fields' + import type { ConditionGroup, FlowNode } from './types' import { summarizeApprover } from './types' // 递归自引用 import FlowBranch from './FlowBranch.vue' @@ -89,8 +94,9 @@ defineProps<{ node: FlowNode depth?: number + candidates?: ConditionCandidate[] }>(), - { depth: 0 } + { depth: 0, candidates: () => [] } ) const emit = defineEmits<{ @@ -107,6 +113,13 @@ return t } + function branchTitle(g: ConditionGroup, idx: number) { + if (g.isDefault) return g.name || '默认 · 其他情况' + const preview = formatConditionPreview(g.rules || [], props.candidates || []) + if (preview && preview !== '未配置') return preview + return g.name || `条件${idx + 1}` + } + function nodeSummary(n: FlowNode) { if (n.type === 'start') return '谁可以发起本审批' if (n.type === 'condition') { diff --git a/web-admin/src/components/oa-approval/flow-tree/FlowDesigner.vue b/web-admin/src/components/oa-approval/flow-tree/FlowDesigner.vue index 8282090a..1a4bb500 100644 --- a/web-admin/src/components/oa-approval/flow-tree/FlowDesigner.vue +++ b/web-admin/src/components/oa-approval/flow-tree/FlowDesigner.vue @@ -8,6 +8,7 @@
- - - + | null - }>() + const props = withDefaults( + defineProps<{ + modelValue: FlowNode | Record | null + candidates?: ConditionCandidate[] + }>(), + { candidates: () => [] } + ) + + const candidates = computed(() => props.candidates || []) const emit = defineEmits<{ 'update:modelValue': [value: FlowNode] @@ -329,7 +338,7 @@ const t = editing.value?.type if (t === 'approval') return '配置本节点审批人与多人会签规则' if (t === 'cc') return '配置本节点抄送人' - if (t === 'condition') return '配置条件分支名称与公式' + if (t === 'condition') return '配置条件分支名称与可视化规则' if (t === 'start') return '流程起点节点' return '配置流程节点' }) @@ -496,6 +505,12 @@ async function openEdit(node: FlowNode) { editing.value = JSON.parse(JSON.stringify(node)) as FlowNode + if (editing.value.type === 'condition') { + editing.value.conditionGroups = (editing.value.conditionGroups || []).map((g) => ({ + ...g, + rules: Array.isArray(g.rules) ? g.rules : [] + })) + } const ac = editing.value.approverConfig || { assigneeType: 'dept_leader' as const } cfg.assigneeType = ac.assigneeType || 'dept_leader' cfg.userIds = [...(ac.userIds || [])] @@ -516,10 +531,24 @@ if (!editing.value?.conditionGroups || !v) return editing.value.conditionGroups.forEach((g, i) => { g.isDefault = i === idx - if (g.isDefault) g.when = '' + if (g.isDefault) { + g.when = '' + g.rules = [] + } }) } + function sanitizeConditionRules(rules?: ConditionRule[]): ConditionRule[] { + return (rules || []).filter( + (r) => + !!r && + String(r.field || '').trim() && + String(r.op || '').trim() && + r.value !== '' && + r.value != null + ) + } + function addConditionGroup() { if (!editing.value) return if (!editing.value.conditionGroups) editing.value.conditionGroups = [] @@ -550,10 +579,16 @@ } if (live.type === 'condition' && editing.value.conditionGroups) { const oldGroups = live.conditionGroups || [] - live.conditionGroups = editing.value.conditionGroups.map((g, i) => ({ - ...g, - child: oldGroups[i]?.child || g.child || createEndNode() - })) + live.conditionGroups = editing.value.conditionGroups.map((g, i) => { + const isDefault = !!g.isDefault + return { + ...g, + isDefault, + when: '', + rules: isDefault ? [] : sanitizeConditionRules(g.rules), + child: oldGroups[i]?.child || g.child || createEndNode() + } + }) } commit() drawerVisible.value = false diff --git a/web-admin/src/components/oa-approval/flow-tree/types.ts b/web-admin/src/components/oa-approval/flow-tree/types.ts index ed6c2dcc..0dd061fd 100644 --- a/web-admin/src/components/oa-approval/flow-tree/types.ts +++ b/web-admin/src/components/oa-approval/flow-tree/types.ts @@ -15,10 +15,18 @@ export interface ApproverConfig { signMode?: 'or' | 'and' } +export type ConditionOp = '>' | '>=' | '<' | '<=' | '==' | '!=' + +export interface ConditionRule { + field: string + op: ConditionOp + value: string | number +} + export interface ConditionGroup { name: string when?: string - rules?: unknown[] + rules?: ConditionRule[] isDefault?: boolean child: FlowNode | null } diff --git a/web-admin/src/oa-fc-host/App.vue b/web-admin/src/oa-fc-host/App.vue index 6251d3e6..1ba9f729 100644 --- a/web-admin/src/oa-fc-host/App.vue +++ b/web-admin/src/oa-fc-host/App.vue @@ -1,6 +1,6 @@ @@ -151,6 +263,7 @@ } .oa-fc-host { + position: relative; box-sizing: border-box; width: 100%; height: 100%; @@ -165,6 +278,46 @@ height: 100% !important; } + .oa-cond-flag { + position: absolute; + top: 56px; + right: 12px; + z-index: 30; + width: 292px; + padding: 12px; + background: #ecfdf5; + border: 1px dashed #a7f3d0; + border-radius: 8px; + box-shadow: 0 2px 8px rgb(15 23 42 / 8%); + } + + .oa-cond-flag__row { + display: flex; + gap: 10px; + align-items: flex-start; + } + + .oa-cond-flag__title { + font-size: 13px; + font-weight: 600; + color: #059669; + } + + .oa-cond-flag__tip { + margin-top: 2px; + font-size: 12px; + line-height: 1.4; + color: #64748b; + } + + /* 参与分支时锁定「是否必填」 */ + .oa-fc-host--lock-required ._fc-r .el-form-item:has([name='$required']), + .oa-fc-host--lock-required ._fc-r .el-form-item:has(.fc-required), + .oa-fc-host--lock-required ._fc-validate { + pointer-events: none; + opacity: 0.55; + } + /* 防止宿主环境意外丢弃 aside 宽度 */ .oa-fc-host .el-aside._fc-l-menu { width: 40px !important; @@ -201,7 +354,6 @@ overflow: auto; } - /* 兜底:若短暂出现水印节点,先视觉藏住(最终由 JS 移除) */ .oa-fc-host ._fc-m-con > a[href*='form-create.com'] { clip-path: inset(50%) !important; transform: scale(0) !important; diff --git a/web-admin/src/views/oa-approval/designer/index.vue b/web-admin/src/views/oa-approval/designer/index.vue index c854f792..7f4ced1a 100644 --- a/web-admin/src/views/oa-approval/designer/index.vue +++ b/web-admin/src/views/oa-approval/designer/index.vue @@ -72,11 +72,12 @@ v-if="formDesignerMounted" ref="formEmbedRef" v-model="formSchema" + :flow-condition-fields="flowConditionFields" />
- +
@@ -107,7 +108,7 @@