@aspire-ui/element-component-pro 1.0.1 → 1.0.2

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,773 @@
1
+ <template>
2
+ <div class="ecp-pro-table">
3
+ <!-- 标题栏 -->
4
+ <div v-if="showTitleBar" class="ecp-pro-table__header">
5
+ <div class="ecp-pro-table__title-wrapper">
6
+ <span class="ecp-pro-table__title">{{ effectiveProps.title }}</span>
7
+ <el-tooltip v-if="effectiveProps.titleHelpMessage" class="ecp-pro-table__help" placement="top">
8
+ <template slot="content">
9
+ <span v-if="Array.isArray(effectiveProps.titleHelpMessage)">
10
+ <div v-for="(msg, i) in effectiveProps.titleHelpMessage" :key="i">{{ msg }}</div>
11
+ </span>
12
+ <span v-else>{{ effectiveProps.titleHelpMessage }}</span>
13
+ </template>
14
+ <i class="el-icon-question" />
15
+ </el-tooltip>
16
+ </div>
17
+ <div class="ecp-pro-table__toolbar">
18
+ <slot name="tableTitle" />
19
+ <slot name="toolbar" />
20
+ <slot name="toolbar-right">
21
+ <el-button
22
+ v-if="effectiveProps.tableSetting?.redo !== false"
23
+ type="text"
24
+ icon="el-icon-refresh"
25
+ size="small"
26
+ @click="handleReload"
27
+ >
28
+ 刷新
29
+ </el-button>
30
+ </slot>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- 表格主体(Element UI el-table 无 loading 属性,使用 v-loading 指令) -->
35
+ <div ref="tableWrapRef" class="ecp-pro-table__body" v-loading="loading">
36
+ <el-table
37
+ ref="tableRef"
38
+ :data="innerData"
39
+ :row-key="effectiveProps.rowKey"
40
+ :border="effectiveProps.bordered"
41
+ :stripe="effectiveProps.striped"
42
+ :size="effectiveProps.size"
43
+ :max-height="effectiveProps.maxHeight"
44
+ :height="effectiveProps.height"
45
+ :default-sort="effectiveProps.defaultSort"
46
+ :span-method="effectiveProps.spanMethod"
47
+ :tree-props="effectiveProps.treeProps"
48
+ :default-expand-all="effectiveProps.defaultExpandAll"
49
+ :expand-row-keys="effectiveProps.expandRowKeys || []"
50
+ :lazy="effectiveProps.lazy"
51
+ :load="effectiveProps.load"
52
+ v-bind="effectiveProps.tableProps"
53
+ :row-class-name="effectiveProps.rowClassName"
54
+ @row-click="handleRowClick"
55
+ @row-dblclick="handleRowDblclick"
56
+ @sort-change="handleSortChange"
57
+ @expand-change="handleExpandChange"
58
+ >
59
+ <!-- 选择列:自定义实现,参考 VbenAdmin,支持单选/多选/禁用/跨页 -->
60
+ <el-table-column
61
+ v-if="effectiveProps.rowSelection"
62
+ :width="effectiveProps.rowSelection.width || 48"
63
+ :fixed="effectiveProps.rowSelection.fixed"
64
+ align="center"
65
+ >
66
+ <template slot="header" slot-scope="_">
67
+ <!-- 多选:表头全选 -->
68
+ <el-checkbox
69
+ v-if="effectiveProps.rowSelection.type !== 'radio'"
70
+ :value="isAllCurrentPageSelected"
71
+ :indeterminate="isIndeterminate"
72
+ :disabled="!hasSelectableRows"
73
+ @change="handleSelectAll"
74
+ />
75
+ <span v-else />
76
+ </template>
77
+ <template slot-scope="scope">
78
+ <!-- 多选 -->
79
+ <el-checkbox
80
+ v-if="effectiveProps.rowSelection.type !== 'radio'"
81
+ :value="isRowSelected(scope.row)"
82
+ :disabled="getCheckboxDisabled(scope.row)"
83
+ @change="(val) => handleCheckboxChange(scope.row, val)"
84
+ @click.native.stop
85
+ />
86
+ <!-- 单选 -->
87
+ <el-radio
88
+ v-else
89
+ :value="selectedRows[0]?.[rowKeyField]"
90
+ :label="scope.row[rowKeyField]"
91
+ :disabled="getRadioDisabled(scope.row)"
92
+ @change="handleRadioSelect(scope.row)"
93
+ @click.native.stop
94
+ >
95
+ <span />
96
+ </el-radio>
97
+ </template>
98
+ </el-table-column>
99
+ <!-- 序号列 -->
100
+ <el-table-column
101
+ v-if="effectiveProps.showIndexColumn"
102
+ type="index"
103
+ :label="effectiveProps.indexColumnProps?.title || '序号'"
104
+ :width="effectiveProps.indexColumnProps?.width || 60"
105
+ :fixed="effectiveProps.indexColumnProps?.fixed"
106
+ :align="effectiveProps.indexColumnProps?.align || 'center'"
107
+ />
108
+ <!-- 数据列 -->
109
+ <template v-for="col in displayColumns">
110
+ <el-table-column
111
+ v-if="shouldShowColumn(col)"
112
+ :key="col.dataIndex || col.key || col.title"
113
+ :prop="col.dataIndex"
114
+ :label="col.title"
115
+ :width="getColumnWidth(col)"
116
+ :min-width="isRatioWidth(col.width) ? undefined : col.minWidth"
117
+ :fixed="col.fixed"
118
+ :align="col.align || 'left'"
119
+ :sortable="col.sortable"
120
+ :formatter="col.formatter"
121
+ :show-overflow-tooltip="col.ellipsis !== false && effectiveProps.ellipsis !== false"
122
+ >
123
+ <template slot="header" slot-scope="_">
124
+ <!-- 1. 列级自定义表头:header-[dataIndex],例如 #header-age -->
125
+ <slot
126
+ v-if="col.dataIndex && $scopedSlots[`header-${col.dataIndex}`]"
127
+ :name="`header-${col.dataIndex}`"
128
+ :column="col"
129
+ />
130
+ <!-- 2. 全局表头渲染:headerCell,类似 Vben 的 headerCell 用法 -->
131
+ <slot
132
+ v-else-if="$scopedSlots['headerCell']"
133
+ name="headerCell"
134
+ :column="col"
135
+ />
136
+ <!-- 3. 默认表头渲染:标题 + helpMessage 提示 -->
137
+ <template v-else>
138
+ <span>{{ col.title }}</span>
139
+ <el-tooltip v-if="col.helpMessage" class="ecp-pro-table__col-help" placement="top" effect="dark">
140
+ <template slot="content">
141
+ <span v-if="Array.isArray(col.helpMessage)">
142
+ <div v-for="(msg, i) in col.helpMessage" :key="i">{{ msg }}</div>
143
+ </span>
144
+ <span v-else>{{ col.helpMessage }}</span>
145
+ </template>
146
+ <i class="el-icon-question" />
147
+ </el-tooltip>
148
+ </template>
149
+ </template>
150
+ <template slot-scope="scope">
151
+ <slot
152
+ v-if="col.dataIndex && $scopedSlots[col.dataIndex]"
153
+ :name="col.dataIndex"
154
+ :row="scope.row"
155
+ :column="col"
156
+ :index="scope.$index"
157
+ :value="scope.row[col.dataIndex]"
158
+ />
159
+ <BodyCellRenderer
160
+ v-else-if="$scopedSlots['bodyCell']"
161
+ :slot-render="$scopedSlots['bodyCell']"
162
+ :column="col"
163
+ :record="scope.row"
164
+ :index="scope.$index"
165
+ :value="scope.row[col.dataIndex]"
166
+ :custom-render="col.customRender"
167
+ :value-enum="col.valueEnum"
168
+ />
169
+ <DefaultCellRenderer v-else :column="col" :record="scope.row" :index="scope.$index" :value="scope.row[col.dataIndex]" />
170
+ </template>
171
+ </el-table-column>
172
+ </template>
173
+ <!-- 操作列 -->
174
+ <el-table-column
175
+ v-if="effectiveProps.actionColumn"
176
+ :label="effectiveProps.actionColumn.title || '操作'"
177
+ :width="effectiveProps.actionColumn.width || 150"
178
+ :fixed="effectiveProps.actionColumn.fixed || 'right'"
179
+ :align="effectiveProps.actionColumn.align || 'center'"
180
+ >
181
+ <template slot-scope="scope">
182
+ <slot
183
+ v-if="$scopedSlots['action']"
184
+ name="action"
185
+ :record="scope.row"
186
+ :column="effectiveProps.actionColumn"
187
+ :index="scope.$index"
188
+ />
189
+ <BodyCellRenderer
190
+ v-else-if="$scopedSlots['bodyCell']"
191
+ :slot-render="$scopedSlots['bodyCell']"
192
+ :column="effectiveProps.actionColumn"
193
+ :record="scope.row"
194
+ :index="scope.$index"
195
+ :value="undefined"
196
+ :custom-render="effectiveProps.actionColumn.customRender"
197
+ :value-enum="effectiveProps.actionColumn.valueEnum"
198
+ />
199
+ </template>
200
+ </el-table-column>
201
+ </el-table>
202
+ </div>
203
+
204
+ <!-- 分页 -->
205
+ <div v-if="showPagination" class="ecp-pro-table__pagination">
206
+ <el-pagination
207
+ :current-page="pagination.page"
208
+ :page-sizes="pagination.pageSizes"
209
+ :page-size="pagination.pageSize"
210
+ :total="pagination.total"
211
+ :layout="(effectiveProps.pagination && typeof effectiveProps.pagination === 'object' ? effectiveProps.pagination.layout : null) || 'total, sizes, prev, pager, next, jumper'"
212
+ v-bind="(effectiveProps.pagination && typeof effectiveProps.pagination === 'object' && effectiveProps.pagination.props) || {}"
213
+ @size-change="handleSizeChange"
214
+ @current-change="handleCurrentChange"
215
+ />
216
+ </div>
217
+ </div>
218
+ </template>
219
+
220
+ <script setup lang="ts">
221
+ // @ts-nocheck - Vue 2 el-table-column slot-scope 类型推断存在已知限制,暂用此方式消除模板内 scope 相关告警
222
+ import { ref, computed, watch, onMounted, onUnmounted, useSlots, nextTick, defineComponent, h } from 'vue'
223
+ import { useComponentSetting } from '../useComponentSetting'
224
+ import type { ProColumn, ProTableProps, TableActionType, FetchSetting, FetchParams } from './types'
225
+
226
+ const DefaultCellRenderer = defineComponent({
227
+ name: 'EcpProTableDefaultCellRenderer',
228
+ props: {
229
+ column: { type: Object, required: true },
230
+ record: { type: Object, required: true },
231
+ index: { type: Number, required: true },
232
+ value: { required: false },
233
+ },
234
+ setup(p) {
235
+ return () => {
236
+ const col = p.column as any
237
+ if (col?.customRender) {
238
+ const r = col.customRender({ text: p.value, record: p.record, index: p.index })
239
+ if (typeof r === 'string' || typeof r === 'number') return h('span', String(r))
240
+ return r as any
241
+ }
242
+ if (col?.valueEnum) {
243
+ const text = col.valueEnum?.[p.value]?.text ?? p.value
244
+ return h('span', text == null ? '' : String(text))
245
+ }
246
+ return h('span', p.value == null ? '' : String(p.value))
247
+ }
248
+ },
249
+ })
250
+
251
+ const BodyCellRenderer = defineComponent({
252
+ name: 'EcpProTableBodyCellRenderer',
253
+ props: {
254
+ slotRender: { type: Function, required: true },
255
+ column: { type: Object, required: true },
256
+ record: { type: Object, required: true },
257
+ index: { type: Number, required: true },
258
+ value: { required: false },
259
+ customRender: { type: Function, required: false },
260
+ valueEnum: { type: Object, required: false },
261
+ },
262
+ setup(p) {
263
+ return () => {
264
+ const slot = p.slotRender as any
265
+ const nodes = slot?.({ column: p.column, record: p.record, index: p.index, value: p.value })
266
+
267
+ const normalize = (n: any) => {
268
+ if (n == null) return []
269
+ if (Array.isArray(n)) return n.filter((x) => x != null && x !== false && !x.isComment)
270
+ return [n]
271
+ }
272
+ const normalized = normalize(nodes)
273
+ if (normalized.length > 0) return nodes
274
+
275
+ // slot 未返回内容时,回退到默认渲染(对齐 vbenAdmin bodyCell 用法)
276
+ const col = { ...(p.column as any) }
277
+ if (p.customRender) col.customRender = p.customRender
278
+ if (p.valueEnum) col.valueEnum = p.valueEnum
279
+ return h(DefaultCellRenderer as any, { props: { column: col, record: p.record, index: p.index, value: p.value } })
280
+ }
281
+ },
282
+ })
283
+
284
+ const props = withDefaults(
285
+ defineProps<{
286
+ columns?: ProColumn[]
287
+ dataSource?: Record<string, unknown>[]
288
+ api?: (params: Record<string, unknown>) => Promise<{ list?: unknown[]; items?: unknown[]; total?: number }>
289
+ rowKey?: string
290
+ title?: string
291
+ titleHelpMessage?: string | string[]
292
+ bordered?: boolean
293
+ striped?: boolean
294
+ size?: 'medium' | 'small' | 'large'
295
+ loading?: boolean
296
+ maxHeight?: number | string
297
+ height?: number | string
298
+ ellipsis?: boolean
299
+ showIndexColumn?: boolean
300
+ indexColumnProps?: Partial<ProColumn>
301
+ actionColumn?: Partial<ProColumn>
302
+ rowSelection?: { type?: 'checkbox' | 'radio'; width?: number; fixed?: 'left' | 'right'; getCheckboxProps?: (r: Record<string, unknown>) => { disabled?: boolean }; getRadioProps?: (r: Record<string, unknown>) => { disabled?: boolean } }
303
+ clearSelectOnPageChange?: boolean
304
+ pagination?: false | { pageSize?: number; pageSizes?: number[]; layout?: string; props?: Record<string, unknown> } | Record<string, unknown>
305
+ tableSetting?: { redo?: boolean; size?: boolean; setting?: boolean; fullScreen?: boolean }
306
+ fetchSetting?: FetchSetting
307
+ beforeFetch?: (params: Record<string, unknown>) => Record<string, unknown>
308
+ afterFetch?: (data: unknown) => unknown
309
+ immediate?: boolean
310
+ searchInfo?: Record<string, unknown>
311
+ defaultSort?: { prop: string; order: 'ascending' | 'descending' }
312
+ tableProps?: Record<string, unknown>
313
+ rowClassName?: string | ((params: { row: Record<string, unknown>; rowIndex: number }) => string)
314
+ spanMethod?: (params: { row: Record<string, unknown>; column: Record<string, unknown>; rowIndex: number; columnIndex: number }) => [number, number] | { rowspan: number; colspan: number }
315
+ treeProps?: { hasChildren?: string; children?: string }
316
+ defaultExpandAll?: boolean
317
+ expandRowKeys?: (string | number)[]
318
+ lazy?: boolean
319
+ load?: (row: Record<string, unknown>, treeNode: { level: number; expanded: boolean; loaded: boolean }, resolve: (data: Record<string, unknown>[]) => void) => void
320
+ }>(),
321
+ {
322
+ rowKey: 'id',
323
+ clearSelectOnPageChange: false,
324
+ bordered: false,
325
+ striped: true,
326
+ size: 'medium',
327
+ loading: false,
328
+ ellipsis: true,
329
+ showIndexColumn: true,
330
+ pagination: () => ({ pageSize: 10, pageSizes: [10, 20, 50, 100] }),
331
+ tableSetting: () => ({ redo: true }),
332
+ fetchSetting: () => ({
333
+ pageField: 'page',
334
+ sizeField: 'pageSize',
335
+ listField: 'list',
336
+ totalField: 'total',
337
+ }),
338
+ immediate: true,
339
+ }
340
+ )
341
+
342
+ const emit = defineEmits<{
343
+ (e: 'register', action: TableActionType): void
344
+ (e: 'fetch-success', data: { items: unknown[]; total: number }): void
345
+ (e: 'fetch-error', error: unknown): void
346
+ (e: 'selection-change', data: { keys: (string | number)[]; rows: Record<string, unknown>[] }): void
347
+ (e: 'row-click', record: Record<string, unknown>, event: Event): void
348
+ (e: 'row-dblclick', record: Record<string, unknown>, event: Event): void
349
+ (e: 'sort-change', sortInfo: { prop: string; order: string }): void
350
+ (e: 'expand-change', row: Record<string, unknown>, expanded: boolean | Record<string, unknown>[]): void
351
+ }>()
352
+
353
+ const slots = useSlots()
354
+ const tableRef = ref()
355
+ const tableWrapRef = ref()
356
+ const containerWidth = ref(0)
357
+ const loading = ref(props.loading ?? false)
358
+ const innerData = ref<Record<string, unknown>[]>([])
359
+ const rawDataSource = ref<Record<string, unknown>>({})
360
+ const innerColumns = ref<ProColumn[]>([])
361
+ const innerProps = ref<Partial<ProTableProps>>({})
362
+ const selectedRows = ref<Record<string, unknown>[]>([])
363
+ const showPaginationRef = ref<boolean | null>(null)
364
+
365
+ const pagination = ref({
366
+ page: 1,
367
+ pageSize: (props.pagination && typeof props.pagination === 'object') ? (props.pagination.pageSize ?? 10) : 10,
368
+ pageSizes: (props.pagination && typeof props.pagination === 'object') ? (props.pagination.pageSizes ?? [10, 20, 50, 100]) : [10, 20, 50, 100],
369
+ total: 0,
370
+ })
371
+
372
+ const { getSetting: getComponentSetting } = useComponentSetting()
373
+ const effectiveProps = computed(() => ({ ...getComponentSetting('ProTable'), ...props, ...innerProps.value }))
374
+ const showTitleBar = computed(() => !!effectiveProps.value.title || !!slots.tableTitle || !!slots.toolbar)
375
+ const showPagination = computed(() => {
376
+ if (showPaginationRef.value !== null) return showPaginationRef.value
377
+ return !!props.pagination && typeof props.pagination === 'object'
378
+ })
379
+
380
+ const rowKeyField = computed(() => effectiveProps.value.rowKey || 'id')
381
+
382
+ /** 选中行 key 集合(用于快速判断) */
383
+ const selectedKeysSet = computed(() => new Set(selectedRows.value.map((r) => r[rowKeyField.value] as string | number)))
384
+
385
+ /** 显示列 */
386
+ const displayColumns = computed(() =>
387
+ innerColumns.value.filter((c) => !c.hideInTable && !c.defaultHidden)
388
+ )
389
+
390
+ /** 固定列总宽度 */
391
+ const fixedColumnsWidth = computed(() => {
392
+ let w = 0
393
+ if (effectiveProps.value.rowSelection) w += Number(effectiveProps.value.rowSelection.width) || 48
394
+ if (effectiveProps.value.showIndexColumn) w += Number(effectiveProps.value.indexColumnProps?.width) || 60
395
+ if (effectiveProps.value.actionColumn) w += Number(effectiveProps.value.actionColumn?.width) || 150
396
+ return w
397
+ })
398
+
399
+ const isRatioWidth = (w: number | string | undefined) => typeof w === 'number' && w > 0
400
+ const totalRatio = computed(() => {
401
+ const cols = displayColumns.value.filter((c) => shouldShowColumn(c) && isRatioWidth(c.width))
402
+ return cols.reduce((sum, c) => sum + (typeof c.width === 'number' ? c.width : 0), 0)
403
+ })
404
+
405
+ const fixedDataColumnsWidth = computed(() => {
406
+ const cols = displayColumns.value.filter((c) => shouldShowColumn(c) && typeof c.width === 'string')
407
+ return cols.reduce((sum, c) => sum + (Number(getColumnWidth(c)) || 80), 0)
408
+ })
409
+
410
+ const parseWidthPx = (v: number | string | undefined): number | null =>
411
+ v == null ? null : typeof v === 'number' ? v : parseInt(String(v).replace(/px$/i, ''), 10) || null
412
+
413
+ const getColumnWidth = (col: ProColumn): number | string | undefined => {
414
+ const w = col.width
415
+ if (isRatioWidth(w) && totalRatio.value > 0 && containerWidth.value > 0 && typeof w === 'number') {
416
+ const available = containerWidth.value - fixedColumnsWidth.value - fixedDataColumnsWidth.value
417
+ let result = Math.floor((available * w) / totalRatio.value)
418
+ const minPx = parseWidthPx(col.minWidth) ?? 60
419
+ const maxPx = parseWidthPx(col.maxWidth)
420
+ result = Math.max(minPx, result)
421
+ if (maxPx != null) result = Math.min(maxPx, result)
422
+ return result
423
+ }
424
+ if (typeof w === 'string') {
425
+ const basePx = parseWidthPx(w) ?? 80
426
+ const minPx = parseWidthPx(col.minWidth)
427
+ const maxPx = parseWidthPx(col.maxWidth)
428
+ let result = basePx
429
+ if (minPx != null) result = Math.max(minPx, result)
430
+ if (maxPx != null) result = Math.min(maxPx, result)
431
+ return result
432
+ }
433
+ return col.width
434
+ }
435
+
436
+ const shouldShowColumn = (col: ProColumn) => {
437
+ if (col.ifShow === false) return false
438
+ if (typeof col.ifShow === 'function') return col.ifShow({ column: col })
439
+ return true
440
+ }
441
+
442
+ /** 当前页可选行(未禁用) */
443
+ const selectableRows = computed(() => {
444
+ const getDisabled = effectiveProps.value.rowSelection?.getCheckboxProps
445
+ if (!getDisabled) return innerData.value
446
+ return innerData.value.filter((row) => !getDisabled(row)?.disabled)
447
+ })
448
+
449
+ const hasSelectableRows = computed(() => selectableRows.value.length > 0)
450
+
451
+ /** 当前页是否全选 */
452
+ const isAllCurrentPageSelected = computed(() => {
453
+ if (selectableRows.value.length === 0) return false
454
+ return selectableRows.value.every((row) => selectedKeysSet.value.has(row[rowKeyField.value] as string | number))
455
+ })
456
+
457
+ /** 半选状态 */
458
+ const isIndeterminate = computed(() => {
459
+ const selectedCount = selectableRows.value.filter((row) => selectedKeysSet.value.has(row[rowKeyField.value] as string | number)).length
460
+ return selectedCount > 0 && selectedCount < selectableRows.value.length
461
+ })
462
+
463
+ const isRowSelected = (row: Record<string, unknown>) =>
464
+ selectedKeysSet.value.has(row[rowKeyField.value] as string | number)
465
+
466
+ const getCheckboxDisabled = (row: Record<string, unknown>) =>
467
+ effectiveProps.value.rowSelection?.getCheckboxProps?.(row)?.disabled ?? false
468
+
469
+ const getRadioDisabled = (row: Record<string, unknown>) =>
470
+ effectiveProps.value.rowSelection?.getRadioProps?.(row)?.disabled ?? false
471
+
472
+ const emitSelectionChange = () => {
473
+ const keys = selectedRows.value.map((r) => r[rowKeyField.value] as string | number)
474
+ emit('selection-change', { keys, rows: selectedRows.value })
475
+ }
476
+
477
+ const handleCheckboxChange = (row: Record<string, unknown>, checked: boolean) => {
478
+ const key = row[rowKeyField.value] as string | number
479
+ if (checked) {
480
+ selectedRows.value = [...selectedRows.value.filter((r) => r[rowKeyField.value] !== key), row]
481
+ } else {
482
+ selectedRows.value = selectedRows.value.filter((r) => r[rowKeyField.value] !== key)
483
+ }
484
+ emitSelectionChange()
485
+ }
486
+
487
+ const handleRadioSelect = (row: Record<string, unknown>) => {
488
+ selectedRows.value = [row]
489
+ emitSelectionChange()
490
+ }
491
+
492
+ const handleSelectAll = (checked: boolean) => {
493
+ if (checked) {
494
+ const keySet = new Set(selectedRows.value.map((r) => r[rowKeyField.value]))
495
+ const toAdd = selectableRows.value.filter((row) => !keySet.has(row[rowKeyField.value]))
496
+ selectedRows.value = [...selectedRows.value, ...toAdd]
497
+ } else {
498
+ const currentPageKeys = new Set(innerData.value.map((r) => r[rowKeyField.value]))
499
+ selectedRows.value = selectedRows.value.filter((r) => !currentPageKeys.has(r[rowKeyField.value]))
500
+ }
501
+ emitSelectionChange()
502
+ }
503
+
504
+ const fetchData = async (opt?: FetchParams) => {
505
+ if (!props.api) {
506
+ if (props.dataSource) return
507
+ innerData.value = []
508
+ return
509
+ }
510
+ loading.value = true
511
+ try {
512
+ const fs = effectiveProps.value.fetchSetting ?? {}
513
+ const pageField = fs.pageField ?? 'page'
514
+ const sizeField = fs.sizeField ?? 'pageSize'
515
+ const listField = fs.listField ?? 'list'
516
+ const totalField = fs.totalField ?? 'total'
517
+ const params: Record<string, unknown> = {
518
+ [pageField]: opt?.page ?? pagination.value.page,
519
+ [sizeField]: opt?.pageSize ?? pagination.value.pageSize,
520
+ ...props.searchInfo,
521
+ ...opt?.searchInfo,
522
+ }
523
+ if (opt?.page != null) pagination.value.page = opt.page
524
+ if (opt?.pageSize != null) pagination.value.pageSize = opt.pageSize
525
+ const processedParams = props.beforeFetch ? props.beforeFetch(params) : params
526
+ const res = await props.api!(processedParams)
527
+ rawDataSource.value = (res || {}) as Record<string, unknown>
528
+ const data = (props.afterFetch ? props.afterFetch(res) : res) as Record<string, unknown>
529
+ const list = (data[listField] ?? data.items ?? data.list ?? []) as unknown[]
530
+ const total = (data[totalField] ?? 0) as number
531
+ innerData.value = list as Record<string, unknown>[]
532
+ pagination.value.total = total
533
+ emit('fetch-success', { items: list, total })
534
+ } catch (e) {
535
+ emit('fetch-error', e)
536
+ } finally {
537
+ loading.value = false
538
+ }
539
+ }
540
+
541
+ const handleReload = () => fetchData(undefined)
542
+ const handleSizeChange = (size: number) => {
543
+ if (effectiveProps.value.clearSelectOnPageChange) {
544
+ selectedRows.value = []
545
+ }
546
+ pagination.value.pageSize = size
547
+ pagination.value.page = 1
548
+ fetchData(undefined)
549
+ }
550
+ const handleCurrentChange = (page: number) => {
551
+ if (effectiveProps.value.clearSelectOnPageChange) {
552
+ selectedRows.value = []
553
+ }
554
+ pagination.value.page = page
555
+ fetchData(undefined)
556
+ }
557
+ const handleRowClick = (row: Record<string, unknown>, _column: unknown, event: Event) => emit('row-click', row, event)
558
+ const handleRowDblclick = (row: Record<string, unknown>, _column: unknown, event: Event) => emit('row-dblclick', row, event)
559
+ const handleSortChange = ({ prop, order }: { prop: string; order: string }) => emit('sort-change', { prop, order })
560
+ const handleExpandChange = (row: Record<string, unknown>, expanded: boolean | Record<string, unknown>[]) => emit('expand-change', row, expanded)
561
+
562
+ const findRowIndex = (key: string | number) =>
563
+ innerData.value.findIndex((r) => r[rowKeyField.value] === key)
564
+
565
+ const tableAction: TableActionType = {
566
+ setProps: (p) => { innerProps.value = { ...innerProps.value, ...p } },
567
+ reload: (opt) => fetchData(opt),
568
+ redoHeight: () => { tableRef.value?.doLayout?.() },
569
+ setLoading: (v) => { loading.value = v },
570
+ getDataSource: () => innerData.value,
571
+ getRawDataSource: () => rawDataSource.value,
572
+ setTableData: (data) => { innerData.value = data ?? [] },
573
+ getColumns: () => innerColumns.value,
574
+ setColumns: (cols) => {
575
+ if (Array.isArray(cols) && cols.length > 0 && typeof cols[0] === 'string') {
576
+ const keyList = cols as string[]
577
+ const fromProps = (props.columns ?? []).filter((c) => keyList.includes((c.key ?? c.dataIndex) as string))
578
+ const ordered = keyList.map((k) => fromProps.find((c) => (c.key ?? c.dataIndex) === k)).filter(Boolean) as ProColumn[]
579
+ if (ordered.length) innerColumns.value = ordered
580
+ } else {
581
+ innerColumns.value = (cols as ProColumn[]) ?? []
582
+ }
583
+ },
584
+ setPagination: (info) => {
585
+ if (info?.page) pagination.value.page = info.page
586
+ if (info?.pageSize) pagination.value.pageSize = info.pageSize
587
+ if (info?.total !== undefined) pagination.value.total = info.total
588
+ },
589
+ getSelectRowKeys: () => selectedRows.value.map((r) => r[rowKeyField.value] as string | number),
590
+ getSelectRows: () => selectedRows.value,
591
+ clearSelectedRowKeys: () => { selectedRows.value = []; emitSelectionChange() },
592
+ setSelectedRowKeys: (keys) => {
593
+ const keySet = new Set(keys)
594
+ const rows = innerData.value.filter((r) => keySet.has(r[rowKeyField.value] as string | number))
595
+ keys.forEach((k) => {
596
+ if (!rows.some((r) => r[rowKeyField.value] === k)) {
597
+ rows.push({ [rowKeyField.value]: k } as Record<string, unknown>)
598
+ }
599
+ })
600
+ selectedRows.value = rows
601
+ emitSelectionChange()
602
+ },
603
+ deleteSelectRowByKey: (key) => {
604
+ selectedRows.value = selectedRows.value.filter((r) => r[rowKeyField.value] !== key)
605
+ emitSelectionChange()
606
+ },
607
+ updateTableData: (index, key, value) => {
608
+ if (index < 0 || index >= innerData.value.length) return
609
+ innerData.value = [...innerData.value]
610
+ innerData.value[index] = { ...innerData.value[index], [key]: value }
611
+ },
612
+ updateTableDataRecord: (rowKey, record) => {
613
+ const idx = findRowIndex(rowKey)
614
+ if (idx < 0) return
615
+ innerData.value = [...innerData.value]
616
+ innerData.value[idx] = { ...innerData.value[idx], ...record }
617
+ return innerData.value[idx]
618
+ },
619
+ deleteTableDataRecord: (rowKey) => {
620
+ const keys = Array.isArray(rowKey) ? rowKey : [rowKey]
621
+ const keySet = new Set(keys)
622
+ innerData.value = innerData.value.filter((r) => !keySet.has(r[rowKeyField.value] as string | number))
623
+ },
624
+ insertTableDataRecord: (record, index) => {
625
+ const arr = [...innerData.value]
626
+ if (index == null || index >= arr.length) arr.push(record)
627
+ else arr.splice(index, 0, record)
628
+ innerData.value = arr
629
+ return record
630
+ },
631
+ getPaginationRef: () =>
632
+ showPagination.value
633
+ ? { page: pagination.value.page, pageSize: Number(pagination.value.pageSize) || 10, total: pagination.value.total }
634
+ : false,
635
+ getShowPagination: () => showPagination.value,
636
+ setShowPagination: (show) => { showPaginationRef.value = show },
637
+ getRowSelection: () => effectiveProps.value.rowSelection,
638
+ expandAll: () => {
639
+ const childrenKey = effectiveProps.value.treeProps?.children ?? 'children'
640
+ const flattenRows = (rows: Record<string, unknown>[]): Record<string, unknown>[] => {
641
+ const result: Record<string, unknown>[] = []
642
+ rows.forEach((row) => {
643
+ result.push(row)
644
+ const children = row[childrenKey] as Record<string, unknown>[] | undefined
645
+ if (Array.isArray(children) && children.length > 0) {
646
+ result.push(...flattenRows(children))
647
+ }
648
+ })
649
+ return result
650
+ }
651
+ const allRows = flattenRows(innerData.value)
652
+ allRows.forEach((row) => tableRef.value?.toggleRowExpansion?.(row, true))
653
+ },
654
+ collapseAll: () => {
655
+ const childrenKey = effectiveProps.value.treeProps?.children ?? 'children'
656
+ const flattenRows = (rows: Record<string, unknown>[]): Record<string, unknown>[] => {
657
+ const result: Record<string, unknown>[] = []
658
+ rows.forEach((row) => {
659
+ result.push(row)
660
+ const children = row[childrenKey] as Record<string, unknown>[] | undefined
661
+ if (Array.isArray(children) && children.length > 0) {
662
+ result.push(...flattenRows(children))
663
+ }
664
+ })
665
+ return result
666
+ }
667
+ const allRows = flattenRows(innerData.value)
668
+ allRows.forEach((row) => tableRef.value?.toggleRowExpansion?.(row, false))
669
+ },
670
+ }
671
+
672
+ defineExpose(tableAction)
673
+
674
+ const syncColumns = () => { innerColumns.value = [...(props.columns ?? [])] }
675
+
676
+ const loadData = () => {
677
+ if (props.api && effectiveProps.value.immediate !== false) {
678
+ fetchData(undefined)
679
+ } else if (props.dataSource) {
680
+ innerData.value = [...props.dataSource]
681
+ if (!props.api && props.pagination !== false) {
682
+ pagination.value.total = props.dataSource.length
683
+ }
684
+ }
685
+ }
686
+
687
+ const updateContainerWidth = () => {
688
+ if (tableWrapRef.value) containerWidth.value = tableWrapRef.value.offsetWidth || 0
689
+ }
690
+
691
+ let resizeObserver: ResizeObserver | null = null
692
+ let observedEl: Element | null = null
693
+
694
+ onMounted(() => {
695
+ syncColumns()
696
+ emit('register', tableAction)
697
+ loadData()
698
+ if (typeof window !== 'undefined') {
699
+ window.addEventListener('resize', updateContainerWidth)
700
+ resizeObserver = new ResizeObserver(updateContainerWidth)
701
+ nextTick(() => {
702
+ updateContainerWidth()
703
+ observedEl = tableWrapRef.value
704
+ if (observedEl) resizeObserver?.observe(observedEl)
705
+ })
706
+ }
707
+ })
708
+
709
+ onUnmounted(() => {
710
+ if (typeof window !== 'undefined') {
711
+ window.removeEventListener('resize', updateContainerWidth)
712
+ if (resizeObserver && observedEl) {
713
+ resizeObserver.unobserve(observedEl)
714
+ observedEl = null
715
+ }
716
+ }
717
+ })
718
+
719
+ watch(() => props.columns, syncColumns, { deep: true })
720
+ watch(() => props.dataSource, () => {
721
+ if (!props.api && props.dataSource) innerData.value = [...props.dataSource]
722
+ }, { deep: true })
723
+ watch(() => props.loading, (v) => { loading.value = v ?? false })
724
+ </script>
725
+
726
+ <style scoped>
727
+ .ecp-pro-table {
728
+ padding: 16px;
729
+ background: #fff;
730
+ width: 100%;
731
+ box-sizing: border-box;
732
+ }
733
+ .ecp-pro-table :deep(.el-table) {
734
+ width: 100% !important;
735
+ }
736
+ .ecp-pro-table__header {
737
+ display: flex;
738
+ justify-content: space-between;
739
+ align-items: center;
740
+ margin-bottom: 16px;
741
+ }
742
+ .ecp-pro-table__title-wrapper {
743
+ display: flex;
744
+ align-items: center;
745
+ gap: 4px;
746
+ }
747
+ .ecp-pro-table__title {
748
+ font-size: 16px;
749
+ font-weight: 600;
750
+ }
751
+ .ecp-pro-table__help {
752
+ color: #909399;
753
+ cursor: help;
754
+ }
755
+ .ecp-pro-table__toolbar {
756
+ display: flex;
757
+ align-items: center;
758
+ gap: 8px;
759
+ }
760
+ .ecp-pro-table__body {
761
+ width: 100%;
762
+ }
763
+ .ecp-pro-table__pagination {
764
+ margin-top: 16px;
765
+ display: flex;
766
+ justify-content: flex-end;
767
+ }
768
+ .ecp-pro-table__col-help {
769
+ margin-left: 4px;
770
+ color: #909399;
771
+ cursor: help;
772
+ }
773
+ </style>