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

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.
Files changed (37) hide show
  1. package/dist/ProTableForm/CellEditor.vue.d.ts +33 -0
  2. package/dist/ProTableForm/ProTableForm.vue.d.ts +122 -102
  3. package/dist/ProTableForm/index.d.ts +4 -2
  4. package/dist/ProTableForm/types.d.ts +1 -69
  5. package/dist/ProTableForm/useProTableForm.d.ts +55 -0
  6. package/dist/element-component-pro.es.js +1433 -1255
  7. package/dist/element-component-pro.es.js.map +1 -1
  8. package/dist/element-component-pro.umd.js +2 -2
  9. package/dist/element-component-pro.umd.js.map +1 -1
  10. package/dist/index.d.ts +445 -282
  11. package/dist/style.css +1 -1
  12. package/dist/types/index.d.ts +227 -0
  13. package/docs/CollapseContainer.md +100 -0
  14. package/docs/ComponentSetting.md +113 -0
  15. package/docs/ProDescriptions.md +215 -0
  16. package/docs/ProForm.md +879 -0
  17. package/docs/ProTable-Redesign.md +214 -0
  18. package/docs/ProTable.md +556 -0
  19. package/docs/ProTableForm.md +207 -0
  20. package/docs/image.png +0 -0
  21. package/package.json +3 -2
  22. package/src/CollapseContainer/CollapseContainer.vue +1 -1
  23. package/src/ProDescriptions/ProDescriptions.vue +1 -1
  24. package/src/ProForm/FormActions.vue +1 -1
  25. package/src/ProForm/ProForm.vue +1 -1
  26. package/src/ProForm/ProFormItem.vue +1 -1
  27. package/src/ProForm/TreeSelect.vue +1 -1
  28. package/src/ProTable/ProTable.vue +1 -2
  29. package/src/ProTable/TableAction.vue +1 -1
  30. package/src/ProTableForm/CellEditor.vue +192 -0
  31. package/src/ProTableForm/ProTableForm.vue +350 -453
  32. package/src/ProTableForm/TableFormCell.vue +46 -0
  33. package/src/ProTableForm/index.ts +6 -7
  34. package/src/ProTableForm/types.ts +12 -72
  35. package/src/ProTableForm/useProTableForm.ts +442 -0
  36. package/src/index.ts +6 -4
  37. package/src/types/index.ts +241 -0
@@ -1,170 +1,226 @@
1
1
  <template>
2
- <div class="ecp-pro-table-form">
2
+ <div class="ecp-pro-table-form" v-bind="$attrs">
3
3
  <el-form
4
4
  ref="formRef"
5
- :model="modelValue"
5
+ :model="formModelRef"
6
6
  :rules="mergedRules"
7
7
  :label-width="labelWidth"
8
+ :size="formSize"
9
+ :label-position="labelPosition"
10
+ :disabled="disabled"
11
+ v-bind="formProps"
8
12
  class="ecp-pro-table-form__form"
9
13
  >
10
14
  <el-table
15
+ ref="tableRef"
11
16
  :data="tableRows"
12
17
  :border="bordered"
18
+ :stripe="stripe"
19
+ :size="size"
20
+ :max-height="maxHeight"
21
+ :height="height"
13
22
  :row-key="rowKeyFn"
23
+ :row-class-name="rowClassName"
24
+ :default-expand-all="defaultExpandAll"
25
+ :span-method="spanMethodAdapter"
14
26
  header-cell-class-name="ecp-pro-table-form__header-cell"
27
+ v-bind="tableProps"
15
28
  class="ecp-pro-table-form__table"
29
+ @row-click="handleTableRowClick"
30
+ @row-dblclick="handleTableRowDblclick"
31
+ @sort-change="handleTableSortChange"
32
+ @expand-change="handleTableExpandChange"
16
33
  >
