@antsoo-lib/core 0.0.0 → 1.0.0

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 (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/core.css +1 -0
  3. package/dist/index.cjs +61 -0
  4. package/dist/index.js +50858 -0
  5. package/dist/types/core/index.d.ts +6 -0
  6. package/dist/types/core/src/BaseTable/helpers.d.ts +9 -0
  7. package/dist/types/core/src/BaseTable/index.d.ts +190 -0
  8. package/dist/types/core/src/BaseTable/registry.d.ts +9 -0
  9. package/dist/types/core/src/BaseTable/renderers/AreaCascader.d.ts +5 -0
  10. package/dist/types/core/src/BaseTable/renderers/AutoComplete.d.ts +4 -0
  11. package/dist/types/core/src/BaseTable/renderers/Button.d.ts +4 -0
  12. package/dist/types/core/src/BaseTable/renderers/Cascader.d.ts +5 -0
  13. package/dist/types/core/src/BaseTable/renderers/Checkbox.d.ts +4 -0
  14. package/dist/types/core/src/BaseTable/renderers/CheckboxGroup.d.ts +4 -0
  15. package/dist/types/core/src/BaseTable/renderers/DatePicker.d.ts +4 -0
  16. package/dist/types/core/src/BaseTable/renderers/Input.d.ts +4 -0
  17. package/dist/types/core/src/BaseTable/renderers/InputGroup.d.ts +5 -0
  18. package/dist/types/core/src/BaseTable/renderers/InputNumber.d.ts +4 -0
  19. package/dist/types/core/src/BaseTable/renderers/InputPassword.d.ts +4 -0
  20. package/dist/types/core/src/BaseTable/renderers/RadioGroup.d.ts +4 -0
  21. package/dist/types/core/src/BaseTable/renderers/Select.d.ts +4 -0
  22. package/dist/types/core/src/BaseTable/renderers/SselectPage.d.ts +4 -0
  23. package/dist/types/core/src/BaseTable/renderers/Switch.d.ts +4 -0
  24. package/dist/types/core/src/BaseTable/renderers/TreeSelect.d.ts +4 -0
  25. package/dist/types/core/src/BaseTable/renderers/Upload.d.ts +4 -0
  26. package/dist/types/core/src/BaseTable/renderers/index.d.ts +24 -0
  27. package/dist/types/core/src/BaseTable/state.d.ts +19 -0
  28. package/dist/types/core/src/BaseTable/types.d.ts +391 -0
  29. package/dist/types/core/src/BaseTable/utils.d.ts +8 -0
  30. package/dist/types/core/src/index.d.ts +2 -0
  31. package/index.css +2 -0
  32. package/index.ts +21 -0
  33. package/package.json +39 -3
  34. package/src/BaseTable/helpers.tsx +91 -0
  35. package/src/BaseTable/index.vue +881 -0
  36. package/src/BaseTable/registry.ts +20 -0
  37. package/src/BaseTable/renderers/AreaCascader.tsx +64 -0
  38. package/src/BaseTable/renderers/AutoComplete.tsx +101 -0
  39. package/src/BaseTable/renderers/Button.tsx +62 -0
  40. package/src/BaseTable/renderers/Cascader.tsx +45 -0
  41. package/src/BaseTable/renderers/Checkbox.tsx +65 -0
  42. package/src/BaseTable/renderers/CheckboxGroup.tsx +57 -0
  43. package/src/BaseTable/renderers/DatePicker.tsx +83 -0
  44. package/src/BaseTable/renderers/Input.tsx +140 -0
  45. package/src/BaseTable/renderers/InputGroup.tsx +115 -0
  46. package/src/BaseTable/renderers/InputNumber.tsx +205 -0
  47. package/src/BaseTable/renderers/InputPassword.tsx +81 -0
  48. package/src/BaseTable/renderers/RadioGroup.tsx +63 -0
  49. package/src/BaseTable/renderers/Select.tsx +96 -0
  50. package/src/BaseTable/renderers/SselectPage.tsx +107 -0
  51. package/src/BaseTable/renderers/Switch.tsx +60 -0
  52. package/src/BaseTable/renderers/TreeSelect.tsx +81 -0
  53. package/src/BaseTable/renderers/Upload.tsx +92 -0
  54. package/src/BaseTable/renderers/index.ts +67 -0
  55. package/src/BaseTable/state.ts +37 -0
  56. package/src/BaseTable/types.ts +507 -0
  57. package/src/BaseTable/utils.tsx +144 -0
  58. package/src/index.ts +3 -0
  59. package/vite.config.ts +48 -0
@@ -0,0 +1,881 @@
1
+ <script lang="ts" setup>
2
+ import { Tooltip } from '@antsoo-lib/components'
3
+ import { QuestionCircleOutlined } from '@antsoo-lib/icons'
4
+ import type { AnyObject } from '@antsoo-lib/shared'
5
+ import { PRECISION } from '@antsoo-lib/shared'
6
+ import { env, generateUniqueId, isNaN } from '@antsoo-lib/utils'
7
+ import { cloneDeep } from 'lodash-es'
8
+ import type { VxeComponentSizeType } from 'vxe-pc-ui'
9
+ import type { VxeGridPropTypes, VxeGridProps } from 'vxe-table'
10
+
11
+ import { computed, ref } from 'vue'
12
+
13
+ import { renderToolbar, useToolbarState } from './utils'
14
+ import type { ToolbarConfig } from './utils'
15
+
16
+ // Props默认值
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ checkbox: true,
19
+ radio: false,
20
+ drag: false,
21
+ height: '100%',
22
+ loading: false,
23
+ pager: true,
24
+ seq: false,
25
+ showFooter: true,
26
+ total: 0,
27
+ tree: false,
28
+ lToolBarCount: 4,
29
+ rToolBarCount: 4,
30
+ permissions: () => [],
31
+ getCommonCodeOptions: undefined,
32
+ })
33
+ // 抛出Emit方法(类型化)
34
+ const emit = defineEmits<{
35
+ (e: 'pageChange', page: number, size: number): void
36
+ (e: 'pageShowSizeChange', current: number, size: number): void
37
+ (e: 'selectAllChangeEvent', checked: boolean, records: any[]): void
38
+ (
39
+ e: 'selectChangeEvent',
40
+ params: { checked: boolean; row: any; rowIndex: number; records: any[] },
41
+ ): void
42
+ (e: 'radioChangeEvent', params: { row: any; rowIndex: number; records: any }): void
43
+ (e: 'buttonClick', params: { button: Button; row: any; grid: any }): void
44
+ }>()
45
+ const { isDev, isTest } = env()
46
+ const showHelpText = isDev || isTest
47
+
48
+ // Interface
49
+ interface Button {
50
+ name?: (row: any) => string
51
+ permission?: string
52
+ event?: (row: any, controls: any, grid: any) => void
53
+ disabled?: (row: any, grid: any) => boolean
54
+ beforeCreate?: (row: any) => boolean
55
+ props?: AnyObject
56
+ }
57
+
58
+ interface Column {
59
+ type?: 'checkbox' | 'radio' | 'seq' | 'html' | 'expand' | null
60
+ buttons?: Button[]
61
+ [key: string]: any
62
+ }
63
+
64
+ interface Props {
65
+ // 增加一个 scope 属性,用于区分同构表格
66
+ scope?: string
67
+ // 是否开启选择器
68
+ checkbox?: boolean
69
+ // 复选框配置项
70
+ checkboxConfig?: AnyObject
71
+ // 是否开启单选框
72
+ radio?: boolean
73
+ // 单选框配置项
74
+ radioConfig?: AnyObject
75
+ // 列配置项
76
+ columns?: Column[]
77
+ // 个性化信息配置项
78
+ customConfig?: AnyObject
79
+ // 列配置信息
80
+ columnConfig?: AnyObject
81
+ // 渲染数据
82
+ data?: any[]
83
+ // 是否开启拖拽
84
+ drag?: boolean
85
+ // 表尾数据,数组-支持多行
86
+ footerData?: any[]
87
+ // 表格高度
88
+ height?: string
89
+ // 左侧工具栏
90
+ lToolBar?: ToolbarConfig
91
+ // 是否开启加载
92
+ loading?: boolean
93
+ // 个性化定制额外的列配置项,注意:若自定义配置项与默认配置项冲突,以自定义配置项为准
94
+ options?: Partial<VxeGridProps>
95
+ // 是否开启分页
96
+ pager?: boolean
97
+ // 右侧工具栏
98
+ rToolBar?: ToolbarConfig
99
+ // 行配置信息
100
+ rowConfig?: AnyObject
101
+ // 行拖拽配置项
102
+ rowDragConfig?: AnyObject
103
+ // 是否开启序号
104
+ seq?: boolean
105
+ // 是否展示表尾数据
106
+ showFooter?: boolean
107
+ // 工具栏配置
108
+ toolbarConfig?: AnyObject
109
+ // tooltip 配置项
110
+ tooltipConfig?: AnyObject
111
+ // 页码:当前加载总数据条数
112
+ total?: number
113
+ // 是否开启树状结构
114
+ tree?: boolean
115
+ // 树状结构配置项
116
+ treeConfig?: AnyObject
117
+ // 左侧工具栏显示个数
118
+ lToolBarCount?: number | (() => number)
119
+ // 右侧工具栏显示个数
120
+ rToolBarCount?: number | (() => number)
121
+ // 分页当前页(受控)
122
+ currentPage?: number
123
+ // 分页每页条数(受控)
124
+ pageSize?: number
125
+
126
+ // Decoupled: Permissions array
127
+ permissions?: string[]
128
+ // Decoupled: Function to get common code options
129
+ getCommonCodeOptions?: (type: string) => Array<{ code: string; name: string }>
130
+ }
131
+
132
+ // table 实例
133
+ const basetable = ref()
134
+ // 分页器实例
135
+ const paginationRef = ref()
136
+ // 当前每页数量,默认10条/页
137
+ const currentPageSize = ref(10)
138
+
139
+ // 受控分页:优先使用外部传入,否则回退默认
140
+ const paginationCurrent = computed(() => props.currentPage ?? 1)
141
+ const paginationPageSize = computed(() => props.pageSize ?? currentPageSize.value)
142
+
143
+ // 格式化数字
144
+ const formatNumber = (value: any, precision: number) => {
145
+ if (value === null || value === undefined || value === '') return ''
146
+ const num = Number(value)
147
+ if (isNaN(num)) return value
148
+ return num.toFixed(precision)
149
+ }
150
+
151
+ // 顶部左右侧工具栏状态实例
152
+ const lToolBarState = useToolbarState()
153
+ const rToolBarState = useToolbarState()
154
+
155
+ // 抽出操作栏,根据是否存在buttons来判断
156
+ const actionColumn = computed(() => {
157
+ return props.columns?.find((column) => column.buttons)
158
+ })
159
+
160
+ // 预过滤拥有权限的按钮,减少每行的计算量
161
+ const permittedButtons = computed(() => {
162
+ const all = actionColumn.value?.buttons || []
163
+ // Decoupled: Use props.permissions
164
+ return all.filter((b) => !b.permission || props.permissions.includes(b.permission))
165
+ })
166
+
167
+ // 左侧工具栏
168
+ const lToolBarItems = computed(() => {
169
+ if (!props.lToolBar) return []
170
+ // Decoupled: Pass props.permissions
171
+ return renderToolbar(props.lToolBar, lToolBarState, props.lToolBarCount, props.permissions)
172
+ })
173
+
174
+ // 右侧工具栏
175
+ const rToolBarItems = computed(() => {
176
+ if (!props.rToolBar) return []
177
+ // Decoupled: Pass props.permissions
178
+ return renderToolbar(props.rToolBar, rToolBarState, props.rToolBarCount, props.permissions)
179
+ })
180
+
181
+ // 树状配置
182
+ const treeConfig = computed(() => {
183
+ return props.tree
184
+ ? {
185
+ treeConfig: {
186
+ rowField: 'id',
187
+ parentField: 'pid',
188
+ transform: true,
189
+ expandAll: true,
190
+ reserve: true,
191
+ ...props.treeConfig,
192
+ },
193
+ }
194
+ : {}
195
+ })
196
+
197
+ const tableId = computed(() => {
198
+ if (props.options?.id) {
199
+ return props.options.id
200
+ }
201
+ // 获取当前路径
202
+ const path = location.pathname
203
+ // 结合唯一标识(若有),生成唯一码
204
+ const rawId = `${path}::${props.scope || 'default'}`
205
+ return generateUniqueId(rawId)
206
+ })
207
+
208
+ const gridColumns = computed(() => handleGridColumns())
209
+ const options = computed(() => {
210
+ const gridColumnsVal = gridColumns.value
211
+ return {
212
+ id: tableId.value,
213
+ // 表格基础尺寸
214
+ size: 'mini' as VxeComponentSizeType,
215
+ // 高度自适应
216
+ height: props.height,
217
+ // 给表头的单元格附加 className
218
+ headerCellClassName: 'base-columns-cell',
219
+ // 单元格class
220
+ cellClassName: 'base-columns-cell',
221
+ // 斑马纹
222
+ stripe: true,
223
+ // 边框
224
+ border: true,
225
+ // 内容过长时显示为省略号
226
+ showOverflow: true,
227
+ // 自动监听父元素的变化去重新调用 recalculate 方法计算样式
228
+ autoResize: true,
229
+ // 列配置
230
+ columns: gridColumnsVal as VxeGridPropTypes.Column[],
231
+ // 表格数据
232
+ data: props.data,
233
+ // 表格是否显示加载中
234
+ loading: props.loading,
235
+ // 是否显示表尾
236
+ showFooter: props.showFooter,
237
+ // 列配置信息
238
+ columnConfig: {
239
+ // 每一列启用列宽调整
240
+ resizable: true,
241
+ useKey: true,
242
+ ...props.columnConfig,
243
+ },
244
+ // 个性化信息配置项
245
+ customConfig: {
246
+ // 操作模式
247
+ mode: 'modal' as const,
248
+ storage: true,
249
+ ...props.customConfig,
250
+ },
251
+ // 复选框配置项
252
+ checkboxConfig: {
253
+ // 整行触发选择
254
+ trigger: 'row' as const,
255
+ ...props.checkboxConfig,
256
+ },
257
+ // 单选框配置项
258
+ radioConfig: {
259
+ // 整行触发选择
260
+ trigger: 'row' as const,
261
+ ...props.radioConfig,
262
+ },
263
+ // 行配置信息
264
+ rowConfig: {
265
+ keyField: 'id',
266
+ // 当鼠标移到行时,是否要高亮当前行
267
+ isHover: true,
268
+ // 启用列拖拽调整顺序
269
+ drag: props.drag,
270
+ useKey: true,
271
+ ...props.rowConfig,
272
+ },
273
+ // 行拖拽配置项
274
+ rowDragConfig: {
275
+ // 只对 tree-config 启用有效,是否允许同级行拖拽,用于树结构,启用后允许同层级之间进行拖拽
276
+ isPeerDrag: true,
277
+ ...props.rowDragConfig,
278
+ },
279
+ // 工具栏配置
280
+ toolbarConfig: {
281
+ slots: {
282
+ buttons: 'lToolBar',
283
+ tools: 'rToolBar',
284
+ },
285
+ // 自定义容器类名
286
+ className: 'base-table-toolbar',
287
+ // 最大化表格
288
+ zoom: true,
289
+ // 表格设置
290
+ custom: true,
291
+ ...props.toolbarConfig,
292
+ },
293
+ // tooltip 配置项
294
+ tooltipConfig: {
295
+ maxWidth: 200,
296
+ ...props.tooltipConfig,
297
+ },
298
+ // 树形结构配置项
299
+ ...treeConfig.value,
300
+ // 其他个性化定制
301
+ ...props.options,
302
+ }
303
+ })
304
+
305
+ /**
306
+ * 控制按钮是否展示
307
+ * @param {object} row - 当前按钮数据
308
+ */
309
+ function getVisibleButtons(row: any) {
310
+ if (!permittedButtons.value.length) return []
311
+ return permittedButtons.value.filter((button) => !button.beforeCreate || button.beforeCreate(row))
312
+ }
313
+ const rowButtonLoading = ref<Record<string, boolean>>({})
314
+ function getButtonKey(row: any, button: any) {
315
+ const n = typeof button.name === 'function' ? button.name(row) : button.name
316
+ const id = row?.id ?? ''
317
+ return `${id}::${String(n)}`
318
+ }
319
+ function isButtonLoading(row: any, button: any) {
320
+ const k = getButtonKey(row, button)
321
+ return !!rowButtonLoading.value[k]
322
+ }
323
+ function handleRowButtonClick(button: any, row: any) {
324
+ const k = getButtonKey(row, button)
325
+ const controls = {
326
+ setLoading: (v: boolean) => {
327
+ rowButtonLoading.value[k] = v
328
+ },
329
+ getLoading: () => !!rowButtonLoading.value[k],
330
+ }
331
+ const fn = button.event
332
+ if (typeof fn === 'function') fn(row, controls, basetable.value)
333
+ emit('buttonClick', { button, row, grid: basetable.value })
334
+ }
335
+
336
+ /**
337
+ * 跳转指定页码
338
+ * @param {number} page - 页码
339
+ */
340
+ function goToPage(page: number) {
341
+ if (paginationRef.value && page >= 1) {
342
+ onChange(page, currentPageSize.value)
343
+ }
344
+ }
345
+
346
+ /**
347
+ * 分页改变时触发
348
+ * @param {number} page - 页码
349
+ * @param {number} size - 每页条数
350
+ */
351
+ function onChange(page: number, size: number) {
352
+ currentPageSize.value = size
353
+ emit('pageChange', page, size)
354
+ }
355
+
356
+ /**
357
+ * 分页每页条数改变时触发
358
+ * @param {number} current - 页码
359
+ * @param {number} size - 每页条数
360
+ */
361
+ function onShowSizeChange(current: number, size: number) {
362
+ currentPageSize.value = size
363
+ emit('pageShowSizeChange', current, size)
364
+ }
365
+
366
+ /**
367
+ * 用于 type=checkbox,触发全选
368
+ * @param {object} params - 事件参数对象
369
+ * @param {boolean} params.checked - 复选框是否被选中
370
+ */
371
+ function selectAllChangeEvent({ checked }: { checked: boolean }) {
372
+ const $grid = basetable.value
373
+ if ($grid) {
374
+ const records = $grid.getCheckboxRecords()
375
+ emit('selectAllChangeEvent', checked, records)
376
+ }
377
+ }
378
+
379
+ /**
380
+ * 用于 type=checkbox,触发行选择
381
+ * @param {object} params - 事件参数对象
382
+ * @param {boolean} params.checked - 复选框是否被选中
383
+ * @param {object} params.row - 当前选中的行数据
384
+ * @param {number} params.rowIndex - 当前选中的行下标
385
+ */
386
+ function selectChangeEvent({
387
+ checked,
388
+ row,
389
+ rowIndex,
390
+ }: {
391
+ checked: boolean
392
+ row: any
393
+ rowIndex: number
394
+ }) {
395
+ const $grid = basetable.value
396
+ if ($grid) {
397
+ const records = $grid.getCheckboxRecords()
398
+ emit('selectChangeEvent', {
399
+ checked,
400
+ row,
401
+ rowIndex,
402
+ records,
403
+ })
404
+ }
405
+ }
406
+
407
+ /**
408
+ * 用于 type=radio,触发行选择
409
+ * @param {object} params - 事件参数对象
410
+ * @param {boolean} params.checked - 单选框是否被选中
411
+ * @param {object} params.row - 当前选中的行数据
412
+ * @param {number} params.rowIndex - 当前选中的行下标
413
+ */
414
+ function radioChangeEvent({ row, rowIndex }: { row: any; rowIndex: number }) {
415
+ const $grid = basetable.value
416
+ if ($grid) {
417
+ const records = $grid.getRadioRecord()
418
+ emit('radioChangeEvent', {
419
+ row,
420
+ rowIndex,
421
+ records,
422
+ })
423
+ }
424
+ }
425
+
426
+ // 处理表格列配置
427
+ function handleGridColumns() {
428
+ // 找到第一个没有type属性的列索引
429
+ const firstNonTypeIndex = props.columns?.findIndex((c) => !c.type) ?? -1
430
+ // 拷贝列配置
431
+ const columns = cloneDeep(props.columns) || []
432
+ // 是否支持序号列,默认不支持
433
+ if (props.seq) {
434
+ columns.unshift({
435
+ type: 'seq',
436
+ field: 'BaseTableSeq',
437
+ width: 70,
438
+ })
439
+ }
440
+ // 是否支持复选框,默认支持。给予field字段是为了给表尾数据的【总计】文案做关联匹配。
441
+ if (props.checkbox) {
442
+ columns.unshift({
443
+ type: 'checkbox',
444
+ field: 'BaseTablePlaceholder',
445
+ })
446
+ }
447
+ // 是否支持单选框,默认不支持。给予field字段是为了给表尾数据的【总计】文案做关联匹配。
448
+ if (props.radio) {
449
+ columns.unshift({
450
+ type: 'radio',
451
+ field: 'BaseTablePlaceholder',
452
+ })
453
+ }
454
+ // 递归处理列配置
455
+ const processColumn = (column: Column): any => {
456
+ const { buttons, ...col } = column
457
+ // 选择器居中时,若启用了溢出选项,会引起3px的误差
458
+ if (col.type === 'checkbox' || col.type === 'radio') {
459
+ return {
460
+ ...col,
461
+ width: 'auto',
462
+ align: 'center',
463
+ fixed: 'left',
464
+ showOverflow: false,
465
+ }
466
+ }
467
+ // 处理按钮插槽
468
+ if (buttons) {
469
+ return {
470
+ ...col,
471
+ field: 'BaseTableEditColumns',
472
+ slots: { default: 'buttons' },
473
+ }
474
+ }
475
+ // 处理 helpText
476
+ if (showHelpText || (col.helpText && col.helpText !== false)) {
477
+ if (col.type !== 'seq') {
478
+ col.slots = { ...col.slots, header: col.slots?.header ?? 'headerHelp' }
479
+ col.params = { ...col.params, helpText: col.helpText || col.field }
480
+ }
481
+ }
482
+
483
+ // 为第一个没有type属性的列添加树节点和拖拽排序功能
484
+ const baseColumn: any = {
485
+ width: 'auto',
486
+ minWidth: 80,
487
+ footerAlign: 'right',
488
+ footerClassName: 'base-table-footer-cell',
489
+ ...col,
490
+ }
491
+
492
+ // 处理 commonCode 格式化
493
+ if (col.commonCode) {
494
+ const originalFormatter = col.formatter
495
+ // 缓存 getCommonCodeOptions 的结果,避免每次 formatter 执行都重新获取
496
+ let cachedCodes: Array<{ code: string; name: string }> | null = null
497
+ const commonCodeType = col.commonCode
498
+
499
+ baseColumn.formatter = (value: any) => {
500
+ // 注意:不能用 `||`,否则当 cellValue 为 null 时会回退到整个参数对象,导致出现 [object Object]
501
+ const hasCellValue = value && Object.prototype.hasOwnProperty.call(value, 'cellValue')
502
+ const v = hasCellValue ? value.cellValue : value
503
+
504
+ // 优化:直接在 formatter 内部处理 commonCode 逻辑,避免频繁调用 getCommonCodeName
505
+ let name = v
506
+ if (v !== null && v !== undefined && commonCodeType && props.getCommonCodeOptions) {
507
+ if (!cachedCodes) {
508
+ cachedCodes = props.getCommonCodeOptions(commonCodeType)
509
+ }
510
+ if (cachedCodes) {
511
+ const codeItem = cachedCodes.find((item) => item.code === String(v))
512
+ if (codeItem) {
513
+ name = codeItem.name
514
+ }
515
+ }
516
+ }
517
+ name = name ?? ''
518
+
519
+ if (originalFormatter) {
520
+ try {
521
+ return originalFormatter(name)
522
+ } catch (e) {
523
+ console.warn('commonCode formatter 执行异常', e)
524
+ }
525
+ }
526
+ return name
527
+ }
528
+ }
529
+
530
+ // 处理 xweight/xmoney/xprice/xtaxrate/xnumber 格式化
531
+ // 优先级:precision > xweight > xmoney ...
532
+ let precision: number | undefined = col.precision
533
+ if (precision === undefined) {
534
+ if (col.xweight) precision = PRECISION.weight
535
+ else if (col.xmoney) precision = PRECISION.amount
536
+ else if (col.xprice) precision = PRECISION.price
537
+ else if (col.xtaxrate) precision = PRECISION.taxRate
538
+ else if (col.xnumber) precision = PRECISION.number
539
+ }
540
+
541
+ if (precision !== undefined) {
542
+ const originalFormatter = baseColumn.formatter || col.formatter
543
+ baseColumn.align = baseColumn.align || 'right' // 数字默认右对齐
544
+ baseColumn.formatter = (value: any) => {
545
+ const hasCellValue = value && Object.prototype.hasOwnProperty.call(value, 'cellValue')
546
+ let v = hasCellValue ? value.cellValue : value
547
+
548
+ // 如果已经有 formatter (比如 commonCode 处理过的),先执行它
549
+ if (originalFormatter) {
550
+ try {
551
+ v = originalFormatter(value)
552
+ } catch (e) {
553
+ console.warn('formatter 执行异常', e)
554
+ }
555
+ }
556
+
557
+ return formatNumber(v, precision!)
558
+ }
559
+ }
560
+
561
+ if (col?.event) {
562
+ const oldClass = baseColumn.className || ''
563
+ baseColumn.className = `${oldClass} cell-events`.trim()
564
+ }
565
+
566
+ // 递归处理子列
567
+ if (baseColumn.children && baseColumn.children.length > 0) {
568
+ baseColumn.children = baseColumn.children.map(processColumn)
569
+ }
570
+
571
+ return baseColumn
572
+ }
573
+
574
+ // 返回处理数据
575
+ return columns.map((column: Column, idx: number) => {
576
+ const baseColumn = processColumn(column)
577
+
578
+ if (idx === firstNonTypeIndex && props.tree) {
579
+ baseColumn.treeNode = true
580
+ }
581
+ if (idx === firstNonTypeIndex && props.drag) {
582
+ baseColumn.dragSort = true
583
+ }
584
+
585
+ return baseColumn
586
+ })
587
+ }
588
+
589
+ // 处理表尾数据,注意:footerData是个数组,支持多行表尾
590
+ function handleFooterData() {
591
+ // 是否展示表尾
592
+ if (props.showFooter) {
593
+ // 因为表尾需要有一列占位,用于展示【总计】这个文案,默认使用复选框
594
+ const footerData = cloneDeep(props?.footerData ?? [])
595
+ return footerData.map((item) => ({
596
+ ...item,
597
+ BaseTablePlaceholder: '总计',
598
+ }))
599
+ }
600
+ return []
601
+ }
602
+
603
+ const footerRenderData = computed(() => handleFooterData())
604
+
605
+ /**
606
+ * 获取当前选中的复选框数据
607
+ * @returns {Array} 选中的行数据数组
608
+ */
609
+ function getCheckboxRecords() {
610
+ const $grid = basetable.value
611
+ if ($grid) {
612
+ return $grid.getCheckboxRecords()
613
+ }
614
+ return []
615
+ }
616
+
617
+ /**
618
+ * 获取当前选中的单选框数据
619
+ * @returns {object | null} 选中的行数据对象,如果没有选中则返回 null
620
+ */
621
+ function getRadioRecord() {
622
+ const $grid = basetable.value
623
+ if ($grid) {
624
+ const records = $grid.getRadioRecord()
625
+ return records ?? null
626
+ }
627
+ return null
628
+ }
629
+
630
+ /**
631
+ * 获取当前选中的数据(兼容复选框和单选框)
632
+ * @returns {Array | object | null}
633
+ * - 复选框模式:返回选中的行数据数组
634
+ * - 单选框模式:返回选中的行数据对象,如果没有选中则返回 null
635
+ */
636
+ function getSelectedRecords() {
637
+ if (props.radio) {
638
+ return getRadioRecord()
639
+ }
640
+ if (props.checkbox) {
641
+ return getCheckboxRecords()
642
+ }
643
+ return null
644
+ }
645
+
646
+ /**
647
+ * 递归查找列配置
648
+ * @param columns 列数组
649
+ * @param field 目标字段名
650
+ * @returns 找到的列配置或 undefined
651
+ */
652
+ function findColumnByField(columns: Column[], field: string): Column | undefined {
653
+ for (const col of columns) {
654
+ if (col.field === field) {
655
+ return col
656
+ }
657
+ if (col.children && col.children.length > 0) {
658
+ const found = findColumnByField(col.children, field)
659
+ return found
660
+ }
661
+ }
662
+ return undefined
663
+ }
664
+
665
+ /**
666
+ * 处理单元格点击事件
667
+ * @param {object} params - 事件参数对象
668
+ * @param {object} params.row - 当前点击的行
669
+ * @param {string} params.column - 当前点击的列
670
+ */
671
+ function handleCellClick({ row, column }: { row: any; column: any }) {
672
+ // 当用户进行文本选择时,不触发点击事件
673
+ const selection = window.getSelection()
674
+ if (selection && selection.toString().length > 0) return
675
+
676
+ if (column?.className?.includes('cell-events')) {
677
+ const findColumn = findColumnByField(options.value.columns, column.field)
678
+ ;(findColumn as any)?.event?.(row, column)
679
+ }
680
+ }
681
+
682
+ // Expose
683
+ defineExpose({
684
+ grid: basetable,
685
+ getAllToolBarValues: () => ({
686
+ lToolBar: lToolBarState.getAllValues(),
687
+ rToolBar: rToolBarState.getAllValues(),
688
+ }),
689
+ getLToolBarValues: () => lToolBarState.getAllValues(),
690
+ getRToolBarValues: () => rToolBarState.getAllValues(),
691
+ goToPage,
692
+ lToolBarState,
693
+ rToolBarState,
694
+ getSelectedRecords,
695
+ })
696
+ </script>
697
+
698
+ <template>
699
+ <vxe-grid
700
+ ref="basetable"
701
+ v-bind="options"
702
+ :footer-data="footerRenderData"
703
+ :style="{
704
+ '--vxe-ui-table-row-hover-background-color': '#E3FCF7',
705
+ '--vxe-ui-table-column-hover-background-color': '#E3FCF7',
706
+ '--vxe-ui-table-row-hover-striped-background-color': '#E3FCF7',
707
+ }"
708
+ @cell-click="handleCellClick"
709
+ @checkbox-all="selectAllChangeEvent"
710
+ @checkbox-change="selectChangeEvent"
711
+ @radio-change="radioChangeEvent"
712
+ >
713
+ <!-- 顶部左侧工具栏配置 -->
714
+ <template #lToolBar>
715
+ <div v-if="lToolBarItems.length > 0" class="base-table-toolbar-container">
716
+ <component :is="item" v-for="item in lToolBarItems" :key="item.key" />
717
+ </div>
718
+ </template>
719
+ <!-- 顶部右侧工具栏配置 -->
720
+ <template #rToolBar>
721
+ <div v-if="rToolBarItems.length > 0" class="base-table-toolbar-container">
722
+ <component :is="item" v-for="item in rToolBarItems" :key="item.key" />
723
+ </div>
724
+ </template>
725
+ <!-- 通用表头提示插槽 -->
726
+ <template #headerHelp="{ column }">
727
+ <span>{{ column.title }}</span>
728
+ <Tooltip v-if="column.params?.helpText" :title="column.params.helpText">
729
+ <QuestionCircleOutlined class="base-table-header-help-icon" />
730
+ </Tooltip>
731
+ </template>
732
+ <template #buttons="{ row }">
733
+ <div class="base-table-buttons-container">
734
+ <template v-for="(button, index) in getVisibleButtons(row)" :key="button.name">
735
+ <AButton
736
+ v-if="index < 2 || getVisibleButtons(row).length <= 3"
737
+ type="link"
738
+ size="small"
739
+ :disabled="button.disabled?.(row, basetable)"
740
+ :loading="isButtonLoading(row, button)"
741
+ v-bind="button.props"
742
+ @click="handleRowButtonClick(button, row)"
743
+ >
744
+ {{ typeof button.name === 'function' ? button.name(row) : button.name }}
745
+ </AButton>
746
+ </template>
747
+ <APopover v-if="getVisibleButtons(row).length > 3" placement="bottomRight">
748
+ <template #content>
749
+ <template v-for="button in getVisibleButtons(row).slice(2)" :key="button.name">
750
+ <AButton
751
+ style="display: block"
752
+ type="link"
753
+ size="small"
754
+ :disabled="button.disabled?.(row, basetable)"
755
+ :loading="isButtonLoading(row, button)"
756
+ v-bind="button.props"
757
+ @click="handleRowButtonClick(button, row)"
758
+ >
759
+ {{ typeof button.name === 'function' ? button.name(row) : button.name }}
760
+ </AButton>
761
+ </template>
762
+ </template>
763
+ <AButton type="link" size="small"> 更多 </AButton>
764
+ </APopover>
765
+ </div>
766
+ </template>
767
+ <!-- 分页器 -->
768
+ <template v-if="pager" #pager>
769
+ <div class="base-table-pager">
770
+ <APagination
771
+ ref="paginationRef"
772
+ size="small"
773
+ :total="total"
774
+ :current="paginationCurrent"
775
+ :page-size="paginationPageSize"
776
+ show-size-changer
777
+ show-quick-jumper
778
+ @change="onChange"
779
+ @show-size-change="onShowSizeChange"
780
+ />
781
+ <p class="base-table-pager-total">总计 {{ total }} 条数据</p>
782
+ </div>
783
+ </template>
784
+ <!-- 透传所有其他插槽到vxe-grid -->
785
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
786
+ <template v-if="!['lToolBar', 'rToolBar', 'buttons', 'pager'].includes(String(slotName))">
787
+ <slot :name="String(slotName)" v-bind="slotProps" />
788
+ </template>
789
+ </template>
790
+ </vxe-grid>
791
+ </template>
792
+
793
+ <style lang="scss" scoped>
794
+ .base-table-header-help-icon {
795
+ margin-left: 4px;
796
+ color: #999;
797
+ cursor: help;
798
+ }
799
+
800
+ :deep(.base-table-toolbar) {
801
+ padding-top: 0;
802
+ padding-bottom: 0;
803
+
804
+ .base-table-toolbar-container {
805
+ display: flex;
806
+ align-items: center;
807
+ gap: 12px;
808
+ margin-bottom: 12px;
809
+ }
810
+
811
+ .vxe-tools--operate {
812
+ margin-bottom: 12px;
813
+ }
814
+
815
+ .vxe-button--dropdown.size--mini,
816
+ .vxe-button.type--button.size--mini {
817
+ margin: 0 0 0 12px;
818
+ }
819
+ }
820
+
821
+ .base-table-buttons-container {
822
+ display: flex;
823
+ align-items: center;
824
+ gap: 8px;
825
+
826
+ .ant-btn {
827
+ padding: 0;
828
+ font-size: 12px;
829
+ }
830
+ }
831
+
832
+ :deep(.ant-popover) {
833
+ padding-top: 0;
834
+ }
835
+
836
+ :deep(.ant-popover-inner) {
837
+ padding: 0 !important;
838
+ }
839
+
840
+ :deep(.base-columns-cell) {
841
+ .vxe-cell {
842
+ padding: 8px 12px !important;
843
+ }
844
+ }
845
+
846
+ :deep(.base-table-footer-cell) {
847
+ .vxe-cell {
848
+ height: 40px !important;
849
+ padding: 8px 12px !important;
850
+ }
851
+ }
852
+
853
+ .base-table-pager {
854
+ display: flex;
855
+ align-items: end;
856
+ justify-content: space-between;
857
+ height: fit-content;
858
+ min-height: 36px;
859
+
860
+ :deep(.ant-pagination-options) {
861
+ .ant-select {
862
+ width: 100px;
863
+ }
864
+ }
865
+
866
+ .base-table-pager-total {
867
+ color: #262626;
868
+ line-height: 24px;
869
+ }
870
+ }
871
+
872
+ :deep(.cell-events) {
873
+ color: #1288fc;
874
+ cursor: pointer;
875
+ }
876
+
877
+ /* 斑马纹行悬浮背景颜色 */
878
+ :deep(.vxe-table--body .vxe-body--row.row--stripe.row--hover) {
879
+ background-color: red !important;
880
+ }
881
+ </style>