feat(oa): confirmation kit defaults and confirmation-date linkage on start form

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 17:27:05 +08:00
1 parent 898a337f96
commit 493ce698df
3 files changed
+470 -4

No files matched your search

@@ -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<Record<ConfirmationKitFieldKey, string>>
visible: Record<ConfirmationKitFieldKey, boolean>
confirmationDateField?: string
hireDateField?: string
probationMonthsField?: string
}
function asRules(schema: unknown): Record<string, unknown>[] {
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<string, unknown>[], out: Record<string, unknown>[]) {
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<string, unknown>[],
out
)
}
const props = r.props as Record<string, unknown> | undefined
if (props && Array.isArray(props.rule)) {
walk(
props.rule.filter((c) => c && typeof c === 'object') as Record<string, unknown>[],
out
)
}
}
}
function kitChildren(kit: Record<string, unknown>): Record<string, any>[] {
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<string, unknown>): ConfirmationKitBinding {
const props = (kit.props && typeof kit.props === 'object' ? kit.props : {}) as Record<
string,
any
>
const fields: Partial<Record<ConfirmationKitFieldKey, string>> = {}
const propFieldMap: Record<string, ConfirmationKitFieldKey> = {
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<ConfirmationKitFieldKey, boolean>
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<string, unknown>[] = []
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<string, unknown>
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<string, unknown>,
user: Record<string, unknown> | null | undefined
): Promise<Record<string, unknown>> {
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<string, unknown>
): { data: Record<string, unknown>; 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<string, unknown>,
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<string, unknown>,
confirmationDateField: string
) {
let data = markConfirmationDateManual(formData, confirmationDateField, false)
const linked = applyConfirmationKitDateLinkage(schema, data)
return linked.data
}
export function listManualConfirmationDateFields(schema: unknown, formData: Record<string, unknown>) {
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
)
}
@@ -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 || {}),
+173 -3
View File
@@ -105,7 +105,17 @@
:rule="formRule"
:option="formOption"
/>
<p v-else-if="!detailLoading" class="oa-start__tip">该模板暂无表单字段,仍可直接提交。</p>
<p
v-if="confirmationDateManualFields.length"
class="oa-start__tip oa-start__confirm-relink"
>
<el-button link type="primary" @click="restoreConfirmationDateAuto">
恢复转正日期自动计算
</el-button>
</p>
<p v-else-if="!formRule.length && !detailLoading" class="oa-start__tip">
该模板暂无表单字段,仍可直接提交。
</p>
<OaFlowPreview
v-if="!detailLoading"
v-model:select-by-node="initiatorSelectByNode"
@@ -154,11 +164,34 @@
OA_FC_DEFAULT_GUTTER,
ensureOaUploadRuleProps
} from '@/utils/oa-form-create-upload'
import {
applyLeaveKitDurationsToFormData,
ensureLeaveKitRuleProps,
LEAVE_MANUAL_PREFIX,
listLeaveKitBindings,
markLeaveDurationManual
} from '@/utils/oa-leave-kit-runtime'
import {
applyConfirmationKitDateLinkage,
applyConfirmationKitDefaults,
clearConfirmationDateManualAndRelink,
CONFIRMATION_MANUAL_PREFIX,
ensureConfirmationKitVisibility,
listConfirmationKitBindings,
listManualConfirmationDateFields,
markConfirmationDateManual
} from '@/utils/oa-confirmation-kit-runtime'
import {
applyOwnerFieldDefaults,
ensureOwnerFieldRuleProps
} from '@/utils/oa-owner-field-defaults'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'OaApprovalStart' })
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loading = ref(false)
const templates = ref<any[]>([])
const groupOptions = ref<Array<{ id: string; groupName: string }>>([])
@@ -184,6 +217,15 @@
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
const previewLoading = ref(false)
let previewTimer: ReturnType<typeof setTimeout> | null = null
/** 避免时长自动回写再次触发手改判定 */
let leaveKitSyncing = false
let leaveKitPrevSnapshot: Record<string, unknown> = {}
let confirmationKitSyncing = false
let confirmationKitPrevSnapshot: Record<string, unknown> = {}
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 {
/* 预填失败仍可手工填 */
}