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,136 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, h } from 'vue'
|
|
3
|
+
import DataTable from '@/components/DataTable.vue'
|
|
4
|
+
import { fetchQueryMeta, fetchQueryData } from '@/api/viewer'
|
|
5
|
+
import { buildTableColumns } from '@/utils/schemaHelper'
|
|
6
|
+
import type { EnhancedColumnSchema, TableConfig } from '@/types'
|
|
7
|
+
|
|
8
|
+
// 示例1: 显式指定列及顺序
|
|
9
|
+
const tableConfig: TableConfig = {
|
|
10
|
+
visibleColumns: ['orderId', 'orderDate', 'customerName', 'amount', 'status', 'isPaid', 'metadata'],
|
|
11
|
+
customizations: [
|
|
12
|
+
{
|
|
13
|
+
name: 'orderId',
|
|
14
|
+
width: 150,
|
|
15
|
+
fixed: 'left'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'orderDate',
|
|
19
|
+
width: 120
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'customerName',
|
|
23
|
+
width: 150
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
// formatter: 用于格式化数据(导出时使用)
|
|
27
|
+
name: 'amount',
|
|
28
|
+
width: 120,
|
|
29
|
+
formatter: (value) => `¥${Number(value).toFixed(2)}`
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
// render: 用于自定义显示(仅前端显示,不影响导出)
|
|
33
|
+
name: 'status',
|
|
34
|
+
width: 100,
|
|
35
|
+
render: ({ value }) => {
|
|
36
|
+
const statusMap: Record<string, { text: string; color: string }> = {
|
|
37
|
+
paid: { text: '已支付', color: '#67c23a' },
|
|
38
|
+
pending: { text: '待支付', color: '#e6a23c' },
|
|
39
|
+
cancelled: { text: '已取消', color: '#909399' }
|
|
40
|
+
}
|
|
41
|
+
const status = statusMap[value as string] || { text: value as string, color: '#909399' }
|
|
42
|
+
return h('span', { style: { color: status.color, fontWeight: 'bold' } }, status.text)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
// render: 将 true 渲染成勾符号
|
|
47
|
+
name: 'isPaid',
|
|
48
|
+
width: 80,
|
|
49
|
+
render: ({ value }) => {
|
|
50
|
+
return h('span', {
|
|
51
|
+
style: { fontSize: '18px', color: value ? '#67c23a' : '#f56c6c' }
|
|
52
|
+
}, value ? '✓' : '✗')
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
// formatter: 将 JSON 对象转成文字(用于导出)
|
|
57
|
+
name: 'metadata',
|
|
58
|
+
width: 200,
|
|
59
|
+
formatter: (value) => {
|
|
60
|
+
if (!value) return ''
|
|
61
|
+
if (typeof value === 'object') {
|
|
62
|
+
return JSON.stringify(value)
|
|
63
|
+
}
|
|
64
|
+
return String(value)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 示例2: 显示所有列(按 QM schema 顺序)
|
|
71
|
+
const tableConfigShowAll: TableConfig = {
|
|
72
|
+
showAll: true,
|
|
73
|
+
customizations: [
|
|
74
|
+
{
|
|
75
|
+
name: 'orderId',
|
|
76
|
+
width: 150,
|
|
77
|
+
fixed: 'left'
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const columns = ref<EnhancedColumnSchema[]>([])
|
|
83
|
+
const data = ref<Record<string, unknown>[]>([])
|
|
84
|
+
const total = ref(0)
|
|
85
|
+
const loading = ref(false)
|
|
86
|
+
|
|
87
|
+
async function loadData() {
|
|
88
|
+
try {
|
|
89
|
+
loading.value = true
|
|
90
|
+
|
|
91
|
+
// 1. 获取 QM schema
|
|
92
|
+
const meta = await fetchQueryMeta('your-query-id')
|
|
93
|
+
|
|
94
|
+
// 2. 合并 schema 和定制参数
|
|
95
|
+
columns.value = buildTableColumns(meta.schema, tableConfig)
|
|
96
|
+
|
|
97
|
+
// 3. 加载数据
|
|
98
|
+
const response = await fetchQueryData('your-query-id', {
|
|
99
|
+
start: 0,
|
|
100
|
+
limit: 50
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
data.value = response.items
|
|
104
|
+
total.value = response.total
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Failed to load data:', error)
|
|
107
|
+
} finally {
|
|
108
|
+
loading.value = false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
loadData()
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<div class="example-container">
|
|
119
|
+
<h2>增强表格示例</h2>
|
|
120
|
+
<DataTable
|
|
121
|
+
:columns="columns"
|
|
122
|
+
:data="data"
|
|
123
|
+
:total="total"
|
|
124
|
+
:loading="loading"
|
|
125
|
+
@page-change="(page, size) => console.log('Page changed:', page, size)"
|
|
126
|
+
@sort-change="(field, order) => console.log('Sort changed:', field, order)"
|
|
127
|
+
@filter-change="(slices) => console.log('Filter changed:', slices)"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<style scoped>
|
|
133
|
+
.example-container {
|
|
134
|
+
padding: 20px;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// 统一引入所有依赖样式(用户只需引入一次)
|
|
2
|
+
import 'vxe-table/lib/style.css'
|
|
3
|
+
import 'element-plus/dist/index.css'
|
|
4
|
+
|
|
5
|
+
// 导出组件
|
|
6
|
+
export { default as DataTable } from './components/DataTable.vue'
|
|
7
|
+
export { default as DataViewer } from './components/DataViewer.vue'
|
|
8
|
+
export { default as SearchToolbar } from './components/SearchToolbar.vue'
|
|
9
|
+
export { default as DataTableWithSearch } from './components/DataTableWithSearch.vue'
|
|
10
|
+
|
|
11
|
+
// 导出过滤器组件
|
|
12
|
+
export * from './components/filters'
|
|
13
|
+
|
|
14
|
+
// 导出 Composables
|
|
15
|
+
export * from './components/composables'
|
|
16
|
+
|
|
17
|
+
// 导出工具函数
|
|
18
|
+
export { buildTableColumns } from './utils/schemaHelper'
|
|
19
|
+
|
|
20
|
+
// 导出类型定义
|
|
21
|
+
export type {
|
|
22
|
+
ColumnSchema,
|
|
23
|
+
EnhancedColumnSchema,
|
|
24
|
+
TableConfig,
|
|
25
|
+
ColumnCustomization,
|
|
26
|
+
QueryMetaResponse,
|
|
27
|
+
ViewerQueryRequest,
|
|
28
|
+
ViewerDataResponse,
|
|
29
|
+
SliceRequestDef,
|
|
30
|
+
OrderRequestDef,
|
|
31
|
+
FilterOption
|
|
32
|
+
} from './types'
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import VXETable from 'vxe-table'
|
|
3
|
+
import 'vxe-table/lib/style.css'
|
|
4
|
+
import ElementPlus from 'element-plus'
|
|
5
|
+
import 'element-plus/dist/index.css'
|
|
6
|
+
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
|
7
|
+
import App from './App.vue'
|
|
8
|
+
|
|
9
|
+
const app = createApp(App)
|
|
10
|
+
|
|
11
|
+
app.use(VXETable)
|
|
12
|
+
app.use(ElementPlus, { locale: zhCn })
|
|
13
|
+
|
|
14
|
+
app.mount('#app')
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字典项
|
|
3
|
+
*/
|
|
4
|
+
export interface DictItem {
|
|
5
|
+
value: string | number
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 列定义类型
|
|
11
|
+
*/
|
|
12
|
+
export interface ColumnSchema {
|
|
13
|
+
name: string
|
|
14
|
+
type: string
|
|
15
|
+
title?: string
|
|
16
|
+
filterable?: boolean
|
|
17
|
+
aggregatable?: boolean
|
|
18
|
+
|
|
19
|
+
// 过滤器元数据
|
|
20
|
+
filterType?: 'text' | 'number' | 'date' | 'datetime' | 'dict' | 'dimension' | 'bool' | 'custom'
|
|
21
|
+
dictId?: string
|
|
22
|
+
dictItems?: DictItem[]
|
|
23
|
+
dimensionRef?: string
|
|
24
|
+
format?: string
|
|
25
|
+
measure?: boolean
|
|
26
|
+
uiConfig?: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* DSL 过滤条件 (SliceRequestDef)
|
|
31
|
+
* 直接对应后端 DSL 格式
|
|
32
|
+
*/
|
|
33
|
+
export interface SliceRequestDef {
|
|
34
|
+
field: string
|
|
35
|
+
op: string // =, !=, >, >=, <, <=, in, like, right_like, [], [), is null, is not null 等
|
|
36
|
+
value?: unknown
|
|
37
|
+
link?: 1 | 2 // 1=AND, 2=OR
|
|
38
|
+
children?: SliceRequestDef[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DSL 排序条件 (OrderRequestDef)
|
|
43
|
+
*/
|
|
44
|
+
export interface OrderRequestDef {
|
|
45
|
+
field: string
|
|
46
|
+
order: 'asc' | 'desc'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 查询元数据响应(重构后的版本)
|
|
51
|
+
*/
|
|
52
|
+
export interface QueryMetaResponse {
|
|
53
|
+
title: string
|
|
54
|
+
tableConfig: TableConfig // 改为 tableConfig
|
|
55
|
+
estimatedRowCount: number | null
|
|
56
|
+
expiresAt: string
|
|
57
|
+
/** 初始过滤条件(来自缓存) */
|
|
58
|
+
initialSlice?: SliceRequestDef[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 数据查询请求 (使用 DSL 格式)
|
|
63
|
+
*/
|
|
64
|
+
export interface ViewerQueryRequest {
|
|
65
|
+
start?: number
|
|
66
|
+
limit?: number
|
|
67
|
+
/** 过滤条件 (DSL slice 格式) */
|
|
68
|
+
slice?: SliceRequestDef[]
|
|
69
|
+
/** 排序条件 (DSL orderBy 格式) */
|
|
70
|
+
orderBy?: OrderRequestDef[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 过滤选项(用于下拉)
|
|
75
|
+
*/
|
|
76
|
+
export interface FilterOption {
|
|
77
|
+
value: string | number
|
|
78
|
+
label: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 过滤选项响应
|
|
83
|
+
*/
|
|
84
|
+
export interface FilterOptionsResponse {
|
|
85
|
+
options: FilterOption[]
|
|
86
|
+
total: number
|
|
87
|
+
error?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 数据响应
|
|
92
|
+
*/
|
|
93
|
+
export interface ViewerDataResponse {
|
|
94
|
+
success: boolean
|
|
95
|
+
items: Record<string, unknown>[]
|
|
96
|
+
total: number
|
|
97
|
+
start: number
|
|
98
|
+
limit: number
|
|
99
|
+
errorMessage?: string
|
|
100
|
+
expired?: boolean
|
|
101
|
+
/** 全量数据汇总(包含总记录数和度量合计) */
|
|
102
|
+
totalData?: Record<string, unknown>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 分页状态
|
|
107
|
+
*/
|
|
108
|
+
export interface PaginationState {
|
|
109
|
+
currentPage: number
|
|
110
|
+
pageSize: number
|
|
111
|
+
total: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 排序状态
|
|
116
|
+
*/
|
|
117
|
+
export interface SortState {
|
|
118
|
+
field: string | null
|
|
119
|
+
order: 'asc' | 'desc' | null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 列定制配置
|
|
124
|
+
*/
|
|
125
|
+
export interface ColumnCustomization {
|
|
126
|
+
name: string
|
|
127
|
+
width?: number
|
|
128
|
+
minWidth?: number
|
|
129
|
+
fixed?: 'left' | 'right'
|
|
130
|
+
render?: (params: { row: Record<string, unknown>; value: unknown }) => unknown
|
|
131
|
+
filterComponent?: unknown
|
|
132
|
+
formatter?: (value: unknown) => string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 增强的列配置(合并 QM schema 和前端定制)
|
|
137
|
+
*/
|
|
138
|
+
export interface EnhancedColumnSchema extends ColumnSchema {
|
|
139
|
+
width?: number
|
|
140
|
+
minWidth?: number
|
|
141
|
+
fixed?: 'left' | 'right'
|
|
142
|
+
customRender?: (params: { row: Record<string, unknown>; value: unknown }) => unknown
|
|
143
|
+
customFilterComponent?: unknown
|
|
144
|
+
customFormatter?: (value: unknown) => string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 表格配置
|
|
149
|
+
*/
|
|
150
|
+
export interface TableConfig {
|
|
151
|
+
/** QM 模型名称 */
|
|
152
|
+
qmModel: string
|
|
153
|
+
/** 显式指定显示的列及顺序(必填,除非 showAll=true) */
|
|
154
|
+
visibleColumns?: string[]
|
|
155
|
+
/** 显示所有列 */
|
|
156
|
+
showAll?: boolean
|
|
157
|
+
/** 列定制配置 */
|
|
158
|
+
customizations?: ColumnCustomization[]
|
|
159
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Schema Helper 使用说明
|
|
2
|
+
|
|
3
|
+
## 核心概念
|
|
4
|
+
|
|
5
|
+
### formatter vs render
|
|
6
|
+
|
|
7
|
+
- **formatter**: 用于数据格式化,影响**导出**和**显示**
|
|
8
|
+
- 返回值:`string`
|
|
9
|
+
- 使用场景:格式化金额、日期、将 JSON 转文字等
|
|
10
|
+
- 示例:`formatter: (v) => '¥' + v`
|
|
11
|
+
- **不会修改原始数据**
|
|
12
|
+
|
|
13
|
+
- **render**: 用于自定义渲染,仅影响**前端显示**
|
|
14
|
+
- 返回值:`VNode | string`
|
|
15
|
+
- 使用场景:添加图标、颜色、特殊样式等
|
|
16
|
+
- 示例:`render: ({ value }) => h('span', { style: { color: 'red' } }, value)`
|
|
17
|
+
- **不会修改原始数据**
|
|
18
|
+
|
|
19
|
+
> **重要**:
|
|
20
|
+
> - formatter 和 render 都只影响显示/导出,**不会修改 data 中的原始数据**
|
|
21
|
+
> - 有 formatter 一般不需要 render,但不绝对
|
|
22
|
+
> - 如果同时存在,render 用于显示,formatter 用于导出
|
|
23
|
+
|
|
24
|
+
## 基本用法
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { buildTableColumns } from '@/utils/schemaHelper'
|
|
28
|
+
import type { TableConfig } from '@/types'
|
|
29
|
+
|
|
30
|
+
// 1. 定义表格配置
|
|
31
|
+
const config: TableConfig = {
|
|
32
|
+
// 必须指定显示的列及顺序
|
|
33
|
+
visibleColumns: ['id', 'name', 'amount', 'status'],
|
|
34
|
+
|
|
35
|
+
// 或者显示所有列
|
|
36
|
+
// showAll: true,
|
|
37
|
+
|
|
38
|
+
// 列定制
|
|
39
|
+
customizations: [
|
|
40
|
+
{
|
|
41
|
+
name: 'id',
|
|
42
|
+
width: 150,
|
|
43
|
+
fixed: 'left'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'amount',
|
|
47
|
+
formatter: (v) => `¥${Number(v).toFixed(2)}`
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'status',
|
|
51
|
+
render: ({ value }) => h('span', {
|
|
52
|
+
style: { color: value === 'active' ? 'green' : 'red' }
|
|
53
|
+
}, value)
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. 合并 QM schema 和定制配置
|
|
59
|
+
const columns = buildTableColumns(qmSchema, config)
|
|
60
|
+
|
|
61
|
+
// 3. 传给 DataTable 组件
|
|
62
|
+
<DataTable :columns="columns" :data="data" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 常见场景
|
|
66
|
+
|
|
67
|
+
### 场景1: 布尔值渲染成符号
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
{
|
|
71
|
+
name: 'isPaid',
|
|
72
|
+
render: ({ value }) => h('span', {
|
|
73
|
+
style: { fontSize: '18px', color: value ? '#67c23a' : '#f56c6c' }
|
|
74
|
+
}, value ? '✓' : '✗')
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 场景2: JSON 导出为文字
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
{
|
|
82
|
+
name: 'metadata',
|
|
83
|
+
formatter: (value) => {
|
|
84
|
+
if (typeof value === 'object') {
|
|
85
|
+
return JSON.stringify(value)
|
|
86
|
+
}
|
|
87
|
+
return String(value)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 场景3: 状态带颜色显示
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
{
|
|
96
|
+
name: 'status',
|
|
97
|
+
render: ({ value }) => {
|
|
98
|
+
const colors = { success: '#67c23a', warning: '#e6a23c', error: '#f56c6c' }
|
|
99
|
+
return h('span', { style: { color: colors[value] } }, value)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 场景4: 同时使用 formatter 和 render
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
{
|
|
108
|
+
name: 'price',
|
|
109
|
+
// 导出时格式化为文字
|
|
110
|
+
formatter: (v) => `¥${Number(v).toFixed(2)}`,
|
|
111
|
+
// 显示时添加样式
|
|
112
|
+
render: ({ value }) => h('span', {
|
|
113
|
+
style: { fontWeight: 'bold', color: '#e6a23c' }
|
|
114
|
+
}, `¥${Number(value).toFixed(2)}`)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 配置选项
|
|
119
|
+
|
|
120
|
+
### TableConfig
|
|
121
|
+
|
|
122
|
+
| 属性 | 类型 | 必填 | 说明 |
|
|
123
|
+
|------|------|------|------|
|
|
124
|
+
| visibleColumns | string[] | 是* | 显示的列及顺序 |
|
|
125
|
+
| showAll | boolean | 是* | 显示所有列 |
|
|
126
|
+
| customizations | ColumnCustomization[] | 否 | 列定制配置 |
|
|
127
|
+
|
|
128
|
+
*注:`visibleColumns` 和 `showAll` 必须二选一
|
|
129
|
+
|
|
130
|
+
### ColumnCustomization
|
|
131
|
+
|
|
132
|
+
| 属性 | 类型 | 说明 |
|
|
133
|
+
|------|------|------|
|
|
134
|
+
| name | string | 列名(必填) |
|
|
135
|
+
| width | number | 列宽 |
|
|
136
|
+
| minWidth | number | 最小列宽 |
|
|
137
|
+
| fixed | 'left' \| 'right' | 固定列 |
|
|
138
|
+
| formatter | (value) => string | 格式化函数(用于导出) |
|
|
139
|
+
| render | ({ row, value }) => VNode \| string | 渲染函数(用于显示) |
|
|
140
|
+
| filterComponent | Component | 自定义过滤器组件 |
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { buildTableColumns } from './schemaHelper'
|
|
3
|
+
import type { ColumnSchema, TableConfig } from '@/types'
|
|
4
|
+
|
|
5
|
+
describe('schemaHelper', () => {
|
|
6
|
+
const mockQMSchema: ColumnSchema[] = [
|
|
7
|
+
{ name: 'id', type: 'INTEGER', title: 'ID' },
|
|
8
|
+
{ name: 'name', type: 'TEXT', title: '名称' },
|
|
9
|
+
{ name: 'amount', type: 'MONEY', title: '金额' },
|
|
10
|
+
{ name: 'status', type: 'TEXT', title: '状态' }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('buildTableColumns', () => {
|
|
14
|
+
it('should return all columns when config is empty (defaults to showAll)', () => {
|
|
15
|
+
const config: TableConfig = {}
|
|
16
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
17
|
+
|
|
18
|
+
// 空配置时默认显示所有列
|
|
19
|
+
expect(result).toHaveLength(4)
|
|
20
|
+
expect(result[0].name).toBe('id')
|
|
21
|
+
expect(result[1].name).toBe('name')
|
|
22
|
+
expect(result[2].name).toBe('amount')
|
|
23
|
+
expect(result[3].name).toBe('status')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return all columns when showAll is true', () => {
|
|
27
|
+
const config: TableConfig = { showAll: true }
|
|
28
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
29
|
+
|
|
30
|
+
expect(result).toHaveLength(4)
|
|
31
|
+
expect(result[0].name).toBe('id')
|
|
32
|
+
expect(result[1].name).toBe('name')
|
|
33
|
+
expect(result[2].name).toBe('amount')
|
|
34
|
+
expect(result[3].name).toBe('status')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should return columns in specified order', () => {
|
|
38
|
+
const config: TableConfig = {
|
|
39
|
+
visibleColumns: ['amount', 'name', 'id']
|
|
40
|
+
}
|
|
41
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
42
|
+
|
|
43
|
+
expect(result).toHaveLength(3)
|
|
44
|
+
expect(result[0].name).toBe('amount')
|
|
45
|
+
expect(result[1].name).toBe('name')
|
|
46
|
+
expect(result[2].name).toBe('id')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should apply width customization', () => {
|
|
50
|
+
const config: TableConfig = {
|
|
51
|
+
visibleColumns: ['id', 'name'],
|
|
52
|
+
customizations: [
|
|
53
|
+
{ name: 'id', width: 150 },
|
|
54
|
+
{ name: 'name', width: 200 }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
58
|
+
|
|
59
|
+
expect(result[0].width).toBe(150)
|
|
60
|
+
expect(result[1].width).toBe(200)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should apply minWidth customization with default fallback', () => {
|
|
64
|
+
const config: TableConfig = {
|
|
65
|
+
visibleColumns: ['id', 'name'],
|
|
66
|
+
customizations: [
|
|
67
|
+
{ name: 'id', minWidth: 100 }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
71
|
+
|
|
72
|
+
expect(result[0].minWidth).toBe(100)
|
|
73
|
+
expect(result[1].minWidth).toBe(120) // default
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should apply fixed column customization', () => {
|
|
77
|
+
const config: TableConfig = {
|
|
78
|
+
visibleColumns: ['id', 'name', 'amount'],
|
|
79
|
+
customizations: [
|
|
80
|
+
{ name: 'id', fixed: 'left' },
|
|
81
|
+
{ name: 'amount', fixed: 'right' }
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
85
|
+
|
|
86
|
+
expect(result[0].fixed).toBe('left')
|
|
87
|
+
expect(result[1].fixed).toBeUndefined()
|
|
88
|
+
expect(result[2].fixed).toBe('right')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should apply formatter customization', () => {
|
|
92
|
+
const formatter = (v: unknown) => `¥${v}`
|
|
93
|
+
const config: TableConfig = {
|
|
94
|
+
visibleColumns: ['amount'],
|
|
95
|
+
customizations: [
|
|
96
|
+
{ name: 'amount', formatter }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
100
|
+
|
|
101
|
+
expect(result[0].customFormatter).toBe(formatter)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should apply render customization', () => {
|
|
105
|
+
const render = ({ value }: { value: unknown }) => String(value)
|
|
106
|
+
const config: TableConfig = {
|
|
107
|
+
visibleColumns: ['status'],
|
|
108
|
+
customizations: [
|
|
109
|
+
{ name: 'status', render }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
113
|
+
|
|
114
|
+
expect(result[0].customRender).toBe(render)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should apply filterComponent customization', () => {
|
|
118
|
+
const filterComponent = {}
|
|
119
|
+
const config: TableConfig = {
|
|
120
|
+
visibleColumns: ['status'],
|
|
121
|
+
customizations: [
|
|
122
|
+
{ name: 'status', filterComponent }
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
126
|
+
|
|
127
|
+
expect(result[0].customFilterComponent).toBe(filterComponent)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should warn when column not found in schema', () => {
|
|
131
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
132
|
+
|
|
133
|
+
const config: TableConfig = {
|
|
134
|
+
visibleColumns: ['id', 'nonexistent', 'name']
|
|
135
|
+
}
|
|
136
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
137
|
+
|
|
138
|
+
expect(result).toHaveLength(2)
|
|
139
|
+
expect(result[0].name).toBe('id')
|
|
140
|
+
expect(result[1].name).toBe('name')
|
|
141
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
142
|
+
'Column "nonexistent" not found in QM schema'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
consoleSpy.mockRestore()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should handle empty visibleColumns (defaults to showAll)', () => {
|
|
149
|
+
const config: TableConfig = {
|
|
150
|
+
visibleColumns: []
|
|
151
|
+
}
|
|
152
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
153
|
+
|
|
154
|
+
// 空的 visibleColumns 时默认显示所有列
|
|
155
|
+
expect(result).toHaveLength(4)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should merge multiple customizations correctly', () => {
|
|
159
|
+
const formatter = (v: unknown) => `¥${v}`
|
|
160
|
+
const render = ({ value }: { value: unknown }) => String(value)
|
|
161
|
+
|
|
162
|
+
const config: TableConfig = {
|
|
163
|
+
visibleColumns: ['id', 'amount'],
|
|
164
|
+
customizations: [
|
|
165
|
+
{
|
|
166
|
+
name: 'id',
|
|
167
|
+
width: 150,
|
|
168
|
+
minWidth: 100,
|
|
169
|
+
fixed: 'left'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'amount',
|
|
173
|
+
width: 120,
|
|
174
|
+
formatter,
|
|
175
|
+
render
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
180
|
+
|
|
181
|
+
expect(result[0]).toMatchObject({
|
|
182
|
+
name: 'id',
|
|
183
|
+
width: 150,
|
|
184
|
+
minWidth: 100,
|
|
185
|
+
fixed: 'left'
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
expect(result[1]).toMatchObject({
|
|
189
|
+
name: 'amount',
|
|
190
|
+
width: 120
|
|
191
|
+
})
|
|
192
|
+
expect(result[1].customFormatter).toBe(formatter)
|
|
193
|
+
expect(result[1].customRender).toBe(render)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should preserve original schema properties', () => {
|
|
197
|
+
const config: TableConfig = {
|
|
198
|
+
visibleColumns: ['id', 'amount']
|
|
199
|
+
}
|
|
200
|
+
const result = buildTableColumns(mockQMSchema, config)
|
|
201
|
+
|
|
202
|
+
expect(result[0]).toMatchObject({
|
|
203
|
+
name: 'id',
|
|
204
|
+
type: 'INTEGER',
|
|
205
|
+
title: 'ID'
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect(result[1]).toMatchObject({
|
|
209
|
+
name: 'amount',
|
|
210
|
+
type: 'MONEY',
|
|
211
|
+
title: '金额'
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|