create-lumfall 1.0.0 → 1.0.1
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/cli.js +2 -2
- package/package.json +1 -1
- package/templates/document/README.md +21 -108
- package/templates/document/app/pages/docs/docs-config.js +23 -109
- package/templates/document/docs/guide/example.md +41 -0
- package/templates/document/docs/guide/introduction.md +20 -44
- package/templates/document/docs/advanced/dashboard.md +0 -66
- package/templates/document/docs/advanced/health.md +0 -72
- package/templates/document/docs/advanced/monitoring.md +0 -60
- package/templates/document/docs/advanced/security.md +0 -88
- package/templates/document/docs/core/app-instance.md +0 -114
- package/templates/document/docs/core/controller-service.md +0 -113
- package/templates/document/docs/core/lifecycle.md +0 -60
- package/templates/document/docs/core/middleware.md +0 -83
- package/templates/document/docs/core/plugins.md +0 -77
- package/templates/document/docs/core/router-schema.md +0 -102
- package/templates/document/docs/dsl/api-contract.md +0 -88
- package/templates/document/docs/dsl/extend.md +0 -311
- package/templates/document/docs/dsl/menu.md +0 -101
- package/templates/document/docs/dsl/model-project.md +0 -122
- package/templates/document/docs/dsl/overview.md +0 -116
- package/templates/document/docs/dsl/reference.md +0 -175
- package/templates/document/docs/dsl/schema-actions.md +0 -135
- package/templates/document/docs/dsl/schema.md +0 -121
- package/templates/document/docs/frontend/build.md +0 -118
- package/templates/document/docs/frontend/curl.md +0 -75
- package/templates/document/docs/frontend/page.md +0 -99
- package/templates/document/docs/frontend/widgets.md +0 -151
- package/templates/document/docs/guide/config.md +0 -108
- package/templates/document/docs/guide/deployment.md +0 -115
- package/templates/document/docs/guide/getting-started.md +0 -199
- package/templates/document/docs/guide/structure.md +0 -98
- package/templates/document/docs/reference/commands.md +0 -70
- package/templates/document/docs/reference/faq.md +0 -94
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# 接口契约
|
|
2
|
-
|
|
3
|
-
schema 模块对 `schemaConfig.api` 有**固定的调用方式**:`api` 是接口**基址**
|
|
4
|
-
(不是完整列表地址),前端自动拼接出六个标准接口。后端按需实现即可。
|
|
5
|
-
|
|
6
|
-
## 六个标准接口
|
|
7
|
-
|
|
8
|
-
| 调用 | 方法 | 地址与入参 | 响应 | 调用方 |
|
|
9
|
-
| --- | --- | --- | --- | --- |
|
|
10
|
-
| 查询列表 | `GET` | `<api>/list`,query: 搜索字段 + `page` + `pageSize`(默认 50) | `{ success, data: [...], metadata: { total } }`,`total` 必须存在 | 表格 |
|
|
11
|
-
| 单条查询 | `GET` | `<api>`,query: `{ [mainKey]: 值 }` | `{ success, data: {...} }` | 编辑/详情回显 |
|
|
12
|
-
| 新增 | `POST` | `<api>`,body: 新增表单值 | `{ success }` | 新增表单 |
|
|
13
|
-
| 更新 | `PUT` | `<api>`,body: `{ [mainKey]: 值, ...编辑表单值 }` | `{ success }` | 编辑表单 |
|
|
14
|
-
| 删除 | `DELETE` | `<api>`,body: `{ <参数名>: <值> }` | `{ success }` | 删除按钮 |
|
|
15
|
-
| 枚举选项 | `GET` | 搜索项 `dynamicSelect` 配置的 `api` 原样请求 | `{ success, data: [{label, value}] }` | 动态下拉 |
|
|
16
|
-
|
|
17
|
-
::: warning api 是基址
|
|
18
|
-
列表接口由前端自动拼接 `/list` 后缀(`GET <api>/list`),删除走
|
|
19
|
-
`DELETE <api>`。写后端路由时按基址注册,不要在 `schemaConfig.api` 里
|
|
20
|
-
写上 `/list`。
|
|
21
|
-
:::
|
|
22
|
-
|
|
23
|
-
搜索字段值原样并入 query:`dateRange` 拆为 `<field>_start` / `<field>_end`
|
|
24
|
-
(见 [schema 模块](./schema.md));**空字符串参数表示「不过滤」**,
|
|
25
|
-
后端要处理这个语义。
|
|
26
|
-
|
|
27
|
-
## 响应包络
|
|
28
|
-
|
|
29
|
-
所有接口遵循框架统一响应结构(controller 基类的 `this.success` /
|
|
30
|
-
`this.fail`,前端 `$lumfallCurl` 按此消费,见[请求工具](../frontend/curl.md)):
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"success": true,
|
|
35
|
-
"data": {},
|
|
36
|
-
"metadata": {}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
失败时 `success: false`,附 `code` 与 `message`:
|
|
41
|
-
|
|
42
|
-
| code | 含义 | 来源 |
|
|
43
|
-
| --- | --- | --- |
|
|
44
|
-
| `442` | 参数校验失败(router-schema ajv 校验) | apiParamsVerify |
|
|
45
|
-
| `445` | 非法请求:签名校验失败或时间戳超时 | apiSignVerify |
|
|
46
|
-
| `446` | 缺少 `project_key` header | projectHandler |
|
|
47
|
-
| `5000` | 服务端未捕获异常 | errorHandler |
|
|
48
|
-
| `50000` | 业务错误(message 为具体文案) | `this.fail` 约定 |
|
|
49
|
-
| `504` | 请求超时(>60s) | curl |
|
|
50
|
-
|
|
51
|
-
## 后端登记步骤
|
|
52
|
-
|
|
53
|
-
以商品管理为例,四个文件对齐契约(参考实现见 `lumfall-business/` 的
|
|
54
|
-
`app/controller/business.js` 等四件套):
|
|
55
|
-
|
|
56
|
-
1. **service** 实现数据逻辑(分页查询单条增删改)
|
|
57
|
-
2. **controller** 实现处理器,遵循 `this.success(ctx, data, { total })` /
|
|
58
|
-
`this.fail(ctx, message, code)`
|
|
59
|
-
3. **router** 注册路由,与契约对齐:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
router.get("/api/project/product/list", controller.getBusinessList.bind(controller));
|
|
63
|
-
router.get("/api/project/product", controller.getBusiness.bind(controller));
|
|
64
|
-
router.get("/api/project/productEnum/list", controller.getProductEnumList.bind(controller));
|
|
65
|
-
router.post("/api/project/product", controller.createBusiness.bind(controller));
|
|
66
|
-
router.put("/api/project/product", controller.updateBusiness.bind(controller));
|
|
67
|
-
router.delete("/api/project/product", controller.deleteBusinessList.bind(controller));
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
4. **router-schema** 登记参数校验 schema——key 必须与路由 path
|
|
71
|
-
**完全一致**,否则该校验**静默不生效**(无 schema 的 path 直接放行)。
|
|
72
|
-
|
|
73
|
-
::: tip 路径写错的坑
|
|
74
|
-
router-schema 的 path 与路由 path 不一致时**不会报错**,只是校验被跳过
|
|
75
|
-
(如 schema 写了 `/api/product` 而路由是 `/api/project/product`)。
|
|
76
|
-
排查用 `app.diagnostics.getManifest().routes` 对照。
|
|
77
|
-
:::
|
|
78
|
-
|
|
79
|
-
## Dashboard 数据接口
|
|
80
|
-
|
|
81
|
-
除业务接口外,框架为 Dashboard 本身提供三个内置数据接口(消费链路见
|
|
82
|
-
[Dashboard 与 Model 配置](../advanced/dashboard.md)):
|
|
83
|
-
|
|
84
|
-
| 接口 | 免 project_key | 说明 |
|
|
85
|
-
| --- | --- | --- |
|
|
86
|
-
| `GET /api/project/model_list` | ✓ | 全部 Model 与 Project 概要 |
|
|
87
|
-
| `GET /api/project/list?projectKey=` | ✓ | 项目列表(可按 key 过滤) |
|
|
88
|
-
| `GET /api/project?projectKey=` | ✗(需 `project_key` 头) | 合并后的完整项目配置(含 menu) |
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
# 扩展 DSL
|
|
2
|
-
|
|
3
|
-
DSL 不是封闭的。schema 模块的**搜索控件、表单控件、动态组件**以及
|
|
4
|
-
Dashboard 的 **custom 路由**都留了业务侧扩展点:在业务项目的 `app/pages/`
|
|
5
|
-
下创建与框架同名的配置文件,框架构建时通过 webpack 别名自动识别并
|
|
6
|
-
**与默认实现合并**(同名覆盖、新名追加)。
|
|
7
|
-
|
|
8
|
-
## 四个扩展点
|
|
9
|
-
|
|
10
|
-
| 业务文件(`<app-root>/app/pages/` 下) | webpack 别名 | 作用 |
|
|
11
|
-
| --- | --- | --- |
|
|
12
|
-
| `widgets/schema-search-bar/complex-view/search-item-config.js` | `$businessSearchItemConfig` | 注册 / 覆盖搜索控件 `componentType` |
|
|
13
|
-
| `widgets/schema-form/form-item-config.js` | `$businessFormItemConfig` | 注册 / 覆盖表单控件 `componentType` |
|
|
14
|
-
| `dashboard/complex-view/schema-view/components/component-config.js` | `$businessComponentConfig` | 注册 / 覆盖 schema-view 动态组件 |
|
|
15
|
-
| `dashboard/router.js` | `$businessDashboardRouterConfig` | 注册 `custom` 模块的前端路由 |
|
|
16
|
-
|
|
17
|
-
合并规则(三个组件注册表):
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
// 框架内部实现:默认注册表与业务注册表展开合并,业务同名 key 覆盖默认
|
|
21
|
-
export default {
|
|
22
|
-
...DefaultConfig,
|
|
23
|
-
...BusinessConfig,
|
|
24
|
-
};
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
::: tip 不建文件也完全不影响
|
|
28
|
-
这四个文件都是**可选**的:业务项目没创建时,别名指向框架的空模块,
|
|
29
|
-
DSL 按内置能力工作。只有需要扩展时才创建对应文件。
|
|
30
|
-
:::
|
|
31
|
-
|
|
32
|
-
## 扩展搜索控件
|
|
33
|
-
|
|
34
|
-
搜索项控件注册进 `search-item-config.js`,DSL 里通过
|
|
35
|
-
`searchOption.componentType` 使用。
|
|
36
|
-
|
|
37
|
-
**1. 写控件组件**——契约:props 收 `schemaKey` / `schema`,
|
|
38
|
-
expose `getValue()` 与 `reset()`,动态拉取选项的控件在就绪后 emit `load`:
|
|
39
|
-
|
|
40
|
-
```vue
|
|
41
|
-
<!-- app/pages/widgets/schema-search-bar/complex-view/rate/rate.vue -->
|
|
42
|
-
<template>
|
|
43
|
-
<a-rate v-model="dtoValue" v-bind="schema.option" allow-half />
|
|
44
|
-
</template>
|
|
45
|
-
|
|
46
|
-
<script setup>
|
|
47
|
-
import { ref } from "vue";
|
|
48
|
-
|
|
49
|
-
const { schemaKey, schema } = defineProps({
|
|
50
|
-
schemaKey: String,
|
|
51
|
-
schema: Object,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const emit = defineEmits(["load"]);
|
|
55
|
-
const dtoValue = ref(0);
|
|
56
|
-
|
|
57
|
-
// getValue 返回 { [字段名]: 值 };值为 undefined 时返回 {}(不下发该参数)
|
|
58
|
-
const getValue = () =>
|
|
59
|
-
dtoValue.value !== undefined ? { [schemaKey]: dtoValue.value } : {};
|
|
60
|
-
|
|
61
|
-
// reset 恢复为 DSL 里配置的 option.default
|
|
62
|
-
const reset = () => {
|
|
63
|
-
dtoValue.value = schema?.option?.default ?? 0;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
defineExpose({ getValue, reset });
|
|
67
|
-
</script>
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**2. 注册**(新建配置文件,导出「componentType → { component }」映射):
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
// app/pages/widgets/schema-search-bar/complex-view/search-item-config.js
|
|
74
|
-
import rate from "./rate/rate.vue";
|
|
75
|
-
|
|
76
|
-
export default {
|
|
77
|
-
rate: { component: rate },
|
|
78
|
-
};
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**3. 在 DSL 里使用**:
|
|
82
|
-
|
|
83
|
-
```js
|
|
84
|
-
score: {
|
|
85
|
-
type: "number",
|
|
86
|
-
label: "评分",
|
|
87
|
-
searchOption: { componentType: "rate", default: 0 },
|
|
88
|
-
},
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
内置的四种控件(`input` / `select` / `dynamicSelect` / `dateRange`)无需注册;
|
|
92
|
-
业务注册**同名** `componentType`(如 `input`)会覆盖内置控件——覆盖会影响
|
|
93
|
-
所有使用 SchemaSearchBar 的地方,谨慎操作。
|
|
94
|
-
|
|
95
|
-
## 扩展表单控件
|
|
96
|
-
|
|
97
|
-
表单控件注册进 `form-item-config.js`,供 `createFormOption` /
|
|
98
|
-
`editFormOption` 的 `componentType` 使用。契约比搜索控件多一项校验:
|
|
99
|
-
|
|
100
|
-
- props:`schemaKey` / `schema` / `model`(回显值)
|
|
101
|
-
- 可 `inject("ajv")` 拿到校验器(schema-form 已 provide)
|
|
102
|
-
- expose:`validate()`(返回布尔,失败时自行展示错误提示)、
|
|
103
|
-
`getValue()`(返回 `{ [schemaKey]: value }`)、`name`
|
|
104
|
-
|
|
105
|
-
```vue
|
|
106
|
-
<!-- app/pages/widgets/schema-form/complex-view/textarea/textarea.vue -->
|
|
107
|
-
<template>
|
|
108
|
-
<a-row class="form-item" align="center" justify="space-between">
|
|
109
|
-
<a-row class="item-label" v-if="schema.label" justify="end">
|
|
110
|
-
<span v-if="schema.option?.required" class="required">*</span>
|
|
111
|
-
{{ schema.label }}
|
|
112
|
-
</a-row>
|
|
113
|
-
<a-row class="item-value" justify="start">
|
|
114
|
-
<a-textarea
|
|
115
|
-
v-model="dtoValue"
|
|
116
|
-
v-bind="schema.option"
|
|
117
|
-
:max-length="schema.maxLength"
|
|
118
|
-
:placeholder="`请输入${schema.label}`"
|
|
119
|
-
/>
|
|
120
|
-
</a-row>
|
|
121
|
-
</a-row>
|
|
122
|
-
</template>
|
|
123
|
-
|
|
124
|
-
<script setup>
|
|
125
|
-
import { ref, toRefs, watch, inject } from "vue";
|
|
126
|
-
|
|
127
|
-
const ajv = inject("ajv");
|
|
128
|
-
|
|
129
|
-
const props = defineProps({
|
|
130
|
-
schemaKey: String,
|
|
131
|
-
schema: Object,
|
|
132
|
-
model: String,
|
|
133
|
-
});
|
|
134
|
-
const { schemaKey } = props;
|
|
135
|
-
const { schema, model } = toRefs(props);
|
|
136
|
-
|
|
137
|
-
const dtoValue = ref("");
|
|
138
|
-
|
|
139
|
-
watch([model, schema], () => {
|
|
140
|
-
dtoValue.value = model.value ?? schema.value.option?.default;
|
|
141
|
-
}, { immediate: true, deep: true });
|
|
142
|
-
|
|
143
|
-
const validate = () => {
|
|
144
|
-
// 必填 + 字段级 JSON-Schema 校验(与内置控件同一套 ajv 约定)
|
|
145
|
-
if (schema.value.option?.required && !dtoValue.value) return false;
|
|
146
|
-
if (dtoValue.value) {
|
|
147
|
-
return ajv.compile(schema.value)(dtoValue.value);
|
|
148
|
-
}
|
|
149
|
-
return true;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const getValue = () =>
|
|
153
|
-
dtoValue.value !== undefined ? { [schemaKey]: dtoValue.value } : {};
|
|
154
|
-
|
|
155
|
-
defineExpose({ validate, getValue });
|
|
156
|
-
</script>
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
// app/pages/widgets/schema-form/form-item-config.js
|
|
161
|
-
import textarea from "./complex-view/textarea/textarea.vue";
|
|
162
|
-
|
|
163
|
-
export default {
|
|
164
|
-
textarea: { component: textarea },
|
|
165
|
-
};
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
```js
|
|
169
|
-
// DSL:createFormOption.componentType: "textarea"
|
|
170
|
-
remark: {
|
|
171
|
-
type: "string",
|
|
172
|
-
label: "备注",
|
|
173
|
-
maxLength: 200,
|
|
174
|
-
createFormOption: { componentType: "textarea" },
|
|
175
|
-
},
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## 扩展 schema-view 动态组件
|
|
179
|
-
|
|
180
|
-
最强大的扩展点:给 schema 模块加**自定义抽屉 / 面板**(批量导入、
|
|
181
|
-
审计日志、自定义向导……),与内置的新增 / 编辑 / 详情同一套触发与刷新机制。
|
|
182
|
-
|
|
183
|
-
一个自定义动态组件要同时做两件事:
|
|
184
|
-
|
|
185
|
-
1. **代码侧注册**(component-config.js)
|
|
186
|
-
2. **DSL 侧声明**(`tableConfig.componentConfig` 写同名 key)——两者缺一不可,
|
|
187
|
-
schema-view 只渲染 DSL 里声明了的组件
|
|
188
|
-
|
|
189
|
-
组件契约:
|
|
190
|
-
|
|
191
|
-
| 成员 | 说明 |
|
|
192
|
-
| --- | --- |
|
|
193
|
-
| `inject("schemaViewData")` | 拿到 `api`、`components`(其中自己 comName 对应 `{ schema, config }`) |
|
|
194
|
-
| expose `name` | 必须等于 componentConfig 的 key,schema-view 按它找组件 ref |
|
|
195
|
-
| expose `show(rowData)` | 按钮触发时被调用;行按钮传行数据,**表头按钮不传** |
|
|
196
|
-
| emit `command` | `{ event: "loadTableData" }` 让框架刷新表格 |
|
|
197
|
-
|
|
198
|
-
```vue
|
|
199
|
-
<!-- app/pages/dashboard/complex-view/schema-view/components/batch-import.vue -->
|
|
200
|
-
<template>
|
|
201
|
-
<a-drawer v-model:visible="isShow" :title="config.title || '批量导入'" :width="550">
|
|
202
|
-
<!-- 自己的表单 / 上传逻辑;schema 来自 components.value.batchImport.schema -->
|
|
203
|
-
<a-button type="primary" @click="save">导入</a-button>
|
|
204
|
-
</a-drawer>
|
|
205
|
-
</template>
|
|
206
|
-
|
|
207
|
-
<script setup>
|
|
208
|
-
import { ref, inject, defineEmits } from "vue";
|
|
209
|
-
|
|
210
|
-
const { api, components } = inject("schemaViewData") || {};
|
|
211
|
-
const emit = defineEmits(["command"]);
|
|
212
|
-
|
|
213
|
-
const isShow = ref(false);
|
|
214
|
-
const config = ref({});
|
|
215
|
-
const name = ref("batchImport"); // 与 componentConfig 的 key 一致
|
|
216
|
-
|
|
217
|
-
const show = (rowData) => {
|
|
218
|
-
// 表头按钮触发时 rowData 为 undefined
|
|
219
|
-
config.value = components.value?.batchImport?.config || {};
|
|
220
|
-
isShow.value = true;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const save = async () => {
|
|
224
|
-
// ...调用接口
|
|
225
|
-
isShow.value = false;
|
|
226
|
-
emit("command", { event: "loadTableData" }); // 保存成功后刷新表格
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
defineExpose({ name, show });
|
|
230
|
-
</script>
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
```js
|
|
234
|
-
// app/pages/dashboard/complex-view/schema-view/components/component-config.js
|
|
235
|
-
import batchImport from "./batch-import.vue";
|
|
236
|
-
|
|
237
|
-
export default {
|
|
238
|
-
batchImport: { component: batchImport },
|
|
239
|
-
};
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
```js
|
|
243
|
-
// DSL:componentConfig 声明 + 按钮触发
|
|
244
|
-
tableConfig: {
|
|
245
|
-
headerButtons: [
|
|
246
|
-
{ label: "批量导入", eventKey: "showComponent", type: "outline",
|
|
247
|
-
eventOption: { comName: "batchImport" } },
|
|
248
|
-
],
|
|
249
|
-
componentConfig: {
|
|
250
|
-
batchImport: { title: "批量导入" },
|
|
251
|
-
// 与内置的 createForm / editForm / detailPanel 共存
|
|
252
|
-
},
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
::: tip 自定义组件的字段 schema 自动派生
|
|
257
|
-
`buildDtoSchema` 对 componentConfig 里的**任意** key 生效:DSL 字段里配置
|
|
258
|
-
`batchImportOption: { ... }` 的字段会自动进入该组件的 schema
|
|
259
|
-
(`components.value.batchImport.schema`),与内置表单同一套机制。
|
|
260
|
-
:::
|
|
261
|
-
|
|
262
|
-
## 注册 custom 模块路由
|
|
263
|
-
|
|
264
|
-
`custom` 模块的 `customConfig.path` 指向的前端路由由业务注册。
|
|
265
|
-
框架启动 Dashboard 时会调用业务的路由配置:
|
|
266
|
-
|
|
267
|
-
```js
|
|
268
|
-
// app/pages/dashboard/router.js
|
|
269
|
-
module.exports = ({ routes, siderRoutes }) => {
|
|
270
|
-
// 顶层路由:customConfig.path: "/article-manage" 对应这里
|
|
271
|
-
routes.push({
|
|
272
|
-
path: "/view/dashboard/article-manage",
|
|
273
|
-
component: () => import("./article-manage/article-manage.vue"),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// sider 子路由:相对路径,最终挂到 /sider/<path>
|
|
277
|
-
siderRoutes.push({
|
|
278
|
-
path: "article-sider",
|
|
279
|
-
component: () => import("./article-manage/article-manage.vue"),
|
|
280
|
-
});
|
|
281
|
-
};
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
- `routes` 是顶层路由数组(框架的 `/iframe`、`/schema`、`/sider` 已在里面),
|
|
285
|
-
path 写完整 `/view/dashboard/...`
|
|
286
|
-
- `siderRoutes` 是 sider 的 children,path 写**相对路径**(不带前导 `/`),
|
|
287
|
-
最终路由为 `/view/dashboard/sider/<path>`
|
|
288
|
-
- 业务路由注册发生在 sider 通配路由之前,同名时业务优先
|
|
289
|
-
|
|
290
|
-
::: warning 与 menu DSL 的对应关系
|
|
291
|
-
`custom` 模块的 `customConfig.path: "/article-manage"` 必须能在 `routes`
|
|
292
|
-
里匹配到;sider 子菜单的 custom 项 `path` 必须以 `/` 开头且对应
|
|
293
|
-
`siderRoutes` 注册的子路由(或框架已注册的 `/sider/iframe`、`/sider/schema`),
|
|
294
|
-
否则点击后内容区为空。
|
|
295
|
-
:::
|
|
296
|
-
|
|
297
|
-
## 事件与行为的边界
|
|
298
|
-
|
|
299
|
-
- 按钮内置行为只有两种:`showComponent`(打开动态组件)与 `delete`
|
|
300
|
-
(确认框 + `DELETE <api>` + 刷新)
|
|
301
|
-
- 其他 `eventKey`(如 `edit`)会向上 emit `operate` 事件,但 schema-view
|
|
302
|
-
内置的处理器当前只响应 `showComponent`——**扩展新的按钮行为时,
|
|
303
|
-
优先用 `showComponent` + 自定义动态组件**,而不是自定义 eventKey
|
|
304
|
-
- 需要完全自定义列表页交互(超出抽屉形态)时,直接用 `custom` 模块 +
|
|
305
|
-
自定义页面,页面里仍可独立使用 SchemaTable / SchemaSearchBar /
|
|
306
|
-
SchemaForm(见[内置组件](../frontend/widgets.md))
|
|
307
|
-
|
|
308
|
-
## 下一步
|
|
309
|
-
|
|
310
|
-
- [内置组件](../frontend/widgets.md):脱离 DSL 独立使用这些组件
|
|
311
|
-
- [完整模板与注意事项](./reference.md):DSL 全量避坑清单
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# 菜单项 DSL
|
|
2
|
-
|
|
3
|
-
菜单项是 DSL 的骨架:定义 Dashboard 的导航结构与每个入口的页面形态。
|
|
4
|
-
`menu[]` 中的每个元素就是一个菜单项。
|
|
5
|
-
|
|
6
|
-
## 通用字段
|
|
7
|
-
|
|
8
|
-
| 字段 | 类型 | 必填 | 说明 |
|
|
9
|
-
| --- | --- | --- | --- |
|
|
10
|
-
| `key` | `string` | 是 | 菜单项唯一标识,**用于路由 query 参数 `?key=xxx`**,也是 Model/Project 合并的匹配键 |
|
|
11
|
-
| `name` | `string` | 否 | 显示名称(Project 覆盖时可省略,继承 Model) |
|
|
12
|
-
| `menuType` | `string` | 是* | `"module"` 普通模块 / `"group"` 分组 |
|
|
13
|
-
| `moduleType` | `string` | — | `menuType: "module"` 时有效,见下表 |
|
|
14
|
-
|
|
15
|
-
**默认行为**:不写 `menuType` 时,该菜单项是**仅覆盖属性的 module**——
|
|
16
|
-
从 Model 继承 `menuType` / `moduleType` / 对应 config,只改 `name` 等字段。
|
|
17
|
-
这是 Project 覆盖 Model 的最简形式。
|
|
18
|
-
|
|
19
|
-
::: warning group 必须带 subMenu
|
|
20
|
-
前端通过**是否含 `subMenu`** 决定渲染为下拉子菜单(`a-sub-menu`),
|
|
21
|
-
所以 `menuType: "group"` 的菜单项必须同时包含 `subMenu` 数组才能正确渲染。
|
|
22
|
-
:::
|
|
23
|
-
|
|
24
|
-
## moduleType:四种模块形态
|
|
25
|
-
|
|
26
|
-
| 值 | 前端路由 | 必填 config | 说明 |
|
|
27
|
-
| --- | --- | --- | --- |
|
|
28
|
-
| `custom` | `customConfig.path` | `customConfig` | 自定义页,跳到业务注册的前端路由 |
|
|
29
|
-
| `sider` | `/view/dashboard/sider` | `siderConfig` | 侧边复合视图:左侧子菜单 + 右侧子页面 |
|
|
30
|
-
| `iframe` | `/view/dashboard/iframe` | `iframeConfig` | iframe 嵌入页 |
|
|
31
|
-
| `schema` | `/view/dashboard/schema` | `schemaConfig` | schema 驱动列表页(主要形态),见 [schema 模块 DSL](./schema.md) |
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
// 四种形态的配置形状
|
|
35
|
-
{
|
|
36
|
-
key: "home", name: "首页",
|
|
37
|
-
menuType: "module", moduleType: "custom",
|
|
38
|
-
customConfig: { path: "/todo" }, // 页面内路由
|
|
39
|
-
}
|
|
40
|
-
{
|
|
41
|
-
key: "external", name: "外部链接",
|
|
42
|
-
menuType: "module", moduleType: "iframe",
|
|
43
|
-
iframeConfig: { path: "https://example.com" }, // 完整 URL 或页面内路径
|
|
44
|
-
}
|
|
45
|
-
{
|
|
46
|
-
key: "management", name: "管理",
|
|
47
|
-
menuType: "module", moduleType: "sider",
|
|
48
|
-
siderConfig: { menu: [/* 子菜单,元素结构与 menu[] 一致 */] },
|
|
49
|
-
}
|
|
50
|
-
{
|
|
51
|
-
key: "product", name: "商品管理",
|
|
52
|
-
menuType: "module", moduleType: "schema",
|
|
53
|
-
schemaConfig: { api: "...", schema: { /* ... */ } },
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
- `iframe` 的 `path` 原样作为 `<iframe src>`:完整 URL 嵌第三方页面;
|
|
58
|
-
相对路径(如 `todo`)基于当前页面 URL 解析,落到服务端页面路由。未配置时显示空态
|
|
59
|
-
- `siderConfig` 是**对象**(不是数组),其 `menu` 子菜单支持 custom / iframe /
|
|
60
|
-
schema,也支持嵌套 group(层级不宜过深)
|
|
61
|
-
|
|
62
|
-
## 分组菜单(group + subMenu)
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
{
|
|
66
|
-
key: "system",
|
|
67
|
-
name: "系统管理",
|
|
68
|
-
menuType: "group",
|
|
69
|
-
subMenu: [
|
|
70
|
-
{ key: "users", name: "用户管理", menuType: "module", moduleType: "custom", customConfig: { path: "/todo" } },
|
|
71
|
-
{ key: "roles", name: "角色管理", menuType: "module", moduleType: "custom", customConfig: { path: "/todo" } },
|
|
72
|
-
],
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`subMenu` 中每一项的结构与顶层 `menu[]` 完全一致,支持任意嵌套;
|
|
77
|
-
子项必须写 `menuType: "module"`。
|
|
78
|
-
|
|
79
|
-
## 路由与 query 参数
|
|
80
|
-
|
|
81
|
-
Dashboard 是 history 路由的单页,基址 `/view/dashboard`(与框架服务端
|
|
82
|
-
`/view/:page/*` 路由前缀一致)。DSL 中的所有路径(`homePage`、
|
|
83
|
-
`customConfig.path`)都写**不带基址**的页面内路由。
|
|
84
|
-
|
|
85
|
-
| query 参数 | 说明 | 示例 |
|
|
86
|
-
| --- | --- | --- |
|
|
87
|
-
| `projectKey` | 当前项目标识(= Project 文件名) | `pdd` |
|
|
88
|
-
| `key` | 当前选中的头部菜单项 key | `product` |
|
|
89
|
-
| `siderKey` | 当前选中的 sider 子菜单项 key(仅 sider 模块) | `cpopon` |
|
|
90
|
-
| 任意字段名 | 与 schema 字段同名时**预填该搜索项默认值**(仅 schema 模块) | `?productName=手机` |
|
|
91
|
-
|
|
92
|
-
::: warning sider 子菜单 custom 路径必须以 / 开头
|
|
93
|
-
sider 子菜单点击跳转 `/sider/<子模块路由>`,custom 子项的
|
|
94
|
-
`customConfig.path` 会**原样拼接**到 `/sider` 后。不带前导斜杠的
|
|
95
|
-
`path: "taobao/cpopon"` 会拼出非法路由 `/sidertaobao/cpopon`,
|
|
96
|
-
点击无法跳转——必须写 `path: "/todo"` 这样以 `/` 开头的页面内路由。
|
|
97
|
-
:::
|
|
98
|
-
|
|
99
|
-
## 下一步
|
|
100
|
-
|
|
101
|
-
- [schema 模块 DSL](./schema.md):主要形态的完整配置
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# Model 与 Project
|
|
2
|
-
|
|
3
|
-
DSL 的两层结构:**Model 定义公共默认**,**Project 定义差异化**,两者深度合并后
|
|
4
|
-
就是前端拿到的最终项目配置。
|
|
5
|
-
|
|
6
|
-
## Model 配置(`model/<modelKey>/model.js`)
|
|
7
|
-
|
|
8
|
-
| 字段 | 类型 | 必填 | 说明 |
|
|
9
|
-
| --- | --- | --- | --- |
|
|
10
|
-
| `model` | `string` | 是 | 模式类型标识,固定 `"dashboard"`(注意字段名是 `model`,不是 `mode`) |
|
|
11
|
-
| `name` | `string` | 是 | Model 显示名称 |
|
|
12
|
-
| `menu` | `array` | 是 | 默认菜单数组,该 Model 下所有 Project 共享的菜单骨架 |
|
|
13
|
-
|
|
14
|
-
> `key` 由扫描器按**目录名**自动注入,不要手写。
|
|
15
|
-
> `desc` / `icon` / `homePage` 是 Project 顶层字段,不属于 Model。
|
|
16
|
-
|
|
17
|
-
## Project 配置(`model/<modelKey>/project/<projectKey>.js`)
|
|
18
|
-
|
|
19
|
-
| 字段 | 类型 | 必填 | 说明 |
|
|
20
|
-
| --- | --- | --- | --- |
|
|
21
|
-
| `name` | `string` | 是 | 项目显示名称 |
|
|
22
|
-
| `desc` | `string` | 是 | 项目描述 |
|
|
23
|
-
| `homePage` | `string` | 是 | 默认首页,**页面内路由**(如 `/schema?projectKey=pdd&key=product`,不带 `/view/dashboard` 前缀) |
|
|
24
|
-
| `menu` | `array` | 是 | 项目菜单,与 Model 的 `menu` 深度合并 |
|
|
25
|
-
| `icon` | `string` | 否 | 预留字段 |
|
|
26
|
-
|
|
27
|
-
> `key`(= 文件名)与 `modelKey`(= 所属目录名)由扫描器自动注入。
|
|
28
|
-
|
|
29
|
-
## 合并规则
|
|
30
|
-
|
|
31
|
-
合并逻辑在框架 `model/index.js` 的 `projectExtendModel` 中,基于
|
|
32
|
-
`lodash.mergeWith`,但对**数组有特殊的按 key 合并规则**:
|
|
33
|
-
|
|
34
|
-
### 普通对象:深度合并
|
|
35
|
-
|
|
36
|
-
Project 的同名属性覆盖 Model 的,Model 独有的属性保留:
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
// Model: { name: "商品管理", customConfig: { path: "/todo" } }
|
|
40
|
-
// Project:{ name: "商品管理(pdd)" }
|
|
41
|
-
// 结果: { name: "商品管理(pdd)", customConfig: { path: "/todo" } }
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### 数组:按元素 `key` 智能合并
|
|
45
|
-
|
|
46
|
-
以 `menu` 为例,合并不是拼接或整体覆盖,而是:
|
|
47
|
-
|
|
48
|
-
1. 遍历 Model 数组:每个元素去 Project 数组里找**同 `key`** 元素——
|
|
49
|
-
找到则递归深度合并,找不到则原样保留
|
|
50
|
-
2. 遍历 Project 数组:Model 中没有的新 `key`,**追加到结果末尾**
|
|
51
|
-
|
|
52
|
-
```text
|
|
53
|
-
Model.menu = [{ key: "product", name: "商品管理", moduleType: "custom", ... },
|
|
54
|
-
{ key: "order", name: "订单管理", ... }]
|
|
55
|
-
Project.menu = [{ key: "product", name: "商品管理(pdd)" },
|
|
56
|
-
{ key: "data", name: "数据管理(pdd)", moduleType: "sider", ... }]
|
|
57
|
-
|
|
58
|
-
结果 = [{ key: "product", name: "商品管理(pdd)", moduleType: "custom", ... }, ← 覆盖
|
|
59
|
-
{ key: "order", name: "订单管理", ... }, ← 保留
|
|
60
|
-
{ key: "data", name: "数据管理(pdd)", moduleType: "sider", ... }] ← 追加
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 递归生效
|
|
64
|
-
|
|
65
|
-
合并是**递归**的:`siderConfig.menu`、`subMenu` 等嵌套数组里的元素同样按 `key`
|
|
66
|
-
匹配合并,行为在所有层级一致。
|
|
67
|
-
|
|
68
|
-
::: warning 数组元素必须带 key
|
|
69
|
-
数组合并完全依赖 `key` 匹配——`menu`(以及需要被 Project 覆盖的嵌套数组)里
|
|
70
|
-
**每个元素都必须写 `key`**,否则合并不生效。
|
|
71
|
-
:::
|
|
72
|
-
|
|
73
|
-
## 三种典型覆盖姿势
|
|
74
|
-
|
|
75
|
-
```js
|
|
76
|
-
// model/business/project/pdd.js
|
|
77
|
-
module.exports = {
|
|
78
|
-
name: "拼多多",
|
|
79
|
-
desc: "拼多多电商项目",
|
|
80
|
-
homePage: "/schema?projectKey=pdd&key=product",
|
|
81
|
-
menu: [
|
|
82
|
-
// 1. 只覆盖属性:只写 key + 差异字段,其余从 Model 继承
|
|
83
|
-
{ key: "product", name: "商品管理(pdd)" },
|
|
84
|
-
|
|
85
|
-
// 2. 覆盖并改变形态:从 custom 换成 schema 模块(要带全对应 config)
|
|
86
|
-
{
|
|
87
|
-
key: "client",
|
|
88
|
-
name: "客户管理(pdd)",
|
|
89
|
-
moduleType: "schema",
|
|
90
|
-
schemaConfig: { api: "/api/client", schema: {} },
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
// 3. 新增菜单项:Model 中不存在,追加到末尾
|
|
94
|
-
{
|
|
95
|
-
key: "data",
|
|
96
|
-
name: "数据管理(pdd)",
|
|
97
|
-
menuType: "module",
|
|
98
|
-
moduleType: "sider",
|
|
99
|
-
siderConfig: { menu: [/* ... */] },
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
};
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
::: warning 覆盖形态时要带全 config
|
|
106
|
-
把某个菜单项的 `moduleType` 从 `custom` 改成 `iframe` 时,必须同时提供
|
|
107
|
-
`iframeConfig`;改成 `schema` 时必须提供 `schemaConfig`,否则前端跳转失败。
|
|
108
|
-
:::
|
|
109
|
-
|
|
110
|
-
## 新增一套 DSL 的完整步骤
|
|
111
|
-
|
|
112
|
-
1. 在 `model/` 下创建目录(目录名 = Model key),编写 `model.js`
|
|
113
|
-
2. 在其 `project/` 子目录下创建文件(文件名 = Project key),编写 Project 配置
|
|
114
|
-
3. 重启服务——扫描器自动发现,无需注册任何代码
|
|
115
|
-
|
|
116
|
-
验证:`GET /api/project/list` 能看到新项目,
|
|
117
|
-
`GET /api/project?projectKey=<key>` 返回合并后的完整配置
|
|
118
|
-
(接口详情见 [Dashboard 数据接口](../advanced/dashboard.md))。
|
|
119
|
-
|
|
120
|
-
## 下一步
|
|
121
|
-
|
|
122
|
-
- [菜单项 DSL](./menu.md):菜单项的字段与四种模块形态
|