17
- <!-- 首列:维度 / 友商名称(可插槽自定义) -->
18
- <el-table-column
19
- v-if="showFirstColumnComputed"
20
- :min-width="firstColMinWidth"
21
- :fixed="firstColumnFixed"
22
- >
23
- <template #header>
24
- <slot name="firstColumnHeader">
25
- <span class="ecp-pro-table-form__th-text">
26
- <span class="ecp-pro-table-form__req">*</span>{{ firstColumnTitle }}
27
- </span>
28
- </slot>
29
- </template>
30
- <template #default="slotProps">
31
- <slot name="firstColumn" v-bind="firstColumnScope(slotProps)">
32
- <template v-if="slotProps.row._type === 'fixed'">
33
- <span class="ecp-pro-table-form__fixed-label">{{ slotProps.row.rowLabel }}</span>
34
+ <template v-for="col in flatColumns">
35
+ <!-- 分组列:有 children -->
36
+ <el-table-column
37
+ v-if="col.children && col.children.length > 0"
38
+ :key="'col-' + col._key"
39
+ :label="col.title"
40
+ :min-width="col.minWidth || col.width || 120"
41
+ :width="col.width"
42
+ :align="col.align || 'center'"
43
+ :header-align="col.headerAlign"
44
+ :fixed="col.fixed"
45
+ :cell-style="col.cellStyle"
46
+ :header-cell-style="col.headerCellStyle"
47
+ :cell-class-name="col.cellClassName"
48
+ :header-cell-class-name="col.headerCellClassName"
49
+ :sortable="col.sortable"
50
+ :resizable="col.resizable !== false"
51
+ >
52
+ <template #header>
53
+ <slot :name="`header-${col.key}`" :column="col">
54
+ <span class="ecp-pro-table-form__th-text">
55
+ <span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
56
+ </span>
57
+ </slot>
58
+ </template>
59
+ <template v-for="child in col.children">
60
+ <el-table-column
61
+ v-if="!child.hideInTable"
62
+ :key="'col-' + col.key + '-' + child.key"
63
+ :label="child.title"
64
+ :min-width="child.minWidth || child.width || 120"
65
+ :width="child.width"
66
+ :align="child.align || 'center'"
67
+ :header-align="child.headerAlign"
68
+ :fixed="child.fixed"
69
+ :cell-style="child.cellStyle"
70
+ :header-cell-style="child.headerCellStyle"
71
+ :cell-class-name="child.cellClassName"
72
+ :header-cell-class-name="child.headerCellClassName"
73
+ :sortable="child.sortable"
74
+ :resizable="child.resizable !== false"
75
+ >
76
+ <template #header>
77
+ <slot :name="`header-${child.key}`" :column="child">
78
+ <span class="ecp-pro-table-form__th-text">
79
+ <span v-if="child.required" class="ecp-pro-table-form__req">*</span>{{ child.title }}
80
+ </span>
81
+ </slot>
82
+ </template>
83
+ <template #default="slotProps">
84
+ <!-- 渲染函数 -->
85
+ <template v-if="child.render">
86
+ <el-form-item
87
+ :prop="getCellProp(slotProps.row, col, child.key)"
88
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
89
+ class="ecp-pro-table-form__cell-item"
90
+ >
91
+ <template v-if="getRender(child, slotProps.row, col, child.key).isText">
92
+ <span>{{ getRender(child, slotProps.row, col, child.key).value }}</span>
93
+ </template>
94
+ <template v-else>
95
+ <RenderCell :render-fn="child.render" :render-params="{ row: slotProps.row, col, childKey: child.key }" />
96
+ </template>
97
+ </el-form-item>
98
+ </template>
99
+ <!-- 插槽列 -->
100
+ <template v-else-if="child.component === 'slot' && child.slotName">
101
+ <el-form-item
102
+ :prop="getCellProp(slotProps.row, col, child.key)"
103
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
104
+ class="ecp-pro-table-form__cell-item"
105
+ >
106
+ <slot
107
+ :name="`cell-${child.slotName}`"
108
+ :column="child"
109
+ :row="slotProps.row"
110
+ :index="slotProps.$index"
111
+ :value="getCellValue(slotProps.row, col, child.key)"
112
+ :update-value="slotUpdateHandler(slotProps, col, child.key)"
113
+ />
114
+ </el-form-item>
115
+ </template>
116
+ <!-- 内置组件 -->
117
+ <el-form-item
118
+ v-else
119
+ :prop="getCellProp(slotProps.row, col, child.key)"
120
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
121
+ class="ecp-pro-table-form__cell-item"
122
+ >
123
+ <CellEditor
124
+ :col="child"
125
+ :value="getCellValue(slotProps.row, col, child.key)"
126
+ :row="slotProps.row"
127
+ :size="size"
128
+ :placeholder="col.placeholder || metricPlaceholder"
129
+ :action="action"
130
+ @update="setCellValue(slotProps.row, col, child.key, $event)"
131
+ />
132
+ </el-form-item>
133
+ </template>
134
+ </el-table-column>
135
+ </template>
136
+ </el-table-column>
137
+
138
+ <!-- 叶子列:无 children -->
139
+ <el-table-column
140
+ v-else-if="!col.hideInTable"
141
+ :key="'leaf-' + col._key"
142
+ :label="col.title"
143
+ :min-width="col.minWidth || col.width || 120"
144
+ :width="col.width"
145
+ :align="col.align || 'center'"
146
+ :header-align="col.headerAlign"
147
+ :fixed="col.fixed"
148
+ :cell-style="col.cellStyle"
149
+ :header-cell-style="col.headerCellStyle"
150
+ :cell-class-name="col.cellClassName"
151
+ :header-cell-class-name="col.headerCellClassName"
152
+ :sortable="col.sortable"
153
+ :resizable="col.resizable !== false"
154
+ >
155
+ <template #header>
156
+ <slot :name="`header-${col.key}`" :column="col">
157
+ <span class="ecp-pro-table-form__th-text">
158
+ <span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
159
+ </span>
160
+ </slot>
161
+ </template>
162
+ <template #default="slotProps">
163
+ <!-- 渲染函数 -->
164
+ <template v-if="col.render">
165
+ <el-form-item
166
+ :prop="getCellProp(slotProps.row, col)"
167
+ :rules="mergedRules[getCellProp(slotProps.row, col)]"
168
+ class="ecp-pro-table-form__cell-item"
169
+ >
170
+ <template v-if="getRender(col, slotProps.row, col).isText">
171
+ <span>{{ getRender(col, slotProps.row, col).value }}</span>
172
+ </template>
173
+ <template v-else>
174
+ <RenderCell :render-fn="col.render" :render-params="{ row: slotProps.row, col }" />
175
+ </template>
176
+ </el-form-item>
34
177
  </template>
