foggy-data-viewer 1.0.1-beta.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 (34) hide show
  1. package/README.md +273 -0
  2. package/dist/favicon.svg +4 -0
  3. package/dist/index.js +1531 -0
  4. package/dist/index.umd +1 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +51 -0
  7. package/src/App.vue +469 -0
  8. package/src/api/viewer.ts +163 -0
  9. package/src/components/DataTable.test.ts +533 -0
  10. package/src/components/DataTable.vue +810 -0
  11. package/src/components/DataTableWithSearch.test.ts +628 -0
  12. package/src/components/DataTableWithSearch.vue +277 -0
  13. package/src/components/DataViewer.vue +310 -0
  14. package/src/components/SearchToolbar.test.ts +521 -0
  15. package/src/components/SearchToolbar.vue +406 -0
  16. package/src/components/composables/index.ts +2 -0
  17. package/src/components/composables/useTableSelection.test.ts +248 -0
  18. package/src/components/composables/useTableSelection.ts +44 -0
  19. package/src/components/composables/useTableSummary.test.ts +341 -0
  20. package/src/components/composables/useTableSummary.ts +129 -0
  21. package/src/components/filters/BoolFilter.vue +103 -0
  22. package/src/components/filters/DateRangeFilter.vue +194 -0
  23. package/src/components/filters/NumberRangeFilter.vue +160 -0
  24. package/src/components/filters/SelectFilter.vue +464 -0
  25. package/src/components/filters/TextFilter.vue +230 -0
  26. package/src/components/filters/index.ts +5 -0
  27. package/src/examples/EnhancedTableExample.vue +136 -0
  28. package/src/index.ts +32 -0
  29. package/src/main.ts +14 -0
  30. package/src/types/index.ts +159 -0
  31. package/src/utils/README.md +140 -0
  32. package/src/utils/schemaHelper.test.ts +215 -0
  33. package/src/utils/schemaHelper.ts +44 -0
  34. package/src/vite-env.d.ts +7 -0
