@aspire-ui/element-component-pro 1.0.24 → 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.
@@ -1,170 +1,225 @@
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
+ :validate-on-rule-change="false"
7
8
  :label-width="labelWidth"
9
+ :size="formSize"
10
+ :label-position="labelPosition"
11
+ :disabled="disabled"
12
+ v-bind="formProps"
8
13
  class="ecp-pro-table-form__form"
9
14
  >
10
15
  <el-table
16
+ ref="tableRef"
11
17
  :data="tableRows"
12
18
  :border="bordered"
19
+ :stripe="stripe"
20
+ :size="size"
21
+ :max-height="maxHeight"
22
+ :height="height"
13
23
  :row-key="rowKeyFn"
24
+ :row-class-name="rowClassName"
25
+ :default-expand-all="defaultExpandAll"
26
+ :span-method="spanMethodAdapter"
14
27
  header-cell-class-name="ecp-pro-table-form__header-cell"
28
+ v-bind="tableProps"
15
29
  class="ecp-pro-table-form__table"
30
+ @row-click="handleTableRowClick"
31
+ @row-dblclick="handleTableRowDblclick"
32
+ @sort-change="handleTableSortChange"
33
+ @expand-change="handleTableExpandChange"
16
34
  >
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>
35
+ <template v-for="col in flatColumns">
36
+ <!-- 分组列:有 children -->
37
+ <el-table-column
38
+ v-if="col.children && col.children.length > 0"
39
+ :key="'col-' + col._key"
40
+ :label="col.title"
41
+ :min-width="col.minWidth || col.width || 120"
42
+ :width="col.width"
43
+ :align="col.align || 'center'"
44
+ :header-align="col.headerAlign"
45
+ :fixed="col.fixed"
46
+ :cell-style="col.cellStyle"
47
+ :header-cell-style="col.headerCellStyle"
48
+ :cell-class-name="col.cellClassName"
49
+ :header-cell-class-name="col.headerCellClassName"
50
+ :sortable="col.sortable"
51
+ :resizable="col.resizable !== false"
52
+ >
53
+ <template #header>
54
+ <slot :name="`header-${col.key}`" :column="col">
55
+ <span class="ecp-pro-table-form__th-text">
56
+ <span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
57
+ </span>
58
+ </slot>
59
+ </template>
60
+ <template v-for="child in col.children">
61
+ <el-table-column
62
+ v-if="!child.hideInTable"
63
+ :key="'col-' + col.key + '-' + child.key"
64
+ :label="child.title"
65
+ :min-width="child.minWidth || child.width || 120"
66
+ :width="child.width"
67
+ :align="child.align || 'center'"
68
+ :header-align="child.headerAlign"
69
+ :fixed="child.fixed"
70
+ :cell-style="child.cellStyle"
71
+ :header-cell-style="child.headerCellStyle"
72
+ :cell-class-name="child.cellClassName"
73
+ :header-cell-class-name="child.headerCellClassName"
74
+ :sortable="child.sortable"
75
+ :resizable="child.resizable !== false"
76
+ >
77
+ <template #header>
78
+ <slot :name="`header-${child.key}`" :column="child">
79
+ <span class="ecp-pro-table-form__th-text">
80
+ <span v-if="child.required" class="ecp-pro-table-form__req">*</span>{{ child.title }}
81
+ </span>
82
+ </slot>
83
+ </template>
84
+ <template #default="slotProps">
85
+ <!-- 渲染函数 -->
86
+ <template v-if="child.render">
87
+ <el-form-item
88
+ :prop="getCellProp(slotProps.row, col, child.key)"
89
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
90
+ class="ecp-pro-table-form__cell-item"
91
+ >
92
+ <template v-if="getRender(child, slotProps.row, col, child.key).isText">
93
+ <span>{{ getRender(child, slotProps.row, col, child.key).value }}</span>
94
+ </template>
95
+ <template v-else>
96
+ <RenderCell :render-fn="child.render" :render-params="{ row: slotProps.row, col, childKey: child.key }" />
97
+ </template>
98
+ </el-form-item>
99
+ </template>
100
+ <!-- 插槽列 -->
101
+ <template v-else-if="child.component === 'slot' && child.slotName">
102
+ <el-form-item
103
+ :prop="getCellProp(slotProps.row, col, child.key)"
104
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
105
+ class="ecp-pro-table-form__cell-item"
106
+ >
107
+ <slot
108
+ :name="`cell-${child.slotName}`"
109
+ :column="child"
110
+ :row="slotProps.row"
111
+ :index="slotProps.$index"
112
+ :value="getCellValue(slotProps.row, col, child.key)"
113
+ :update-value="slotUpdateHandler(slotProps, col, child.key)"
114
+ />
115
+ </el-form-item>
116
+ </template>
117
+ <!-- 内置组件 -->
118
+ <el-form-item
119
+ v-else
120
+ :prop="getCellProp(slotProps.row, col, child.key)"
121
+ :rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
122
+ class="ecp-pro-table-form__cell-item"
123
+ >
124
+ <CellEditor
125
+ :col="child"
126
+ :value="getCellValue(slotProps.row, col, child.key)"
127
+ :row="slotProps.row"
128
+ :size="size"
129
+ :placeholder="col.placeholder || metricPlaceholder"
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
+ @update="setCellValue(slotProps.row, col, undefined, $event)"
120
209
  />