178
+ <!-- 插槽列 -->
179
+ <template v-else-if="col.component === 'slot' && col.slotName">
180
+ <el-form-item
181
+ :prop="getCellProp(slotProps.row, col)"
182
+ :rules="mergedRules[getCellProp(slotProps.row, col)]"
183
+ class="ecp-pro-table-form__cell-item"
184
+ >
185
+ <slot
186
+ :name="`cell-${col.slotName}`"
187
+ :column="col"
188
+ :row="slotProps.row"
189
+ :index="slotProps.$index"
190
+ :value="getCellValue(slotProps.row, col)"
191
+ :update-value="slotUpdateHandler(slotProps, col)"
192
+ />
193
+ </el-form-item>
194
+ </template>
195
+ <!-- 内置组件 -->
35
196
  <el-form-item
36
197
  v-else
37
- :prop="competitorNameProp(slotProps.row._index)"
38
- class="ecp-pro-table-form__cell-item"
39
- >
40
- <el-input
41
- :value="getCompetitorName(slotProps.row._index)"
42
- :placeholder="competitorNamePlaceholder"
43
- @input="setCompetitorName(slotProps.row._index, $event)"
44
- />
45
- </el-form-item>
46
- </slot>
47
- </template>
48
- </el-table-column>
49
-
50
- <!-- 数据列:内置 input / formatted-number 或插槽 cell-{slotName} -->
51
- <el-table-column
52
- v-for="col in columns"
53
- :key="col.key"
54
- :min-width="col.minWidth || col.width || 120"
55
- :width="col.width"
56
- >
57
- <template #header>
58
- <slot :name="'header-' + col.key" :column="col">
59
- <span class="ecp-pro-table-form__th-text">
60
- <span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
61
- </span>
62
- </slot>
63
- </template>
64
- <template #default="slotProps">
65
- <!-- 完全自定义列 -->
66
- <template v-if="col.component === 'slot' && col.slotName">
67
- <el-form-item
68
- v-if="slotProps.row._type === 'fixed'"
69
- :prop="fixedMetricProp(slotProps.row.rowKey, col.key)"
198
+ :prop="getCellProp(slotProps.row, col)"
199
+ :rules="mergedRules[getCellProp(slotProps.row, col)]"
70
200
  class="ecp-pro-table-form__cell-item"
71
201
  >
72
- <slot
73
- :name="'cell-' + col.slotName"
74
- :column="col"
75
- :row="slotProps.row"
202
+ <CellEditor
203
+ :col="col"
76
204
  :value="getCellValue(slotProps.row, col)"
77
- :update-value="slotUpdateHandler(slotProps, col)"
78
- />
79
- </el-form-item>
80
- <el-form-item
81
- v-else
82
- :prop="competitorMetricProp(slotProps.row._index, col.key)"
83
- class="ecp-pro-table-form__cell-item"
84
- >
85
- <slot
86
- :name="'cell-' + col.slotName"
87
- :column="col"
88
205
  :row="slotProps.row"
89
- :value="getCellValue(slotProps.row, col)"
90
- :update-value="slotUpdateHandler(slotProps, col)"
91
- />
92
- </el-form-item>
93
- </template>
94
- <!-- 内置组件 -->
95
- <template v-else>
96
- <el-form-item
97
- v-if="slotProps.row._type === 'fixed'"
98
- :prop="fixedMetricProp(slotProps.row.rowKey, col.key)"
99
- class="ecp-pro-table-form__cell-item"
100
- >
101
- <component
102
- :is="cellComponent(col)"
103
- :value="getFixedMetric(slotProps.row.rowKey, col.key)"
104
- v-bind="cellBind(col)"
206
+ :size="size"
105
207
  :placeholder="col.placeholder || metricPlaceholder"
