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.
- package/README.md +273 -0
- package/dist/favicon.svg +4 -0
- package/dist/index.js +1531 -0
- package/dist/index.umd +1 -0
- package/dist/style.css +1 -0
- package/package.json +51 -0
- package/src/App.vue +469 -0
- package/src/api/viewer.ts +163 -0
- package/src/components/DataTable.test.ts +533 -0
- package/src/components/DataTable.vue +810 -0
- package/src/components/DataTableWithSearch.test.ts +628 -0
- package/src/components/DataTableWithSearch.vue +277 -0
- package/src/components/DataViewer.vue +310 -0
- package/src/components/SearchToolbar.test.ts +521 -0
- package/src/components/SearchToolbar.vue +406 -0
- package/src/components/composables/index.ts +2 -0
- package/src/components/composables/useTableSelection.test.ts +248 -0
- package/src/components/composables/useTableSelection.ts +44 -0
- package/src/components/composables/useTableSummary.test.ts +341 -0
- package/src/components/composables/useTableSummary.ts +129 -0
- package/src/components/filters/BoolFilter.vue +103 -0
- package/src/components/filters/DateRangeFilter.vue +194 -0
- package/src/components/filters/NumberRangeFilter.vue +160 -0
- package/src/components/filters/SelectFilter.vue +464 -0
- package/src/components/filters/TextFilter.vue +230 -0
- package/src/components/filters/index.ts +5 -0
- package/src/examples/EnhancedTableExample.vue +136 -0
- package/src/index.ts +32 -0
- package/src/main.ts +14 -0
- package/src/types/index.ts +159 -0
- package/src/utils/README.md +140 -0
- package/src/utils/schemaHelper.test.ts +215 -0
- package/src/utils/schemaHelper.ts +44 -0
- package/src/vite-env.d.ts +7 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useTableSummary } from './useTableSummary'
|
|
4
|
+
import type { ColumnSchema } from '@/types'
|
|
5
|
+
|
|
6
|
+
describe('useTableSummary', () => {
|
|
7
|
+
const mockColumns = ref<ColumnSchema[]>([
|
|
8
|
+
{ name: 'id', type: 'INTEGER', title: 'ID' },
|
|
9
|
+
{ name: 'name', type: 'TEXT', title: '名称' },
|
|
10
|
+
{ name: 'amount', type: 'MONEY', title: '金额' },
|
|
11
|
+
{ name: 'count', type: 'NUMBER', title: '数量' },
|
|
12
|
+
{ name: 'status', type: 'TEXT', title: '状态' }
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
describe('measureColumns', () => {
|
|
16
|
+
it('should identify measure columns correctly', () => {
|
|
17
|
+
const { measureColumns } = useTableSummary(mockColumns)
|
|
18
|
+
|
|
19
|
+
expect(measureColumns.value).toHaveLength(3) // id, amount, count
|
|
20
|
+
expect(measureColumns.value[0].name).toBe('id')
|
|
21
|
+
expect(measureColumns.value[1].name).toBe('amount')
|
|
22
|
+
expect(measureColumns.value[2].name).toBe('count')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should handle columns without type', () => {
|
|
26
|
+
const columnsWithoutType = ref<ColumnSchema[]>([
|
|
27
|
+
{ name: 'col1', type: '', title: 'Column 1' },
|
|
28
|
+
{ name: 'col2', type: 'MONEY', title: 'Column 2' }
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const { measureColumns } = useTableSummary(columnsWithoutType)
|
|
32
|
+
|
|
33
|
+
expect(measureColumns.value).toHaveLength(1)
|
|
34
|
+
expect(measureColumns.value[0].name).toBe('col2')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should recognize all measure types', () => {
|
|
38
|
+
const measureTypeColumns = ref<ColumnSchema[]>([
|
|
39
|
+
{ name: 'col1', type: 'NUMBER', title: 'Number' },
|
|
40
|
+
{ name: 'col2', type: 'MONEY', title: 'Money' },
|
|
41
|
+
{ name: 'col3', type: 'BIGDECIMAL', title: 'BigDecimal' },
|
|
42
|
+
{ name: 'col4', type: 'INTEGER', title: 'Integer' },
|
|
43
|
+
{ name: 'col5', type: 'BIGINT', title: 'BigInt' },
|
|
44
|
+
{ name: 'col6', type: 'LONG', title: 'Long' },
|
|
45
|
+
{ name: 'col7', type: 'TEXT', title: 'Text' }
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
const { measureColumns } = useTableSummary(measureTypeColumns)
|
|
49
|
+
|
|
50
|
+
expect(measureColumns.value).toHaveLength(6)
|
|
51
|
+
expect(measureColumns.value.map(c => c.name)).toEqual([
|
|
52
|
+
'col1', 'col2', 'col3', 'col4', 'col5', 'col6'
|
|
53
|
+
])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('calculateSelectedSummary', () => {
|
|
58
|
+
it('should calculate summary for selected rows', () => {
|
|
59
|
+
const { calculateSelectedSummary } = useTableSummary(mockColumns)
|
|
60
|
+
|
|
61
|
+
const selectedRows = [
|
|
62
|
+
{ id: 1, name: 'Test 1', amount: 100, count: 5 },
|
|
63
|
+
{ id: 2, name: 'Test 2', amount: 200, count: 10 },
|
|
64
|
+
{ id: 3, name: 'Test 3', amount: 300, count: 15 }
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const summary = calculateSelectedSummary(selectedRows)
|
|
68
|
+
|
|
69
|
+
expect(summary._count).toBe(3)
|
|
70
|
+
expect(summary.id).toBe(6) // 1 + 2 + 3
|
|
71
|
+
expect(summary.amount).toBe(600)
|
|
72
|
+
expect(summary.count).toBe(30)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle empty selection', () => {
|
|
76
|
+
const { calculateSelectedSummary } = useTableSummary(mockColumns)
|
|
77
|
+
|
|
78
|
+
const summary = calculateSelectedSummary([])
|
|
79
|
+
|
|
80
|
+
expect(summary._count).toBe(0)
|
|
81
|
+
expect(summary.id).toBe(0)
|
|
82
|
+
expect(summary.amount).toBe(0)
|
|
83
|
+
expect(summary.count).toBe(0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should handle null/undefined values', () => {
|
|
87
|
+
const { calculateSelectedSummary } = useTableSummary(mockColumns)
|
|
88
|
+
|
|
89
|
+
const selectedRows = [
|
|
90
|
+
{ id: 1, name: 'Test 1', amount: null, count: 5 },
|
|
91
|
+
{ id: 2, name: 'Test 2', amount: undefined, count: 10 },
|
|
92
|
+
{ id: 3, name: 'Test 3', amount: 100, count: null }
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const summary = calculateSelectedSummary(selectedRows)
|
|
96
|
+
|
|
97
|
+
expect(summary._count).toBe(3)
|
|
98
|
+
expect(summary.id).toBe(6)
|
|
99
|
+
expect(summary.amount).toBe(100)
|
|
100
|
+
expect(summary.count).toBe(15)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle non-numeric values', () => {
|
|
104
|
+
const { calculateSelectedSummary } = useTableSummary(mockColumns)
|
|
105
|
+
|
|
106
|
+
const selectedRows = [
|
|
107
|
+
{ id: 1, name: 'Test 1', amount: 'abc', count: 5 },
|
|
108
|
+
{ id: 2, name: 'Test 2', amount: 100, count: 'xyz' }
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
const summary = calculateSelectedSummary(selectedRows)
|
|
112
|
+
|
|
113
|
+
expect(summary._count).toBe(2)
|
|
114
|
+
expect(summary.id).toBe(3)
|
|
115
|
+
expect(summary.amount).toBe(100)
|
|
116
|
+
expect(summary.count).toBe(5)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('formatValue', () => {
|
|
121
|
+
it('should format MONEY type', () => {
|
|
122
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
123
|
+
|
|
124
|
+
expect(formatValue(1234.5, 'MONEY')).toBe('1,234.50')
|
|
125
|
+
expect(formatValue(1000000, 'MONEY')).toBe('1,000,000.00')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should format NUMBER type', () => {
|
|
129
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
130
|
+
|
|
131
|
+
expect(formatValue(1234.567, 'NUMBER')).toBe('1,234.57')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should format BIGDECIMAL type', () => {
|
|
135
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
136
|
+
|
|
137
|
+
expect(formatValue(9999.99, 'BIGDECIMAL')).toBe('9,999.99')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should format INTEGER type without decimals', () => {
|
|
141
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
142
|
+
|
|
143
|
+
expect(formatValue(1234, 'INTEGER')).toBe('1,234')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle null/undefined values', () => {
|
|
147
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
148
|
+
|
|
149
|
+
expect(formatValue(null, 'MONEY')).toBe('')
|
|
150
|
+
expect(formatValue(undefined, 'MONEY')).toBe('')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle non-numeric values', () => {
|
|
154
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
155
|
+
|
|
156
|
+
expect(formatValue('text', 'MONEY')).toBe('text')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should format zero correctly', () => {
|
|
160
|
+
const { formatValue } = useTableSummary(mockColumns)
|
|
161
|
+
|
|
162
|
+
expect(formatValue(0, 'MONEY')).toBe('0.00')
|
|
163
|
+
expect(formatValue(0, 'INTEGER')).toBe('0')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('generateFooterData', () => {
|
|
168
|
+
it('should generate footer data with two rows', () => {
|
|
169
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
170
|
+
|
|
171
|
+
setServerSummary({ total: 100, amount: 10000, count: 500 })
|
|
172
|
+
|
|
173
|
+
const visibleColumns = [
|
|
174
|
+
{ field: undefined }, // checkbox column
|
|
175
|
+
{ field: 'id', type: 'INTEGER' },
|
|
176
|
+
{ field: 'amount', type: 'MONEY' },
|
|
177
|
+
{ field: 'count', type: 'NUMBER' }
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
const selectedSummary = { _count: 3, amount: 600, count: 30 }
|
|
181
|
+
|
|
182
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
183
|
+
|
|
184
|
+
expect(footerData).toHaveLength(2)
|
|
185
|
+
expect(footerData[0]).toHaveLength(4) // 选中行
|
|
186
|
+
expect(footerData[1]).toHaveLength(4) // 全量汇总
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should show labels in first column', () => {
|
|
190
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
191
|
+
|
|
192
|
+
setServerSummary({ total: 100 })
|
|
193
|
+
|
|
194
|
+
const visibleColumns = [{ field: undefined }]
|
|
195
|
+
const selectedSummary = { _count: 3 }
|
|
196
|
+
|
|
197
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
198
|
+
|
|
199
|
+
expect(footerData[0][0]).toBe('选中')
|
|
200
|
+
expect(footerData[1][0]).toBe('合计')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should show count in second column', () => {
|
|
204
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
205
|
+
|
|
206
|
+
setServerSummary({ total: 100 })
|
|
207
|
+
|
|
208
|
+
const visibleColumns = [
|
|
209
|
+
{ field: undefined },
|
|
210
|
+
{ field: 'id', type: 'INTEGER' }
|
|
211
|
+
]
|
|
212
|
+
const selectedSummary = { _count: 5 }
|
|
213
|
+
|
|
214
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
215
|
+
|
|
216
|
+
expect(footerData[0][1]).toBe('5 条')
|
|
217
|
+
expect(footerData[1][1]).toBe('100 条')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should show measure values in other columns', () => {
|
|
221
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
222
|
+
|
|
223
|
+
setServerSummary({ total: 100, amount: 10000 })
|
|
224
|
+
|
|
225
|
+
const visibleColumns = [
|
|
226
|
+
{ field: undefined },
|
|
227
|
+
{ field: 'id', type: 'INTEGER' },
|
|
228
|
+
{ field: 'amount', type: 'MONEY' }
|
|
229
|
+
]
|
|
230
|
+
const selectedSummary = { _count: 3, amount: 600 }
|
|
231
|
+
|
|
232
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
233
|
+
|
|
234
|
+
expect(footerData[0][2]).toBe('600.00')
|
|
235
|
+
expect(footerData[1][2]).toBe('10,000.00')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should show null for non-measure columns', () => {
|
|
239
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
240
|
+
|
|
241
|
+
setServerSummary({ total: 100 })
|
|
242
|
+
|
|
243
|
+
const visibleColumns = [
|
|
244
|
+
{ field: undefined },
|
|
245
|
+
{ field: 'id', type: 'INTEGER' },
|
|
246
|
+
{ field: 'name', type: 'TEXT' } // non-measure
|
|
247
|
+
]
|
|
248
|
+
const selectedSummary = { _count: 3 }
|
|
249
|
+
|
|
250
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
251
|
+
|
|
252
|
+
expect(footerData[0][2]).toBeNull()
|
|
253
|
+
expect(footerData[1][2]).toBeNull()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should handle zero counts', () => {
|
|
257
|
+
const { generateFooterData, setServerSummary } = useTableSummary(mockColumns)
|
|
258
|
+
|
|
259
|
+
setServerSummary({ total: 0 })
|
|
260
|
+
|
|
261
|
+
const visibleColumns = [
|
|
262
|
+
{ field: undefined },
|
|
263
|
+
{ field: 'id', type: 'INTEGER' }
|
|
264
|
+
]
|
|
265
|
+
const selectedSummary = { _count: 0 }
|
|
266
|
+
|
|
267
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
268
|
+
|
|
269
|
+
expect(footerData[0][1]).toBe('0 条')
|
|
270
|
+
expect(footerData[1][1]).toBe('0 条')
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('setServerSummary', () => {
|
|
275
|
+
it('should set server summary data', () => {
|
|
276
|
+
const { serverSummary, setServerSummary } = useTableSummary(mockColumns)
|
|
277
|
+
|
|
278
|
+
const summaryData = { total: 100, amount: 10000 }
|
|
279
|
+
setServerSummary(summaryData)
|
|
280
|
+
|
|
281
|
+
expect(serverSummary.value).toEqual(summaryData)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should handle null server summary', () => {
|
|
285
|
+
const { serverSummary, setServerSummary } = useTableSummary(mockColumns)
|
|
286
|
+
|
|
287
|
+
setServerSummary(null)
|
|
288
|
+
|
|
289
|
+
expect(serverSummary.value).toBeNull()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should update existing server summary', () => {
|
|
293
|
+
const { serverSummary, setServerSummary } = useTableSummary(mockColumns)
|
|
294
|
+
|
|
295
|
+
setServerSummary({ total: 50 })
|
|
296
|
+
expect(serverSummary.value?.total).toBe(50)
|
|
297
|
+
|
|
298
|
+
setServerSummary({ total: 100 })
|
|
299
|
+
expect(serverSummary.value?.total).toBe(100)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('Integration', () => {
|
|
304
|
+
it('should work together for complete workflow', () => {
|
|
305
|
+
const {
|
|
306
|
+
measureColumns,
|
|
307
|
+
calculateSelectedSummary,
|
|
308
|
+
generateFooterData,
|
|
309
|
+
setServerSummary
|
|
310
|
+
} = useTableSummary(mockColumns)
|
|
311
|
+
|
|
312
|
+
// 1. 识别度量列
|
|
313
|
+
expect(measureColumns.value).toHaveLength(3) // id, amount, count
|
|
314
|
+
|
|
315
|
+
// 2. 设置服务端汇总
|
|
316
|
+
setServerSummary({ total: 100, amount: 50000, count: 1000 })
|
|
317
|
+
|
|
318
|
+
// 3. 计算选中行汇总
|
|
319
|
+
const selectedRows = [
|
|
320
|
+
{ id: 1, amount: 100, count: 5 },
|
|
321
|
+
{ id: 2, amount: 200, count: 10 }
|
|
322
|
+
]
|
|
323
|
+
const selectedSummary = calculateSelectedSummary(selectedRows)
|
|
324
|
+
|
|
325
|
+
expect(selectedSummary._count).toBe(2)
|
|
326
|
+
expect(selectedSummary.amount).toBe(300)
|
|
327
|
+
|
|
328
|
+
// 4. 生成 footer 数据
|
|
329
|
+
const visibleColumns = [
|
|
330
|
+
{ field: undefined },
|
|
331
|
+
{ field: 'id', type: 'INTEGER' },
|
|
332
|
+
{ field: 'amount', type: 'MONEY' }
|
|
333
|
+
]
|
|
334
|
+
const footerData = generateFooterData(visibleColumns, selectedSummary)
|
|
335
|
+
|
|
336
|
+
expect(footerData).toHaveLength(2)
|
|
337
|
+
expect(footerData[0][1]).toBe('2 条')
|
|
338
|
+
expect(footerData[1][1]).toBe('100 条')
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ref, computed, type Ref } from 'vue'
|
|
2
|
+
import type { ColumnSchema } from '@/types'
|
|
3
|
+
|
|
4
|
+
/** 度量类型列表 */
|
|
5
|
+
const MEASURE_TYPES = ['NUMBER', 'MONEY', 'BIGDECIMAL', 'INTEGER', 'BIGINT', 'LONG']
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 表格汇总数据计算逻辑
|
|
9
|
+
*/
|
|
10
|
+
export function useTableSummary(columns: Ref<ColumnSchema[]>) {
|
|
11
|
+
/** 后端返回的全量汇总 */
|
|
12
|
+
const serverSummary = ref<Record<string, unknown> | null>(null)
|
|
13
|
+
|
|
14
|
+
/** 度量列(用于汇总计算) */
|
|
15
|
+
const measureColumns = computed(() => {
|
|
16
|
+
return columns.value.filter(col =>
|
|
17
|
+
MEASURE_TYPES.includes(col.type?.toUpperCase() || '')
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 计算选中行的汇总
|
|
23
|
+
*/
|
|
24
|
+
function calculateSelectedSummary(
|
|
25
|
+
selectedRows: Record<string, unknown>[]
|
|
26
|
+
): Record<string, unknown> {
|
|
27
|
+
const summary: Record<string, unknown> = {
|
|
28
|
+
_count: selectedRows.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const col of measureColumns.value) {
|
|
32
|
+
summary[col.name] = selectedRows.reduce((sum, row) => {
|
|
33
|
+
const val = row[col.name]
|
|
34
|
+
return sum + (typeof val === 'number' ? val : 0)
|
|
35
|
+
}, 0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return summary
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 格式化数值用于显示
|
|
43
|
+
*/
|
|
44
|
+
function formatValue(value: unknown, type?: string): string {
|
|
45
|
+
if (value == null) return ''
|
|
46
|
+
if (typeof value !== 'number') return String(value)
|
|
47
|
+
|
|
48
|
+
const upperType = type?.toUpperCase()
|
|
49
|
+
if (upperType === 'MONEY' || upperType === 'NUMBER' || upperType === 'BIGDECIMAL') {
|
|
50
|
+
return value.toLocaleString('zh-CN', {
|
|
51
|
+
minimumFractionDigits: 2,
|
|
52
|
+
maximumFractionDigits: 2
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
return value.toLocaleString('zh-CN')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 生成 footer 数据(二维数组)
|
|
60
|
+
* @param visibleColumns 可见列配置(包含 checkbox 列)
|
|
61
|
+
* @param selectedSummary 选中行汇总
|
|
62
|
+
* @returns footer 数据,第一行选中汇总,第二行全量汇总
|
|
63
|
+
*/
|
|
64
|
+
function generateFooterData(
|
|
65
|
+
visibleColumns: { field?: string; type?: string }[],
|
|
66
|
+
selectedSummary: Record<string, unknown>
|
|
67
|
+
): (string | number | null)[][] {
|
|
68
|
+
const row1: (string | number | null)[] = [] // 选中汇总
|
|
69
|
+
const row2: (string | number | null)[] = [] // 全量汇总
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < visibleColumns.length; i++) {
|
|
72
|
+
const col = visibleColumns[i]
|
|
73
|
+
const field = col.field
|
|
74
|
+
|
|
75
|
+
// 第一列(checkbox 列)显示标签
|
|
76
|
+
if (i === 0) {
|
|
77
|
+
row1.push('选中')
|
|
78
|
+
row2.push('合计')
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 第二列显示记录数
|
|
83
|
+
if (i === 1) {
|
|
84
|
+
const selectedCount = selectedSummary._count as number || 0
|
|
85
|
+
const totalCount = serverSummary.value?.total as number || 0
|
|
86
|
+
row1.push(`${selectedCount} 条`)
|
|
87
|
+
row2.push(`${totalCount} 条`)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 其他列:如果是度量列则显示汇总值
|
|
92
|
+
if (field) {
|
|
93
|
+
const colSchema = columns.value.find(c => c.name === field)
|
|
94
|
+
const isMeasure = colSchema && MEASURE_TYPES.includes(colSchema.type?.toUpperCase() || '')
|
|
95
|
+
|
|
96
|
+
if (isMeasure) {
|
|
97
|
+
const selectedVal = selectedSummary[field]
|
|
98
|
+
const serverVal = serverSummary.value?.[field]
|
|
99
|
+
row1.push(formatValue(selectedVal, colSchema?.type))
|
|
100
|
+
row2.push(formatValue(serverVal, colSchema?.type))
|
|
101
|
+
} else {
|
|
102
|
+
row1.push(null)
|
|
103
|
+
row2.push(null)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
row1.push(null)
|
|
107
|
+
row2.push(null)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [row1, row2]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 设置服务端汇总数据
|
|
116
|
+
*/
|
|
117
|
+
function setServerSummary(data: Record<string, unknown> | null) {
|
|
118
|
+
serverSummary.value = data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
serverSummary,
|
|
123
|
+
measureColumns,
|
|
124
|
+
calculateSelectedSummary,
|
|
125
|
+
generateFooterData,
|
|
126
|
+
setServerSummary,
|
|
127
|
+
formatValue
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import type { SliceRequestDef } from '@/types'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
field: string
|
|
7
|
+
modelValue?: SliceRequestDef[] | null
|
|
8
|
+
trueLabel?: string
|
|
9
|
+
falseLabel?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
trueLabel: '是',
|
|
14
|
+
falseLabel: '否'
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: 'update:modelValue', value: SliceRequestDef[] | null): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const selectedValue = ref<boolean | null>(null)
|
|
22
|
+
|
|
23
|
+
// 从 modelValue 初始化
|
|
24
|
+
watch(() => props.modelValue, (slices) => {
|
|
25
|
+
if (!slices || slices.length === 0) {
|
|
26
|
+
selectedValue.value = null
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const slice = slices[0]
|
|
31
|
+
if (slice.op === '=') {
|
|
32
|
+
selectedValue.value = slice.value === true || slice.value === 'true' || slice.value === 1
|
|
33
|
+
} else {
|
|
34
|
+
selectedValue.value = null
|
|
35
|
+
}
|
|
36
|
+
}, { immediate: true })
|
|
37
|
+
|
|
38
|
+
function select(val: boolean | null) {
|
|
39
|
+
selectedValue.value = val
|
|
40
|
+
if (val === null) {
|
|
41
|
+
emit('update:modelValue', null)
|
|
42
|
+
} else {
|
|
43
|
+
emit('update:modelValue', [{
|
|
44
|
+
field: props.field,
|
|
45
|
+
op: '=',
|
|
46
|
+
value: val
|
|
47
|
+
}])
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div class="filter-bool">
|
|
54
|
+
<button
|
|
55
|
+
:class="{ active: selectedValue === null }"
|
|
56
|
+
@click="select(null)"
|
|
57
|
+
>
|
|
58
|
+
全部
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
:class="{ active: selectedValue === true }"
|
|
62
|
+
@click="select(true)"
|
|
63
|
+
>
|
|
64
|
+
{{ trueLabel }}
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
:class="{ active: selectedValue === false }"
|
|
68
|
+
@click="select(false)"
|
|
69
|
+
>
|
|
70
|
+
{{ falseLabel }}
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.filter-bool {
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: 4px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.filter-bool button {
|
|
82
|
+
height: 26px;
|
|
83
|
+
padding: 0 8px;
|
|
84
|
+
border: 1px solid #dcdfe6;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
background: white;
|
|
87
|
+
font-size: 11px;
|
|
88
|
+
color: #606266;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: all 0.2s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.filter-bool button:hover {
|
|
94
|
+
border-color: #409eff;
|
|
95
|
+
color: #409eff;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.filter-bool button.active {
|
|
99
|
+
background: #409eff;
|
|
100
|
+
border-color: #409eff;
|
|
101
|
+
color: white;
|
|
102
|
+
}
|
|
103
|
+
</style>
|