@@ -0,0 +1,406 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import type { EnhancedColumnSchema, SliceRequestDef } from '@/types'
4
+ import { TextFilter, NumberRangeFilter, DateRangeFilter, SelectFilter, BoolFilter } from './filters'
5
+
6
+ /**
7
+ * SearchToolbar 组件
8
+ *
9
+ * 独立的搜索工具栏组件,可单独使用或集成到 DataTable 的 toolbar 插槽
10
+ * 支持字段级快速筛选,与 DataTable 的过滤器体系保持一致
11
+ */
12
+ interface Props {
13
+ /** 列配置(从 QM Schema 构建) */
14
+ columns: EnhancedColumnSchema[]
15
+ /** 可搜索的字段列表(不指定则显示所有可筛选字段) */
16
+ searchableFields?: string[]
17
+ /** 当前筛选条件(v-model) */
18
+ modelValue?: SliceRequestDef[]
19
+ /** 布局方式 */
20
+ layout?: 'horizontal' | 'vertical'
21
+ /** 是否显示操作按钮(搜索、重置) */
22
+ showActions?: boolean
23
+ /** 过滤选项加载器(用于维度列) */
24
+ filterOptionsLoader?: (columnName: string) => Promise<{ label: string; value: string | number }[]>
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ layout: 'horizontal',
29
+ showActions: true
30
+ })
31
+
32
+ const emit = defineEmits<{
33
+ (e: 'update:modelValue', value: SliceRequestDef[]): void
34
+ (e: 'search'): void
35
+ (e: 'reset'): void
36
+ }>()
37
+
38
+ // 内部筛选状态:按字段存储
39
+ const filterValues = ref<Record<string, SliceRequestDef[] | null>>({})
40
+
41
+ // 根据配置获取可搜索的列
42
+ const searchableColumns = computed(() => {
43
+ let cols = props.columns.filter(col => col.filterable !== false)
44
+
45
+ if (props.searchableFields && props.searchableFields.length > 0) {
46
+ cols = cols.filter(col => props.searchableFields!.includes(col.name))
47
+ }
48
+
49
+ return cols
50
+ })
51
+
52
+ // 从 modelValue 初始化
53
+ watch(() => props.modelValue, (slices) => {
54
+ if (!slices || slices.length === 0) {
55
+ filterValues.value = {}
56
+ return
57
+ }
58
+
59
+ // 按 field 分组
60
+ const grouped: Record<string, SliceRequestDef[]> = {}
61
+ for (const slice of slices) {
62
+ if (!slice.field) continue
63
+ if (!grouped[slice.field]) {
64
+ grouped[slice.field] = []
65
+ }
66
+ grouped[slice.field].push(slice)
67
+ }
68
+
69
+ filterValues.value = grouped
70
+ }, { immediate: true })
71
+
72
+ // 推断过滤器类型
73
+ function inferFilterType(col: EnhancedColumnSchema): string {
74
+ const type = col.type?.toUpperCase()
75
+
76
+ // 日期类型优先
77
+ switch (type) {
78
+ case 'DAY':
79
+ case 'DATE':
80
+ return 'date'
81
+ case 'DATETIME':
82
+ return 'datetime'
83
+ }
84
+
85
+ // 使用后端返回的 filterType
86
+ if (col.filterType) {
87
+ return col.filterType
88
+ }
89
+
90
+ // 回退:根据 type 推断
91
+ switch (type) {
92
+ case 'NUMBER':
93
+ case 'MONEY':
94
+ case 'BIGDECIMAL':
95
+ case 'INTEGER':
96
+ case 'BIGINT':
97
+ case 'LONG':
98
+ return 'number'
99
+ case 'BOOL':
100
+ case 'BOOLEAN':
101
+ return 'bool'
102
+ case 'DICT':
103
+ return 'dict'
104
+ default:
105
+ return 'text'
106
+ }
107
+ }
108
+
109
+ // 获取过滤器组件
110
+ function getFilterComponent(col: EnhancedColumnSchema) {
111
+ if (col.customFilterComponent) {
112
+ return col.customFilterComponent
113
+ }
114
+
115
+ const filterType = inferFilterType(col)
116
+
117
+ switch (filterType) {
118
+ case 'text':
119
+ return TextFilter
120
+ case 'number':
121
+ return NumberRangeFilter
122
+ case 'date':
123
+ return DateRangeFilter
124
+ case 'datetime':
125
+ return DateRangeFilter
126
+ case 'dict':
127
+ return SelectFilter
128
+ case 'dimension':
129
+ return SelectFilter
130
+ case 'bool':
131
+ return BoolFilter
132
+ default:
133
+ return TextFilter
134
+ }
135
+ }
136
+
137
+ // 获取过滤器属性
138
+ function getFilterProps(col: EnhancedColumnSchema) {
139
+ const filterType = inferFilterType(col)
140
+ const baseProps: Record<string, unknown> = {
141
+ field: col.name,
142
+ modelValue: filterValues.value[col.name] || null,
143
+ 'onUpdate:modelValue': (val: SliceRequestDef[] | null) => updateFilter(col.name, val)
144
+ }
145
+
146
+ switch (filterType) {
147
+ case 'datetime':
148
+ return { ...baseProps, showTime: true, format: col.format }
149
+ case 'date':
150
+ return { ...baseProps, format: col.format }
151
+ case 'dict':
152
+ return {
153
+ ...baseProps,
154
+ options: col.dictItems || [],
155
+ placeholder: `请选择${col.title || col.name}`
156
+ }
157
+ case 'dimension':
158
+ return {
159
+ ...baseProps,
160
+ options: [],
161
+ loading: false,
162
+ placeholder: `请选择${col.title || col.name}`
163
+ }
164
+ default:
165
+ return {
166
+ ...baseProps,
167
+ placeholder: `搜索${col.title || col.name}...`
168
+ }
169
+ }
170
+ }
171
+
172
+ // 更新过滤值
173
+ function updateFilter(columnName: string, value: SliceRequestDef[] | null) {
174
+ if (value === null || value.length === 0) {
175
+ delete filterValues.value[columnName]
176
+ } else {
177
+ filterValues.value[columnName] = value
178
+ }
179
+
180
+ // 立即同步到 modelValue(实时搜索)
181
+ emitFilterChange()
182
+ }
183
+
184
+ // 发送过滤变更事件
185
+ function emitFilterChange() {
186
+ const allSlices: SliceRequestDef[] = []
187
+ for (const slices of Object.values(filterValues.value)) {
188
+ if (slices && slices.length > 0) {
189
+ allSlices.push(...slices)
190
+ }
191
+ }
192
+ emit('update:modelValue', allSlices)
193
+ }
194
+
195
+ // 点击搜索按钮
196
+ function handleSearch() {
197
+ emitFilterChange()
198
+ emit('search')
199
+ }
200
+
201
+ // 点击重置按钮
202
+ function handleReset() {
203
+ filterValues.value = {}
204
+ emit('update:modelValue', [])
205
+ emit('reset')
206
+ }
207
+
208
+ // 暴露方法
209
+ defineExpose({
210
+ /** 清空所有筛选 */
211
+ clearFilters: handleReset,
212
+ /** 获取当前筛选条件 */
213
+ getFilters: (): SliceRequestDef[] => {
214
+ const allSlices: SliceRequestDef[] = []
215
+ for (const slices of Object.values(filterValues.value)) {
216
+ if (slices && slices.length > 0) {
217
+ allSlices.push(...slices)
218
+ }
219
+ }
220
+ return allSlices
221
+ }
222
+ })
223
+ </script>
224
+
225
+ <template>
226
+ <div class="search-toolbar" :class="`layout-${layout}`">
227
+ <div class="search-fields">
228
+ <div
229
+ v-for="col in searchableColumns"
230
+ :key="col.name"
231
+ class="search-field-item"
232
+ >
233
+ <label class="field-label">{{ col.title || col.name }}</label>
234
+ <div class="field-filter">
235
+ <component
236
+ :is="getFilterComponent(col)"
237
+ v-bind="getFilterProps(col)"
238
+ />
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <div v-if="showActions" class="search-actions">
244
+ <button class="btn btn-primary" @click="handleSearch">
245
+ <span class="btn-icon">🔍</span>
246
+ 搜索
247
+ </button>
248
+ <button class="btn btn-default" @click="handleReset">
249
+ <span class="btn-icon">↻</span>
250
+ 重置
251
+ </button>
252
+ </div>
253
+ </div>
254
+ </template>
255
+
256
+ <style scoped>
257
+ .search-toolbar {
258
+ display: flex;
259
+ gap: 16px;
260
+ padding: 16px;
261
+ background: #fafafa;
262
+ border-radius: 4px;
263
+ border: 1px solid #e4e7ed;
264
+ }
265
+
266
+ /* 水平布局(默认) */
267
+ .search-toolbar.layout-horizontal {
268
+ flex-direction: row;
269
+ align-items: flex-end;
270
+ }
271
+
272
+ .search-toolbar.layout-horizontal .search-fields {
273
+ flex: 1;
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 12px;
277
+ }
278
+
279
+ .search-toolbar.layout-horizontal .search-field-item {
280
+ min-width: 200px;
281
+ flex: 0 1 auto;
282
+ }
283
+
284
+ /* 垂直布局 */
285
+ .search-toolbar.layout-vertical {
286
+ flex-direction: column;
287
+ }
288
+
289
+ .search-toolbar.layout-vertical .search-fields {
290
+ display: flex;
291
+ flex-direction: column;
292
+ gap: 12px;
293
+ }
294
+
295
+ .search-toolbar.layout-vertical .search-field-item {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 12px;
299
+ }
300
+
301
+ .search-toolbar.layout-vertical .field-label {
302
+ min-width: 100px;
303
+ text-align: right;
304
+ }
305
+
306
+ .search-toolbar.layout-vertical .field-filter {
307
+ flex: 1;
308
+ }
309
+
310
+ /* 字段样式 */
311
+ .search-field-item {
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: 6px;
315
+ }
316
+
317
+ .field-label {
318
+ font-size: 13px;
319
+ font-weight: 500;
320
+ color: #606266;
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .field-filter {
325
+ min-width: 0;
326
+ }
327
+
328
+ /* 操作按钮 */
329
+ .search-actions {
330
+ display: flex;
331
+ gap: 8px;
332
+ flex-shrink: 0;
333
+ }
334
+
335
+ .btn {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 4px;
339
+ padding: 8px 16px;
340
+ border: 1px solid transparent;
341
+ border-radius: 4px;
342
+ font-size: 13px;
343
+ font-weight: 500;
344
+ cursor: pointer;
345
+ transition: all 0.2s;
346
+ white-space: nowrap;
347
+ height: 32px;
348
+ }
349
+
350
+ .btn-icon {
351
+ font-size: 14px;
352
+ }
353
+
354
+ .btn-primary {
355
+ background: #409eff;
356
+ color: white;
357
+ border-color: #409eff;
358
+ }
359
+
360
+ .btn-primary:hover {
361
+ background: #66b1ff;
362
+ border-color: #66b1ff;
363
+ }
364
+
365
+ .btn-primary:active {
366
+ background: #3a8ee6;
367
+ border-color: #3a8ee6;
368
+ }
369
+
370
+ .btn-default {
371
+ background: white;
372
+ color: #606266;
373
+ border-color: #dcdfe6;
374
+ }
375
+
376
+ .btn-default:hover {
377
+ color: #409eff;
378
+ border-color: #c6e2ff;
379
+ background: #ecf5ff;
380
+ }
381
+
382
+ .btn-default:active {
383
+ color: #3a8ee6;
384
+ border-color: #3a8ee6;
385
+ }
386
+
387
+ /* 响应式 */
388
+ @media (max-width: 768px) {
389
+ .search-toolbar.layout-horizontal {
390
+ flex-direction: column;
391
+ align-items: stretch;
392
+ }
393
+
394
+ .search-toolbar.layout-horizontal .search-fields {
395
+ flex-direction: column;
396
+ }
397
+
398
+ .search-toolbar.layout-horizontal .search-field-item {
399
+ min-width: 0;
400
+ }
401
+
402
+ .search-actions {
403
+ justify-content: flex-end;
404
+ }
405
+ }
406
+ </style>
@@ -0,0 +1,2 @@
1
+ export { useTableSelection } from './useTableSelection'
2
+ export { useTableSummary } from './useTableSummary'
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { useTableSelection } from './useTableSelection'
3
+
4
+ describe('useTableSelection', () => {
5
+ describe('selectedRows', () => {
6
+ it('should initialize with empty array', () => {
7
+ const { selectedRows } = useTableSelection()
8
+
9
+ expect(selectedRows.value).toEqual([])
10
+ })
11
+ })
12
+
13
+ describe('onCheckboxChange', () => {
14
+ it('should update selected rows when checkbox changes', () => {
15
+ const { selectedRows, onCheckboxChange } = useTableSelection()
16
+
17
+ const mockRecords = [
18
+ { id: 1, name: 'Test 1' },
19
+ { id: 2, name: 'Test 2' }
20
+ ]
21
+
22
+ onCheckboxChange({ records: mockRecords })
23
+
24
+ expect(selectedRows.value).toEqual(mockRecords)
25
+ })
26
+
27
+ it('should handle single row selection', () => {
28
+ const { selectedRows, onCheckboxChange } = useTableSelection()
29
+
30
+ const mockRecord = [{ id: 1, name: 'Test 1' }]
31
+
32
+ onCheckboxChange({ records: mockRecord })
33
+
34
+ expect(selectedRows.value).toHaveLength(1)
35
+ expect(selectedRows.value[0]).toEqual(mockRecord[0])
36
+ })
37
+
38
+ it('should handle deselection', () => {
39
+ const { selectedRows, onCheckboxChange } = useTableSelection()
40
+
41
+ // 先选中
42
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
43
+ expect(selectedRows.value).toHaveLength(1)
44
+
45
+ // 取消选中
46
+ onCheckboxChange({ records: [] })
47
+ expect(selectedRows.value).toHaveLength(0)
48
+ })
49
+
50
+ it('should replace previous selection', () => {
51
+ const { selectedRows, onCheckboxChange } = useTableSelection()
52
+
53
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
54
+ expect(selectedRows.value[0].id).toBe(1)
55
+
56
+ onCheckboxChange({ records: [{ id: 2, name: 'Test 2' }] })
57
+ expect(selectedRows.value).toHaveLength(1)
58
+ expect(selectedRows.value[0].id).toBe(2)
59
+ })
60
+ })
61
+
62
+ describe('onCheckboxAll', () => {
63
+ it('should select all rows', () => {
64
+ const { selectedRows, onCheckboxAll } = useTableSelection()
65
+
66
+ const mockRecords = [
67
+ { id: 1, name: 'Test 1' },
68
+ { id: 2, name: 'Test 2' },
69
+ { id: 3, name: 'Test 3' }
70
+ ]
71
+
72
+ onCheckboxAll({ records: mockRecords })
73
+
74
+ expect(selectedRows.value).toEqual(mockRecords)
75
+ expect(selectedRows.value).toHaveLength(3)
76
+ })
77
+
78
+ it('should deselect all rows', () => {
79
+ const { selectedRows, onCheckboxAll } = useTableSelection()
80
+
81
+ // 先全选
82
+ onCheckboxAll({ records: [
83
+ { id: 1, name: 'Test 1' },
84
+ { id: 2, name: 'Test 2' }
85
+ ] })
86
+ expect(selectedRows.value).toHaveLength(2)
87
+
88
+ // 取消全选
89
+ onCheckboxAll({ records: [] })
90
+ expect(selectedRows.value).toHaveLength(0)
91
+ })
92
+
93
+ it('should replace previous selection with all rows', () => {
94
+ const { selectedRows, onCheckboxChange, onCheckboxAll } = useTableSelection()
95
+
96
+ // 先选中一行
97
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
98
+ expect(selectedRows.value).toHaveLength(1)
99
+
100
+ // 全选
101
+ const allRecords = [
102
+ { id: 1, name: 'Test 1' },
103
+ { id: 2, name: 'Test 2' },
104
+ { id: 3, name: 'Test 3' }
105
+ ]
106
+ onCheckboxAll({ records: allRecords })
107
+ expect(selectedRows.value).toHaveLength(3)
108
+ })
109
+ })
110
+
111
+ describe('clearSelection', () => {
112
+ it('should clear all selected rows', () => {
113
+ const { selectedRows, onCheckboxChange, clearSelection } = useTableSelection()
114
+
115
+ onCheckboxChange({ records: [
116
+ { id: 1, name: 'Test 1' },
117
+ { id: 2, name: 'Test 2' }
118
+ ] })
119
+ expect(selectedRows.value).toHaveLength(2)
120
+
121
+ clearSelection()
122
+ expect(selectedRows.value).toEqual([])
123
+ })
124
+
125
+ it('should work when no rows are selected', () => {
126
+ const { selectedRows, clearSelection } = useTableSelection()
127
+
128
+ expect(selectedRows.value).toHaveLength(0)
129
+ clearSelection()
130
+ expect(selectedRows.value).toHaveLength(0)
131
+ })
132
+
133
+ it('should be idempotent', () => {
134
+ const { selectedRows, onCheckboxChange, clearSelection } = useTableSelection()
135
+
136
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
137
+
138
+ clearSelection()
139
+ expect(selectedRows.value).toHaveLength(0)
140
+
141
+ clearSelection()
142
+ expect(selectedRows.value).toHaveLength(0)
143
+ })
144
+ })
145
+
146
+ describe('getSelectedCount', () => {
147
+ it('should return 0 when no rows selected', () => {
148
+ const { getSelectedCount } = useTableSelection()
149
+
150
+ expect(getSelectedCount()).toBe(0)
151
+ })
152
+
153
+ it('should return correct count of selected rows', () => {
154
+ const { onCheckboxChange, getSelectedCount } = useTableSelection()
155
+
156
+ onCheckboxChange({ records: [
157
+ { id: 1, name: 'Test 1' },
158
+ { id: 2, name: 'Test 2' }
159
+ ] })
160
+
161
+ expect(getSelectedCount()).toBe(2)
162
+ })
163
+
164
+ it('should update when selection changes', () => {
165
+ const { onCheckboxChange, clearSelection, getSelectedCount } = useTableSelection()
166
+
167
+ expect(getSelectedCount()).toBe(0)
168
+
169
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
170
+ expect(getSelectedCount()).toBe(1)
171
+
172
+ onCheckboxChange({ records: [
173
+ { id: 1, name: 'Test 1' },
174
+ { id: 2, name: 'Test 2' }
175
+ ] })
176
+ expect(getSelectedCount()).toBe(2)
177
+
178
+ clearSelection()
179
+ expect(getSelectedCount()).toBe(0)
180
+ })
181
+ })
182
+
183
+ describe('Type Safety', () => {
184
+ interface CustomRow {
185
+ customId: number
186
+ customName: string
187
+ }
188
+
189
+ it('should support custom row types', () => {
190
+ const { selectedRows, onCheckboxChange } = useTableSelection<CustomRow>()
191
+
192
+ const mockRecords: CustomRow[] = [
193
+ { customId: 1, customName: 'Custom 1' },
194
+ { customId: 2, customName: 'Custom 2' }
195
+ ]
196
+
197
+ onCheckboxChange({ records: mockRecords })
198
+
199
+ expect(selectedRows.value).toEqual(mockRecords)
200
+ expect(selectedRows.value[0].customId).toBe(1)
201
+ expect(selectedRows.value[0].customName).toBe('Custom 1')
202
+ })
203
+ })
204
+
205
+ describe('Integration', () => {
206
+ it('should work together in complete workflow', () => {
207
+ const { selectedRows, onCheckboxChange, onCheckboxAll, clearSelection, getSelectedCount } =
208
+ useTableSelection()
209
+
210
+ // 初始状态
211
+ expect(getSelectedCount()).toBe(0)
212
+
213
+ // 选中一行
214
+ onCheckboxChange({ records: [{ id: 1, name: 'Test 1' }] })
215
+ expect(getSelectedCount()).toBe(1)
216
+
217
+ // 全选
218
+ onCheckboxAll({ records: [
219
+ { id: 1, name: 'Test 1' },
220
+ { id: 2, name: 'Test 2' },
221
+ { id: 3, name: 'Test 3' }
222
+ ] })
223
+ expect(getSelectedCount()).toBe(3)
224
+
225
+ // 取消选中一行
226
+ onCheckboxChange({ records: [
227
+ { id: 1, name: 'Test 1' },
228
+ { id: 3, name: 'Test 3' }
229
+ ] })
230
+ expect(getSelectedCount()).toBe(2)
231
+
232
+ // 清空选择
233
+ clearSelection()
234
+ expect(getSelectedCount()).toBe(0)
235
+ expect(selectedRows.value).toEqual([])
236
+ })
237
+
238
+ it('should handle rapid selection changes', () => {
239
+ const { onCheckboxChange, getSelectedCount } = useTableSelection()
240
+
241
+ // 快速连续选择
242
+ for (let i = 1; i <= 5; i++) {
243
+ onCheckboxChange({ records: Array.from({ length: i }, (_, j) => ({ id: j + 1 })) })
244
+ expect(getSelectedCount()).toBe(i)
245
+ }
246
+ })
247
+ })
248
+ })
@@ -0,0 +1,44 @@
1
+ import { ref } from 'vue'
2
+
3
+ /**
4
+ * 表格行选择逻辑
5
+ */
6
+ export function useTableSelection<T = Record<string, unknown>>() {
7
+ const selectedRows = ref<T[]>([])
8
+
9
+ /**
10
+ * checkbox 变化处理
11
+ */
12
+ function onCheckboxChange({ records }: { records: T[] }) {
13
+ selectedRows.value = records
14
+ }
15
+
16
+ /**
17
+ * 全选变化处理
18
+ */
19
+ function onCheckboxAll({ records }: { records: T[] }) {
20
+ selectedRows.value = records
21
+ }
22
+
23
+ /**
24
+ * 清空选择
25
+ */
26
+ function clearSelection() {
27
+ selectedRows.value = []
28
+ }
29
+
30
+ /**
31
+ * 获取选中行数
32
+ */
33
+ function getSelectedCount() {
34
+ return selectedRows.value.length
35
+ }
36
+
37
+ return {
38
+ selectedRows,
39
+ onCheckboxChange,
40
+ onCheckboxAll,
41
+ clearSelection,
42
+ getSelectedCount
43
+ }
44
+ }