import { computed, ref } from 'vue' import type { Ref, ComputedRef } from 'vue' import type { ProTableFormActionType, ProTableFormColumn, ProTableFormColumnChild, ProTableFormProps, FormInstance, } from '../types' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ProTableFormOptions { props: Readonly emit: (event: 'update:modelValue', value: Record[]) => void /** 新增行事件 */ emitAddRow?: () => void /** 删除行事件 */ emitRemoveRow?: (index: number) => void } /** useProTableForm 返回 */ export interface UseProTableFormReturn { // --- 注册 --- register: (formAction: ProTableFormActionType) => void // --- el-form 绑定 --- currentModelValue: ComputedRef[]> formModelRef: ComputedRef<{ rows: Record[] }> mergedRules: ComputedRef> // --- el-table 绑定 --- tableRows: ComputedRef rowKeyFn: (row: TableRow) => string spanMethodAdapter: (params: { row: TableRow column: { property: string; label: string } rowIndex: number columnIndex: number }) => [number, number] | { rowspan: number; colspan: number } | void allLeafColumnKeys: ComputedRef // --- ref --- formRef: Ref /** 获取 el-form 实例 */ getFormRef: () => FormInstance | null // --- 单元格渲染辅助 --- cellComponent: (col: ProTableFormColumn | ProTableFormColumnChild) => unknown cellBind: (col: ProTableFormColumn | ProTableFormColumnChild) => Record slotUpdateHandler: (slotProps: { row: TableRow }, col: ProTableFormColumn, childKey?: string) => (v: unknown) => void getCellProp: (tableRow: TableRow, col: ProTableFormColumn, childKey?: string) => string // --- 单元格值读写 --- getCellValue: (row: TableRow, col: ProTableFormColumn, childKey?: string) => unknown setCellValue: (tableRow: TableRow, col: ProTableFormColumn, childKey: string | undefined, val: unknown) => void // --- 行操作 --- handleAddRow: () => void handleRemoveRow: (index: number) => void // --- 行读写(供 action 暴露) --- getRow: (index: number) => Record setRow: (index: number, row: Record) => void // --- el-form 实例方法 --- validate: () => Promise clearValidate: (propsArg?: string | string[]) => void } // --------------------------------------------------------------------------- // Composable // --------------------------------------------------------------------------- type TableRow = { _index: number } export function useProTableForm(options: ProTableFormOptions): UseProTableFormReturn { const { props, emit, emitAddRow, emitRemoveRow } = options // --------------------------------------------------------------------------- // 注册机制(与 useForm 一致) // --------------------------------------------------------------------------- const formActionRef = ref(null) const register = (action: ProTableFormActionType) => { formActionRef.value = action } // --------------------------------------------------------------------------- // el-form ref // --------------------------------------------------------------------------- const formRef = ref(null) // --------------------------------------------------------------------------- // 受控 / 非受控双轨(与 ProForm 一致) // --------------------------------------------------------------------------- const controlledModelValue = computed(() => props.modelValue) const isControlled = computed(() => controlledModelValue.value !== undefined) const currentModelValue = computed[]>(() => { return isControlled.value ? (controlledModelValue.value ?? []) : [] }) /** * 传给 el-form :model 的包装对象。 * el-form 要求 model 为 Object,直接传入数组会触发 Element UI 的 prop type 校验报错。 * 这里包装为 { rows: [...] },el-form-item 的 prop 路径相应改为 rows.${index}.${key}。 */ const formModelRef = computed(() => ({ rows: currentModelValue.value })) // --------------------------------------------------------------------------- // Table rows // --------------------------------------------------------------------------- const tableRows = computed(() => { return currentModelValue.value.map((item, i) => ({ ...item, _index: i })) }) function rowKeyFn(row: TableRow) { return `r-${row._index}` } // --------------------------------------------------------------------------- // 工具函数 // --------------------------------------------------------------------------- function cloneRows(): Record[] { return JSON.parse(JSON.stringify(currentModelValue.value)) as Record[] } function emptyRow(): Record { const o: Record = {} for (const c of props.columns) { if (c.children && c.children.length > 0) { for (const child of c.children) { o[child.key] = '' } } else { o[c.key] = '' } } return o } // --------------------------------------------------------------------------- // Value getters / setters // --------------------------------------------------------------------------- function emitNext(next: Record[]) { emit('update:modelValue', next) } function getCell(row: TableRow): Record { return currentModelValue.value[row._index] ?? {} } function getCellValue(row: TableRow, col: ProTableFormColumn, childKey?: string): unknown { if (childKey) { const colVal = getCell(row)[col.key] // Support both nested ({ economy: { costReduction: '' } }) and flat ({ costReduction: '' }) models if (colVal && typeof colVal === 'object') { return (colVal as Record)[childKey] ?? '' } return getCell(row)[childKey] ?? '' } return getCell(row)[col.key] ?? '' } function setCellValue(row: TableRow, col: ProTableFormColumn, childKey: string | undefined, val: unknown) { const next = cloneRows() const target = { ...next[row._index] } if (childKey) { const colVal = target[col.key] // Support both nested and flat models for grouped columns if (colVal && typeof colVal === 'object') { const nested = { ...(colVal as Record) } nested[childKey] = val target[col.key] = nested } else { target[childKey] = val } } else { target[col.key] = val } next[row._index] = target emitNext(next) } // --------------------------------------------------------------------------- // Row operations // --------------------------------------------------------------------------- function handleAddRow() { emitAddRow?.() const next = cloneRows() next.push(emptyRow()) emitNext(next) } function handleRemoveRow(index: number) { if (!canDeleteRow.value) return emitRemoveRow?.(index) const next = cloneRows() next.splice(index, 1) emitNext(next) } /** * 按索引获取行数据。 */ function getRow(index: number): Record { return currentModelValue.value[index] ?? {} } /** * 按索引替换行数据(触发 update:modelValue)。 */ function setRow(index: number, row: Record): void { if (index < 0 || index >= currentModelValue.value.length) return const next = cloneRows() next[index] = row emitNext(next) } const canDeleteRow = computed(() => { return currentModelValue.value.length > (props.minRows ?? 0) }) // --------------------------------------------------------------------------- // Prop paths // --------------------------------------------------------------------------- function getCellProp(tableRow: TableRow, col: ProTableFormColumn, childKey?: string): string { // Use the leaf key directly so the prop path matches the flat data model // (e.g. rows.0.costReduction, not rows.0.economy.costReduction) return childKey ? `rows.${tableRow._index}.${childKey}` : `rows.${tableRow._index}.${col.key}` } // --------------------------------------------------------------------------- // Validation rules // --------------------------------------------------------------------------- // Stable rules object — we mutate it in place and only replace the reference // when the top-level keys or their array identities actually change. const rulesResult: Record = {} /** Signature of a rules array (compares content, not function identity) */ function rulesSig(arr: unknown[]): string { return JSON.stringify(arr.map((r) => { if (r == null || typeof r !== 'object') return String(r) const o = r as Record return { required: o.required, type: o.type, message: o.message, pattern: String(o.pattern ?? ''), min: o.min, max: o.max, trigger: Array.isArray(o.trigger) ? [...o.trigger] : o.trigger, hasValidator: typeof o.validator === 'function', hasAsyncValidator: typeof o.asyncValidator === 'function', } })) } const mergedRules = computed(() => { const req = (title: string): unknown[] => [ { required: true, message: `请输入${title}`, trigger: 'change' }, ] let anyChange = false // Build the new rules snapshot const newSnapshot: Record = {} currentModelValue.value.forEach((row, i) => { for (const col of props.columns) { const process = (key: string, colOrChild: ProTableFormColumn | ProTableFormColumnChild, value: unknown) => { const path = `rows.${i}.${key}` let rules: unknown[] = [] if ('dynamicRules' in colOrChild && colOrChild.dynamicRules !== undefined) { rules = typeof colOrChild.dynamicRules === 'function' ? colOrChild.dynamicRules({ row, value, column: colOrChild as never }) as unknown[] : (colOrChild.dynamicRules as unknown[]) } else if ('rules' in colOrChild && colOrChild.rules !== undefined) { rules = colOrChild.rules as unknown[] } else if (colOrChild.required) { rules = req(colOrChild.title) } // Only create a new array reference if content actually changed const sig = rulesSig(rules) const prevArr = rulesResult[path] const prevSig = (prevArr as unknown as { __sig?: string } & unknown[])?.__sig const arr = (prevSig === sig && prevArr) ? prevArr : rules ;(arr as unknown as { __sig: string } & unknown[]).__sig = sig newSnapshot[path] = arr if (arr !== prevArr) anyChange = true } if (col.children && col.children.length > 0) { for (const child of col.children) { const value = getCellValue({ _index: i } as TableRow, col, child.key) process(child.key, child, value) } } else { const value = getCellValue({ _index: i } as TableRow, col) process(col.key, col, value) } } }) // Check props.rules keys const extraRules = props.rules || {} for (const key of Object.keys(extraRules)) { const arr = extraRules[key] as unknown[] newSnapshot[key] = arr if (arr !== rulesResult[key]) anyChange = true } // Only replace rulesResult when something actually changed if (anyChange || Object.keys(newSnapshot).length !== Object.keys(rulesResult).length) { Object.keys(rulesResult).forEach((k) => { delete rulesResult[k] }) Object.assign(rulesResult, newSnapshot) } return rulesResult as Record }) // --------------------------------------------------------------------------- // el-form 实例方法 // --------------------------------------------------------------------------- function validate(): Promise { return new Promise((resolve) => { const f = formRef.value if (!f || typeof f.validate !== 'function') { resolve(true) return } f.validate((valid: boolean) => resolve(valid)) }) } function clearValidate(propsArg?: string | string[]) { formRef.value?.clearValidate?.(propsArg) } // --------------------------------------------------------------------------- // Cell rendering helpers // --------------------------------------------------------------------------- function cellComponent(col: ProTableFormColumn | ProTableFormColumnChild): unknown { return col.component === 'formatted-number' ? 'ecp-formatted-number-input' : 'el-input' } function cellBind(col: ProTableFormColumn | ProTableFormColumnChild): Record { const cp = col.componentProps || {} // Separate event handlers (onXxx) from regular props, so they don't get v-bound // as fake attrs and bypass the @update flow. Match Vue camelCase convention. const result: Record = {} for (const [key, val] of Object.entries(cp)) { if (/^on[A-Za-z]/.test(key) && typeof val === 'function') continue result[key] = val } if (col.component === 'formatted-number') { return { integerDigits: 5, decimalPlaces: 6, rounding: 'round', inputLimit: true, ...result } } return result } function slotUpdateHandler(slotProps: { row: TableRow }, col: ProTableFormColumn, childKey?: string) { return (v: unknown) => setCellValue(slotProps.row, col, childKey, v) } // --------------------------------------------------------------------------- // Span method // --------------------------------------------------------------------------- const allLeafColumnKeys = computed(() => { const keys: string[] = [] for (const col of props.columns) { if (col.children && col.children.length > 0) { for (const child of col.children) { keys.push(`${col.key}.${child.key}`) } } else if (!col.hideInTable) { keys.push(col.key) } } return keys }) const spanMethodAdapter = ({ rowIndex, column, columnIndex, row, }: { row: TableRow column: { property: string; label: string } rowIndex: number columnIndex: number }): [number, number] | { rowspan: number; colspan: number } | void => { if (!props.spanMethod) return const colKey = allLeafColumnKeys.value[columnIndex] return props.spanMethod({ row, column: { ...column, property: colKey ?? column.property }, rowIndex, columnIndex, }) } // --------------------------------------------------------------------------- // Return // --------------------------------------------------------------------------- return { // 注册 register, // el-form 绑定(formModelRef 包装了数组,满足 el-form :model Object 类型要求) formModelRef, currentModelValue, mergedRules, // el-table 绑定 tableRows, rowKeyFn, spanMethodAdapter, allLeafColumnKeys, // ref formRef, getFormRef: () => formRef.value, // 单元格渲染 cellComponent, cellBind, slotUpdateHandler, getCellProp, // 单元格值读写 getCellValue, setCellValue, // 行操作 handleAddRow, handleRemoveRow, // 行读写 getRow, setRow, // el-form 实例方法 validate, clearValidate, } }