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>
This commit is contained in:
1 parent
9ae239347a
commit
638c4184d2
7 files changed
+529
-37
No files matched your search
@@ -22,6 +22,8 @@
|
||||
const props = defineProps<{
|
||||
/** { rule, option } */
|
||||
modelValue?: { rule?: unknown; option?: unknown } | null
|
||||
/** 已被流程条件引用的 field 列表(用于拦截取消勾选) */
|
||||
flowConditionFields?: string[]
|
||||
}>()
|
||||
|
||||
const frameRef = ref<HTMLIFrameElement | null>(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)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<!-- 条件分支可视化规则:字段 + 运算符 + 值 -->
|
||||
<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>
|
||||
@@ -34,13 +34,13 @@
|
||||
class="flow-cond-col"
|
||||
>
|
||||
<div class="flow-cond-col__title" @click.stop="emit('edit', node)">
|
||||
{{ g.name || (g.isDefault ? '默认' : `条件${idx + 1}`) }}
|
||||
<span v-if="g.when" class="muted">{{ g.when }}</span>
|
||||
{{ branchTitle(g, idx) }}
|
||||
</div>
|
||||
<FlowBranch
|
||||
v-if="g.child"
|
||||
:node="g.child"
|
||||
:depth="depth + 1"
|
||||
:candidates="candidates"
|
||||
@edit="(n) => emit('edit', n)"
|
||||
@remove="(id) => emit('remove', id)"
|
||||
@add="(payload) => emit('add', payload)"
|
||||
@@ -66,6 +66,7 @@
|
||||
v-if="node.child"
|
||||
:node="node.child"
|
||||
:depth="depth"
|
||||
:candidates="candidates"
|
||||
@edit="(n) => emit('edit', n)"
|
||||
@remove="(id) => emit('remove', id)"
|
||||
@add="(payload) => emit('add', payload)"
|
||||
@@ -78,7 +79,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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') {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<div class="oa-flow-designer__canvas-inner">
|
||||
<FlowBranch
|
||||
:node="root"
|
||||
:candidates="candidates"
|
||||
@edit="openEdit"
|
||||
@remove="onRemove"
|
||||
@add="onAdd"
|
||||
@@ -217,13 +218,12 @@
|
||||
<el-form-item :label="`分支 ${idx + 1} 名称`">
|
||||
<el-input v-model="g.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="条件公式(空=默认分支)">
|
||||
<el-input
|
||||
v-model="g.when"
|
||||
placeholder="如 {days} > 3"
|
||||
:disabled="g.isDefault"
|
||||
/>
|
||||
</el-form-item>
|
||||
<ConditionRuleEditor
|
||||
v-model="g.rules"
|
||||
:disabled="!!g.isDefault"
|
||||
:candidates="candidates"
|
||||
:legacy-when="g.when"
|
||||
/>
|
||||
<el-form-item label="默认分支">
|
||||
<el-switch
|
||||
v-model="g.isDefault"
|
||||
@@ -252,6 +252,8 @@
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import { fetchOaRoleList } from '@/api/oaApproval'
|
||||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||||
import type { ConditionCandidate } from '@/components/oa-approval/oa-condition-fields'
|
||||
import ConditionRuleEditor from './ConditionRuleEditor.vue'
|
||||
import FlowBranch from './FlowBranch.vue'
|
||||
import {
|
||||
createEndNode,
|
||||
@@ -260,6 +262,7 @@
|
||||
findParent,
|
||||
type ApproverConfig,
|
||||
type AssigneeType,
|
||||
type ConditionRule,
|
||||
type FlowNode
|
||||
} from './types'
|
||||
|
||||
@@ -278,9 +281,15 @@
|
||||
'#ec4899'
|
||||
]
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: FlowNode | Record<string, unknown> | null
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: FlowNode | Record<string, unknown> | 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- FormCreate 全屏宿主(无 Tailwind) -->
|
||||
<template>
|
||||
<div class="oa-fc-host">
|
||||
<div class="oa-fc-host" :class="{ 'oa-fc-host--lock-required': lockRequired }">
|
||||
<fc-designer
|
||||
ref="designerRef"
|
||||
class="oa-fc-host__designer"
|
||||
@@ -8,13 +8,24 @@
|
||||
:config="designerConfig"
|
||||
:locale="fcLocale"
|
||||
/>
|
||||
<div v-show="showCondFlag" class="oa-cond-flag">
|
||||
<div class="oa-cond-flag__row">
|
||||
<el-switch :model-value="flagOn" @change="onFlagChange" />
|
||||
<div>
|
||||
<div class="oa-cond-flag__title">参与流程条件分支</div>
|
||||
<div class="oa-cond-flag__tip">勾选后该字段必填且不可改为选填;可在「3 流程设计」条件中选用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import formCreateZh from '@form-create/designer/locale/zh-cn'
|
||||
import { registerOaDesignerAttachment } from '@/components/oa-approval/oa-form-components'
|
||||
import { CONDITION_ELIGIBLE_TYPES } from '@/components/oa-approval/oa-condition-fields'
|
||||
|
||||
defineOptions({ name: 'OaFcHostApp' })
|
||||
|
||||
@@ -41,19 +52,102 @@
|
||||
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 flowConditionFields = ref<string[]>([])
|
||||
const showCondFlag = ref(false)
|
||||
const flagOn = ref(false)
|
||||
const lockRequired = ref(false)
|
||||
let pollTimer: number | null = null
|
||||
let lastActiveId = ''
|
||||
|
||||
function getDesigner(): DesignerApi | null {
|
||||
const inst = designerRef.value as any
|
||||
if (!inst) return null
|
||||
// Vue3 组件实例可能挂在 expose / proxy 上
|
||||
if (typeof inst.setRule === 'function') return inst
|
||||
if (inst.$ && typeof inst.$.exposed?.setRule === 'function') return inst.$.exposed
|
||||
if (typeof inst.setupState?.setRule === 'function') return inst.setupState
|
||||
return inst
|
||||
}
|
||||
|
||||
function getSetupState(): any {
|
||||
const inst = designerRef.value as any
|
||||
if (!inst) return null
|
||||
return inst.setupState || inst.$?.setupState || inst.$.exposed?.setupState || null
|
||||
}
|
||||
|
||||
function getActiveRule(): any {
|
||||
const state = getSetupState()
|
||||
return state?.activeRule || null
|
||||
}
|
||||
|
||||
function readFlag(rule: any): boolean {
|
||||
return rule?.useInCondition === true || rule?.props?.useInCondition === true
|
||||
}
|
||||
|
||||
function applyFlagToRule(rule: any, on: boolean) {
|
||||
rule.useInCondition = on
|
||||
if (!rule.props || typeof rule.props !== 'object') rule.props = {}
|
||||
rule.props.useInCondition = on
|
||||
if (on) {
|
||||
rule.$required = true
|
||||
rule.required = true
|
||||
}
|
||||
}
|
||||
|
||||
function syncActivePanel() {
|
||||
const rule = getActiveRule()
|
||||
const type = String(rule?.type || '')
|
||||
const eligible = !!(rule?.field && CONDITION_ELIGIBLE_TYPES.has(type))
|
||||
showCondFlag.value = eligible
|
||||
if (!eligible) {
|
||||
flagOn.value = false
|
||||
lockRequired.value = false
|
||||
lastActiveId = ''
|
||||
return
|
||||
}
|
||||
const id = String(rule._fc_id || rule.field || '')
|
||||
const on = readFlag(rule)
|
||||
flagOn.value = on
|
||||
lockRequired.value = on
|
||||
if (on) applyFlagToRule(rule, true)
|
||||
lastActiveId = id
|
||||
}
|
||||
|
||||
function onFlagChange(val: string | number | boolean) {
|
||||
const on = !!val
|
||||
const rule = getActiveRule()
|
||||
if (!rule?.field) return
|
||||
if (!on) {
|
||||
const field = String(rule.field)
|
||||
if (flowConditionFields.value.includes(field)) {
|
||||
ElMessage.warning('请先在流程设计中删除对该字段的条件')
|
||||
flagOn.value = true
|
||||
return
|
||||
}
|
||||
applyFlagToRule(rule, false)
|
||||
flagOn.value = false
|
||||
lockRequired.value = false
|
||||
return
|
||||
}
|
||||
applyFlagToRule(rule, true)
|
||||
flagOn.value = true
|
||||
lockRequired.value = true
|
||||
// 同步校验面板必填开关
|
||||
try {
|
||||
const state = getSetupState()
|
||||
const vapi = state?.validateForm?.api
|
||||
if (vapi?.setValue) {
|
||||
const cur = typeof vapi.formData === 'function' ? vapi.formData() : {}
|
||||
vapi.setValue({ ...cur, $required: true })
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function readSchema() {
|
||||
const d = getDesigner()
|
||||
return {
|
||||
@@ -82,6 +176,7 @@
|
||||
d.setOption(JSON.parse(JSON.stringify(option)))
|
||||
}
|
||||
pendingSchema = null
|
||||
window.setTimeout(() => syncActivePanel(), 80)
|
||||
} catch (e) {
|
||||
console.warn('[oa-fc-host] applySchema failed', e)
|
||||
if (attempt < 5) {
|
||||
@@ -102,6 +197,12 @@
|
||||
{ type: MSG.SCHEMA, schema: readSchema(), requestId: data.requestId },
|
||||
'*'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (data.type === MSG.FLOW_REFS) {
|
||||
flowConditionFields.value = Array.isArray(data.fields)
|
||||
? data.fields.map((x: unknown) => String(x))
|
||||
: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +230,23 @@
|
||||
if (pendingSchema) {
|
||||
void applySchema(pendingSchema)
|
||||
}
|
||||
pollTimer = window.setInterval(() => {
|
||||
const rule = getActiveRule()
|
||||
const id = rule ? String(rule._fc_id || rule.field || '') : ''
|
||||
if (id !== lastActiveId || (rule && readFlag(rule) !== flagOn.value)) {
|
||||
syncActivePanel()
|
||||
}
|
||||
}, 250)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
brandObserver?.disconnect()
|
||||
brandObserver = null
|
||||
if (pollTimer != null) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -72,11 +72,12 @@
|
||||
v-if="formDesignerMounted"
|
||||
ref="formEmbedRef"
|
||||
v-model="formSchema"
|
||||
:flow-condition-fields="flowConditionFields"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="activeStep === 'flow'" class="oa-designer__step oa-designer__flow-step">
|
||||
<FlowDesigner v-model="flowTreeJson" />
|
||||
<FlowDesigner v-model="flowTreeJson" :candidates="conditionCandidates" />
|
||||
</div>
|
||||
|
||||
<div v-show="activeStep === 'advanced'" class="oa-designer__step">
|
||||
@@ -107,7 +108,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
@@ -117,6 +118,11 @@
|
||||
import FlowDesigner from '@/components/oa-approval/flow-tree/FlowDesigner.vue'
|
||||
import { createDefaultFlow, ensureFlowRoot } from '@/components/oa-approval/flow-tree/types'
|
||||
import OaFormDesignerEmbed from '@/components/oa-approval/OaFormDesignerEmbed.vue'
|
||||
import {
|
||||
collectConditionReferencedFields,
|
||||
listConditionCandidates,
|
||||
normalizeSchemaConditionRequired
|
||||
} from '@/components/oa-approval/oa-condition-fields'
|
||||
import { pickRandomOaAppIconUrl, resolveOaAppIconUrl } from '@/utils/oa-app-icons'
|
||||
|
||||
defineOptions({ name: 'OaApprovalDesigner' })
|
||||
@@ -157,20 +163,31 @@
|
||||
|
||||
const flowTreeJson = ref(createDefaultFlow())
|
||||
|
||||
const flowConditionFields = computed(() =>
|
||||
Array.from(collectConditionReferencedFields(flowTreeJson.value))
|
||||
)
|
||||
const conditionCandidates = computed(() => listConditionCandidates(formSchema.value))
|
||||
|
||||
async function collectFormSchema() {
|
||||
let raw: { rule: unknown; option: unknown }
|
||||
if (formEmbedRef.value?.fetchSchema) {
|
||||
try {
|
||||
const schema = await formEmbedRef.value.fetchSchema()
|
||||
formSchema.value = schema
|
||||
return schema
|
||||
raw = await formEmbedRef.value.fetchSchema()
|
||||
} catch {
|
||||
/* 超时则回退本地缓存 */
|
||||
raw = {
|
||||
rule: formSchema.value?.rule ?? draft.value?.formSchemaJson?.rule ?? [],
|
||||
option: formSchema.value?.option ?? draft.value?.formSchemaJson?.option ?? {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
raw = {
|
||||
rule: formSchema.value?.rule ?? draft.value?.formSchemaJson?.rule ?? [],
|
||||
option: formSchema.value?.option ?? draft.value?.formSchemaJson?.option ?? {}
|
||||
}
|
||||
}
|
||||
return {
|
||||
rule: formSchema.value?.rule ?? draft.value?.formSchemaJson?.rule ?? [],
|
||||
option: formSchema.value?.option ?? draft.value?.formSchemaJson?.option ?? {}
|
||||
}
|
||||
const normalized = normalizeSchemaConditionRequired(raw)
|
||||
formSchema.value = normalized
|
||||
return normalized
|
||||
}
|
||||
|
||||
async function uploadBasicIcon(options: UploadRequestOptions) {
|
||||
|
||||
Reference in new issue
Block a user