106
- @input="setFixedMetric(slotProps.row.rowKey, col.key, $event)"
107
- />
108
- </el-form-item>
109
- <el-form-item
110
- v-else
111
- :prop="competitorMetricProp(slotProps.row._index, col.key)"
112
- class="ecp-pro-table-form__cell-item"
113
- >
114
- <component
115
- :is="cellComponent(col)"
116
- :value="getCompetitorMetric(slotProps.row._index, col.key)"
117
- v-bind="cellBind(col)"
118
- :placeholder="col.placeholder || metricPlaceholder"
119
- @input="setCompetitorMetric(slotProps.row._index, col.key, $event)"
208
+ :action="action"
209
+ @update="setCellValue(slotProps.row, col, undefined, $event)"
120
210
  />
121
211
  </el-form-item>
122
212
  </template>
123
- </template>
124
- </el-table-column>
213
+ </el-table-column>
214
+ </template>
125
215
 
126
- <!-- 操作列:表头 / 行内均可插槽 -->
127
- <el-table-column v-if="showActionColumn" v-bind="actionColumnBind">
128
- <template #header>
129
- <slot name="actionHeader">
130
- <span v-if="actionColumn?.title" class="ecp-pro-table-form__action-title">{{ actionColumn.title }}</span>
131
- <el-button type="text" class="ecp-pro-table-form__add-btn" @click="addCompetitor">
132
- {{ addCompetitorText }}
133
- </el-button>
134
- </slot>
135
- </template>
136
- <template #default="slotProps">
137
- <slot
138
- name="action"
139
- :row="slotProps.row"
140
- :can-delete="canDeleteCompetitor"
141
- :add-competitor="addCompetitor"
142
- :remove-competitor="removeCompetitor"
143
- >
144
- <el-button
145
- v-if="slotProps.row._type === 'competitor'"
146
- type="text"
147
- class="ecp-pro-table-form__del-btn"
148
- :disabled="!canDeleteCompetitor"
149
- @click="removeCompetitor(slotProps.row._index)"
150
- >
151
- 删除
152
- </el-button>
153
- <el-button v-else type="text" class="ecp-pro-table-form__del-btn" disabled>
154
- 删除
155
- </el-button>
156
- </slot>
157
- </template>
158
- </el-table-column>
216
+ <!-- 操作列 -->
217
+ <slot name="action" :add-row="handleAddRow" :remove-row="handleRemoveRow" />
159
218
  </el-table>
160
219
  </el-form>
161
220
  </div>
162
221
  </template>
163
222
 
164
223
  <script lang="ts">
