@antsoo-lib/core 3.0.0 → 3.0.1

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