@axiom-lattice/gateway 2.1.39 → 2.1.41
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +15 -0
- package/dist/index.js +464 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +415 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/public/sdk/README.md +695 -0
- package/public/sdk/dashboard-engine-skill.md +1122 -0
- package/public/sdk/dashboard-single-file-spec.md +357 -0
- package/public/sdk/data-query-sdk-skill.md +307 -0
- package/public/sdk/data-query-sdk.d.ts +252 -0
- package/public/sdk/data-query-sdk.js +970 -0
- package/public/sdk/occupancy-dashboard.html +363 -0
- package/public/sdk/test-dashboard.html +690 -0
- package/src/__tests__/data-query.test.ts +77 -0
- package/src/controllers/data-query.ts +236 -0
- package/src/controllers/metrics-configs.ts +29 -25
- package/src/controllers/workspace.ts +95 -1
- package/src/index.ts +11 -0
- package/src/routes/index.ts +11 -0
- package/src/schemas/data-query.ts +69 -0
- package/src/schemas/index.ts +3 -0
- package/src/services/agent_task_consumer.ts +2 -0
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
# 技能:使用 DashboardEngine 创建数据看板(v3.0 状态机版本)
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
本技能指导如何使用 Data Query SDK v3.0 的 DashboardEngine 创建声明式数据看板,支持状态机联动机制。
|
|
6
|
+
|
|
7
|
+
**适用场景**:
|
|
8
|
+
- 需要创建可交互的数据看板
|
|
9
|
+
- 图表间需要联动(点击下钻、过滤器联动)
|
|
10
|
+
- 跨数据源字段名不一致的场景
|
|
11
|
+
|
|
12
|
+
**核心特性**:
|
|
13
|
+
- ✅ 声明式配置(纯 JSON,无 JS 函数)
|
|
14
|
+
- ✅ 状态机架构(emit/receiveFilters)
|
|
15
|
+
- ✅ 字段映射(解决跨数据源不一致)
|
|
16
|
+
- ✅ 配置验证(防止模型幻觉)
|
|
17
|
+
- ✅ 闭环校验(自动检查 emit/receive 配对)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 依赖
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<!-- 必需 -->
|
|
25
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
26
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
27
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 核心概念
|
|
33
|
+
|
|
34
|
+
### 状态机架构
|
|
35
|
+
|
|
36
|
+
DashboardEngine 基于**发布-订阅**模型:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
过滤器/图表点击
|
|
40
|
+
↓
|
|
41
|
+
emit(发射状态)
|
|
42
|
+
↓
|
|
43
|
+
全局状态池(State Pool)
|
|
44
|
+
↓
|
|
45
|
+
receiveFilters(订阅状态)
|
|
46
|
+
↓
|
|
47
|
+
图表自动刷新
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**关键字段:**
|
|
51
|
+
- **`emitsToState`**: 过滤器选择后写入全局状态
|
|
52
|
+
- **`emit`**: 图表点击时发射状态
|
|
53
|
+
- **`receiveFilters`**: 图表订阅状态并应用过滤
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 步骤 1:数据探查(必须先完成)
|
|
58
|
+
|
|
59
|
+
**⚠️ 关键原则:没有数据探查,就没有 Dashboard 设计**
|
|
60
|
+
|
|
61
|
+
在设计任何 Dashboard 之前,必须完成完整的数据探查。你需要了解有哪些指标可用、维度如何分布、过滤条件如何影响数据,才能决定什么作为 KPI、什么用什么图表展示。
|
|
62
|
+
|
|
63
|
+
**🚫 严禁伪造数据**
|
|
64
|
+
|
|
65
|
+
- **所有图表和表格的数据必须通过查询获取**,禁止在配置中硬编码或伪造任何数据
|
|
66
|
+
- **筛选器的选项值如果是静态值**(如固定的年份列表、固定的地区选项),可以写入配置
|
|
67
|
+
- **任何动态数据**(指标数值、维度值、统计结果)必须通过 `query` 查询获取
|
|
68
|
+
- **禁止示例数据**:不要在 `mapping`、`tableConfig` 或任何配置字段中放置假数据作为示例
|
|
69
|
+
|
|
70
|
+
### 1.1 获取可用指标和维度
|
|
71
|
+
|
|
72
|
+
**第一步:查询数据源的元数据**
|
|
73
|
+
|
|
74
|
+
使用现有工具查询数据源的指标和维度信息,获取:
|
|
75
|
+
- **指标列表**:有哪些数值字段可用(销售额、订单量、用户数等)
|
|
76
|
+
- **维度列表**:有哪些分组字段可用(时间、地区、产品类别等)
|
|
77
|
+
- **时间粒度**:时间维度支持哪些粒度(日、周、月、季度、年)
|
|
78
|
+
|
|
79
|
+
**第二步:验证每个指标的实际查询**
|
|
80
|
+
|
|
81
|
+
对每个候选指标执行实际查询,确认:
|
|
82
|
+
- 指标名称是否正确(大小写敏感)
|
|
83
|
+
- 返回的数据格式是否符合预期
|
|
84
|
+
- 数值范围是否合理
|
|
85
|
+
|
|
86
|
+
**第三步:验证每个维度的下钻能力**
|
|
87
|
+
|
|
88
|
+
对想要下钻的维度执行带过滤条件的查询:
|
|
89
|
+
- 测试维度过滤是否生效
|
|
90
|
+
- 确认过滤后的数据量是否适合展示
|
|
91
|
+
- 验证维度值的格式(如日期格式、地区编码)
|
|
92
|
+
|
|
93
|
+
### 1.2 Query 查询规则
|
|
94
|
+
|
|
95
|
+
每个 Widget 的 `query` 字段遵循以下结构:
|
|
96
|
+
|
|
97
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
98
|
+
|------|------|------|------|
|
|
99
|
+
| `serverKey` | `string` | ✅ | 指标服务器 key |
|
|
100
|
+
| `datasourceId` | `string` | ✅ | 数据源 ID |
|
|
101
|
+
| `metrics` | `string[]` | ✅ | 要查询的指标名列表,名称来自 meta 中的指标定义(metric name) |
|
|
102
|
+
| `groupBy` | `string[]` | ❌ | 分组维度。可为**维度名**或**带粒度的维度**(如 `{dimension}__day`、`{dimension}__week`、`{dimension}__month`、`{dimension}__quarter`、`{dimension}__year`),具体支持粒度以 meta 中该维度的 `time_granularities` 或等价定义为准 |
|
|
103
|
+
| `filters` | `object[]` | ❌ | 过滤条件数组,见下方 Filter 项结构 |
|
|
104
|
+
| `orderBy` | `object[]` | ❌ | 排序规则数组,见下方 OrderBy 项结构 |
|
|
105
|
+
| `limit` | `integer` | ❌ | 返回行数上限,区间 [1, 20000],未传时由服务端默认 |
|
|
106
|
+
| `debug` | `boolean` | ❌ | 为 `true` 时,响应 `data.debug` 中可包含执行信息(如生成 SQL、参数等),便于排查 |
|
|
107
|
+
|
|
108
|
+
**Filter 项结构:**
|
|
109
|
+
|
|
110
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
111
|
+
|------|------|------|------|
|
|
112
|
+
| `dimension` | `string` | ✅ | 维度名,须与 meta 中定义的维度一致(如时间维度、分类维度等) |
|
|
113
|
+
| `operator` | `string` | ✅ | 操作符,见下方支持的操作符列表 |
|
|
114
|
+
| `values` | `string[]` | 依操作符 | 用于比较的值;`BETWEEN` 时为 `[起, 止]`,`IN`/`NOT_IN` 时为多值列表;部分操作符可为空 |
|
|
115
|
+
|
|
116
|
+
**支持的操作符:**
|
|
117
|
+
|
|
118
|
+
| 操作符 | 说明 | 适用场景 |
|
|
119
|
+
|--------|------|----------|
|
|
120
|
+
| `EQ` | 等于(=) | 精确匹配单个值 |
|
|
121
|
+
| `NEQ` | 不等于(!=) | 排除单个值 |
|
|
122
|
+
| `IN` | 包含在列表中 | 匹配多个值中的任意一个 |
|
|
123
|
+
| `NOT_IN` | 不包含在列表中 | 排除多个值 |
|
|
124
|
+
| `BETWEEN` | 范围(闭区间) | 时间范围、数值范围,values 为 `[min, max]` |
|
|
125
|
+
| `GT` | 大于(>) | 数值比较 |
|
|
126
|
+
| `GTE` | 大于等于(>=) | 数值比较 |
|
|
127
|
+
| `LT` | 小于(<) | 数值比较 |
|
|
128
|
+
| `LTE` | 小于等于(<=) | 数值比较 |
|
|
129
|
+
| `LIKE` | 模糊匹配 | 字符串匹配,支持 `%` 通配符 |
|
|
130
|
+
| `ILIKE` | 模糊匹配(不区分大小写) | 字符串匹配 |
|
|
131
|
+
| `IS_NULL` | 为空 | 检查空值,values 可省略 |
|
|
132
|
+
| `IS_NOT_NULL` | 不为空 | 检查非空值,values 可省略 |
|
|
133
|
+
|
|
134
|
+
**⚠️ 数据探查时必须使用过滤条件**
|
|
135
|
+
|
|
136
|
+
在进行数据探查时,**必须**带上过滤条件执行查询,原因如下:
|
|
137
|
+
|
|
138
|
+
1. **验证指标可用性**:有些指标在全局查询时正常,但加上过滤条件后可能返回空数据或报错
|
|
139
|
+
2. **确认维度有效性**:过滤条件能帮你确认维度值的真实格式(如日期是 `2025-01-01` 还是 `2025/01/01`)
|
|
140
|
+
3. **评估数据量**:带过滤的查询能反映 Dashboard 实际使用时的数据量,避免配置后发现数据过多或过少
|
|
141
|
+
4. **测试下钻路径**:只有通过带过滤的查询,才能验证下钻逻辑是否可行
|
|
142
|
+
|
|
143
|
+
**数据探查时的过滤条件示例:**
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
// ✅ 正确:数据探查时带上时间范围过滤
|
|
147
|
+
{
|
|
148
|
+
dimension: 'date',
|
|
149
|
+
operator: 'BETWEEN',
|
|
150
|
+
values: ['2025-01-01', '2025-01-31']
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ✅ 正确:数据探查时带上分类维度过滤
|
|
154
|
+
{
|
|
155
|
+
dimension: 'region',
|
|
156
|
+
operator: 'IN',
|
|
157
|
+
values: ['north', 'south']
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**OrderBy 项结构:**
|
|
162
|
+
|
|
163
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
164
|
+
|------|------|------|------|
|
|
165
|
+
| `field` | `string` | ✅ | 排序字段,一般为响应中的列名(如指标名或 groupBy 中的维度/粒度表达式) |
|
|
166
|
+
| `direction` | `string` | ✅ | `"ASC"`(升序)或 `"DESC"`(降序) |
|
|
167
|
+
|
|
168
|
+
**时间粒度表达式:**
|
|
169
|
+
|
|
170
|
+
**仅用于 `groupBy`**(决定数据如何分组聚合):
|
|
171
|
+
|
|
172
|
+
| 表达式 | 说明 |
|
|
173
|
+
|--------|------|
|
|
174
|
+
| `{dimension}__day` | 按天分组 |
|
|
175
|
+
| `{dimension}__week` | 按周分组 |
|
|
176
|
+
| `{dimension}__month` | 按月分组 |
|
|
177
|
+
| `{dimension}__quarter` | 按季度分组 |
|
|
178
|
+
| `{dimension}__year` | 按年分组 |
|
|
179
|
+
|
|
180
|
+
**⚠️ 重要区别:**
|
|
181
|
+
|
|
182
|
+
- **`groupBy`**:可以使用粒度表达式(如 `date__month`)
|
|
183
|
+
- **`filters.dimension`**:必须使用**原始维度字段**(如 `date`),不能用粒度表达式
|
|
184
|
+
|
|
185
|
+
**正确示例:**
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// ✅ 正确:groupBy 使用粒度表达式
|
|
189
|
+
query: {
|
|
190
|
+
metrics: ['sales'],
|
|
191
|
+
groupBy: ['date__month'], // 按月分组
|
|
192
|
+
filters: [{
|
|
193
|
+
dimension: 'date', // 使用原始维度字段
|
|
194
|
+
operator: 'BETWEEN',
|
|
195
|
+
values: ['2025-01-01', '2025-12-31']
|
|
196
|
+
}]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ❌ 错误:filters 中使用粒度表达式
|
|
200
|
+
query: {
|
|
201
|
+
filters: [{
|
|
202
|
+
dimension: 'date__year', // 错误!不能用粒度表达式
|
|
203
|
+
operator: 'EQ',
|
|
204
|
+
values: ['2025']
|
|
205
|
+
}]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
> **⚠️ 重要提醒:日期过滤必须使用 `BETWEEN` 操作符**
|
|
210
|
+
>
|
|
211
|
+
> 查询日期范围时,必须使用 `BETWEEN` 操作符,并提供具体的起始和结束日期。
|
|
212
|
+
>
|
|
213
|
+
> **❌ 错误示例:**
|
|
214
|
+
> ```javascript
|
|
215
|
+
> // 不要用 EQ 查询年份
|
|
216
|
+
> {
|
|
217
|
+
> dimension: 'date__year',
|
|
218
|
+
> operator: 'EQ',
|
|
219
|
+
> values: ['2025'] // 错误!
|
|
220
|
+
> }
|
|
221
|
+
> ```
|
|
222
|
+
>
|
|
223
|
+
> **✅ 正确示例:**
|
|
224
|
+
> ```javascript
|
|
225
|
+
> // 使用 BETWEEN 查询具体日期范围
|
|
226
|
+
> {
|
|
227
|
+
> dimension: 'date',
|
|
228
|
+
> operator: 'BETWEEN',
|
|
229
|
+
> values: ['2025-01-01', '2025-12-31'] // 正确!
|
|
230
|
+
> }
|
|
231
|
+
>
|
|
232
|
+
> // 查询 2025 年 Q1
|
|
233
|
+
> {
|
|
234
|
+
> dimension: 'date',
|
|
235
|
+
> operator: 'BETWEEN',
|
|
236
|
+
> values: ['2025-01-01', '2025-03-31']
|
|
237
|
+
> }
|
|
238
|
+
>
|
|
239
|
+
> // 查询最近 30 天
|
|
240
|
+
> {
|
|
241
|
+
> dimension: 'date',
|
|
242
|
+
> operator: 'BETWEEN',
|
|
243
|
+
> values: ['2025-01-01', '2025-01-31']
|
|
244
|
+
> }
|
|
245
|
+
> ```
|
|
246
|
+
>
|
|
247
|
+
> **原因:** 日期在底层存储通常是时间戳或日期类型,使用 `EQ` 匹配年份字符串会导致查询失败。`BETWEEN` 操作符会在 SQL 中生成 `date >= 'start' AND date <= 'end'`,能正确处理日期范围。
|
|
248
|
+
|
|
249
|
+
### 1.3 基于数据探查结果设计 Dashboard
|
|
250
|
+
|
|
251
|
+
**在完成数据探查后,回答以下问题:**
|
|
252
|
+
|
|
253
|
+
| 问题 | 决策依据 | Dashboard 配置 |
|
|
254
|
+
|------|---------|---------------|
|
|
255
|
+
| 哪些指标作为 KPI 展示? | 核心业务指标、领导关注的关键数据 | 使用 `type: 'kpi'` |
|
|
256
|
+
| 哪些指标用于趋势分析? | 时间序列数据、需要观察变化的指标 | 使用 `type: 'line'` |
|
|
257
|
+
| 哪些维度用于对比分析? | 分类数据、需要横向比较的维度 | 使用 `type: 'bar'` 或 `type: 'pie'` |
|
|
258
|
+
| 哪些维度支持下钻? | 有层级关系的维度(如年→月→日) | 配置 `emit` 和 `receiveFilters` |
|
|
259
|
+
| 需要什么过滤条件? | 常用的筛选维度(地区、产品类型等) | 配置 `filters` |
|
|
260
|
+
|
|
261
|
+
**⚠️ 重要:所有设计决策必须基于实际查询结果**
|
|
262
|
+
|
|
263
|
+
- 不要假设某个指标存在,必须先查询确认
|
|
264
|
+
- 不要假设维度有数据,必须带过滤条件测试
|
|
265
|
+
- 不要假设时间粒度可用,必须查看 meta 中的 `time_granularities`
|
|
266
|
+
|
|
267
|
+
### 1.4 确定图表类型
|
|
268
|
+
|
|
269
|
+
根据数据探查结果选择图表类型:
|
|
270
|
+
|
|
271
|
+
| 数据特征 | 推荐图表类型 | 说明 |
|
|
272
|
+
|---------|------------|------|
|
|
273
|
+
| 时间序列 + 数值 | line, bar | 趋势分析 |
|
|
274
|
+
| 分类 + 数值对比 | bar, pie | 对比分析 |
|
|
275
|
+
| 占比关系 | pie | 构成分析 |
|
|
276
|
+
| 单指标展示 | kpi | 关键指标卡片 |
|
|
277
|
+
| 详细数据 | table | 表格展示(支持分页) |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 步骤 2:设计看板布局
|
|
282
|
+
|
|
283
|
+
### 2.1 网格系统
|
|
284
|
+
|
|
285
|
+
使用 Tailwind CSS 的 12 列网格:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
col-span-12 - 全宽(趋势图、表格)
|
|
289
|
+
col-span-6 - 半宽(对比图、饼图)
|
|
290
|
+
col-span-4 - 1/3 宽(KPI)
|
|
291
|
+
col-span-3 - 1/4 宽(小卡片)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 2.2 布局示例
|
|
295
|
+
|
|
296
|
+
**联动型看板:**
|
|
297
|
+
```
|
|
298
|
+
┌─────────────────────────────────────┐
|
|
299
|
+
│ [年份过滤器] → emitsToState: year │
|
|
300
|
+
├──────────┬──────────────────────────┤
|
|
301
|
+
│ 年度饼图 │ 月度柱状图 │
|
|
302
|
+
│ (emit) │ (receiveFilters) │
|
|
303
|
+
│ 点击年份 │ 接收 year 状态并过滤 │
|
|
304
|
+
└──────────┴──────────────────────────┘
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 步骤 3:配置 Widgets
|
|
310
|
+
|
|
311
|
+
### 3.1 基础配置模板
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
{
|
|
315
|
+
id: 'unique-id', // 必填:唯一标识
|
|
316
|
+
title: '图表标题', // 必填:显示标题
|
|
317
|
+
type: 'line', // 必填:图表类型
|
|
318
|
+
gridClass: 'col-span-12', // 可选:网格类
|
|
319
|
+
containerId: 'chart-id', // 可选:外部容器 ID(用于自定义 HTML 布局)
|
|
320
|
+
|
|
321
|
+
query: { // 必填:查询配置
|
|
322
|
+
// serverKey 和 datasourceId 可选,默认使用 SDK 配置
|
|
323
|
+
metrics: ['metric_name'],
|
|
324
|
+
groupBy: ['dimension']
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
mapping: { // 必填:数据映射
|
|
328
|
+
xAxis: 'dimension',
|
|
329
|
+
yAxis: ['metric_name']
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// 可选:接收全局过滤器
|
|
333
|
+
receiveFilters: [{
|
|
334
|
+
fromState: 'selected_year',
|
|
335
|
+
mapToDimension: 'date', // 映射到原始维度字段,不是粒度表达式
|
|
336
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
337
|
+
ignoreIfEmpty: true
|
|
338
|
+
}],
|
|
339
|
+
|
|
340
|
+
// 可选:点击发射状态
|
|
341
|
+
emit: [{
|
|
342
|
+
event: 'click',
|
|
343
|
+
extractValueFrom: 'dimension',
|
|
344
|
+
updateState: 'drill_state'
|
|
345
|
+
}]
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### 3.2 图表类型配置示例
|
|
352
|
+
|
|
353
|
+
#### 折线图(趋势)
|
|
354
|
+
|
|
355
|
+
```javascript
|
|
356
|
+
{
|
|
357
|
+
id: 'trend-chart',
|
|
358
|
+
title: '销售趋势',
|
|
359
|
+
type: 'line',
|
|
360
|
+
gridClass: 'col-span-12',
|
|
361
|
+
query: {
|
|
362
|
+
serverKey: 'prod',
|
|
363
|
+
datasourceId: '1',
|
|
364
|
+
metrics: ['sales_amt'],
|
|
365
|
+
groupBy: ['date__month']
|
|
366
|
+
},
|
|
367
|
+
mapping: {
|
|
368
|
+
xAxis: 'date__month',
|
|
369
|
+
yAxis: ['sales_amt']
|
|
370
|
+
},
|
|
371
|
+
receiveFilters: [{
|
|
372
|
+
fromState: 'selected_year',
|
|
373
|
+
mapToDimension: 'date', // 映射到原始维度字段
|
|
374
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
375
|
+
ignoreIfEmpty: true
|
|
376
|
+
}]
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### 柱状图(对比)
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
{
|
|
384
|
+
id: 'bar-chart',
|
|
385
|
+
title: '门店对比',
|
|
386
|
+
type: 'bar',
|
|
387
|
+
gridClass: 'col-span-6',
|
|
388
|
+
query: {
|
|
389
|
+
serverKey: 'prod',
|
|
390
|
+
datasourceId: '1',
|
|
391
|
+
metrics: ['sales_amt'],
|
|
392
|
+
groupBy: ['shop']
|
|
393
|
+
},
|
|
394
|
+
mapping: {
|
|
395
|
+
xAxis: 'shop',
|
|
396
|
+
yAxis: ['sales_amt']
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### 饼图(占比 + 点击下钻)
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
{
|
|
405
|
+
id: 'yearly-pie',
|
|
406
|
+
title: '年度占比(点击下钻)',
|
|
407
|
+
type: 'pie',
|
|
408
|
+
gridClass: 'col-span-6',
|
|
409
|
+
query: {
|
|
410
|
+
serverKey: 'prod',
|
|
411
|
+
datasourceId: '1',
|
|
412
|
+
metrics: ['sales_amt'],
|
|
413
|
+
groupBy: ['date__year']
|
|
414
|
+
},
|
|
415
|
+
mapping: {
|
|
416
|
+
name: 'date__year',
|
|
417
|
+
value: 'sales_amt'
|
|
418
|
+
},
|
|
419
|
+
emit: [{
|
|
420
|
+
event: 'click',
|
|
421
|
+
extractValueFrom: 'date__year',
|
|
422
|
+
updateState: 'drill_year'
|
|
423
|
+
}]
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### KPI 卡片
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
{
|
|
431
|
+
id: 'kpi-sales',
|
|
432
|
+
title: '总销售额',
|
|
433
|
+
type: 'kpi',
|
|
434
|
+
gridClass: 'col-span-3',
|
|
435
|
+
query: {
|
|
436
|
+
serverKey: 'prod',
|
|
437
|
+
datasourceId: '1',
|
|
438
|
+
metrics: ['sales_amt']
|
|
439
|
+
},
|
|
440
|
+
mapping: {
|
|
441
|
+
value: 'sales_amt',
|
|
442
|
+
valueFormat: 'currency_cny'
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### 表格(支持分页和点击)
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
{
|
|
451
|
+
id: 'detail-table',
|
|
452
|
+
title: '销售明细',
|
|
453
|
+
type: 'table',
|
|
454
|
+
gridClass: 'col-span-12',
|
|
455
|
+
query: {
|
|
456
|
+
serverKey: 'prod',
|
|
457
|
+
datasourceId: '1',
|
|
458
|
+
metrics: ['sales', 'profit'],
|
|
459
|
+
groupBy: ['product', 'region']
|
|
460
|
+
},
|
|
461
|
+
tableConfig: {
|
|
462
|
+
pageSize: 10,
|
|
463
|
+
align: 'left',
|
|
464
|
+
columnFormats: {
|
|
465
|
+
'sales': 'currency_cny',
|
|
466
|
+
'profit': 'currency_cny'
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
emit: [{
|
|
470
|
+
event: 'click',
|
|
471
|
+
extractValueFrom: 'product',
|
|
472
|
+
updateState: 'selected_product'
|
|
473
|
+
}]
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 步骤 4:配置 Filters(全局过滤器)
|
|
480
|
+
|
|
481
|
+
### 4.1 基础过滤器
|
|
482
|
+
|
|
483
|
+
**分类维度过滤器(如区域、产品类型):**
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
{
|
|
487
|
+
id: 'region',
|
|
488
|
+
label: '区域',
|
|
489
|
+
dimension: 'region', // 使用原始维度字段
|
|
490
|
+
operator: 'EQ',
|
|
491
|
+
defaultValue: 'ALL',
|
|
492
|
+
options: [
|
|
493
|
+
{ value: 'ALL', text: '全部' },
|
|
494
|
+
{ value: 'north', text: '华北' },
|
|
495
|
+
{ value: 'south', text: '华南' }
|
|
496
|
+
],
|
|
497
|
+
emitsToState: 'selected_region'
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**日期过滤器:**
|
|
502
|
+
|
|
503
|
+
日期过滤器比较特殊,选择后需要将年份转换为日期范围:
|
|
504
|
+
|
|
505
|
+
```javascript
|
|
506
|
+
{
|
|
507
|
+
id: 'year',
|
|
508
|
+
label: '年份',
|
|
509
|
+
dimension: 'date', // 使用原始维度字段
|
|
510
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
511
|
+
defaultValue: 'ALL',
|
|
512
|
+
options: [
|
|
513
|
+
{ value: 'ALL', text: '全部' },
|
|
514
|
+
{ value: '2025-01-01,2025-12-31', text: '2025年' }, // 值是日期范围
|
|
515
|
+
{ value: '2024-01-01,2024-12-31', text: '2024年' },
|
|
516
|
+
{ value: '2023-01-01,2023-12-31', text: '2023年' }
|
|
517
|
+
],
|
|
518
|
+
emitsToState: 'selected_year' // 选择后写入全局状态(值是日期范围字符串)
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**注意:** 日期过滤器的 `options.value` 是逗号分隔的日期范围,这样 `receiveFilters` 可以直接使用:
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
// 在 receiveFilters 中处理日期范围
|
|
526
|
+
receiveFilters: [{
|
|
527
|
+
fromState: 'selected_year',
|
|
528
|
+
mapToDimension: 'date',
|
|
529
|
+
operator: 'BETWEEN',
|
|
530
|
+
ignoreIfEmpty: true
|
|
531
|
+
// 当选择 "2025-01-01,2025-12-31" 时,会自动解析为 BETWEEN 查询
|
|
532
|
+
}]
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### 4.2 过滤器工作原理
|
|
536
|
+
|
|
537
|
+
1. 用户选择过滤器值
|
|
538
|
+
2. 值通过 `emitsToState` 写入全局状态池
|
|
539
|
+
3. 订阅该状态的图表收到通知
|
|
540
|
+
4. 自动刷新并应用过滤条件
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## 步骤 5:完整联动示例
|
|
545
|
+
|
|
546
|
+
### 跨粒度联动(年度 → 月度)
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
const config = {
|
|
550
|
+
filters: [
|
|
551
|
+
{
|
|
552
|
+
id: 'year',
|
|
553
|
+
label: '年份',
|
|
554
|
+
dimension: 'date', // 使用原始维度字段
|
|
555
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
556
|
+
defaultValue: 'ALL',
|
|
557
|
+
options: [
|
|
558
|
+
{ value: 'ALL', text: '全部年份' },
|
|
559
|
+
{ value: '2024-01-01,2024-12-31', text: '2024年' }, // 日期范围
|
|
560
|
+
{ value: '2023-01-01,2023-12-31', text: '2023年' }
|
|
561
|
+
],
|
|
562
|
+
emitsToState: 'selected_year' // 写入全局状态(值是日期范围)
|
|
563
|
+
}
|
|
564
|
+
],
|
|
565
|
+
widgets: [
|
|
566
|
+
{
|
|
567
|
+
id: 'yearly-pie',
|
|
568
|
+
type: 'pie',
|
|
569
|
+
title: '年度销售占比(点击下钻)',
|
|
570
|
+
gridClass: 'col-span-6',
|
|
571
|
+
query: {
|
|
572
|
+
serverKey: 'sales',
|
|
573
|
+
datasourceId: '1',
|
|
574
|
+
metrics: ['sales'],
|
|
575
|
+
groupBy: ['date__year']
|
|
576
|
+
},
|
|
577
|
+
mapping: {
|
|
578
|
+
name: 'date__year',
|
|
579
|
+
value: 'sales'
|
|
580
|
+
},
|
|
581
|
+
emit: [{
|
|
582
|
+
event: 'click',
|
|
583
|
+
extractValueFrom: 'date__year',
|
|
584
|
+
updateState: 'drill_year'
|
|
585
|
+
}]
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 'monthly-bar',
|
|
589
|
+
type: 'bar',
|
|
590
|
+
title: '月度销售趋势',
|
|
591
|
+
gridClass: 'col-span-6',
|
|
592
|
+
query: {
|
|
593
|
+
serverKey: 'sales',
|
|
594
|
+
datasourceId: '1',
|
|
595
|
+
metrics: ['sales'],
|
|
596
|
+
groupBy: ['date__month']
|
|
597
|
+
},
|
|
598
|
+
mapping: {
|
|
599
|
+
xAxis: 'date__month',
|
|
600
|
+
yAxis: ['sales']
|
|
601
|
+
},
|
|
602
|
+
receiveFilters: [
|
|
603
|
+
{
|
|
604
|
+
fromState: 'selected_year', // 接收过滤器状态
|
|
605
|
+
mapToDimension: 'date', // 映射到原始维度字段
|
|
606
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
607
|
+
ignoreIfEmpty: true
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
fromState: 'drill_year', // 接收点击下钻状态
|
|
611
|
+
mapToDimension: 'date', // 映射到原始维度字段
|
|
612
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
613
|
+
ignoreIfEmpty: true
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
};
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**交互效果:**
|
|
622
|
+
1. 选择全局年份过滤器 → 饼图和柱状图都更新
|
|
623
|
+
2. 点击饼图的某个年份 → 柱状图显示该年份的月度数据
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 步骤 6:解决字段不一致
|
|
628
|
+
|
|
629
|
+
当不同数据源使用不同字段名时,使用 `mapToDimension`:
|
|
630
|
+
|
|
631
|
+
```javascript
|
|
632
|
+
// 全局过滤器
|
|
633
|
+
filters: [{
|
|
634
|
+
id: 'region',
|
|
635
|
+
emitsToState: 'selected_region' // 全局状态名
|
|
636
|
+
}]
|
|
637
|
+
|
|
638
|
+
// Widget A - 销售数据(字段名:region)
|
|
639
|
+
widgets: [{
|
|
640
|
+
receiveFilters: [{
|
|
641
|
+
fromState: 'selected_region',
|
|
642
|
+
mapToDimension: 'region', // 映射到 'region'
|
|
643
|
+
operator: 'EQ'
|
|
644
|
+
}]
|
|
645
|
+
}]
|
|
646
|
+
|
|
647
|
+
// Widget B - 库存数据(字段名:warehouse_location)
|
|
648
|
+
widgets: [{
|
|
649
|
+
receiveFilters: [{
|
|
650
|
+
fromState: 'selected_region',
|
|
651
|
+
mapToDimension: 'warehouse_location', // 映射到 'warehouse_location'
|
|
652
|
+
operator: 'EQ'
|
|
653
|
+
}]
|
|
654
|
+
}]
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## 步骤 7:生成完整 HTML
|
|
660
|
+
|
|
661
|
+
### 7.1 HTML 模板
|
|
662
|
+
|
|
663
|
+
#### 方案 A:零 HTML 自动渲染(推荐)
|
|
664
|
+
|
|
665
|
+
**最简单的用法 - 只需要一个空 body:**
|
|
666
|
+
|
|
667
|
+
SDK 会自动创建所有容器并渲染看板,无需编写任何 HTML 结构:
|
|
668
|
+
|
|
669
|
+
```html
|
|
670
|
+
<!DOCTYPE html>
|
|
671
|
+
<html lang="zh-CN">
|
|
672
|
+
<head>
|
|
673
|
+
<meta charset="UTF-8">
|
|
674
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
675
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
676
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
677
|
+
</head>
|
|
678
|
+
<body>
|
|
679
|
+
<script>
|
|
680
|
+
const config = {
|
|
681
|
+
title: '数据看板', // 可选:看板标题
|
|
682
|
+
filters: [
|
|
683
|
+
// ... 过滤器配置
|
|
684
|
+
],
|
|
685
|
+
widgets: [
|
|
686
|
+
// ... 图表配置
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
async function init() {
|
|
691
|
+
const sdk = new DataQuerySDK({
|
|
692
|
+
baseURL: 'http://localhost:4001',
|
|
693
|
+
serverKey: 'prod',
|
|
694
|
+
datasourceId: '1'
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const engine = new DashboardEngine(sdk, config);
|
|
698
|
+
await engine.init();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
init().catch(console.error);
|
|
702
|
+
</script>
|
|
703
|
+
</body>
|
|
704
|
+
</html>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**自动创建的 DOM 结构:**
|
|
708
|
+
|
|
709
|
+
```html
|
|
710
|
+
<body>
|
|
711
|
+
<div id="dashboard-auto-container" class="max-w-7xl mx-auto px-4 py-8">
|
|
712
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-8">数据看板</h1>
|
|
713
|
+
<div id="filters-container" class="mb-4"><!-- 过滤器 --></div>
|
|
714
|
+
<div id="dashboard-grid" class="grid grid-cols-12 gap-4">
|
|
715
|
+
<!-- 自动生成的图表卡片 -->
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
</body>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
#### 方案 B:使用 dashboard-grid 半自动布局
|
|
722
|
+
|
|
723
|
+
如果你需要自定义看板标题或外层容器,但让 SDK 自动生成图表:
|
|
724
|
+
|
|
725
|
+
```html
|
|
726
|
+
<!DOCTYPE html>
|
|
727
|
+
<html lang="zh-CN">
|
|
728
|
+
<head>
|
|
729
|
+
<meta charset="UTF-8">
|
|
730
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
731
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
732
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
733
|
+
</head>
|
|
734
|
+
<body class="bg-gray-50 p-8">
|
|
735
|
+
<div class="max-w-7xl mx-auto">
|
|
736
|
+
<h1 class="text-3xl font-bold mb-8">数据看板</h1>
|
|
737
|
+
|
|
738
|
+
<!-- 过滤器容器 -->
|
|
739
|
+
<div id="filters-container" class="mb-4"></div>
|
|
740
|
+
|
|
741
|
+
<!-- 看板网格 - SDK 会自动在此创建图表 -->
|
|
742
|
+
<div id="dashboard-grid" class="grid grid-cols-12 gap-4"></div>
|
|
743
|
+
</div>
|
|
744
|
+
|
|
745
|
+
<script>
|
|
746
|
+
const config = {
|
|
747
|
+
filters: [
|
|
748
|
+
// ... 过滤器配置
|
|
749
|
+
],
|
|
750
|
+
widgets: [
|
|
751
|
+
// ... 图表配置
|
|
752
|
+
]
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
async function init() {
|
|
756
|
+
const sdk = new DataQuerySDK({
|
|
757
|
+
baseURL: 'http://localhost:4001',
|
|
758
|
+
serverKey: 'prod',
|
|
759
|
+
datasourceId: '1'
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const engine = new DashboardEngine(sdk, config);
|
|
763
|
+
await engine.init();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
init().catch(console.error);
|
|
767
|
+
</script>
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
#### 方案 C:使用外部容器自定义布局
|
|
771
|
+
|
|
772
|
+
如果你需要完全自定义 HTML 结构,可以为每个 widget 指定 `containerId`:
|
|
773
|
+
|
|
774
|
+
```html
|
|
775
|
+
<!DOCTYPE html>
|
|
776
|
+
<html lang="zh-CN">
|
|
777
|
+
<head>
|
|
778
|
+
<meta charset="UTF-8">
|
|
779
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
780
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
781
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
782
|
+
</head>
|
|
783
|
+
<body class="bg-gray-50 p-8">
|
|
784
|
+
<div class="max-w-7xl mx-auto">
|
|
785
|
+
<h1 class="text-3xl font-bold mb-8">数据看板</h1>
|
|
786
|
+
|
|
787
|
+
<!-- 过滤器容器 -->
|
|
788
|
+
<div id="filters-container" class="mb-4"></div>
|
|
789
|
+
|
|
790
|
+
<!-- 自定义布局 -->
|
|
791
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
792
|
+
<!-- 出租率趋势图 -->
|
|
793
|
+
<div class="bg-white p-5 rounded-xl shadow-sm">
|
|
794
|
+
<h3 class="text-lg font-semibold mb-4">月度出租率趋势</h3>
|
|
795
|
+
<div id="occupancy-trend-chart" class="h-80"></div>
|
|
796
|
+
</div>
|
|
797
|
+
|
|
798
|
+
<!-- 门店出租率对比 -->
|
|
799
|
+
<div class="bg-white p-5 rounded-xl shadow-sm">
|
|
800
|
+
<h3 class="text-lg font-semibold mb-4">各门店出租率对比</h3>
|
|
801
|
+
<div id="shop-occupancy-chart" class="h-80"></div>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<script>
|
|
807
|
+
const config = {
|
|
808
|
+
filters: [
|
|
809
|
+
// ... 过滤器配置
|
|
810
|
+
],
|
|
811
|
+
widgets: [
|
|
812
|
+
{
|
|
813
|
+
id: 'occupancy-trend',
|
|
814
|
+
type: 'line',
|
|
815
|
+
title: '月度出租率趋势',
|
|
816
|
+
containerId: 'occupancy-trend-chart', // 指向外部容器
|
|
817
|
+
// ... 其他配置
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: 'shop-occupancy',
|
|
821
|
+
type: 'bar',
|
|
822
|
+
title: '各门店出租率对比',
|
|
823
|
+
containerId: 'shop-occupancy-chart', // 指向外部容器
|
|
824
|
+
// ... 其他配置
|
|
825
|
+
}
|
|
826
|
+
]
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
async function init() {
|
|
830
|
+
const sdk = new DataQuerySDK({
|
|
831
|
+
baseURL: 'http://localhost:4001',
|
|
832
|
+
serverKey: 'prod',
|
|
833
|
+
datasourceId: '1'
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
const engine = new DashboardEngine(sdk, config);
|
|
837
|
+
await engine.init();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
init().catch(console.error);
|
|
841
|
+
</script>
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**三种方案的区别:**
|
|
845
|
+
|
|
846
|
+
| 特性 | 方案 A (零 HTML) | 方案 B (dashboard-grid) | 方案 C (外部容器) |
|
|
847
|
+
|------|-----------------|------------------------|------------------|
|
|
848
|
+
| HTML 要求 | 只需要空 body | 需要 dashboard-grid 容器 | 完全自定义 HTML |
|
|
849
|
+
| 代码量 | 最少 | 中等 | 最多 |
|
|
850
|
+
| 布局控制 | SDK 完全控制 | SDK 控制网格布局 | 完全自定义 |
|
|
851
|
+
| 适用场景 | 快速原型、标准看板 | 标准看板、需要自定义标题 | 需要精确控制样式 |
|
|
852
|
+
| 灵活性 | 低 | 中 | 高 |
|
|
853
|
+
|
|
854
|
+
**容器查找优先级:**
|
|
855
|
+
|
|
856
|
+
SDK 按以下顺序查找容器:
|
|
857
|
+
|
|
858
|
+
1. **外部容器** (`config.containerId`) - 如果指定且存在
|
|
859
|
+
2. **dashboard-grid** - 如果存在
|
|
860
|
+
3. **自动创建** - 如果以上都不存在,SDK 自动创建完整容器
|
|
861
|
+
|
|
862
|
+
**注意事项:**
|
|
863
|
+
- 使用方案 C 时,必须确保 `containerId` 指向的 DOM 元素存在
|
|
864
|
+
- 如果同时存在 `dashboard-grid` 和外部容器,优先使用外部容器
|
|
865
|
+
- 自动创建模式下,可以通过 `config.title` 设置看板标题
|
|
866
|
+
</body>
|
|
867
|
+
</html>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## 配置验证
|
|
873
|
+
|
|
874
|
+
DashboardEngine 会自动验证配置:
|
|
875
|
+
|
|
876
|
+
```javascript
|
|
877
|
+
try {
|
|
878
|
+
const engine = new DashboardEngine(sdk, invalidConfig);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.error(error.message);
|
|
881
|
+
// 输出:
|
|
882
|
+
// 配置错误:
|
|
883
|
+
// - widgets[0] 缺少 title
|
|
884
|
+
// - widgets[0].query 缺少 serverKey
|
|
885
|
+
// - 闭环校验失败: emit 状态 "drill_year" 没有对应的 receiveFilters 消费
|
|
886
|
+
}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### 手动验证
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
const result = validateDashboardConfig(config);
|
|
893
|
+
|
|
894
|
+
if (!result.valid) {
|
|
895
|
+
console.error('配置错误:');
|
|
896
|
+
result.errors.forEach(error => {
|
|
897
|
+
console.error(' -', error);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## 最佳实践
|
|
905
|
+
|
|
906
|
+
### 1. 数据真实性原则(强制)
|
|
907
|
+
|
|
908
|
+
**🚫 严禁伪造数据**
|
|
909
|
+
|
|
910
|
+
| 类型 | 是否可硬编码 | 说明 |
|
|
911
|
+
|------|------------|------|
|
|
912
|
+
| **筛选器选项** | ✅ 可以 | 静态值如年份列表、地区选项 |
|
|
913
|
+
| **图表数据** | ❌ 禁止 | 必须通过 `query` 查询获取 |
|
|
914
|
+
| **表格数据** | ❌ 禁止 | 必须通过 `query` 查询获取 |
|
|
915
|
+
| **KPI 数值** | ❌ 禁止 | 必须通过 `query` 查询获取 |
|
|
916
|
+
| **维度值** | ❌ 禁止 | 必须通过查询获取,不可假设 |
|
|
917
|
+
|
|
918
|
+
**✅ 正确示例:**
|
|
919
|
+
```javascript
|
|
920
|
+
// 筛选器使用静态选项(允许)
|
|
921
|
+
{
|
|
922
|
+
id: 'year',
|
|
923
|
+
label: '年份',
|
|
924
|
+
options: [
|
|
925
|
+
{ value: '2024', text: '2024年' }, // ✅ 静态选项可以硬编码
|
|
926
|
+
{ value: '2023', text: '2023年' }
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// 图表数据通过查询获取(必须)
|
|
931
|
+
{
|
|
932
|
+
id: 'sales-chart',
|
|
933
|
+
type: 'line',
|
|
934
|
+
query: {
|
|
935
|
+
metrics: ['sales_amt'], // ✅ 通过查询获取真实数据
|
|
936
|
+
groupBy: ['date__month']
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
**❌ 错误示例:**
|
|
942
|
+
```javascript
|
|
943
|
+
// 禁止在配置中伪造数据
|
|
944
|
+
{
|
|
945
|
+
id: 'sales-chart',
|
|
946
|
+
type: 'line',
|
|
947
|
+
data: [ // ❌ 错误!禁止硬编码数据
|
|
948
|
+
{ month: '1月', value: 100 },
|
|
949
|
+
{ month: '2月', value: 200 }
|
|
950
|
+
]
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### 2. 闭环校验原则
|
|
955
|
+
|
|
956
|
+
确保每个 `emit.updateState` 都有对应的 `receiveFilters.fromState`:
|
|
957
|
+
|
|
958
|
+
```javascript
|
|
959
|
+
// ✅ 正确:emit 和 receive 配对
|
|
960
|
+
emit: [{ updateState: 'drill_year' }]
|
|
961
|
+
receiveFilters: [{ fromState: 'drill_year' }]
|
|
962
|
+
|
|
963
|
+
// ❌ 错误:孤儿 emit(会报错)
|
|
964
|
+
emit: [{ updateState: 'orphan_state' }]
|
|
965
|
+
// 没有对应的 receiveFilters
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### 3. 使用 ignoreIfEmpty
|
|
969
|
+
|
|
970
|
+
```javascript
|
|
971
|
+
receiveFilters: [{
|
|
972
|
+
fromState: 'selected_year',
|
|
973
|
+
mapToDimension: 'date', // 映射到原始维度字段
|
|
974
|
+
operator: 'BETWEEN', // 日期范围使用 BETWEEN
|
|
975
|
+
ignoreIfEmpty: true // 当选择 "全部" 时不应用过滤
|
|
976
|
+
}]
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### 4. 响应式布局
|
|
980
|
+
|
|
981
|
+
```javascript
|
|
982
|
+
gridClass: 'col-span-12 md:col-span-6 lg:col-span-4'
|
|
983
|
+
// 移动端:全宽
|
|
984
|
+
// 平板:半宽
|
|
985
|
+
// 桌面:1/3 宽
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### 5. 错误处理
|
|
989
|
+
|
|
990
|
+
```javascript
|
|
991
|
+
engine.init().catch(err => {
|
|
992
|
+
console.error('看板初始化失败:', err);
|
|
993
|
+
document.getElementById('dashboard-grid').innerHTML = `
|
|
994
|
+
<div class="col-span-12 text-center text-red-500">
|
|
995
|
+
配置错误: ${err.message}
|
|
996
|
+
</div>
|
|
997
|
+
`;
|
|
998
|
+
});
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
## 常见问题
|
|
1004
|
+
|
|
1005
|
+
### Q1: 图表不显示
|
|
1006
|
+
|
|
1007
|
+
**排查步骤**:
|
|
1008
|
+
1. 检查浏览器控制台是否有错误
|
|
1009
|
+
2. 确认 ECharts 已加载
|
|
1010
|
+
3. 检查配置验证错误
|
|
1011
|
+
4. 确认容器元素存在
|
|
1012
|
+
|
|
1013
|
+
### Q2: 联动不生效
|
|
1014
|
+
|
|
1015
|
+
**排查步骤**:
|
|
1016
|
+
1. 检查 `emitsToState` 和 `fromState` 名称是否一致
|
|
1017
|
+
2. 确认 `mapToDimension` 字段名正确
|
|
1018
|
+
3. 检查是否设置了 `ignoreIfEmpty: true` 但状态为空
|
|
1019
|
+
4. 查看闭环校验错误
|
|
1020
|
+
|
|
1021
|
+
### Q3: 字段不一致如何处理
|
|
1022
|
+
|
|
1023
|
+
**解决方案**:使用 `mapToDimension` 映射:
|
|
1024
|
+
|
|
1025
|
+
```javascript
|
|
1026
|
+
// 不同数据源使用相同的 fromState,但不同的 mapToDimension
|
|
1027
|
+
receiveFilters: [{
|
|
1028
|
+
fromState: 'selected_region',
|
|
1029
|
+
mapToDimension: 'region_code', // 数据源 A 的字段
|
|
1030
|
+
operator: 'EQ'
|
|
1031
|
+
}]
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### Q4: 如何调试状态变化
|
|
1035
|
+
|
|
1036
|
+
```javascript
|
|
1037
|
+
// 监听全局状态变化
|
|
1038
|
+
globalState.subscribe('selected_year', (newValue, oldValue) => {
|
|
1039
|
+
console.log('年份变化:', oldValue, '->', newValue);
|
|
1040
|
+
});
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## 步骤 8:最终检查清单
|
|
1046
|
+
|
|
1047
|
+
在提交 Dashboard 配置之前,必须完成以下检查:
|
|
1048
|
+
|
|
1049
|
+
### 8.1 数据真实性检查
|
|
1050
|
+
|
|
1051
|
+
- [ ] **所有图表数据通过查询获取**:确认每个 widget 的 `query` 字段配置正确,没有硬编码 `data` 字段
|
|
1052
|
+
- [ ] **所有表格数据通过查询获取**:确认 table 类型的 widget 使用 `query` 而非静态数据
|
|
1053
|
+
- [ ] **所有 KPI 数值通过查询获取**:确认 kpi 类型的 widget 使用 `query` 获取真实数据
|
|
1054
|
+
- [ ] **筛选器选项合理**:静态选项值符合业务逻辑,动态选项通过查询获取
|
|
1055
|
+
|
|
1056
|
+
### 8.2 配置完整性检查
|
|
1057
|
+
|
|
1058
|
+
- [ ] **所有 widget 有唯一 id**:没有重复的 widget id
|
|
1059
|
+
- [ ] **所有 widget 有 title**:每个图表都有显示标题
|
|
1060
|
+
- [ ] **所有 query 配置正确**:
|
|
1061
|
+
- [ ] `serverKey` 和 `datasourceId` 已配置(或在 SDK 初始化时统一配置)
|
|
1062
|
+
- [ ] `metrics` 数组不为空,且指标名来自数据探查结果
|
|
1063
|
+
- [ ] `groupBy` 使用的维度名正确(区分原始字段和粒度表达式)
|
|
1064
|
+
- [ ] **所有 mapping 配置正确**:
|
|
1065
|
+
- [ ] `xAxis`、`yAxis`、`name`、`value` 等字段名与 query 返回的列名匹配
|
|
1066
|
+
- [ ] 日期字段使用正确的粒度表达式(如 `date__month`)
|
|
1067
|
+
|
|
1068
|
+
### 8.3 联动机制检查
|
|
1069
|
+
|
|
1070
|
+
- [ ] **闭环校验通过**:每个 `emit.updateState` 都有对应的 `receiveFilters.fromState`
|
|
1071
|
+
- [ ] **状态名称一致**:`emitsToState` 和 `fromState` 使用相同的命名
|
|
1072
|
+
- [ ] **字段映射正确**:`mapToDimension` 使用原始维度字段,不是粒度表达式
|
|
1073
|
+
- [ ] **日期过滤使用 BETWEEN**:所有日期范围过滤使用 `BETWEEN` 操作符
|
|
1074
|
+
|
|
1075
|
+
### 8.4 HTML 文件检查
|
|
1076
|
+
|
|
1077
|
+
- [ ] **依赖加载正确**:
|
|
1078
|
+
```html
|
|
1079
|
+
<!-- 检查以下脚本是否按顺序加载 -->
|
|
1080
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
1081
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
1082
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
1083
|
+
```
|
|
1084
|
+
- [ ] **容器元素存在**:确认 `filters-container` 和 `dashboard-grid` 元素在 HTML 中存在
|
|
1085
|
+
- [ ] **SDK 初始化正确**:
|
|
1086
|
+
```javascript
|
|
1087
|
+
// 检查 baseURL、serverKey、datasourceId 配置
|
|
1088
|
+
const sdk = new DataQuerySDK({
|
|
1089
|
+
baseURL: 'http://localhost:4001',
|
|
1090
|
+
serverKey: 'prod',
|
|
1091
|
+
datasourceId: '1'
|
|
1092
|
+
});
|
|
1093
|
+
```
|
|
1094
|
+
- [ ] **DashboardEngine 初始化**:确认 `engine.init()` 被调用且有错误处理
|
|
1095
|
+
|
|
1096
|
+
### 8.5 数据探查验证
|
|
1097
|
+
|
|
1098
|
+
- [ ] **所有指标已验证**:每个使用的指标都经过实际查询测试
|
|
1099
|
+
- [ ] **所有维度已验证**:每个使用的维度都经过带过滤条件的查询测试
|
|
1100
|
+
- [ ] **下钻路径已验证**:点击下钻的交互逻辑经过实际测试
|
|
1101
|
+
- [ ] **过滤条件已验证**:每个筛选器的选项值都经过查询确认
|
|
1102
|
+
|
|
1103
|
+
### 8.6 常见错误排查
|
|
1104
|
+
|
|
1105
|
+
如果检查失败,参考以下常见问题:
|
|
1106
|
+
|
|
1107
|
+
| 检查项 | 常见错误 | 解决方案 |
|
|
1108
|
+
|--------|---------|---------|
|
|
1109
|
+
| 数据真实性 | 硬编码了示例数据 | 删除 `data` 字段,使用 `query` 查询 |
|
|
1110
|
+
| 配置完整性 | 缺少 `title` 或 `id` | 为每个 widget 添加必填字段 |
|
|
1111
|
+
| 联动机制 | 状态名称不匹配 | 统一 `emitsToState` 和 `fromState` 的命名 |
|
|
1112
|
+
| HTML 加载 | 脚本加载顺序错误 | 确保 Tailwind → ECharts → SDK 的顺序 |
|
|
1113
|
+
| 数据探查 | 指标名错误 | 重新查询 meta 确认指标名大小写 |
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## 版本信息
|
|
1118
|
+
|
|
1119
|
+
- **版本**: 3.0.0
|
|
1120
|
+
- **更新日期**: 2025-01-15
|
|
1121
|
+
- **重大变更**: 状态机架构、emit/receiveFilters 联动机制
|
|
1122
|
+
- **文件位置**: `http://localhost:4001/sdk/data-query-sdk.js`
|