165
- /**
166
- * Vue 2 默认 v-model 绑定 value + input;本组件使用 modelValue(与 Vue 3 一致),需显式声明 model。
167
- */
168
224
  export default {
169
225
  name: 'ProTableForm',
170
226
  model: {
@@ -176,338 +232,193 @@ export default {
176
232
 
177
233
  <script setup lang="ts">
178
234
  import { computed, ref } from 'vue'
179
- import FormattedNumberInput from '../ProForm/FormattedNumberInput.vue'
180
- import type { ProTableFormActionColumn, ProTableFormColumn, ProTableFormFixedRow } from './types'
235
+ import { useProTableForm } from './useProTableForm'
236
+ import { defineComponent } from 'vue'
237
+ import type { VNode } from 'vue'
238
+ import type {
239
+ ProTableFormColumn,
240
+ ProTableFormColumnChild,
241
+ ProTableFormColumnRender,
242
+ } from '../types'
243
+ import CellEditor from './CellEditor.vue'
244
+
245
+ /** 与 useProTableForm 内部 tableRows 元素类型对齐 */
246
+ type TableRow = { _index: number }
247
+
248
+ /** RenderCell:接收 render 函数,在 td 内渲染 VNode,完全脱离 el-table 插槽作用域 */
249
+ const RenderCell = defineComponent({
250
+ name: 'RenderCell',
251
+ props: {
252
+ renderFn: { type: Function as unknown as () => ProTableFormColumnRender, required: true },
253
+ renderParams: { type: Object as () => Record<string, unknown>, required: true },
254
+ },
255
+ setup(props) {
256
+ return () => {
257
+ const vnode = props.renderFn(props.renderParams as Parameters<ProTableFormColumnRender>[0])
258
+ return Array.isArray(vnode) ? vnode : (vnode as VNode)
259
+ }
260
+ },
261
+ })
262
+
263
+ /** 单次执行 render,返回 { isText, value } */
264
+ function execRenderOnce(
265
+ child: ProTableFormColumn | ProTableFormColumnChild,
266
+ row: Record<string, unknown>,
267
+ col: ProTableFormColumn,
268
+ colKey?: string
269
+ ): { isText: boolean; value: ReturnType<NonNullable<typeof child.render>> } {
270
+ const value = colKey !== undefined
271
+ ? getCellValue(row as TableRow, col, colKey)
272
+ : getCellValue(row as TableRow, col)
273
+ const result = child.render!({ row, value, column: child })
274
+ const isText = isPrimitive(result)
275
+ return { isText, value: result }
276
+ }
277
+
278
+ /** 行→列key→渲染结果的缓存,避免同一 render 在模板中被多次调用 */
279
+ const _renderCache = new WeakMap<object, Map<string, { isText: boolean; value: unknown }>>()
280
+
281
+ function getRender(
282
+ child: ProTableFormColumn | ProTableFormColumnChild,
283
+ row: Record<string, unknown>,
284
+ col: ProTableFormColumn,
285
+ colKey?: string
286
+ ): { isText: boolean; value: unknown } {
287
+ if (!_renderCache.has(row as object)) _renderCache.set(row as object, new Map())
288
+ const cache = _renderCache.get(row as object)!
289
+ const cacheKey = col.key + (colKey ? '.' + colKey : '')
290
+ if (!cache.has(cacheKey)) cache.set(cacheKey, execRenderOnce(child, row, col, colKey))
291
+ return cache.get(cacheKey)!
292
+ }
293
+
294
+ /** 是否为基础类型(string / number / boolean / null / undefined) */
295
+ function isPrimitive(val: unknown): val is string | number | boolean | null | undefined {
296
+ return val === null || typeof val in { string: 1, number: 1, boolean: 1, bigint: 1, symbol: 1 }
297
+ }
181
298
 
182
299
  const props = withDefaults(
183
300
  defineProps<{
184
- modelValue?: Record<string, unknown>
301
+ modelValue?: Record<string, unknown>[]
185
302
  columns: ProTableFormColumn[]
186
- fixedRows: ProTableFormFixedRow[]
187
- competitorsKey?: string
188
- competitorNameKey?: string
189
- firstColumnTitle?: string
190
- competitorNamePlaceholder?: string
191
303
  metricPlaceholder?: string
192
- addCompetitorText?: string
193
- minCompetitors?: number
304
+ minRows?: number
194
305
  rules?: Record<string, unknown>
195
306
  labelWidth?: string
196
307
  bordered?: boolean
197
- firstColMinWidth?: number
198
- actionWidth?: number
199
- showFirstColumn?: boolean
200
- showActionColumn?: boolean
201
- actionColumn?: ProTableFormActionColumn
308
+ spanMethod?: (params: {
309
+ row: Record<string, unknown>
310
+ column: { property: string; label: string }
311
+ rowIndex: number
312
+ columnIndex: number
313
+ }) => [number, number] | { rowspan: number; colspan: number } | void
314
+ // ─── el-form 配置 ────────────────────────────────────────────────
315
+ formSize?: 'medium' | 'small' | 'large'
316
+ labelPosition?: 'left' | 'right' | 'top'
317
+ disabled?: boolean
318
+ // ─── el-table 配置 ────────────────────────────────────────────────
319
+ stripe?: boolean
320
+ size?: 'medium' | 'small' | 'large'
321
+ maxHeight?: number | string
322
+ height?: number | string
323
+ rowClassName?: string | ((params: { row: Record<string, unknown>; rowIndex: number }) => string)
324
+ expandRowKeys?: (string | number)[]
325
+ defaultExpandAll?: boolean
326
+ onRowClick?: (row: Record<string, unknown>, event: Event) => void
327
+ onRowDblclick?: (row: Record<string, unknown>, event: Event) => void
328
+ onSortChange?: (sortInfo: { prop: string; order: string }) => void
329
+ onExpandChange?: (row: Record<string, unknown>, expanded: boolean) => void
330
+ tableProps?: Record<string, unknown>
331
+ formProps?: Record<string, unknown>
202
332
  }>(),
203
333
  {
204
- modelValue: () => ({}),
205
- competitorsKey: 'competitors',
206
- competitorNameKey: 'name',
207
- firstColumnTitle: '维度/友商',
208
- competitorNamePlaceholder: '请输入友商名称',
334
+ modelValue: () => [],
209
335
  metricPlaceholder: '请输入',
210
- addCompetitorText: '+新增友商',
211
- minCompetitors: 0,
336
+ minRows: 0,
212
337
  labelWidth: '0',
213
338
  bordered: true,
214
- firstColMinWidth: 160,
215
- actionWidth: 120,
216
- showFirstColumn: true,
217
- showActionColumn: true,
339
+ stripe: false,
340
+ size: 'medium',
341
+ defaultExpandAll: false,
218
342
  }
219
343
  )
220
344
 
221
345
  const emit = defineEmits<{
222
- (e: 'update:modelValue', v: Record<string, unknown>): void
346
+ (e: 'update:modelValue', v: Record<string, unknown>[]): void
347
+ (e: 'add-row'): void
348
+ (e: 'remove-row', index: number): void
349
+ (e: 'register', action: Record<string, unknown>): void
223
350
  }>()
224
351
 
225
- const formRef = ref<{ validate: (cb?: (valid: boolean) => void) => Promise<unknown> | void; clearValidate: (p?: string | string[]) => void } | null>(null)
226
-
227
- const ck = () => props.competitorsKey ?? 'competitors'
228
- const nk = () => props.competitorNameKey ?? 'name'
229
-
230
- /** 有固定行时必须保留首列 */
231
- const showFirstColumnComputed = computed(() => {
232
- if (props.fixedRows.length > 0) return true
233
- return props.showFirstColumn !== false
234
- })
235
-
236
- const firstColumnFixed = computed(() => 'left' as const)
237
-
238
- const actionColumnBind = computed(() => {
239
- const ac = props.actionColumn
240
- return {
241
- width: ac?.width ?? props.actionWidth,
242
- minWidth: ac?.minWidth,
243
- align: ac?.align ?? 'center',
244
- fixed: ac?.fixed === undefined ? 'right' : ac.fixed,
245
- }
246
- })
247
-
248
- function rowKeyFn(row: TableRow) {
249
- return row._type === 'fixed' ? `f-${row.rowKey}` : `c-${row._index}`
250
- }
251
-
252
- type TableRow =
253
- | { _type: 'fixed'; rowKey: string; rowLabel: string }
254
- | { _type: 'competitor'; _index: number }
255
-
256
- const tableRows = computed<TableRow[]>(() => {
257
- const rows: TableRow[] = []
258
- props.fixedRows.forEach((fr) => {
259
- rows.push({
260
- _type: 'fixed',
261
- rowKey: fr.rowKey,
262
- rowLabel: fr.label,
263
- })
264
- })
265
- const mv = props.modelValue
266
- const list = (mv && typeof mv === 'object' ? (mv[ck()] as Record<string, unknown>[] | undefined) : undefined) ?? []
267
- list.forEach((_, i) => {
268
- rows.push({ _type: 'competitor', _index: i })
269
- })
270
- return rows
271
- })
272
-
273
- const canDeleteCompetitor = computed(() => {
274
- const mv = props.modelValue
275
- const n = ((mv && typeof mv === 'object' ? (mv[ck()] as unknown[]) : undefined) ?? []).length
276
- return n > props.minCompetitors
352
+ const {
353
+ formModelRef,
354
+ currentModelValue,
355
+ mergedRules,
356
+ tableRows,
357
+ rowKeyFn,
358
+ spanMethodAdapter,
359
+ formRef,
360
+ slotUpdateHandler,
361
+ getCellProp,
362
+ getCellValue,
363
+ setCellValue,
364
+ handleAddRow,
365
+ handleRemoveRow,
366
+ validate,
367
+ clearValidate,
368
+ } = useProTableForm({
369
+ props,
370
+ emit,
371
+ emitAddRow: () => emit('add-row'),
372
+ emitRemoveRow: (index) => emit('remove-row', index),
277
373
  })
278
374
 
279
- function cloneModel(): Record<string, unknown> {
280
- const m = props.modelValue
281
- if (!m || typeof m !== 'object') {
282
- return {}
283
- }
284
- return JSON.parse(JSON.stringify(m)) as Record<string, unknown>
285
- }
286
-
287
- function ensureFixedBlock(rowKey: string): Record<string, unknown> {
288
- const mv = props.modelValue
289
- if (!mv || typeof mv !== 'object') {
290
- const o: Record<string, unknown> = {}
291
- for (const c of props.columns) {
292
- o[c.key] = ''
293
- }
294
- return o
295
- }
296
- const m = mv[rowKey]
297
- if (m && typeof m === 'object' && !Array.isArray(m)) return m as Record<string, unknown>
298
- const o: Record<string, unknown> = {}
299
- for (const c of props.columns) {
300
- o[c.key] = ''
301
- }
302
- return o
303
- }
304
-
305
- function emitNext(next: Record<string, unknown>) {
306
- emit('update:modelValue', next)
307
- }
308
-
309
- function getFixedMetric(rowKey: string, key: string): unknown {
310
- const block = ensureFixedBlock(rowKey)
311
- return block[key] ?? ''
312
- }
313
-
314
- function setFixedMetric(rowKey: string, key: string, val: unknown) {
315
- const next = cloneModel()
316
- const b = { ...((next[rowKey] as Record<string, unknown>) || {}) }
317
- b[key] = val
318
- next[rowKey] = b
319
- emitNext(next)
320
- }
321
-
322
- function competitorList(): Record<string, unknown>[] {
323
- const mv = props.modelValue
324
- if (!mv || typeof mv !== 'object') {
325
- return []
326
- }
327
- const list = mv[ck()]
328
- if (!Array.isArray(list)) return []
329
- return list as Record<string, unknown>[]
330
- }
331
-
332
- function getCompetitorName(index: number): string {
333
- const row = competitorList()[index]
334
- const key = nk()
335
- return row ? String(row[key] ?? '') : ''
336
- }
337
-
338
- function setCompetitorName(index: number, val: string) {
339
- const next = cloneModel()
340
- const list = [...competitorList()]
341
- const row = { ...(list[index] || {}) }
342
- row[nk()] = val
343
- list[index] = row
344
- next[ck()] = list
345
- emitNext(next)
346
- }
375
+ void formRef
347
376
 
348
- function getCompetitorMetric(index: number, key: string): unknown {
349
- const row = competitorList()[index]
350
- return row ? row[key] ?? '' : ''
377
+ /** 表格行点击 */
378
+ function handleTableRowClick(row: TableRow, _column: unknown, event: Event) {
379
+ props.onRowClick?.(row as Record<string, unknown>, event)
351
380
  }
352
-
353
- function setCompetitorMetric(index: number, key: string, val: unknown) {
354
- const next = cloneModel()
355
- const list = [...competitorList()]
356
- const row = { ...(list[index] || {}) }
357
- row[key] = val
358
- list[index] = row
359
- next[ck()] = list
360
- emitNext(next)
381
+ /** 表格行双击 */
382
+ function handleTableRowDblclick(row: TableRow, _column: unknown, event: Event) {
383
+ props.onRowDblclick?.(row as Record<string, unknown>, event)
361
384
  }
362
-
363
- function getCellValue(tableRow: TableRow, col: ProTableFormColumn): unknown {
364
- if (tableRow._type === 'fixed') {
365
- return getFixedMetric(tableRow.rowKey, col.key)
366
- }
367
- return getCompetitorMetric(tableRow._index, col.key)
385
+ /** 表格排序变化 */
386
+ function handleTableSortChange(sortInfo: { prop: string; order: string }) {
387
+ props.onSortChange?.(sortInfo)
368
388
  }
369
-
370
- function setCellValue(tableRow: TableRow, col: ProTableFormColumn, val: unknown) {
371
- if (tableRow._type === 'fixed') {
372
- setFixedMetric(tableRow.rowKey, col.key, val)
373
- } else {
374
- setCompetitorMetric(tableRow._index, col.key, val)
375
- }
376
- }
377
-
378
- /** 供插槽列 update-value 绑定,避免模板内箭头参数隐式 any */
379
- function slotUpdateHandler(slotProps: { row: TableRow }, col: ProTableFormColumn) {
380
- return (v: unknown) => setCellValue(slotProps.row, col, v)
381
- }
382
-
383
- function emptyCompetitorRow(): Record<string, unknown> {
384
- const o: Record<string, unknown> = { [nk()]: '' }
385
- for (const c of props.columns) {
386
- o[c.key] = ''
387
- }
388
- return o
389
- }
390
-
391
- function addCompetitor() {
392
- const next = cloneModel()
393
- const list = [...competitorList()]
394
- list.push(emptyCompetitorRow())
395
- next[ck()] = list
396
- emitNext(next)
397
- }
398
-
399
- function removeCompetitor(index: number) {
400
- if (!canDeleteCompetitor.value) return
401
- const next = cloneModel()
402
- const list = [...competitorList()]
403
- list.splice(index, 1)
404
- next[ck()] = list
405
- emitNext(next)
406
- }
407
-
408
- function fixedMetricProp(rowKey: string, key: string) {
409
- return `${rowKey}.${key}`
410
- }
411
-
412
- function competitorNameProp(index: number) {
413
- return `${ck()}.${index}.${nk()}`
389
+ /** 表格展开变化 */
390
+ function handleTableExpandChange(row: TableRow, expanded: boolean) {
391
+ props.onExpandChange?.(row as Record<string, unknown>, expanded)
414
392
  }
415
393
 
416
- function competitorMetricProp(index: number, key: string) {
417
- return `${ck()}.${index}.${key}`
418
- }
419
-
420
- function cellComponent(col: ProTableFormColumn) {
421
- return col.component === 'formatted-number' ? FormattedNumberInput : 'el-input'
422
- }
423
-
424
- function cellBind(col: ProTableFormColumn): Record<string, unknown> {
425
- const cp = col.componentProps || {}
426
- if (col.component === 'formatted-number') {
427
- return {
428
- integerDigits: 5,
429
- decimalPlaces: 6,
430
- rounding: 'round',
431
- inputLimit: true,
432
- ...cp,
433
- }
434
- }
435
- return { ...cp }
436
- }
437
-
438
- function firstColumnScope(slotProps: { row: TableRow }) {
439
- const r = slotProps.row
440
- if (r._type === 'fixed') {
441
- return {
442
- row: r,
443
- rowType: 'fixed' as const,
444
- rowKey: r.rowKey,
445
- rowLabel: r.rowLabel,
446
- }
447
- }
448
- const idx = r._index
449
- return {
450
- row: r,
451
- rowType: 'competitor' as const,
452
- rowIndex: idx,
453
- value: getCompetitorName(idx),
454
- updateValue: (v: string) => setCompetitorName(idx, v),
455
- }
456
- }
457
-
458
- const mergedRules = computed(() => {
459
- const r: Record<string, unknown[]> = {}
460
- const req = (title: string) => [{ required: true, message: `请输入${title}`, trigger: 'blur' }]
461
-
462
- for (const fr of props.fixedRows) {
463
- for (const col of props.columns) {
464
- if (col.rules) {
465
- r[`${fr.rowKey}.${col.key}`] = col.rules as unknown[]
466
- } else if (col.required) {
467
- r[`${fr.rowKey}.${col.key}`] = req(col.title)
468
- }
469
- }
470
- }
471
- const list = competitorList()
472
- list.forEach((_, i) => {
473
- r[`${ck()}.${i}.${nk()}`] = req('友商名称')
474
- for (const col of props.columns) {
475
- if (col.rules) {
476
- r[`${ck()}.${i}.${col.key}`] = col.rules as unknown[]
477
- } else if (col.required) {
478
- r[`${ck()}.${i}.${col.key}`] = req(col.title)
479
- }
480
- }
481
- })
482
- return { ...r, ...(props.rules || {}) }
394
+ /** columns 扁平化,保留 _key 用于 template v-for key */
395
+ const flatColumns = computed<(ProTableFormColumn & { _key: string })[]>(() => {
396
+ return props.columns.map((col, i) => ({
397
+ ...col,
398
+ _key: col.key || `__col-${i}`,
399
+ }))
483
400
  })
484
401
 
485
- function validate(): Promise<boolean> {
486
- return new Promise((resolve) => {
487
- const f = formRef.value
488
- if (!f || typeof f.validate !== 'function') {
489
- resolve(true)
490
- return
491
- }
492
- f.validate((valid: boolean) => {
493
- resolve(valid)
494
- })
495
- })
496
- }
497
-
498
- function clearValidate(propsArg?: string | string[]) {
499
- formRef.value?.clearValidate?.(propsArg)
500
- }
402
+ const tableRef = ref<{ clearSelection: () => void } | null>(null)
501
403
 
502
- defineExpose({
404
+ const action = {
503
405
  validate,
504
406
  clearValidate,
505
- addCompetitor,
506
- removeCompetitor,
507
- })
407
+ addRow: handleAddRow,
408
+ removeRow: handleRemoveRow,
409
+ getRows: () => [...currentModelValue.value],
410
+ getRowCount: () => currentModelValue.value.length,
411
+ getTable: () => tableRef.value,
412
+ getModelValue: () => [...currentModelValue.value],
413
+ setModelValue: (val: Record<string, unknown>[]) => emit('update:modelValue', val),
414
+ getFormRef: () => formRef.value,
415
+ }
416
+
417
+ defineExpose(action)
418
+ emit('register', action)
508
419
  </script>
509
420
 
510
- <style scoped>
421
+ <style>
511
422
  .ecp-pro-table-form__form :deep(.el-form-item) {
512
423
  margin-bottom: 0;
513
424
  }
@@ -518,10 +429,9 @@ defineExpose({
518
429
  margin-left: 0 !important;
519
430
  line-height: normal;
520
431
  }
521
- .ecp-pro-table-form__fixed-label {
522
- color: #303133;
523
- font-size: 14px;
524
- }
432
+ /* .ecp-pro-table-form__cell-item :deep(.el-form-item__error) {
433
+ display: none;
434
+ } */
525
435
  .ecp-pro-table-form__req {
526
436
  color: #f56c6c;
527
437
  margin-right: 2px;
@@ -530,26 +440,13 @@ defineExpose({
530
440
  font-weight: 500;
531
441
  color: #606266;
532
442
  }
533
- .ecp-pro-table-form__action-title {
534
- margin-right: 8px;
535
- font-size: 13px;
536
- color: #606266;
537
- }
538
- .ecp-pro-table-form__add-btn {
539
- padding: 0;
540
- font-size: 14px;
541
- }
542
- .ecp-pro-table-form__del-btn {
543
- padding: 0;
544
- color: #909399;
545
- }
546
- .ecp-pro-table-form__del-btn:not(:disabled) {
547
- color: #409eff;
548
- }
549
443
  </style>
550
444
 
551
445
  <style>
552
446
  .ecp-pro-table-form .ecp-pro-table-form__header-cell {
553
447
  background: #f5f7fa !important;
554
448
  }
449
+ .ecp-pro-table-form__cell-item .el-form-item__error {
450
+ position: relative;
451
+ }
555
452
  </style>