@aspire-ui/element-component-pro 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <el-form-item
3
+ :prop="prop"
4
+ class="ecp-pro-table-form__cell-item"
5
+ >
6
+ <!-- slot 列:完全透传给父级 -->
7
+ <slot
8
+ v-if="col.component === 'slot' && col.slotName"
9
+ :name="'cell-' + col.slotName"
10
+ :column="col"
11
+ :row="row"
12
+ :value="value"
13
+ :update-value="updateValue"
14
+ />
15
+ <!-- 内置组件 -->
16
+ <component
17
+ v-else
18
+ :is="cellComponent(col)"
19
+ :value="value"
20
+ v-bind="cellBind(col)"
21
+ :placeholder="col.placeholder || placeholder"
22
+ @input="updateValue"
23
+ />
24
+ </el-form-item>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import type { ProTableFormColumn } from './types'
29
+
30
+ interface Props {
31
+ col: ProTableFormColumn
32
+ row: TableRow
33
+ prop: string
34
+ value: unknown
35
+ updateValue: (v: unknown) => void
36
+ cellComponent: (col: ProTableFormColumn) => unknown
37
+ cellBind: (col: ProTableFormColumn) => Record<string, unknown>
38
+ placeholder?: string
39
+ }
40
+
41
+ defineProps<Props>()
42
+
43
+ type TableRow =
44
+ | { _type: 'fixed'; rowKey: string; rowLabel: string }
45
+ | { _type: 'competitor'; _index: number }
46
+ </script>
@@ -1,9 +1,8 @@
1
1
  import ProTableForm from './ProTableForm.vue'
2
- export { ProTableForm }
3
- export type {
4
- ProTableFormColumn,
5
- ProTableFormFixedRow,
6
- ProTableFormProps,
7
- ProTableFormActionColumn,
8
- } from './types'
2
+ import { useProTableForm } from './useProTableForm'
3
+
4
+ export { ProTableForm, useProTableForm }
5
+ export type { ProTableFormColumn, ProTableFormColumnChild } from '../types'
6
+ export type { ProTableFormBuiltInComponent, ProTableFormProps, ProTableFormActionType } from '../types'
7
+
9
8
  export default ProTableForm
