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
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foggy-data-viewer",
|
|
3
|
+
"version": "1.0.1-beta.0",
|
|
4
|
+
"description": "A Vue 3 data table component with advanced features",
|
|
5
|
+
"author": "Foggy Framework",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"private": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"./style.css": "./dist/style.css"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "vite",
|
|
26
|
+
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
27
|
+
"build:lib": "vite build --mode lib",
|
|
28
|
+
"preview": "vite preview",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"test:ui": "vitest --ui",
|
|
31
|
+
"test:coverage": "vitest --coverage"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"axios": "^1.6.0",
|
|
35
|
+
"element-plus": "^2.13.0",
|
|
36
|
+
"vue": "^3.4.0",
|
|
37
|
+
"vxe-table": "^4.6.0",
|
|
38
|
+
"xe-utils": "^3.5.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
42
|
+
"@vitest/coverage-v8": "^1.0.0",
|
|
43
|
+
"@vitest/ui": "^1.0.0",
|
|
44
|
+
"@vue/test-utils": "^2.4.0",
|
|
45
|
+
"happy-dom": "^12.10.0",
|
|
46
|
+
"typescript": "^5.3.0",
|
|
47
|
+
"vite": "^5.0.0",
|
|
48
|
+
"vitest": "^1.0.0",
|
|
49
|
+
"vue-tsc": "^1.8.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import DataViewer from './components/DataViewer.vue'
|
|
4
|
+
import { createQuery, type CreateQueryRequest } from './api/viewer'
|
|
5
|
+
|
|
6
|
+
// 从 URL 中获取 queryId
|
|
7
|
+
const queryId = computed(() => {
|
|
8
|
+
const path = window.location.pathname
|
|
9
|
+
// 匹配 /data-viewer/view/{queryId} 格式
|
|
10
|
+
const match = path.match(/\/data-viewer\/view\/([^/]+)/)
|
|
11
|
+
return match ? match[1] : null
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// DSL 输入相关状态
|
|
15
|
+
const dslInput = ref('')
|
|
16
|
+
const isSubmitting = ref(false)
|
|
17
|
+
const errorMessage = ref('')
|
|
18
|
+
|
|
19
|
+
// 示例 DSL 查询
|
|
20
|
+
const examples = [
|
|
21
|
+
{
|
|
22
|
+
name: '销售明细查询',
|
|
23
|
+
description: '查询2024年12月的销售订单明细',
|
|
24
|
+
dsl: {
|
|
25
|
+
model: 'FactSalesQueryModel',
|
|
26
|
+
title: '销售明细查询',
|
|
27
|
+
payload: {
|
|
28
|
+
columns: ['orderId', 'salesDate$caption', 'product$caption', 'customer$caption', 'quantity', 'salesAmount', 'profitAmount'],
|
|
29
|
+
slice: [
|
|
30
|
+
{ field: 'salesDate$caption', op: '>=', value: '2024-12-01' },
|
|
31
|
+
{ field: 'salesDate$caption', op: '<', value: '2024-12-31' }
|
|
32
|
+
],
|
|
33
|
+
orderBy: [{ field: 'salesDate$caption', order: 'desc' }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: '订单查询',
|
|
39
|
+
description: '查询2024年12月的订单信息',
|
|
40
|
+
dsl: {
|
|
41
|
+
model: 'FactOrderQueryModel',
|
|
42
|
+
title: '订单查询',
|
|
43
|
+
payload: {
|
|
44
|
+
columns: ['orderId', 'orderStatus', 'paymentStatus', 'orderTime', 'customer$caption', 'amount', 'payAmount'],
|
|
45
|
+
slice: [
|
|
46
|
+
{ field: 'orderDate$caption', op: '>=', value: '2024-12-01' },
|
|
47
|
+
{ field: 'orderDate$caption', op: '<', value: '2024-12-31' }
|
|
48
|
+
],
|
|
49
|
+
orderBy: [{ field: 'orderTime', order: 'desc' }]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: '商品列表',
|
|
55
|
+
description: '查询所有商品基础信息',
|
|
56
|
+
dsl: {
|
|
57
|
+
model: 'DimProductQueryModel',
|
|
58
|
+
title: '商品列表',
|
|
59
|
+
payload: {
|
|
60
|
+
columns: ['productName', 'productId', 'brand', 'categoryName', 'subCategoryName', 'unitPrice', 'unitCost'],
|
|
61
|
+
slice: [
|
|
62
|
+
{ field: 'status', op: '=', value: '正常' }
|
|
63
|
+
],
|
|
64
|
+
orderBy: [{ field: 'productName', order: 'asc' }]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: '客户列表',
|
|
70
|
+
description: '查询VIP会员客户',
|
|
71
|
+
dsl: {
|
|
72
|
+
model: 'DimCustomerQueryModel',
|
|
73
|
+
title: 'VIP客户列表',
|
|
74
|
+
payload: {
|
|
75
|
+
columns: ['customerName', 'customerId', 'customerType', 'memberLevel', 'gender', 'province', 'city'],
|
|
76
|
+
slice: [
|
|
77
|
+
{ field: 'memberLevel', op: '=', value: 'VIP' }
|
|
78
|
+
],
|
|
79
|
+
orderBy: [{ field: 'customerName', order: 'asc' }]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: '门店业绩',
|
|
85
|
+
description: '2024年Q4按门店汇总销售业绩',
|
|
86
|
+
dsl: {
|
|
87
|
+
model: 'FactSalesQueryModel',
|
|
88
|
+
title: '门店业绩汇总',
|
|
89
|
+
payload: {
|
|
90
|
+
columns: ['store$caption', 'store$storeType', 'store$province', 'store$city', 'quantity', 'salesAmount', 'profitAmount'],
|
|
91
|
+
slice: [
|
|
92
|
+
{ field: 'salesDate$caption', op: '>=', value: '2024-10-01' },
|
|
93
|
+
{ field: 'salesDate$caption', op: '<', value: '2024-12-31' }
|
|
94
|
+
],
|
|
95
|
+
groupBy: [{ field: 'store$id' }],
|
|
96
|
+
orderBy: [{ field: 'salesAmount', order: 'desc' }]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
// 获取相对日期(用于示例)
|
|
103
|
+
function getDateOffset(days: number): string {
|
|
104
|
+
const date = new Date()
|
|
105
|
+
date.setDate(date.getDate() + days)
|
|
106
|
+
return date.toISOString().split('T')[0]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 选择示例
|
|
110
|
+
function selectExample(example: typeof examples[0]) {
|
|
111
|
+
dslInput.value = JSON.stringify(example.dsl, null, 2)
|
|
112
|
+
errorMessage.value = ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 提交查询
|
|
116
|
+
async function submitQuery() {
|
|
117
|
+
errorMessage.value = ''
|
|
118
|
+
|
|
119
|
+
if (!dslInput.value.trim()) {
|
|
120
|
+
errorMessage.value = '请输入查询 DSL'
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let request: CreateQueryRequest
|
|
125
|
+
try {
|
|
126
|
+
request = JSON.parse(dslInput.value)
|
|
127
|
+
} catch (e) {
|
|
128
|
+
errorMessage.value = 'JSON 格式错误: ' + (e as Error).message
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isSubmitting.value = true
|
|
133
|
+
try {
|
|
134
|
+
const response = await createQuery(request)
|
|
135
|
+
if (response.success && response.viewerUrl) {
|
|
136
|
+
// 跳转到查询页面
|
|
137
|
+
window.location.href = response.viewerUrl
|
|
138
|
+
} else {
|
|
139
|
+
errorMessage.value = response.error || '创建查询失败'
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
errorMessage.value = '请求失败: ' + (e as Error).message
|
|
143
|
+
} finally {
|
|
144
|
+
isSubmitting.value = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<div id="app">
|
|
151
|
+
<DataViewer v-if="queryId" :query-id="queryId" />
|
|
152
|
+
<div v-else class="landing-page">
|
|
153
|
+
<div class="hero">
|
|
154
|
+
<h1>Foggy Data Viewer</h1>
|
|
155
|
+
<p class="subtitle">请通过有效的查询链接访问数据浏览器,或者输入 Foggy DSL 查询参数来浏览数据</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="main-content">
|
|
159
|
+
<div class="examples-section">
|
|
160
|
+
<h2>示例查询</h2>
|
|
161
|
+
<p class="section-desc">点击示例将 DSL 填入输入框</p>
|
|
162
|
+
<div class="examples-grid">
|
|
163
|
+
<div
|
|
164
|
+
v-for="example in examples"
|
|
165
|
+
:key="example.name"
|
|
166
|
+
class="example-card"
|
|
167
|
+
@click="selectExample(example)"
|
|
168
|
+
>
|
|
169
|
+
<h3>{{ example.name }}</h3>
|
|
170
|
+
<p>{{ example.description }}</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="input-section">
|
|
176
|
+
<h2>DSL 查询输入</h2>
|
|
177
|
+
<p class="section-desc">输入 JSON 格式的查询参数</p>
|
|
178
|
+
<textarea
|
|
179
|
+
v-model="dslInput"
|
|
180
|
+
class="dsl-textarea"
|
|
181
|
+
placeholder='{
|
|
182
|
+
"model": "FactSalesQueryModel",
|
|
183
|
+
"title": "我的查询",
|
|
184
|
+
"payload": {
|
|
185
|
+
"columns": ["orderId", "salesDate", "salesAmount"],
|
|
186
|
+
"slice": [
|
|
187
|
+
{ "field": "salesDate", "op": ">=", "value": "2024-01-01" }
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}'
|
|
191
|
+
:disabled="isSubmitting"
|
|
192
|
+
></textarea>
|
|
193
|
+
|
|
194
|
+
<div v-if="errorMessage" class="error-message">
|
|
195
|
+
{{ errorMessage }}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<button
|
|
199
|
+
class="submit-btn"
|
|
200
|
+
@click="submitQuery"
|
|
201
|
+
:disabled="isSubmitting"
|
|
202
|
+
>
|
|
203
|
+
{{ isSubmitting ? '提交中...' : '提交查询' }}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div class="help-section">
|
|
209
|
+
<h3>DSL 参数说明</h3>
|
|
210
|
+
<table class="help-table">
|
|
211
|
+
<thead>
|
|
212
|
+
<tr>
|
|
213
|
+
<th>参数</th>
|
|
214
|
+
<th>类型</th>
|
|
215
|
+
<th>必填</th>
|
|
216
|
+
<th>说明</th>
|
|
217
|
+
</tr>
|
|
218
|
+
</thead>
|
|
219
|
+
<tbody>
|
|
220
|
+
<tr>
|
|
221
|
+
<td><code>model</code></td>
|
|
222
|
+
<td>string</td>
|
|
223
|
+
<td>是</td>
|
|
224
|
+
<td>查询模型名称,如 FactSalesQueryModel</td>
|
|
225
|
+
</tr>
|
|
226
|
+
<tr>
|
|
227
|
+
<td><code>payload</code></td>
|
|
228
|
+
<td>object</td>
|
|
229
|
+
<td>是</td>
|
|
230
|
+
<td>查询参数对象(与 dataset.query_model 格式一致)</td>
|
|
231
|
+
</tr>
|
|
232
|
+
<tr>
|
|
233
|
+
<td><code>payload.columns</code></td>
|
|
234
|
+
<td>string[]</td>
|
|
235
|
+
<td>是</td>
|
|
236
|
+
<td>要查询的列名列表</td>
|
|
237
|
+
</tr>
|
|
238
|
+
<tr>
|
|
239
|
+
<td><code>payload.slice</code></td>
|
|
240
|
+
<td>object[]</td>
|
|
241
|
+
<td>是</td>
|
|
242
|
+
<td>过滤条件,每项包含 field、op、value</td>
|
|
243
|
+
</tr>
|
|
244
|
+
<tr>
|
|
245
|
+
<td><code>payload.groupBy</code></td>
|
|
246
|
+
<td>object[]</td>
|
|
247
|
+
<td>否</td>
|
|
248
|
+
<td>分组字段,用于聚合查询</td>
|
|
249
|
+
</tr>
|
|
250
|
+
<tr>
|
|
251
|
+
<td><code>payload.orderBy</code></td>
|
|
252
|
+
<td>object[]</td>
|
|
253
|
+
<td>否</td>
|
|
254
|
+
<td>排序字段,包含 field 和 order</td>
|
|
255
|
+
</tr>
|
|
256
|
+
<tr>
|
|
257
|
+
<td><code>title</code></td>
|
|
258
|
+
<td>string</td>
|
|
259
|
+
<td>否</td>
|
|
260
|
+
<td>查询标题</td>
|
|
261
|
+
</tr>
|
|
262
|
+
</tbody>
|
|
263
|
+
</table>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</template>
|
|
268
|
+
|
|
269
|
+
<style>
|
|
270
|
+
* {
|
|
271
|
+
margin: 0;
|
|
272
|
+
padding: 0;
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
html, body, #app {
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.landing-page {
|
|
283
|
+
min-height: 100%;
|
|
284
|
+
padding: 40px 20px;
|
|
285
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.hero {
|
|
289
|
+
text-align: center;
|
|
290
|
+
margin-bottom: 40px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.hero h1 {
|
|
294
|
+
font-size: 2.5rem;
|
|
295
|
+
color: #303133;
|
|
296
|
+
margin-bottom: 12px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.subtitle {
|
|
300
|
+
font-size: 1.1rem;
|
|
301
|
+
color: #606266;
|
|
302
|
+
max-width: 600px;
|
|
303
|
+
margin: 0 auto;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.main-content {
|
|
307
|
+
display: grid;
|
|
308
|
+
grid-template-columns: 1fr 1fr;
|
|
309
|
+
gap: 30px;
|
|
310
|
+
max-width: 1200px;
|
|
311
|
+
margin: 0 auto 40px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@media (max-width: 900px) {
|
|
315
|
+
.main-content {
|
|
316
|
+
grid-template-columns: 1fr;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.examples-section,
|
|
321
|
+
.input-section {
|
|
322
|
+
background: white;
|
|
323
|
+
border-radius: 12px;
|
|
324
|
+
padding: 24px;
|
|
325
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.examples-section h2,
|
|
329
|
+
.input-section h2 {
|
|
330
|
+
font-size: 1.3rem;
|
|
331
|
+
color: #303133;
|
|
332
|
+
margin-bottom: 8px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.section-desc {
|
|
336
|
+
font-size: 0.9rem;
|
|
337
|
+
color: #909399;
|
|
338
|
+
margin-bottom: 16px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.examples-grid {
|
|
342
|
+
display: grid;
|
|
343
|
+
gap: 12px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.example-card {
|
|
347
|
+
padding: 16px;
|
|
348
|
+
border: 1px solid #e4e7ed;
|
|
349
|
+
border-radius: 8px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
transition: all 0.2s;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.example-card:hover {
|
|
355
|
+
border-color: #409eff;
|
|
356
|
+
background: #f0f7ff;
|
|
357
|
+
transform: translateY(-2px);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.example-card h3 {
|
|
361
|
+
font-size: 1rem;
|
|
362
|
+
color: #303133;
|
|
363
|
+
margin-bottom: 4px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.example-card p {
|
|
367
|
+
font-size: 0.85rem;
|
|
368
|
+
color: #909399;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.dsl-textarea {
|
|
372
|
+
width: 100%;
|
|
373
|
+
height: 300px;
|
|
374
|
+
padding: 12px;
|
|
375
|
+
border: 1px solid #dcdfe6;
|
|
376
|
+
border-radius: 8px;
|
|
377
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
line-height: 1.5;
|
|
380
|
+
resize: vertical;
|
|
381
|
+
transition: border-color 0.2s;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.dsl-textarea:focus {
|
|
385
|
+
outline: none;
|
|
386
|
+
border-color: #409eff;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.dsl-textarea:disabled {
|
|
390
|
+
background: #f5f7fa;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.error-message {
|
|
394
|
+
margin-top: 12px;
|
|
395
|
+
padding: 10px 12px;
|
|
396
|
+
background: #fef0f0;
|
|
397
|
+
border: 1px solid #fde2e2;
|
|
398
|
+
border-radius: 6px;
|
|
399
|
+
color: #f56c6c;
|
|
400
|
+
font-size: 0.9rem;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.submit-btn {
|
|
404
|
+
margin-top: 16px;
|
|
405
|
+
width: 100%;
|
|
406
|
+
padding: 12px 24px;
|
|
407
|
+
background: #409eff;
|
|
408
|
+
color: white;
|
|
409
|
+
border: none;
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
font-size: 1rem;
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
transition: background 0.2s;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.submit-btn:hover:not(:disabled) {
|
|
417
|
+
background: #337ecc;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.submit-btn:disabled {
|
|
421
|
+
background: #a0cfff;
|
|
422
|
+
cursor: not-allowed;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.help-section {
|
|
426
|
+
max-width: 1200px;
|
|
427
|
+
margin: 0 auto;
|
|
428
|
+
background: white;
|
|
429
|
+
border-radius: 12px;
|
|
430
|
+
padding: 24px;
|
|
431
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.help-section h3 {
|
|
435
|
+
font-size: 1.1rem;
|
|
436
|
+
color: #303133;
|
|
437
|
+
margin-bottom: 16px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.help-table {
|
|
441
|
+
width: 100%;
|
|
442
|
+
border-collapse: collapse;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.help-table th,
|
|
446
|
+
.help-table td {
|
|
447
|
+
padding: 10px 12px;
|
|
448
|
+
text-align: left;
|
|
449
|
+
border-bottom: 1px solid #ebeef5;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.help-table th {
|
|
453
|
+
background: #f5f7fa;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
color: #606266;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.help-table td {
|
|
459
|
+
color: #606266;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.help-table code {
|
|
463
|
+
background: #f5f7fa;
|
|
464
|
+
padding: 2px 6px;
|
|
465
|
+
border-radius: 4px;
|
|
466
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
467
|
+
color: #409eff;
|
|
468
|
+
}
|
|
469
|
+
</style>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import type { QueryMetaResponse, ViewerQueryRequest, ViewerDataResponse, FilterOptionsResponse, ColumnSchema } from '@/types'
|
|
3
|
+
|
|
4
|
+
const apiClient = axios.create({
|
|
5
|
+
baseURL: '/data-viewer/api',
|
|
6
|
+
timeout: 30000,
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 查询 payload(与 dataset.query_model 格式一致)
|
|
14
|
+
*/
|
|
15
|
+
export interface QueryPayload {
|
|
16
|
+
columns: string[]
|
|
17
|
+
slice: Array<{ field: string; op: string; value?: unknown }>
|
|
18
|
+
groupBy?: Array<{ field: string; agg?: string }>
|
|
19
|
+
orderBy?: Array<{ field: string; order: 'asc' | 'desc' }>
|
|
20
|
+
calculatedFields?: Array<{ name: string; expression: string; agg?: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 创建查询请求类型
|
|
25
|
+
*/
|
|
26
|
+
export interface CreateQueryRequest {
|
|
27
|
+
model: string
|
|
28
|
+
payload: QueryPayload
|
|
29
|
+
title?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 创建查询响应类型
|
|
34
|
+
*/
|
|
35
|
+
export interface CreateQueryResponse {
|
|
36
|
+
success: boolean
|
|
37
|
+
queryId: string | null
|
|
38
|
+
viewerUrl: string | null
|
|
39
|
+
error: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 创建查询(从 DSL 输入)
|
|
44
|
+
*/
|
|
45
|
+
export async function createQuery(request: CreateQueryRequest): Promise<CreateQueryResponse> {
|
|
46
|
+
const response = await apiClient.post<any>('/query/create', request)
|
|
47
|
+
|
|
48
|
+
// Handle RX response format: { code: 200, msg: "", data: {} }
|
|
49
|
+
if (!response.data || response.data.code !== 200) {
|
|
50
|
+
// Return the error response if available in data
|
|
51
|
+
if (response.data?.data) {
|
|
52
|
+
return response.data.data
|
|
53
|
+
}
|
|
54
|
+
throw new Error(response.data?.msg || '创建查询失败')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response.data.data
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取查询元数据
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchQueryMeta(queryId: string): Promise<QueryMetaResponse> {
|
|
64
|
+
const response = await apiClient.get<any>(`/query/${queryId}/meta`)
|
|
65
|
+
|
|
66
|
+
// Handle RX response format: { code: 200, msg: "", data: {} }
|
|
67
|
+
if (!response.data || response.data.code !== 200) {
|
|
68
|
+
throw new Error(response.data?.msg || '获取查询元数据失败')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return response.data.data
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 查询数据
|
|
76
|
+
*/
|
|
77
|
+
export async function fetchQueryData(
|
|
78
|
+
queryId: string,
|
|
79
|
+
request: ViewerQueryRequest
|
|
80
|
+
): Promise<ViewerDataResponse> {
|
|
81
|
+
const response = await apiClient.post<any>(`/query/${queryId}/data`, request)
|
|
82
|
+
|
|
83
|
+
// Handle RX response format: { code: 200, msg: "", data: {} }
|
|
84
|
+
if (!response.data || response.data.code !== 200) {
|
|
85
|
+
// If it's an expired query (status 410), return the expired response
|
|
86
|
+
if (response.data?.data?.expired) {
|
|
87
|
+
return response.data.data
|
|
88
|
+
}
|
|
89
|
+
throw new Error(response.data?.msg || '查询数据失败')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.data.data
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 获取过滤选项(维度成员或字典项)
|
|
97
|
+
*/
|
|
98
|
+
export async function fetchFilterOptions(
|
|
99
|
+
queryId: string,
|
|
100
|
+
columnName: string
|
|
101
|
+
): Promise<FilterOptionsResponse> {
|
|
102
|
+
const response = await apiClient.get<FilterOptionsResponse>(
|
|
103
|
+
`/query/${queryId}/filter-options/${encodeURIComponent(columnName)}`
|
|
104
|
+
)
|
|
105
|
+
return response.data
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取 QM Schema(查询模型的字段元数据)
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchQmSchema(qmModel: string): Promise<ColumnSchema[]> {
|
|
112
|
+
const response = await apiClient.get<any>(`/schema/${encodeURIComponent(qmModel)}`)
|
|
113
|
+
|
|
114
|
+
// Handle RX response format: { code: 200, msg: "", data: {} }
|
|
115
|
+
if (!response.data || response.data.code !== 200) {
|
|
116
|
+
throw new Error(response.data?.msg || '获取 QM Schema 失败')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = response.data.data
|
|
120
|
+
|
|
121
|
+
// 解析 SemanticMetadataResponse V3 格式
|
|
122
|
+
// 返回格式:{ "version": "v3", "fields": { "fieldName": { "name": "显示名", "type": "TEXT", ... } }, "models": {...} }
|
|
123
|
+
if (!data || !data.fields) {
|
|
124
|
+
return []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 遍历 fields 对象,转换为 ColumnSchema 数组
|
|
128
|
+
const columns: ColumnSchema[] = []
|
|
129
|
+
for (const [fieldName, fieldInfo] of Object.entries(data.fields)) {
|
|
130
|
+
const field = fieldInfo as any
|
|
131
|
+
|
|
132
|
+
// 直接使用后端返回的字段(不再解析 meta)
|
|
133
|
+
columns.push({
|
|
134
|
+
name: fieldName,
|
|
135
|
+
title: field.name || fieldName,
|
|
136
|
+
type: field.type || 'TEXT',
|
|
137
|
+
filterable: field.filterable !== false,
|
|
138
|
+
aggregatable: field.aggregatable || false,
|
|
139
|
+
measure: field.measure || false,
|
|
140
|
+
filterType: field.filterType,
|
|
141
|
+
dictId: field.dictId,
|
|
142
|
+
format: field.format
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return columns
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 错误处理
|
|
151
|
+
*/
|
|
152
|
+
apiClient.interceptors.response.use(
|
|
153
|
+
response => response,
|
|
154
|
+
error => {
|
|
155
|
+
if (error.response?.status === 410) {
|
|
156
|
+
return Promise.reject(new Error('查询链接已过期,请重新获取'))
|
|
157
|
+
}
|
|
158
|
+
if (error.response?.status === 404) {
|
|
159
|
+
return Promise.reject(new Error('查询不存在'))
|
|
160
|
+
}
|
|
161
|
+
return Promise.reject(error)
|
|
162
|
+
}
|
|
163
|
+
)
|