From 493ce698df4338933457b6fd63ccb272261e57c2 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 30 Jul 2026 17:27:05 +0800 Subject: [PATCH] feat(oa): confirmation kit defaults and confirmation-date linkage on start form Co-authored-by: Cursor --- .../src/utils/oa-confirmation-kit-runtime.ts | 291 ++++++++++++++++++ .../views/oa-approval/instances/detail.vue | 7 +- .../src/views/oa-approval/start/index.vue | 176 ++++++++++- 3 files changed, 470 insertions(+), 4 deletions(-) create mode 100644 web-admin/src/utils/oa-confirmation-kit-runtime.ts diff --git a/web-admin/src/utils/oa-confirmation-kit-runtime.ts b/web-admin/src/utils/oa-confirmation-kit-runtime.ts new file mode 100644 index 00000000..4ee732f4 --- /dev/null +++ b/web-admin/src/utils/oa-confirmation-kit-runtime.ts @@ -0,0 +1,291 @@ +import { addMonthsYmd } from '@/utils/oa-confirmation-date' +import { + CONFIRMATION_KIT_FIELD_DEFS, + type ConfirmationKitFieldKey +} from '@/utils/oa-confirmation-kit-rule' +import { resolveCurrentOwnerDept } from '@/utils/oa-owner-field-defaults' + +export const CONFIRMATION_MANUAL_PREFIX = '__manual_' + +export type ConfirmationKitBinding = { + fields: Partial> + visible: Record + confirmationDateField?: string + hireDateField?: string + probationMonthsField?: string +} + +function asRules(schema: unknown): Record[] { + if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === 'object') as any + if (schema && typeof schema === 'object') { + const s = schema as any + const rules = s.rule || s.rules || s.fields + if (Array.isArray(rules)) return rules.filter((r: any) => r && typeof r === 'object') + if (s.type || s.field) return [s] + } + return [] +} + +function walk(rules: Record[], out: Record[]) { + for (const r of rules) { + out.push(r) + const children = r.children + if (Array.isArray(children)) { + walk( + children.filter((c) => c && typeof c === 'object') as Record[], + out + ) + } + const props = r.props as Record | undefined + if (props && Array.isArray(props.rule)) { + walk( + props.rule.filter((c) => c && typeof c === 'object') as Record[], + out + ) + } + } +} + +function kitChildren(kit: Record): Record[] { + const props = (kit.props && typeof kit.props === 'object' ? kit.props : {}) as Record< + string, + any + > + if (Array.isArray(kit.children)) return kit.children as any + if (Array.isArray(props.rule)) return props.rule as any + return [] +} + +function resolveBinding(kit: Record): ConfirmationKitBinding { + const props = (kit.props && typeof kit.props === 'object' ? kit.props : {}) as Record< + string, + any + > + const fields: Partial> = {} + const propFieldMap: Record = { + hireDateField: 'hireDate', + probationMonthsField: 'probationMonths', + confirmationDateField: 'confirmationDate', + postField: 'post', + jobLevelField: 'jobLevel', + nameField: 'name', + emailField: 'email', + deptField: 'dept', + phoneField: 'phone' + } + for (const [propKey, sem] of Object.entries(propFieldMap)) { + const f = props[propKey] + if (f) fields[sem] = String(f) + } + + for (const c of kitChildren(kit)) { + if (!c?.field) continue + const sem = String(c.props?._fc_kit_field || '') as ConfirmationKitFieldKey + if (CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) && !fields[sem]) { + fields[sem] = String(c.field) + } + } + + const visible = {} as Record + for (const d of CONFIRMATION_KIT_FIELD_DEFS) { + const v = props[d.showProp] + visible[d.key] = v === undefined || v === null ? true : !!v + } + + return { + fields, + visible, + confirmationDateField: fields.confirmationDate, + hireDateField: fields.hireDate, + probationMonthsField: fields.probationMonths + } +} + +export function listConfirmationKitBindings(schema: unknown): ConfirmationKitBinding[] { + const flat: Record[] = [] + walk(asRules(schema), flat) + const out: ConfirmationKitBinding[] = [] + for (const r of flat) { + if (String(r.type || '') !== 'confirmationKit') continue + out.push(resolveBinding(r)) + } + return out +} + +function isEmptyValue(raw: unknown): boolean { + if (raw == null || raw === '') return true + if (Array.isArray(raw)) return raw.length === 0 + if (typeof raw === 'object') { + const o = raw as Record + return !String(o.id || '').trim() + } + return false +} + +/** 按 show* props 同步子字段 hidden,并去掉隐藏字段的前端必填 */ +export function ensureConfirmationKitVisibility(rules: unknown): any[] { + if (!Array.isArray(rules)) return [] + return rules.map((raw) => { + if (!raw || typeof raw !== 'object') return raw + const rule: any = { ...(raw as object) } + if (String(rule.type || '') === 'confirmationKit') { + const binding = resolveBinding(rule) + const patchChildren = (list: any[]): any[] => + list.map((c) => { + if (!c || typeof c !== 'object') return c + const child: any = { ...c } + const sem = String(child.props?._fc_kit_field || '') as ConfirmationKitFieldKey + const show = + sem && CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) + ? binding.visible[sem] !== false + : true + child.hidden = !show + if (!show) { + child.$required = false + if (child.validate) child.validate = [] + } + if (Array.isArray(child.children)) { + child.children = patchChildren(child.children) + } + return child + }) + if (Array.isArray(rule.children)) { + rule.children = patchChildren(rule.children) + } + if (Array.isArray(rule.props?.rule)) { + rule.props = { + ...rule.props, + rule: patchChildren(rule.props.rule) + } + } + } + if (Array.isArray(rule.children)) { + rule.children = ensureConfirmationKitVisibility(rule.children) + } + if (Array.isArray(rule.props?.rule)) { + rule.props = { + ...rule.props, + rule: ensureConfirmationKitVisibility(rule.props.rule) + } + } + return rule + }) +} + +export async function applyConfirmationKitDefaults( + schema: unknown, + formData: Record, + user: Record | null | undefined +): Promise> { + const data = { ...formData } + const kits = listConfirmationKitBindings(schema) + if (!kits.length || !user) return data + + const name = String(user.realName || user.nickName || user.userName || '').trim() + const email = String(user.email || '').trim() + const phone = String(user.phone || user.phoneNo || '').trim() + const hireDate = String(user.hireDate || '').trim() + const probationRaw = user.probationMonths + const probationMonths = + probationRaw === null || probationRaw === undefined || probationRaw === '' + ? null + : Number(probationRaw) + + const dept = await resolveCurrentOwnerDept(user as any) + + for (const kit of kits) { + const setIfEmpty = (sem: ConfirmationKitFieldKey, value: unknown) => { + const field = kit.fields[sem] + if (!field) return + if (!isEmptyValue(data[field])) return + if (value === null || value === undefined || value === '') return + data[field] = value + } + + setIfEmpty('hireDate', hireDate || null) + if (probationMonths !== null && Number.isFinite(probationMonths) && probationMonths >= 0) { + setIfEmpty('probationMonths', Math.trunc(probationMonths)) + } + setIfEmpty('name', name || null) + setIfEmpty('email', email || null) + setIfEmpty('phone', phone || null) + if (dept) setIfEmpty('dept', dept) + + const confField = kit.confirmationDateField + const hireField = kit.hireDateField + const probField = kit.probationMonthsField + if (confField && isEmptyValue(data[confField]) && hireField && probField) { + const suggested = addMonthsYmd( + String(data[hireField] || ''), + Number(data[probField]) + ) + if (suggested) data[confField] = suggested + } + } + return data +} + +export function applyConfirmationKitDateLinkage( + schema: unknown, + formData: Record +): { data: Record; changed: boolean } { + const data = { ...formData } + let changed = false + for (const kit of listConfirmationKitBindings(schema)) { + const confField = kit.confirmationDateField + const hireField = kit.hireDateField + const probField = kit.probationMonthsField + if (!confField || !hireField || !probField) continue + if (data[CONFIRMATION_MANUAL_PREFIX + confField]) continue + const suggested = addMonthsYmd(String(data[hireField] || ''), Number(data[probField])) + if (suggested == null) continue + if (data[confField] !== suggested) { + data[confField] = suggested + changed = true + } + } + return { data, changed } +} + +export function markConfirmationDateManual( + formData: Record, + confirmationDateField: string, + manual = true +) { + const data = { ...formData } + const key = CONFIRMATION_MANUAL_PREFIX + confirmationDateField + if (manual) data[key] = true + else delete data[key] + return data +} + +export function clearConfirmationDateManualAndRelink( + schema: unknown, + formData: Record, + confirmationDateField: string +) { + let data = markConfirmationDateManual(formData, confirmationDateField, false) + const linked = applyConfirmationKitDateLinkage(schema, data) + return linked.data +} + +export function listManualConfirmationDateFields(schema: unknown, formData: Record) { + return listConfirmationKitBindings(schema) + .map((k) => k.confirmationDateField) + .filter((f): f is string => !!f && !!formData[CONFIRMATION_MANUAL_PREFIX + f]) +} + +export function isConfirmationDateField(schema: unknown, field: string): boolean { + return listConfirmationKitBindings(schema).some((k) => k.confirmationDateField === field) +} + +export function findConfirmationKitByHireOrProbation( + schema: unknown, + field: string +): ConfirmationKitBinding | null { + return ( + listConfirmationKitBindings(schema).find( + (k) => k.hireDateField === field || k.probationMonthsField === field + ) || null + ) +} diff --git a/web-admin/src/views/oa-approval/instances/detail.vue b/web-admin/src/views/oa-approval/instances/detail.vue index bab02074..3447af97 100644 --- a/web-admin/src/views/oa-approval/instances/detail.vue +++ b/web-admin/src/views/oa-approval/instances/detail.vue @@ -210,6 +210,9 @@ OA_FC_DEFAULT_GUTTER, ensureOaUploadRuleProps } from '@/utils/oa-form-create-upload' + import { ensureLeaveKitRuleProps } from '@/utils/oa-leave-kit-runtime' + import { ensureConfirmationKitVisibility } from '@/utils/oa-confirmation-kit-runtime' + import { ensureOwnerFieldRuleProps } from '@/utils/oa-owner-field-defaults' defineOptions({ name: 'OaApprovalInstanceDetail' }) @@ -340,7 +343,9 @@ function applySchema(schema: any, data: any) { viewFormData.value = { ...(data || {}) } const rule = Array.isArray(schema?.rule) ? schema.rule : Array.isArray(schema) ? schema : [] - viewRule.value = ensureOaUploadRuleProps(rule) + viewRule.value = ensureConfirmationKitVisibility( + ensureOwnerFieldRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rule))) + ) const savedGutter = Number(schema?.option?.row?.gutter) viewOption.value = { ...(schema?.option || {}), diff --git a/web-admin/src/views/oa-approval/start/index.vue b/web-admin/src/views/oa-approval/start/index.vue index 899586e9..6b106c8c 100644 --- a/web-admin/src/views/oa-approval/start/index.vue +++ b/web-admin/src/views/oa-approval/start/index.vue @@ -105,7 +105,17 @@ :rule="formRule" :option="formOption" /> -

该模板暂无表单字段,仍可直接提交。

+ +

+ 该模板暂无表单字段,仍可直接提交。 +

([]) const groupOptions = ref>([]) @@ -184,6 +217,15 @@ const flowPendingMessage = ref('必填信息填写后,流程将自动显示') const previewLoading = ref(false) let previewTimer: ReturnType | null = null + /** 避免时长自动回写再次触发手改判定 */ + let leaveKitSyncing = false + let leaveKitPrevSnapshot: Record = {} + let confirmationKitSyncing = false + let confirmationKitPrevSnapshot: Record = {} + + const confirmationDateManualFields = computed(() => + listManualConfirmationDateFields({ rule: formRule.value }, formData.value) + ) const displayGroups = computed(() => { const kw = (searchKeyword.value || '').trim().toLowerCase() @@ -288,6 +330,8 @@ picked.value = t formData.value = {} formRule.value = [] + leaveKitPrevSnapshot = {} + confirmationKitPrevSnapshot = {} needInitiatorSelect.value = false initiatorSelectByNode.value = {} flowSteps.value = [] @@ -304,7 +348,11 @@ : Array.isArray(schema) ? schema : [] - formRule.value = ensureOaUploadRuleProps(rawRule) + formRule.value = ensureConfirmationKitVisibility( + ensureOwnerFieldRuleProps( + ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule)) + ) + ) const savedGutter = Number(schema.option?.row?.gutter) formOption.value = { ...(schema.option || {}), @@ -320,6 +368,19 @@ submitBtn: false, resetBtn: false } + let data = await applyOwnerFieldDefaults( + formRule.value, + {}, + (userStore.info || userStore.getUserInfo) as any + ) + data = await applyConfirmationKitDefaults( + { rule: formRule.value }, + data, + (userStore.info || userStore.getUserInfo) as any + ) + const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, data) + formData.value = linked.data + confirmationKitPrevSnapshot = { ...linked.data } const flow = detail?.flowTreeJson || {} const root = flow.root || flow needInitiatorSelect.value = flowNeedsInitiatorSelect(root) @@ -337,9 +398,114 @@ } } + function syncLeaveKitDurations() { + if (leaveKitSyncing) return + if (!formRule.value.length) return + const schema = { rule: formRule.value } + const bindings = listLeaveKitBindings(schema) + if (!bindings.length) return + + const curr = { ...formData.value } + for (const kit of bindings) { + const prevDur = leaveKitPrevSnapshot[kit.durationField] + const currDur = curr[kit.durationField] + const startChanged = + leaveKitPrevSnapshot[kit.startField] !== curr[kit.startField] + const endChanged = leaveKitPrevSnapshot[kit.endField] !== curr[kit.endField] + const durChanged = prevDur !== currDur + if (durChanged && !startChanged && !endChanged && currDur !== undefined && currDur !== '') { + Object.assign(curr, markLeaveDurationManual(curr, kit.durationField, true)) + } + } + + const { data, changed } = applyLeaveKitDurationsToFormData(schema, curr) + leaveKitPrevSnapshot = { ...data } + const manualKeysChanged = bindings.some( + (k) => + !!data[LEAVE_MANUAL_PREFIX + k.durationField] !== + !!formData.value[LEAVE_MANUAL_PREFIX + k.durationField] + ) + const durationChanged = bindings.some( + (k) => data[k.durationField] !== formData.value[k.durationField] + ) + if (changed || manualKeysChanged || durationChanged) { + leaveKitSyncing = true + formData.value = data + queueMicrotask(() => { + leaveKitSyncing = false + }) + } + } + + function syncConfirmationKitDates() { + if (confirmationKitSyncing) return + if (!formRule.value.length) return + const schema = { rule: formRule.value } + const kits = listConfirmationKitBindings(schema) + if (!kits.length) return + + let curr = { ...formData.value } + for (const kit of kits) { + const confField = kit.confirmationDateField + const hireField = kit.hireDateField + const probField = kit.probationMonthsField + if (!confField || !hireField || !probField) continue + const confChanged = + confirmationKitPrevSnapshot[confField] !== curr[confField] + const hireChanged = confirmationKitPrevSnapshot[hireField] !== curr[hireField] + const probChanged = confirmationKitPrevSnapshot[probField] !== curr[probField] + if ( + confChanged && + !hireChanged && + !probChanged && + curr[confField] !== undefined && + curr[confField] !== '' + ) { + curr = markConfirmationDateManual(curr, confField, true) + } + } + + const { data, changed } = applyConfirmationKitDateLinkage(schema, curr) + confirmationKitPrevSnapshot = { ...data } + const anyDiff = + changed || + kits.some((k) => { + const f = k.confirmationDateField + if (!f) return false + return ( + data[f] !== formData.value[f] || + !!data[CONFIRMATION_MANUAL_PREFIX + f] !== + !!formData.value[CONFIRMATION_MANUAL_PREFIX + f] + ) + }) + if (anyDiff) { + confirmationKitSyncing = true + formData.value = data + queueMicrotask(() => { + confirmationKitSyncing = false + }) + } + } + + function restoreConfirmationDateAuto() { + const schema = { rule: formRule.value } + let data = { ...formData.value } + for (const field of listManualConfirmationDateFields(schema, data)) { + data = clearConfirmationDateManualAndRelink(schema, data, field) + } + confirmationKitSyncing = true + formData.value = data + confirmationKitPrevSnapshot = { ...data } + queueMicrotask(() => { + confirmationKitSyncing = false + }) + } + watch( formData, () => { + syncLeaveKitDurations() + syncConfirmationKitDates() scheduleFlowPreview() }, { deep: true } @@ -403,7 +569,11 @@ if (qCopyFrom) { try { const old = await fetchOaInstanceDetail(qCopyFrom) - formData.value = { ...(old?.formDataJson || {}) } + formData.value = await applyOwnerFieldDefaults( + formRule.value, + { ...(old?.formDataJson || {}) }, + (userStore.info || userStore.getUserInfo) as any + ) } catch { /* 预填失败仍可手工填 */ }