@@ -1,72 +1,12 @@
1
- /** 指标列(或含友商名称等任意 key) */
2
- export interface ProTableFormColumn {
3
- /** 对应 model 中对象字段名 */
4
- key: string
5
- title: string
6
- required?: boolean
7
- /**
8
- * 单元格渲染方式:
9
- * - input / formatted-number:内置
10
- * - slot:使用具名插槽 `cell-{slotName}` 完全自定义
11
- */
12
- component?: 'input' | 'formatted-number' | 'slot'
13
- /**
14
- * component 为 slot 时必填,对应插槽名为 `cell-{slotName}`(如 slotName: 'score' → #cell-score)
15
- */
16
- slotName?: string
17
- placeholder?: string
18
- width?: number
19
- minWidth?: number
20
- /**
21
- * 透传给单元格组件。`component === 'formatted-number'` 时在此传入
22
- * `integerDigits`、`decimalPlaces`、`rounding`、`inputLimit` 等(与 FormattedNumberInput / ProForm 一致)。
23
- */
24
- componentProps?: Record<string, unknown>
25
- /** 覆盖该列默认必填规则(与 Element 表单 rules 一致) */
26
- rules?: unknown[]
27
- }
28
-
29
- /** 固定行(首列为文案,不可删) */
30
- export interface ProTableFormFixedRow {
31
- rowKey: string
32
- label: string
33
- }
34
-
35
- /** 操作列配置(showActionColumn 为 true 时生效) */
36
- export interface ProTableFormActionColumn {
37
- width?: number
38
- minWidth?: number
39
- align?: 'left' | 'center' | 'right'
40
- fixed?: boolean | 'left' | 'right'
41
- /** 表头文案,使用默认「新增」按钮时显示在按钮左侧,可留空 */
42
- title?: string
43
- }
44
-
45
- export interface ProTableFormProps {
46
- modelValue?: Record<string, unknown>
47
- columns: ProTableFormColumn[]
48
- fixedRows: ProTableFormFixedRow[]
49
- competitorsKey?: string
50
- /** 友商行「名称」字段,默认 name */
51
- competitorNameKey?: string
52
- firstColumnTitle?: string
53
- competitorNamePlaceholder?: string
54
- metricPlaceholder?: string
55
- addCompetitorText?: string
56
- minCompetitors?: number
57
- rules?: Record<string, unknown>
58
- labelWidth?: string
59
- bordered?: boolean
60
- firstColMinWidth?: number
61
- /**
62
- * 是否展示首列(维度/友商)。存在 fixedRows 时强制为 true。
63
- * 无固定行且为 false 时隐藏首列,需在 columns 中自行配置名称等字段。
64
- */
65
- showFirstColumn?: boolean
66
- /** 是否展示操作列,默认 true */
67
- showActionColumn?: boolean
68
- /** 操作列宽度(showActionColumn 时) */
69
- actionWidth?: number
70
- /** 操作列详细配置 */
71
- actionColumn?: ProTableFormActionColumn
72
- }
1
+ // Re-export all ProTableForm types from the centralized types registry
2
+ export type {
3
+ ProTableFormColumn,
4
+ ProTableFormColumnChild,
5
+ ProTableFormActionColumn,
6
+ ProTableFormProps,
7
+ ProTableFormBuiltInComponent,
8
+ TableActionButton,
9
+ TableActionRender,
10
+ ProTableFormActionType,
11
+ ProTableFormRowType,
12
+ } from '../types'
@@ -0,0 +1,442 @@
1
+ import { computed, ref } from 'vue'
2
+ import type { Ref, ComputedRef } from 'vue'
3
+ import type {
4
+ ProTableFormActionType,
5
+ ProTableFormColumn,
6
+ ProTableFormColumnChild,
7
+ ProTableFormProps,
8
+ FormInstance,
9
+ } from '../types'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ProTableFormOptions {
16
+ props: Readonly<ProTableFormProps>
17
+ emit: (event: 'update:modelValue', value: Record<string, unknown>[]) => void
18
+ /** 新增行事件 */
19
+ emitAddRow?: () => void
20
+ /** 删除行事件 */
21
+ emitRemoveRow?: (index: number) => void
22
+ }
23
+
24
+ /** useProTableForm 返回 */
25
+ export interface UseProTableFormReturn {
26
+ // --- 注册 ---
27
+ register: (formAction: ProTableFormActionType) => void
28
+
29
+ // --- el-form 绑定 ---
30
+ currentModelValue: ComputedRef<Record<string, unknown>[]>
31
+ formModelRef: ComputedRef<{ rows: Record<string, unknown>[] }>
32
+ mergedRules: ComputedRef<Record<string, unknown[]>>
33
+
34
+ // --- el-table 绑定 ---
35
+ tableRows: ComputedRef<TableRow[]>
36
+ rowKeyFn: (row: TableRow) => string
37
+ spanMethodAdapter: (params: {
38
+ row: TableRow
39
+ column: { property: string; label: string }
40
+ rowIndex: number
41
+ columnIndex: number
42
+ }) => [number, number] | { rowspan: number; colspan: number } | void
43
+ allLeafColumnKeys: ComputedRef<string[]>
44
+
45
+ // --- ref ---
46
+ formRef: Ref<FormInstance | null>
47
+ /** 获取 el-form 实例 */
48
+ getFormRef: () => FormInstance | null
49
+
50
+ // --- 单元格渲染辅助 ---
51
+ cellComponent: (col: ProTableFormColumn | ProTableFormColumnChild) => unknown
52
+ cellBind: (col: ProTableFormColumn | ProTableFormColumnChild) => Record<string, unknown>
53
+ slotUpdateHandler: (slotProps: { row: TableRow }, col: ProTableFormColumn, childKey?: string) => (v: unknown) => void
54
+ getCellProp: (tableRow: TableRow, col: ProTableFormColumn, childKey?: string) => string
55
+
56
+ // --- 单元格值读写 ---
57
+ getCellValue: (row: TableRow, col: ProTableFormColumn, childKey?: string) => unknown
58
+ setCellValue: (tableRow: TableRow, col: ProTableFormColumn, childKey: string | undefined, val: unknown) => void
59
+
60
+ // --- 行操作 ---
61
+ handleAddRow: () => void
62
+ handleRemoveRow: (index: number) => void
63
+
64
+ // --- el-form 实例方法 ---
65
+ validate: () => Promise<boolean>
66
+ clearValidate: (propsArg?: string | string[]) => void
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Composable
71
+ // ---------------------------------------------------------------------------
72
+
73
+ type TableRow = { _index: number }
74
+
75
+ export function useProTableForm(options: ProTableFormOptions): UseProTableFormReturn {
76
+ const { props, emit, emitAddRow, emitRemoveRow } = options
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 注册机制(与 useForm 一致)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ const formActionRef = ref<ProTableFormActionType | null>(null)
83
+
84
+ const register = (action: ProTableFormActionType) => {
85
+ formActionRef.value = action
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // el-form ref
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const formRef = ref<FormInstance | null>(null)
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // 受控 / 非受控双轨(与 ProForm 一致)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const controlledModelValue = computed(() => props.modelValue)
99
+ const isControlled = computed(() => controlledModelValue.value !== undefined)
100
+ const currentModelValue = computed<Record<string, unknown>[]>(() => {
101
+ return isControlled.value ? (controlledModelValue.value ?? []) : []
102
+ })
103
+
104
+ /**
105
+ * 传给 el-form :model 的包装对象。
106
+ * el-form 要求 model 为 Object,直接传入数组会触发 Element UI 的 prop type 校验报错。
107
+ * 这里包装为 { rows: [...] },el-form-item 的 prop 路径相应改为 rows.${index}.${key}。
108
+ */
109
+ const formModelRef = computed(() => ({ rows: currentModelValue.value }))
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Table rows
113
+ // ---------------------------------------------------------------------------
114
+
115
+ const tableRows = computed<TableRow[]>(() => {
116
+ return currentModelValue.value.map((item, i) => ({ ...item, _index: i }))
117
+ })
118
+
119
+ function rowKeyFn(row: TableRow) {
120
+ return `r-${row._index}`
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // 工具函数
125
+ // ---------------------------------------------------------------------------
126
+
127
+ function cloneRows(): Record<string, unknown>[] {
128
+ return JSON.parse(JSON.stringify(currentModelValue.value)) as Record<string, unknown>[]
129
+ }
130
+
131
+ function emptyRow(): Record<string, unknown> {
132
+ const o: Record<string, unknown> = {}
133
+ for (const c of props.columns) {
134
+ if (c.children && c.children.length > 0) {
135
+ for (const child of c.children) {
136
+ o[child.key] = ''
137
+ }
138
+ } else {
139
+ o[c.key] = ''
140
+ }
141
+ }
142
+ return o
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Value getters / setters
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function emitNext(next: Record<string, unknown>[]) {
150
+ emit('update:modelValue', next)
151
+ }
152
+
153
+ function getCell(row: TableRow): Record<string, unknown> {
154
+ return currentModelValue.value[row._index] ?? {}
155
+ }
156
+
157
+ function getCellValue(row: TableRow, col: ProTableFormColumn, childKey?: string): unknown {
158
+ if (childKey) {
159
+ const colVal = getCell(row)[col.key]
160
+ // Support both nested ({ economy: { costReduction: '' } }) and flat ({ costReduction: '' }) models
161
+ if (colVal && typeof colVal === 'object') {
162
+ return (colVal as Record<string, unknown>)[childKey] ?? ''
163
+ }
164
+ return getCell(row)[childKey] ?? ''
165
+ }
166
+ return getCell(row)[col.key] ?? ''
167
+ }
168
+
169
+ function setCellValue(row: TableRow, col: ProTableFormColumn, childKey: string | undefined, val: unknown) {
170
+ const next = cloneRows()
171
+ const target = { ...next[row._index] }
172
+ if (childKey) {
173
+ const colVal = target[col.key]
174
+ // Support both nested and flat models for grouped columns
175
+ if (colVal && typeof colVal === 'object') {
176
+ const nested = { ...(colVal as Record<string, unknown>) }
177
+ nested[childKey] = val
178
+ target[col.key] = nested
179
+ } else {
180
+ target[childKey] = val
181
+ }
182
+ } else {
183
+ target[col.key] = val
184
+ }
185
+ next[row._index] = target
186
+ emitNext(next)
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Row operations
191
+ // ---------------------------------------------------------------------------
192
+
193
+ function handleAddRow() {
194
+ emitAddRow?.()
195
+ const next = cloneRows()
196
+ next.push(emptyRow())
197
+ emitNext(next)
198
+ }
199
+
200
+ function handleRemoveRow(index: number) {
201
+ if (!canDeleteRow.value) return
202
+ emitRemoveRow?.(index)
203
+ const next = cloneRows()
204
+ next.splice(index, 1)
205
+ emitNext(next)
206
+ }
207
+
208
+ const canDeleteRow = computed(() => {
209
+ return currentModelValue.value.length > (props.minRows ?? 0)
210
+ })
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Prop paths
214
+ // ---------------------------------------------------------------------------
215
+
216
+ function getCellProp(tableRow: TableRow, col: ProTableFormColumn, childKey?: string): string {
217
+ // Use the leaf key directly so the prop path matches the flat data model
218
+ // (e.g. rows.0.costReduction, not rows.0.economy.costReduction)
219
+ return childKey
220
+ ? `rows.${tableRow._index}.${childKey}`
221
+ : `rows.${tableRow._index}.${col.key}`
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Validation rules
226
+ // ---------------------------------------------------------------------------
227
+
228
+ // Stable rules object — we mutate it in place and only replace the reference
229
+ // when the top-level keys or their array identities actually change.
230
+ const rulesResult: Record<string, unknown[]> = {}
231
+
232
+ /** Signature of a rules array (compares content, not function identity) */
233
+ function rulesSig(arr: unknown[]): string {
234
+ return JSON.stringify(arr.map((r) => {
235
+ if (r == null || typeof r !== 'object') return String(r)
236
+ const o = r as Record<string, unknown>
237
+ return {
238
+ required: o.required,
239
+ type: o.type,
240
+ message: o.message,
241
+ pattern: String(o.pattern ?? ''),
242
+ min: o.min,
243
+ max: o.max,
244
+ trigger: Array.isArray(o.trigger) ? [...o.trigger] : o.trigger,
245
+ hasValidator: typeof o.validator === 'function',
246
+ hasAsyncValidator: typeof o.asyncValidator === 'function',
247
+ }
248
+ }))
249
+ }
250
+
251
+ const mergedRules = computed(() => {
252
+ const req = (title: string): unknown[] => [
253
+ { required: true, message: `请输入${title}`, trigger: 'change' },
254
+ ]
255
+
256
+ let anyChange = false
257
+
258
+ // Build the new rules snapshot
259
+ const newSnapshot: Record<string, unknown[]> = {}
260
+
261
+ currentModelValue.value.forEach((row, i) => {
262
+ for (const col of props.columns) {
263
+ const process = (key: string, colOrChild: ProTableFormColumn | ProTableFormColumnChild, value: unknown) => {
264
+ const path = `rows.${i}.${key}`
265
+ let rules: unknown[] = []
266
+
267
+ if ('dynamicRules' in colOrChild && colOrChild.dynamicRules !== undefined) {
268
+ rules = typeof colOrChild.dynamicRules === 'function'
269
+ ? colOrChild.dynamicRules({ row, value, column: colOrChild as never }) as unknown[]
270
+ : (colOrChild.dynamicRules as unknown[])
271
+ } else if ('rules' in colOrChild && colOrChild.rules !== undefined) {
272
+ rules = colOrChild.rules as unknown[]
273
+ } else if (colOrChild.required) {
274
+ rules = req(colOrChild.title)
275
+ }
276
+
277
+ // Only create a new array reference if content actually changed
278
+ const sig = rulesSig(rules)
279
+ const prevArr = rulesResult[path]
280
+ const prevSig = (prevArr as unknown as { __sig?: string } & unknown[])?.__sig
281
+ const arr = (prevSig === sig && prevArr) ? prevArr : rules
282
+ ;(arr as unknown as { __sig: string } & unknown[]).__sig = sig
283
+ newSnapshot[path] = arr
284
+ if (arr !== prevArr) anyChange = true
285
+ }
286
+
287
+ if (col.children && col.children.length > 0) {
288
+ for (const child of col.children) {
289
+ const value = getCellValue({ _index: i } as TableRow, col, child.key)
290
+ process(child.key, child, value)
291
+ }
292
+ } else {
293
+ const value = getCellValue({ _index: i } as TableRow, col)
294
+ process(col.key, col, value)
295
+ }
296
+ }
297
+ })
298
+
299
+ // Check props.rules keys
300
+ const extraRules = props.rules || {}
301
+ for (const key of Object.keys(extraRules)) {
302
+ const arr = extraRules[key] as unknown[]
303
+ newSnapshot[key] = arr
304
+ if (arr !== rulesResult[key]) anyChange = true
305
+ }
306
+
307
+ // Only replace rulesResult when something actually changed
308
+ if (anyChange || Object.keys(newSnapshot).length !== Object.keys(rulesResult).length) {
309
+ Object.keys(rulesResult).forEach((k) => { delete rulesResult[k] })
310
+ Object.assign(rulesResult, newSnapshot)
311
+ }
312
+
313
+ return rulesResult as Record<string, unknown[]>
314
+ })
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // el-form 实例方法
318
+ // ---------------------------------------------------------------------------
319
+
320
+ function validate(): Promise<boolean> {
321
+ return new Promise((resolve) => {
322
+ const f = formRef.value
323
+ if (!f || typeof f.validate !== 'function') {
324
+ resolve(true)
325
+ return
326
+ }
327
+ f.validate((valid: boolean) => resolve(valid))
328
+ })
329
+ }
330
+
331
+ function clearValidate(propsArg?: string | string[]) {
332
+ formRef.value?.clearValidate?.(propsArg)
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Cell rendering helpers
337
+ // ---------------------------------------------------------------------------
338
+
339
+ function cellComponent(col: ProTableFormColumn | ProTableFormColumnChild): unknown {
340
+ return col.component === 'formatted-number' ? 'ecp-formatted-number-input' : 'el-input'
341
+ }
342
+
343
+ function cellBind(col: ProTableFormColumn | ProTableFormColumnChild): Record<string, unknown> {
344
+ const cp = col.componentProps || {}
345
+ // Separate event handlers (onXxx) from regular props, so they don't get v-bound
346
+ // as fake attrs and bypass the @update flow. Match Vue camelCase convention.
347
+ const result: Record<string, unknown> = {}
348
+ for (const [key, val] of Object.entries(cp)) {
349
+ if (/^on[A-Za-z]/.test(key) && typeof val === 'function') continue
350
+ result[key] = val
351
+ }
352
+ if (col.component === 'formatted-number') {
353
+ return { integerDigits: 5, decimalPlaces: 6, rounding: 'round', inputLimit: true, ...result }
354
+ }
355
+ return result
356
+ }
357
+
358
+ function slotUpdateHandler(slotProps: { row: TableRow }, col: ProTableFormColumn, childKey?: string) {
359
+ return (v: unknown) => setCellValue(slotProps.row, col, childKey, v)
360
+ }
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Span method
364
+ // ---------------------------------------------------------------------------
365
+
366
+ const allLeafColumnKeys = computed<string[]>(() => {
367
+ const keys: string[] = []
368
+ for (const col of props.columns) {
369
+ if (col.children && col.children.length > 0) {
370
+ for (const child of col.children) {
371
+ keys.push(`${col.key}.${child.key}`)
372
+ }
373
+ } else if (!col.hideInTable) {
374
+ keys.push(col.key)
375
+ }
376
+ }
377
+ return keys
378
+ })
379
+
380
+ const spanMethodAdapter = ({
381
+ rowIndex,
382
+ column,
383
+ columnIndex,
384
+ row,
385
+ }: {
386
+ row: TableRow
387
+ column: { property: string; label: string }
388
+ rowIndex: number
389
+ columnIndex: number
390
+ }): [number, number] | { rowspan: number; colspan: number } | void => {
391
+ if (!props.spanMethod) return
392
+ const colKey = allLeafColumnKeys.value[columnIndex]
393
+ return props.spanMethod({
394
+ row,
395
+ column: { ...column, property: colKey ?? column.property },
396
+ rowIndex,
397
+ columnIndex,
398
+ })
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Return
403
+ // ---------------------------------------------------------------------------
404
+
405
+ return {
406
+ // 注册
407
+ register,
408
+
409
+ // el-form 绑定(formModelRef 包装了数组,满足 el-form :model Object 类型要求)
410
+ formModelRef,
411
+ currentModelValue,
412
+ mergedRules,
413
+
414
+ // el-table 绑定
415
+ tableRows,
416
+ rowKeyFn,
417
+ spanMethodAdapter,
418
+ allLeafColumnKeys,
419
+
420
+ // ref
421
+ formRef,
422
+ getFormRef: () => formRef.value,
423
+
424
+ // 单元格渲染
425
+ cellComponent,
426
+ cellBind,
427
+ slotUpdateHandler,
428
+ getCellProp,
429
+
430
+ // 单元格值读写
431
+ getCellValue,
432
+ setCellValue,
433
+
434
+ // 行操作
435
+ handleAddRow,
436
+ handleRemoveRow,
437
+
438
+ // el-form 实例方法
439
+ validate,
440
+ clearValidate,
441
+ }
442
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import ProDescriptions from './ProDescriptions'
5
5
  import CollapseContainer from './CollapseContainer'
6
6
  import { ProTableForm } from './ProTableForm'
7
7
  import { useForm } from './ProForm/useForm'
8
+ import { useProTableForm } from './ProTableForm/useProTableForm'
8
9
  import { useDescription } from './ProDescriptions/useDescription'
9
10
  import { useProTable } from './ProTable/useProTable'
10
11
  import { useComponentSetting } from './useComponentSetting'
@@ -13,13 +14,14 @@ export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm }
13
14
  export { ProTable, useProTable, TableAction }
14
15
  export { ProDescriptions, useDescription }
15
16
  export { CollapseContainer }
16
- export { ProTableForm }
17
+ export { ProTableForm, useProTableForm }
17
18
  export type {
18
19
  ProTableFormColumn,
19
- ProTableFormFixedRow,
20
+ ProTableFormColumnChild,
20
21
  ProTableFormProps,
21
- ProTableFormActionColumn,
22
- } from './ProTableForm/types'
22
+ ProTableFormBuiltInComponent,
23
+ ProTableFormActionType,
24
+ } from './types'
23
25
  export { useComponentSetting }
24
26
  export type { UseComponentSettingReturn } from './useComponentSetting'
25
27
  export type { UseProTableReturn, UseProTablePropsReactive } from './ProTable/useProTable'