121
210
  </el-form-item>
122
211
  </template>
123
- </template>
124
- </el-table-column>
212
+ </el-table-column>
213
+ </template>
125
214
 
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>
215
+ <!-- 操作列 -->
216
+ <slot name="action" :add-row="handleAddRow" :remove-row="handleRemoveRow" />
159
217
  </el-table>
160
218
  </el-form>
161
219
  </div>
162
220
  </template>
163
221
 
164
222
  <script lang="ts">
165
- /**
166
- * Vue 2 默认 v-model 绑定 value + input;本组件使用 modelValue(与 Vue 3 一致),需显式声明 model。
167
- */
168
223
  export default {
169
224
  name: 'ProTableForm',
170
225
  model: {
@@ -176,335 +231,190 @@ export default {
176
231
 
177
232
  <script setup lang="ts">
178
233
  import { computed, ref } from 'vue'
179
- import FormattedNumberInput from '../ProForm/FormattedNumberInput.vue'
180
- import type { ProTableFormActionColumn, ProTableFormColumn, ProTableFormFixedRow } from './types'
234
+ import { useProTableForm } from './useProTableForm'
235
+ import { defineComponent } from 'vue'
236
+ import type { VNode } from 'vue'
237
+ import type {
238
+ ProTableFormColumn,
239
+ ProTableFormColumnChild,
240
+ ProTableFormColumnRender,
241
+ } from '../types'
242
+ import CellEditor from './CellEditor.vue'
243
+
244
+ /** 与 useProTableForm 内部 tableRows 元素类型对齐 */
245
+ type TableRow = { _index: number }
246
+
247
+ /** RenderCell:接收 render 函数,在 td 内渲染 VNode,完全脱离 el-table 插槽作用域 */
248
+ const RenderCell = defineComponent({
249
+ name: 'RenderCell',
250
+ props: {
251
+ renderFn: { type: Function as unknown as () => ProTableFormColumnRender, required: true },
252
+ renderParams: { type: Object as () => Record<string, unknown>, required: true },
253
+ },
254
+ setup(props) {
255
+ return () => {
256
+ const vnode = props.renderFn(props.renderParams as Parameters<ProTableFormColumnRender>[0])
257
+ return Array.isArray(vnode) ? vnode : (vnode as VNode)
258
+ }
259
+ },
260
+ })
261
+
262
+ /** 单次执行 render,返回 { isText, value } */
263
+ function execRenderOnce(
264
+ child: ProTableFormColumn | ProTableFormColumnChild,
265
+ row: Record<string, unknown>,
266
+ col: ProTableFormColumn,
267
+ colKey?: string
268
+ ): { isText: boolean; value: ReturnType<NonNullable<typeof child.render>> } {
269
+ const value = colKey !== undefined
270
+ ? getCellValue(row as TableRow, col, colKey)
271
+ : getCellValue(row as TableRow, col)
272
+ const result = child.render!({ row, value, column: child })
273
+ const isText = isPrimitive(result)
274
+ return { isText, value: result }
275
+ }
276
+
277
+ /** 行→列key→渲染结果的缓存,避免同一 render 在模板中被多次调用 */
278
+ const _renderCache = new WeakMap<object, Map<string, { isText: boolean; value: unknown }>>()
279
+
280
+ function getRender(
281
+ child: ProTableFormColumn | ProTableFormColumnChild,
282
+ row: Record<string, unknown>,
283
+ col: ProTableFormColumn,
284
+ colKey?: string
285
+ ): { isText: boolean; value: unknown } {
286
+ if (!_renderCache.has(row as object)) _renderCache.set(row as object, new Map())
287
+ const cache = _renderCache.get(row as object)!
288
+ const cacheKey = col.key + (colKey ? '.' + colKey : '')
289
+ if (!cache.has(cacheKey)) cache.set(cacheKey, execRenderOnce(child, row, col, colKey))
290
+ return cache.get(cacheKey)!
291
+ }
292
+
293
+ /** 是否为基础类型(string / number / boolean / null / undefined) */
294
+ function isPrimitive(val: unknown): val is string | number | boolean | null | undefined {
295
+ return val === null || typeof val in { string: 1, number: 1, boolean: 1, bigint: 1, symbol: 1 }
296
+ }
181
297
 
182
298
  const props = withDefaults(
183
299
  defineProps<{
184
- modelValue?: Record<string, unknown>
300
+ modelValue?: Record<string, unknown>[]
185
301
  columns: ProTableFormColumn[]
186
- fixedRows: ProTableFormFixedRow[]
187
- competitorsKey?: string
188
- competitorNameKey?: string
189
- firstColumnTitle?: string
190
- competitorNamePlaceholder?: string
191
302
  metricPlaceholder?: string
192
- addCompetitorText?: string
193
- minCompetitors?: number
303
+ minRows?: number
194
304
  rules?: Record<string, unknown>
195
305
  labelWidth?: string
196
306
  bordered?: boolean
197
- firstColMinWidth?: number
198
- actionWidth?: number
199
- showFirstColumn?: boolean
200
- showActionColumn?: boolean
201
- actionColumn?: ProTableFormActionColumn
307
+ spanMethod?: (params: {
308
+ row: Record<string, unknown>
309
+ column: { property: string; label: string }
310
+ rowIndex: number
311
+ columnIndex: number
312
+ }) => [number, number] | { rowspan: number; colspan: number } | void
313
+ // ─── el-form 配置 ────────────────────────────────────────────────
314
+ formSize?: 'medium' | 'small' | 'large'
315
+ labelPosition?: 'left' | 'right' | 'top'
316
+ disabled?: boolean
317
+ // ─── el-table 配置 ────────────────────────────────────────────────
318
+ stripe?: boolean
319
+ size?: 'medium' | 'small' | 'large'
320
+ maxHeight?: number | string
321
+ height?: number | string
322
+ rowClassName?: string | ((params: { row: Record<string, unknown>; rowIndex: number }) => string)
323
+ expandRowKeys?: (string | number)[]
324
+ defaultExpandAll?: boolean
325
+ onRowClick?: (row: Record<string, unknown>, event: Event) => void
326
+ onRowDblclick?: (row: Record<string, unknown>, event: Event) => void
327
+ onSortChange?: (sortInfo: { prop: string; order: string }) => void
328
+ onExpandChange?: (row: Record<string, unknown>, expanded: boolean) => void
329
+ tableProps?: Record<string, unknown>
330
+ formProps?: Record<string, unknown>
202
331
  }>(),
203
332
  {
204
- modelValue: () => ({}),
205
- competitorsKey: 'competitors',
206
- competitorNameKey: 'name',
207
- firstColumnTitle: '维度/友商',
208
- competitorNamePlaceholder: '请输入友商名称',
333
+ modelValue: () => [],
209
334
  metricPlaceholder: '请输入',
210
- addCompetitorText: '+新增友商',
211
- minCompetitors: 0,
335
+ minRows: 0,
212
336
  labelWidth: '0',
213
337
  bordered: true,
214
- firstColMinWidth: 160,
215
- actionWidth: 120,
216
- showFirstColumn: true,
217
- showActionColumn: true,
338
+ stripe: false,
339
+ size: 'medium',
340
+ defaultExpandAll: false,
218
341
  }
219
342
  )
220
343
 
221
344
  const emit = defineEmits<{
222
- (e: 'update:modelValue', v: Record<string, unknown>): void
345
+ (e: 'update:modelValue', v: Record<string, unknown>[]): void
346
+ (e: 'add-row'): void
347
+ (e: 'remove-row', index: number): void
348
+ (e: 'register', action: Record<string, unknown>): void
223
349
  }>()
224
350
 
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
351
+ const {
352
+ formModelRef,
353
+ currentModelValue,
354
+ mergedRules,
355
+ tableRows,
356
+ rowKeyFn,
357
+ spanMethodAdapter,
358
+ formRef,
359
+ slotUpdateHandler,
360
+ getCellProp,
361
+ getCellValue,
362
+ setCellValue,
363
+ handleAddRow,
364
+ handleRemoveRow,
365
+ validate,
366
+ clearValidate,
367
+ } = useProTableForm({
368
+ props,
369
+ emit,
370
+ emitAddRow: () => emit('add-row'),
371
+ emitRemoveRow: (index) => emit('remove-row', index),
271
372
  })
272
373
 
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
277
- })
374
+ void formRef
278
375
 
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>
376
+ /** 表格行点击 */
377
+ function handleTableRowClick(row: TableRow, _column: unknown, event: Event) {
378
+ props.onRowClick?.(row as Record<string, unknown>, event)
285
379
  }
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
380
+ /** 表格行双击 */
381
+ function handleTableRowDblclick(row: TableRow, _column: unknown, event: Event) {
382
+ props.onRowDblclick?.(row as Record<string, unknown>, event)
303
383
  }
304
-
305
- function emitNext(next: Record<string, unknown>) {
306
- emit('update:modelValue', next)
384
+ /** 表格排序变化 */
385
+ function handleTableSortChange(sortInfo: { prop: string; order: string }) {
386
+ props.onSortChange?.(sortInfo)
307
387
  }
308
-
309
- function getFixedMetric(rowKey: string, key: string): unknown {
310
- const block = ensureFixedBlock(rowKey)
311
- return block[key] ?? ''
388
+ /** 表格展开变化 */
389
+ function handleTableExpandChange(row: TableRow, expanded: boolean) {
390
+ props.onExpandChange?.(row as Record<string, unknown>, expanded)
312
391
  }
313
392
 
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
- }
347
-
348
- function getCompetitorMetric(index: number, key: string): unknown {
349
- const row = competitorList()[index]
350
- return row ? row[key] ?? '' : ''
351
- }
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)
361
- }
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)
368
- }
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()}`
414
- }
415
-
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 || {}) }
393
+ /** columns 扁平化,保留 _key 用于 template v-for key */
394
+ const flatColumns = computed<(ProTableFormColumn & { _key: string })[]>(() => {
395
+ return props.columns.map((col, i) => ({
396
+ ...col,
397
+ _key: col.key || `__col-${i}`,
398
+ }))
483
399
  })
484
400
 
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
- }
401
+ const tableRef = ref<{ clearSelection: () => void } | null>(null)
501
402
 
502
- defineExpose({
403
+ const action = {
503
404
  validate,
504
405
  clearValidate,
505
- addCompetitor,
506
- removeCompetitor,
507
- })
406
+ addRow: handleAddRow,
407
+ removeRow: handleRemoveRow,
408
+ getRows: () => [...currentModelValue.value],
409
+ getRowCount: () => currentModelValue.value.length,
410
+ getTable: () => tableRef.value,
411
+ getModelValue: () => [...currentModelValue.value],
412
+ setModelValue: (val: Record<string, unknown>[]) => emit('update:modelValue', val),
413
+ getFormRef: () => formRef.value,
414
+ }
415
+
416
+ defineExpose(action)
417
+ emit('register', action)
508
418
  </script>
509
419
 
510
420
  <style scoped>
@@ -518,10 +428,9 @@ defineExpose({
518
428
  margin-left: 0 !important;
519
429
  line-height: normal;
520
430
  }
521
- .ecp-pro-table-form__fixed-label {
522
- color: #303133;
523
- font-size: 14px;
524
- }
431
+ /* .ecp-pro-table-form__cell-item :deep(.el-form-item__error) {
432
+ display: none;
433
+ } */
525
434
  .ecp-pro-table-form__req {
526
435
  color: #f56c6c;
527
436
  margin-right: 2px;
@@ -530,26 +439,13 @@ defineExpose({
530
439
  font-weight: 500;
531
440
  color: #606266;
532
441
  }
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
442
  </style>
550
443
 
551
444
  <style>
552
445
  .ecp-pro-table-form .ecp-pro-table-form__header-cell {
553
446
  background: #f5f7fa !important;
554
447
  }
448
+ .ecp-pro-table-form__cell-item .el-form-item__error {
449
+ position: relative;
450
+ }
555
451
  </style>