@_tc/template-core 0.2.3 → 0.2.5
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 +1500 -1500
- package/cjs/packages/common/http/index.js +1 -1
- package/esm/packages/common/http/index.js +1 -1
- package/fe/packages/common/http/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TemplateCore 是一个基于 TypeScript、Koa、React 的后台框架包。它把后端约定式加载、前端 Dashboard、Schema CRUD 页面、model 类型和类型扩展放到同一个 npm 包里,项目只需要按目录补充自己的 `app/`、`model/`、`frontend/`。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
预览 & 文档地址:https://t-c-s-p.allomg.qzz.io/preview
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -12,33 +12,10 @@ pnpm add @_tc/template-core
|
|
|
12
12
|
|
|
13
13
|
要求:
|
|
14
14
|
|
|
15
|
-
- Node.js >=
|
|
15
|
+
- Node.js >= 22.13.0
|
|
16
16
|
- pnpm >= 8
|
|
17
17
|
- 使用内置前端时,需要安装 peer dependencies;完整清单以包内 `package.json` 的 `peerDependencies` 为准,包括 Koa、React、Vite、路由、状态、表单和 UI 相关依赖。
|
|
18
18
|
|
|
19
|
-
## 可选:给项目 AI 助手的文档与 Skill
|
|
20
|
-
|
|
21
|
-
如果你在项目中让 AI 助手协助开发,可以让它优先读取 npm 包内的 `AGENT_README.md`。安装后的路径通常是 `node_modules/@_tc/template-core/AGENT_README.md`。这份文档面向“协助使用 TemplateCore 的 AI 助手”,覆盖公开入口、最小启动、目录约定、model 配置、前端使用和构建方式。
|
|
22
|
-
|
|
23
|
-
包内附带两份 Agent Skill,本质是可复用的 Agent 指令包,采用渐进式披露(SKILL.md + reference/)结构。
|
|
24
|
-
|
|
25
|
-
| Skill | 用途 |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| `tc-generator` | 按 TemplateCore 约定生成项目、model 配置、Schema CRUD、controller 和 router。 |
|
|
28
|
-
| `tc-component-usage-skills` | 指导 Agent 选用和正确使用内置 React 组件库(Button、Form、DataTable、Modal 等)。 |
|
|
29
|
-
|
|
30
|
-
如果你的 Agent 支持本地 skills 目录,可以从已安装的 npm 包复制安装。以 Codex 为例:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
|
34
|
-
cp -R node_modules/@_tc/template-core/.skills/tc-generator "${CODEX_HOME:-$HOME/.codex}/skills/tc-generator"
|
|
35
|
-
cp -R node_modules/@_tc/template-core/.skills/tc-component-usage-skills "${CODEX_HOME:-$HOME/.codex}/skills/tc-component-usage-skills"
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
安装后重启对应 Agent,让新 skill 生效。使用时可以直接说”用 tc-generator 生成一个商品管理模块”或”用 tc-component-usage-skills 帮我写一个带搜索的分页表格”。
|
|
39
|
-
|
|
40
|
-
其他 Agent 如果不支持自动安装 skill,也可以直接读取 `.skills/tc-generator/SKILL.md` 或 `.skills/tc-component-usage-skills/SKILL.md`,再按需读取 `reference/` 下的参考文件。
|
|
41
|
-
|
|
42
19
|
## 快速使用
|
|
43
20
|
|
|
44
21
|
项目建议结构:
|
|
@@ -186,6 +163,24 @@ http://localhost:9000/dash?projk=demo
|
|
|
186
163
|
|
|
187
164
|
`projk` 对应 `model/{modelKey}/project/{projectKey}.ts` 里的 `projectKey`。
|
|
188
165
|
|
|
166
|
+
## 内置功能
|
|
167
|
+
|
|
168
|
+
- Koa 服务启动:`serverStart(options)`。
|
|
169
|
+
- 约定式加载:自动加载 `config`、`extends`、`router-schema`、`service`、`controller`、`middlewares`、根级 `middleware`、`router`。
|
|
170
|
+
- 双层 app:框架内置 `app/` 和项目 `app/` 一起加载。
|
|
171
|
+
- 默认项目接口:
|
|
172
|
+
- `GET /api/project/model_list`
|
|
173
|
+
- `GET /api/project/list`
|
|
174
|
+
- `GET /api/project/:key`
|
|
175
|
+
- React Dashboard:内置 `/dash` 页面入口。
|
|
176
|
+
- 组件预览页面:内置 `/ui-components` 页面,用于查看和测试框架内置的 UI 组件。
|
|
177
|
+
- 菜单渲染:支持 `schema`、`custom`、`iframe`、`sidebar`。
|
|
178
|
+
- Schema CRUD:根据 `model` 中的 `schemaConfig` 生成搜索、表格、新增、编辑、详情。
|
|
179
|
+
- 内置 UI 组件:提供丰富的 React 组件库,详见下方"内置 UI 组件"章节。
|
|
180
|
+
- API 中间件:错误处理、静态资源、body parser、项目上下文、接口签名、参数校验。
|
|
181
|
+
- 前端构建:扫描框架和项目的 `frontend/**/*.entry.{ts,tsx,js,jsx}`,输出服务端可渲染的 `.entry.tpl`,并写入 `FEBuildKey` 供 HTML ETag 缓存失效使用。
|
|
182
|
+
- 类型扩展:支持扩展 SchemaForm、SchemaTable 单元格渲染组件、CallCom、KoaApp。
|
|
183
|
+
|
|
189
184
|
## 常用公开入口
|
|
190
185
|
|
|
191
186
|
完整 exports 以发布包内 `package.json` 为准,除下表外还包含 `./fe/main`、`./fe/common/request`、`./fe/*`、`./fe/rc/*`、`./fe/rc/hooks` 等子路径。
|
|
@@ -245,1961 +240,1966 @@ export const demoRouter = ((app, router) => {
|
|
|
245
240
|
}) satisfies RouterFN
|
|
246
241
|
```
|
|
247
242
|
|
|
248
|
-
##
|
|
243
|
+
## model 数据如何配置
|
|
249
244
|
|
|
250
|
-
|
|
245
|
+
`model` 是后台菜单、项目和 Schema 页面的数据源。框架会读取:
|
|
251
246
|
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
|
|
247
|
+
```text
|
|
248
|
+
model/{modelKey}/mode.ts
|
|
249
|
+
model/{modelKey}/project/{projectKey}.ts
|
|
255
250
|
```
|
|
256
251
|
|
|
257
|
-
|
|
252
|
+
`mode.ts` 定义模型模板:
|
|
258
253
|
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
@import "@_tc/template-core/fe/tailwind_ui.css";
|
|
254
|
+
```ts
|
|
255
|
+
import type { ModelDataType } from '@_tc/template-core/model'
|
|
262
256
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
const model: ModelDataType = {
|
|
258
|
+
mode: 'MB',
|
|
259
|
+
name: '商品后台',
|
|
260
|
+
desc: '商品管理',
|
|
261
|
+
icon: '',
|
|
262
|
+
homePage: '/_sidebar_/product?projk=demo',
|
|
263
|
+
menuLayout: 'left',
|
|
264
|
+
menu: [
|
|
265
|
+
{
|
|
266
|
+
key: 'product',
|
|
267
|
+
name: '商品列表',
|
|
268
|
+
menuType: 'module',
|
|
269
|
+
moduleType: 'schema',
|
|
270
|
+
schemaConfig: {
|
|
271
|
+
api: '/api/product',
|
|
272
|
+
schema: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties: {
|
|
275
|
+
product_id: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
label: '商品 ID',
|
|
278
|
+
tableOption: { width: 160 },
|
|
279
|
+
detailPanelOption: {},
|
|
280
|
+
editFormOption: {
|
|
281
|
+
comType: 'input',
|
|
282
|
+
disabled: true,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
product_name: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
label: '商品名称',
|
|
288
|
+
tableOption: {},
|
|
289
|
+
searchOption: { comType: 'input' },
|
|
290
|
+
createFormOption: { comType: 'input' },
|
|
291
|
+
editFormOption: { comType: 'input' },
|
|
292
|
+
detailPanelOption: {},
|
|
293
|
+
},
|
|
294
|
+
price: {
|
|
295
|
+
type: 'number',
|
|
296
|
+
label: '价格',
|
|
297
|
+
tableOption: {},
|
|
298
|
+
createFormOption: { comType: 'inputNumber' },
|
|
299
|
+
editFormOption: { comType: 'inputNumber' },
|
|
300
|
+
detailPanelOption: {},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
required: ['product_name'],
|
|
304
|
+
},
|
|
305
|
+
componentConfig: {
|
|
306
|
+
createForm: {
|
|
307
|
+
title: '新增商品',
|
|
308
|
+
saveBtnText: '创建',
|
|
309
|
+
},
|
|
310
|
+
editForm: {
|
|
311
|
+
title: '编辑商品',
|
|
312
|
+
saveBtnText: '保存',
|
|
313
|
+
fetchKey: 'product_id',
|
|
314
|
+
},
|
|
315
|
+
detailPanel: {
|
|
316
|
+
title: '商品详情',
|
|
317
|
+
fetchKey: 'product_id',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
tableConfig: {
|
|
321
|
+
headerButtons: [
|
|
322
|
+
{
|
|
323
|
+
label: '新增',
|
|
324
|
+
eventKey: 'callComponent',
|
|
325
|
+
variant: 'primary',
|
|
326
|
+
eventOption: { comName: 'createForm' },
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
rowButtons: [
|
|
330
|
+
{
|
|
331
|
+
label: '编辑',
|
|
332
|
+
eventKey: 'callComponent',
|
|
333
|
+
eventOption: { comName: 'editForm' },
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
label: '详情',
|
|
337
|
+
eventKey: 'callComponent',
|
|
338
|
+
eventOption: { comName: 'detailPanel' },
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
label: '删除',
|
|
342
|
+
eventKey: 'remove',
|
|
343
|
+
eventOption: {
|
|
344
|
+
params: {
|
|
345
|
+
product_id: '$schema::product_id',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default model
|
|
266
357
|
```
|
|
267
358
|
|
|
268
|
-
|
|
359
|
+
`name`、`desc`、`icon`、`homePage` 都是可选项。Dashboard 页头标题优先使用 `desc`,没有时回退到 `name`;`icon` 可以是图片地址、图片路径、data image 或普通文本。未配置 `homePage` 时会自动跳到第一个可用菜单;配置时必须命中菜单生成的真实路由,左侧布局通常是 `/_sidebar_/{menuKey}`,顶部布局通常是 `/{menuKey}`。
|
|
269
360
|
|
|
270
|
-
|
|
271
|
-
/* frontend/main.css */
|
|
272
|
-
@import "tailwindcss";
|
|
361
|
+
`project/demo.ts` 定义项目覆盖:
|
|
273
362
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
363
|
+
```ts
|
|
364
|
+
export default {
|
|
365
|
+
name: 'Demo 企业',
|
|
366
|
+
desc: 'Demo 企业商品后台',
|
|
367
|
+
homePage: '/_sidebar_/product?projk=demo',
|
|
368
|
+
}
|
|
277
369
|
```
|
|
278
370
|
|
|
279
|
-
|
|
371
|
+
合并规则:
|
|
280
372
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
373
|
+
- `project` 会继承同目录上层 `mode`。
|
|
374
|
+
- 对象递归合并,`project` 覆盖 `mode`。
|
|
375
|
+
- 数组按 `key` 合并:同 key 覆盖,不同 key 新增。
|
|
376
|
+
- `model/index.ts` 或 `model/index.js` 会被 loader 跳过。
|
|
377
|
+
|
|
378
|
+
### model 数组合并示例
|
|
379
|
+
|
|
380
|
+
假设 `mode.ts` 定义了两个菜单项:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
// model/product/mode.ts
|
|
384
|
+
export default {
|
|
385
|
+
name: '商品后台',
|
|
386
|
+
menu: [
|
|
387
|
+
{
|
|
388
|
+
key: 'product',
|
|
389
|
+
name: '商品列表',
|
|
390
|
+
menuType: 'module',
|
|
391
|
+
moduleType: 'schema',
|
|
392
|
+
schemaConfig: {
|
|
393
|
+
api: '/api/product',
|
|
394
|
+
// ... 省略 schema 配置
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
key: 'category',
|
|
399
|
+
name: '分类管理',
|
|
400
|
+
menuType: 'module',
|
|
401
|
+
moduleType: 'schema',
|
|
402
|
+
schemaConfig: {
|
|
403
|
+
api: '/api/category',
|
|
404
|
+
// ... 省略 schema 配置
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
],
|
|
288
408
|
}
|
|
289
409
|
```
|
|
290
410
|
|
|
291
|
-
|
|
411
|
+
`project/demo.ts` 可以:
|
|
292
412
|
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
413
|
+
```ts
|
|
414
|
+
// model/product/project/demo.ts
|
|
415
|
+
export default {
|
|
416
|
+
name: 'Demo 企业商品后台',
|
|
417
|
+
menu: [
|
|
418
|
+
// 1. 覆盖:修改 product 菜单的名称和配置
|
|
419
|
+
{
|
|
420
|
+
key: 'product',
|
|
421
|
+
name: 'Demo 商品', // 覆盖 name
|
|
422
|
+
schemaConfig: {
|
|
423
|
+
api: '/api/demo/product', // 覆盖 api
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
// 2. 新增:添加 brand 菜单
|
|
427
|
+
{
|
|
428
|
+
key: 'brand',
|
|
429
|
+
name: '品牌管理',
|
|
430
|
+
menuType: 'module',
|
|
431
|
+
moduleType: 'schema',
|
|
432
|
+
schemaConfig: {
|
|
433
|
+
api: '/api/brand',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
// 3. 继承:category 菜单未在 project 中定义,会完整继承 mode 的配置
|
|
437
|
+
],
|
|
299
438
|
}
|
|
300
439
|
```
|
|
301
440
|
|
|
302
|
-
|
|
441
|
+
合并后的结果:
|
|
303
442
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
443
|
+
```ts
|
|
444
|
+
{
|
|
445
|
+
name: 'Demo 企业商品后台', // project 覆盖
|
|
446
|
+
menu: [
|
|
447
|
+
{
|
|
448
|
+
key: 'product',
|
|
449
|
+
name: 'Demo 商品', // project 覆盖
|
|
450
|
+
menuType: 'module', // mode 继承
|
|
451
|
+
moduleType: 'schema', // mode 继承
|
|
452
|
+
schemaConfig: {
|
|
453
|
+
api: '/api/demo/product', // project 覆盖
|
|
454
|
+
// schema 配置会递归合并
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
key: 'category', // mode 完整继承
|
|
459
|
+
name: '分类管理',
|
|
460
|
+
menuType: 'module',
|
|
461
|
+
moduleType: 'schema',
|
|
462
|
+
schemaConfig: {
|
|
463
|
+
api: '/api/category',
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
key: 'brand', // project 新增
|
|
468
|
+
name: '品牌管理',
|
|
469
|
+
menuType: 'module',
|
|
470
|
+
moduleType: 'schema',
|
|
471
|
+
schemaConfig: {
|
|
472
|
+
api: '/api/brand',
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
}
|
|
477
|
+
```
|
|
319
478
|
|
|
320
|
-
|
|
479
|
+
**关键点**:
|
|
480
|
+
- 数组合并依赖 `key` 字段,确保每个菜单项都有唯一的 `key`
|
|
481
|
+
- 同 `key` 的项会递归合并,不是简单替换
|
|
482
|
+
- `project` 中未定义的项会完整继承 `mode` 的配置
|
|
321
483
|
|
|
322
|
-
|
|
484
|
+
## model 数据如何引用
|
|
485
|
+
|
|
486
|
+
后端可以直接调用 loader:
|
|
323
487
|
|
|
324
488
|
```ts
|
|
325
|
-
import {
|
|
326
|
-
api,
|
|
327
|
-
apiFreezerStore,
|
|
328
|
-
addLanguageResources,
|
|
329
|
-
AsyncSelect,
|
|
330
|
-
clearAuthToken,
|
|
331
|
-
eventsInfo,
|
|
332
|
-
getCurrentLanguage,
|
|
333
|
-
getFallbackLanguage,
|
|
334
|
-
FreezeState,
|
|
335
|
-
get,
|
|
336
|
-
getSupportedLanguages,
|
|
337
|
-
getAuthToken,
|
|
338
|
-
LanguageSwitch,
|
|
339
|
-
localKeyMap,
|
|
340
|
-
merge,
|
|
341
|
-
modeStore,
|
|
342
|
-
post,
|
|
343
|
-
renderImportComponent,
|
|
344
|
-
registerFrontendI18nResources,
|
|
345
|
-
request,
|
|
346
|
-
schemaEventBus,
|
|
347
|
-
schemaStore,
|
|
348
|
-
setFallbackLanguage,
|
|
349
|
-
setLanguage,
|
|
350
|
-
setResources,
|
|
351
|
-
setAuthToken,
|
|
352
|
-
ThemeSwitch,
|
|
353
|
-
t,
|
|
354
|
-
useApiFreezer,
|
|
355
|
-
useModeStore,
|
|
356
|
-
useSchemaStore,
|
|
357
|
-
useText,
|
|
358
|
-
} from '@_tc/template-core/fe'
|
|
489
|
+
import { modelLoader, type KoaApp } from '@_tc/template-core'
|
|
359
490
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
DashComponentsMap,
|
|
365
|
-
DashRouteGuard,
|
|
366
|
-
DashRoutesExtender,
|
|
367
|
-
FormFieldSchema,
|
|
368
|
-
ApiFreezerRequestMatcher,
|
|
369
|
-
PageParams,
|
|
370
|
-
PageResponse,
|
|
371
|
-
RequestConfig,
|
|
372
|
-
ResponseConfig,
|
|
373
|
-
SchemaTableRenderComponent,
|
|
374
|
-
SchemaTableRenderComponentsMap,
|
|
375
|
-
SchemaFormComponentsMap,
|
|
376
|
-
SchemaFormNamespace,
|
|
377
|
-
SelectProps,
|
|
378
|
-
} from '@_tc/template-core/fe'
|
|
491
|
+
export default (app: KoaApp) => {
|
|
492
|
+
const modelList = modelLoader(app)
|
|
493
|
+
return modelList
|
|
494
|
+
}
|
|
379
495
|
```
|
|
380
496
|
|
|
381
|
-
|
|
497
|
+
前端默认不直接读文件,而是通过内置接口获取:
|
|
382
498
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
| Dash 扩展类型 | `DashRoutesExtender`、`DashRoutesContext`、`DashComponentsMap`、`DashHeaderUserAreaProps`、`DashRouteGuard` |
|
|
389
|
-
| SchemaPage 类型 | `CallComComponentsMap`、`CallComRenderer`、`SchemaTableRenderComponent`、`SchemaTableRenderComponentsMap` |
|
|
390
|
-
| SchemaForm 类型 | `FormFieldSchema`、`SchemaFormComponentsMap`、`SchemaFormNamespace`、`SelectProps` |
|
|
391
|
-
| 请求方法 | `api`、`request`、`get`、`post`、`put`、`patch`、`del` |
|
|
392
|
-
| 请求类型 | `BaseResponse`、`PageParams`、`PageResponse`、`RequestConfig`、`ResponseConfig`、`AxiosError` |
|
|
393
|
-
| RAF 风格计时器 | `rafSetTimeout`、`rafSetInterval`、`rafClearTimeout`、`rafClearInterval`、`clearRafTimer`、`RafTimerId`、`RafTimerCallback` |
|
|
394
|
-
| Token 工具 | `getAuthToken`、`setAuthToken`、`clearAuthToken`、`localKeyMap` |
|
|
395
|
-
| 多语言工具 | `useText`、`getText`、`t`、`registerFrontendI18nResources`、`addLanguageResources`、`setLanguage`、`setFallbackLanguage`、`setResources`、`getCurrentLanguage`、`getFallbackLanguage`、`getSupportedLanguages` |
|
|
396
|
-
| 请求冻结 | `apiFreezerStore`、`useApiFreezer`、`FreezeState`、`ApiFreezerRequestMatcher` |
|
|
397
|
-
| 事件工具 | `eventsInfo`、`merge` |
|
|
398
|
-
| 共享状态 | `modeStore`、`useModeStore`、`schemaStore`、`useSchemaStore`、`schemaEventBus` |
|
|
499
|
+
```text
|
|
500
|
+
GET /api/project/model_list
|
|
501
|
+
GET /api/project/list?projk=demo
|
|
502
|
+
GET /api/project/demo
|
|
503
|
+
```
|
|
399
504
|
|
|
400
|
-
`
|
|
505
|
+
Dashboard 会根据 `projk` 请求 `/api/project/:key`,再按返回的 `menu` 渲染页面。
|
|
401
506
|
|
|
402
|
-
|
|
507
|
+
Schema 页面的接口约定:
|
|
403
508
|
|
|
404
|
-
|
|
509
|
+
```text
|
|
510
|
+
GET {api}/list -> 表格列表
|
|
511
|
+
GET {api} -> 编辑/详情拉取,参数由 fetchKey 决定
|
|
512
|
+
POST {api} -> 新增
|
|
513
|
+
PUT {api} -> 编辑
|
|
514
|
+
DELETE {api} -> 删除
|
|
515
|
+
```
|
|
405
516
|
|
|
406
|
-
|
|
517
|
+
`$schema::字段名` 表示从当前表格行读取字段值,常用于删除按钮参数。
|
|
407
518
|
|
|
408
|
-
|
|
519
|
+
### 服务端兜底路由守卫
|
|
409
520
|
|
|
410
|
-
|
|
521
|
+
项目可以创建 `app/router-guard.ts` 或 `app/router-guard.js`。它只在所有正常 `app/router/*` 路由都没有命中时执行。
|
|
411
522
|
|
|
412
523
|
```ts
|
|
413
|
-
import {
|
|
524
|
+
import type { Ctx, KoaApp } from '@_tc/template-core'
|
|
414
525
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
526
|
+
export default (_app: KoaApp) => {
|
|
527
|
+
return (ctx: Ctx) => {
|
|
528
|
+
if (ctx.path.startsWith('/api/')) {
|
|
529
|
+
return {
|
|
530
|
+
status: 404,
|
|
531
|
+
body: { code: 404, message: `${ctx.path} not found` },
|
|
532
|
+
}
|
|
533
|
+
}
|
|
421
534
|
|
|
422
|
-
|
|
535
|
+
return '/dash'
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
423
539
|
|
|
424
|
-
|
|
540
|
+
返回字符串会 `ctx.redirect()`;返回 `{ status, body }` 会直接设置响应;无返回则使用框架默认兜底:API 路径返回 HTTP 200 + `{ code: 404, message }`,非 API 路径返回 HTTP 404 + `notFound`。
|
|
425
541
|
|
|
426
|
-
|
|
427
|
-
import { addLanguageResources } from '@_tc/template-core/fe'
|
|
542
|
+
## 后端约定
|
|
428
543
|
|
|
429
|
-
|
|
430
|
-
app: {
|
|
431
|
-
product: {
|
|
432
|
-
menu: '商品管理',
|
|
433
|
-
name: '商品名称',
|
|
434
|
-
create: '新增商品',
|
|
435
|
-
createTitle: '新增商品',
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
})
|
|
544
|
+
项目目录会按下面规则加载:
|
|
439
545
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
})
|
|
546
|
+
```text
|
|
547
|
+
app/controller/**/*.(js|ts) -> app.controller
|
|
548
|
+
app/service/**/*.(js|ts) -> app.service
|
|
549
|
+
app/middlewares/**/*.(js|ts) -> app.middlewares
|
|
550
|
+
app/middleware.(js|ts) -> 全局中间件编排,按框架 -> 项目顺序执行
|
|
551
|
+
app/router/**/*.(js|ts) -> Koa router
|
|
552
|
+
app/router-schema/**/*.(js|ts) -> app.routerSchema
|
|
553
|
+
app/extends/*.(js|ts) -> app.extends
|
|
554
|
+
config/config.default.(js|ts) -> app.config
|
|
555
|
+
config/config.{env}.(js|ts) -> app.config
|
|
556
|
+
model/**/*.(js|ts) -> 内置 project service 按需读取的项目模型配置
|
|
450
557
|
```
|
|
451
558
|
|
|
452
|
-
|
|
559
|
+
内置扩展:
|
|
453
560
|
|
|
454
|
-
|
|
455
|
-
|
|
561
|
+
- `app.extends.$fetch`:Node 侧基于 `fetch` 的 axios 风格请求实例,支持 `get/post/put/patch/delete` 和 `create(config)`。
|
|
562
|
+
- `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
|
|
563
|
+
- `app.extends.db`:默认 SQLite 框架数据库,提供 `getDBData/setDBData` 等通用数据方法。
|
|
456
564
|
|
|
457
|
-
|
|
458
|
-
app: {
|
|
459
|
-
product: {
|
|
460
|
-
menu: '商品管理',
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
})
|
|
565
|
+
API controller 可以通过 `ctx.reqData` 读取统一请求参数:
|
|
464
566
|
|
|
465
|
-
|
|
567
|
+
```ts
|
|
568
|
+
const { query, body, headers, data } = ctx.reqData ?? {
|
|
569
|
+
query: {},
|
|
570
|
+
body: undefined,
|
|
571
|
+
headers: {},
|
|
572
|
+
data: {},
|
|
573
|
+
}
|
|
466
574
|
```
|
|
467
575
|
|
|
468
|
-
|
|
576
|
+
- `query` 来自 `ctx.query`。
|
|
577
|
+
- `body` 来自 `ctx.request.body`。
|
|
578
|
+
- `headers` 来自 `ctx.headers`。
|
|
579
|
+
- `data` 是便捷合并层,当前按 `query -> body` 顺序合并;字段冲突时 body 覆盖 query。
|
|
469
580
|
|
|
470
|
-
|
|
581
|
+
`ctx.reqData` 只在 API 请求中由内置中间件注入,类型上是可选字段。router params 不在 `reqData` 里,仍然从 `ctx.params` 获取。
|
|
471
582
|
|
|
472
|
-
|
|
583
|
+
### 数据库扩展
|
|
473
584
|
|
|
474
|
-
|
|
475
|
-
import { addLanguageResources } from '@_tc/template-core/fe'
|
|
585
|
+
默认 `app.extends.db` 使用 SQLite 保存数据,数据库文件位于项目根目录 `.template-core/template-core.sqlite`。可以在配置中调整路径:
|
|
476
586
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
|
483
|
-
monthFormat: 'yyyy年 M月',
|
|
484
|
-
},
|
|
485
|
-
tableSearch: {
|
|
486
|
-
search: '筛选',
|
|
487
|
-
reset: '清空',
|
|
488
|
-
},
|
|
587
|
+
```ts
|
|
588
|
+
// config/config.default.ts
|
|
589
|
+
export default {
|
|
590
|
+
db: {
|
|
591
|
+
path: 'data/app.sqlite',
|
|
489
592
|
},
|
|
490
|
-
}
|
|
593
|
+
}
|
|
491
594
|
```
|
|
492
595
|
|
|
493
|
-
|
|
596
|
+
Controller 中使用:
|
|
494
597
|
|
|
495
598
|
```ts
|
|
496
|
-
import {
|
|
497
|
-
import { ja } from 'date-fns/locale'
|
|
599
|
+
import { baseFn, type ControllerFN, type Ctx } from '@_tc/template-core'
|
|
498
600
|
|
|
499
|
-
|
|
500
|
-
|
|
601
|
+
const getSettingController = ((app) => {
|
|
602
|
+
const BaseController = baseFn.baseControllerFn(app)
|
|
501
603
|
|
|
502
|
-
|
|
604
|
+
return class SettingController extends BaseController {
|
|
605
|
+
detail = async (ctx: Ctx) => {
|
|
606
|
+
const data = app.extends.db.getDBData('site-setting', {
|
|
607
|
+
namespace: 'setting',
|
|
608
|
+
})
|
|
503
609
|
|
|
504
|
-
|
|
610
|
+
this.success(ctx, data ?? {})
|
|
611
|
+
}
|
|
505
612
|
|
|
506
|
-
|
|
507
|
-
|
|
613
|
+
save = async (ctx: Ctx) => {
|
|
614
|
+
app.extends.db.setDBData('site-setting', ctx.request.body, {
|
|
615
|
+
namespace: 'setting',
|
|
616
|
+
})
|
|
508
617
|
|
|
509
|
-
|
|
510
|
-
|
|
618
|
+
this.success(ctx, true)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}) satisfies ControllerFN
|
|
511
622
|
|
|
512
|
-
|
|
513
|
-
<>
|
|
514
|
-
<h1>{text('$i18n::app.product.menu')}</h1>
|
|
515
|
-
<span>{text('Plain title')}</span>
|
|
516
|
-
</>
|
|
517
|
-
)
|
|
518
|
-
}
|
|
623
|
+
export default getSettingController
|
|
519
624
|
```
|
|
520
625
|
|
|
521
|
-
|
|
626
|
+
常用方法:
|
|
522
627
|
|
|
523
|
-
|
|
628
|
+
- `getDBData(key, options)`:读取 JSON 数据。
|
|
629
|
+
- `setDBData(key, value, options)`:写入 JSON 数据。
|
|
630
|
+
- `hasDBData(key, options)`:判断数据是否存在。
|
|
631
|
+
- `deleteDBData(key, options)`:删除单条数据。
|
|
632
|
+
- `listDBData(options)`:分页列出当前 namespace 下的数据。
|
|
633
|
+
- `countDBData(options)` / `clearDBData(options)`:统计和清空当前 namespace。
|
|
634
|
+
- `queryDB(sql, params)` / `runDB(sql, params)`:直接执行 SQLite SQL。
|
|
635
|
+
- `transactionDB(handler)`:事务执行。
|
|
636
|
+
- `closeDB()`:关闭连接。
|
|
524
637
|
|
|
525
|
-
|
|
526
|
-
const title = '$i18n::app.product.createTitle'
|
|
527
|
-
```
|
|
638
|
+
SQL 注入边界:
|
|
528
639
|
|
|
529
|
-
|
|
640
|
+
- `getDBData`、`setDBData`、`deleteDBData`、`listDBData` 这类通用方法内部使用参数绑定,不需要手写 SQL。
|
|
641
|
+
- `queryDB`、`runDB`、`getDBFirst` 是原始 SQL 入口,用户输入必须放到 `params`,不要拼接到 SQL 字符串里。
|
|
642
|
+
- `execDB` 没有参数绑定能力,只建议用于固定 SQL,例如建表或迁移。
|
|
530
643
|
|
|
531
|
-
|
|
532
|
-
{
|
|
533
|
-
name: '$i18n::app.product.menu',
|
|
534
|
-
moduleType: 'schema',
|
|
535
|
-
schemaConfig: {
|
|
536
|
-
schema: {
|
|
537
|
-
type: 'object',
|
|
538
|
-
properties: {
|
|
539
|
-
name: {
|
|
540
|
-
type: 'string',
|
|
541
|
-
label: '$i18n::app.product.name',
|
|
542
|
-
minLength: 2,
|
|
543
|
-
createFormOption: {
|
|
544
|
-
comType: 'input',
|
|
545
|
-
},
|
|
546
|
-
tableOption: {},
|
|
547
|
-
searchOption: {},
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
required: ['name'],
|
|
551
|
-
},
|
|
552
|
-
tableConfig: {
|
|
553
|
-
headerButtons: [
|
|
554
|
-
{
|
|
555
|
-
label: '$i18n::app.product.create',
|
|
556
|
-
eventKey: 'callComponent',
|
|
557
|
-
eventOption: { comName: 'createForm' },
|
|
558
|
-
},
|
|
559
|
-
],
|
|
560
|
-
},
|
|
561
|
-
componentConfig: {
|
|
562
|
-
createForm: {
|
|
563
|
-
title: '$i18n::app.product.createTitle',
|
|
564
|
-
saveBtnText: '$i18n::common.submit',
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
}
|
|
569
|
-
```
|
|
644
|
+
### 加密扩展
|
|
570
645
|
|
|
571
|
-
|
|
646
|
+
`app.extends.crypto` 默认使用 `app.config.signKey` 作为签名密钥,适合生成登录 token、接口签名和安全比较。
|
|
572
647
|
|
|
573
|
-
|
|
574
|
-
- 项目标题:`projectInfo.desc` 或回退的 `projectInfo.name`
|
|
575
|
-
- Schema 字段名:`schema.properties.*.label`
|
|
576
|
-
- 表格按钮和行按钮:`headerButtons.*.label`、`rowButtons.*.label`
|
|
577
|
-
- 弹窗、抽屉标题:`componentConfig.*.title`
|
|
578
|
-
- 表单保存按钮:`componentConfig.*.saveBtnText`
|
|
579
|
-
- 接口错误提示:`BaseResponse.message`
|
|
648
|
+
常用方法:
|
|
580
649
|
|
|
581
|
-
|
|
650
|
+
- `base64urlEncode(value)`:把字符串或二进制数据编码为 base64url。
|
|
651
|
+
- `base64urlDecode(value)`:把 base64url 解码回 UTF-8 字符串。
|
|
652
|
+
- `base64urlDecodeToBuffer(value)`:把 base64url 解码回 Buffer。
|
|
653
|
+
- `hmacSign(payload, options)`:对文本做 HMAC 签名,默认算法 `sha256`。
|
|
654
|
+
- `safeEqual(left, right)`:常量时间比较字符串。
|
|
655
|
+
- `createSignedPayload(payload, options)`:生成 `payload.signature` 形式的签名串。
|
|
656
|
+
- `verifySignedPayload(token, options)`:验证并解析签名串,失败返回 `null`。
|
|
582
657
|
|
|
583
658
|
```ts
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
659
|
+
const token = app.extends.crypto.createSignedPayload({
|
|
660
|
+
account: 'admin',
|
|
661
|
+
exp: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
|
662
|
+
iat: Date.now(),
|
|
663
|
+
})
|
|
589
664
|
|
|
590
|
-
|
|
665
|
+
const payload = app.extends.crypto.verifySignedPayload<{
|
|
666
|
+
account: string
|
|
667
|
+
exp: number
|
|
668
|
+
iat: number
|
|
669
|
+
}>(token)
|
|
591
670
|
|
|
592
|
-
|
|
671
|
+
const same = app.extends.crypto.safeEqual('abc', 'abc')
|
|
672
|
+
```
|
|
593
673
|
|
|
594
|
-
```
|
|
595
|
-
|
|
674
|
+
```ts
|
|
675
|
+
// 推荐:参数绑定
|
|
676
|
+
const users = app.extends.db.queryDB(
|
|
677
|
+
'SELECT * FROM user WHERE name = ?',
|
|
678
|
+
[ctx.query.name as string]
|
|
679
|
+
)
|
|
596
680
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
681
|
+
// 不推荐:拼接用户输入,有 SQL 注入风险
|
|
682
|
+
const unsafeUsers = app.extends.db.queryDB(
|
|
683
|
+
`SELECT * FROM user WHERE name = '${ctx.query.name}'`
|
|
684
|
+
)
|
|
600
685
|
```
|
|
601
686
|
|
|
602
|
-
|
|
687
|
+
项目可以用自己的 `app/extends/db.ts` 覆盖默认实现。建议保留同一组方法名,这样 controller/service 调用方不用调整。
|
|
603
688
|
|
|
604
|
-
|
|
689
|
+
```ts
|
|
690
|
+
// app/extends/db.ts
|
|
691
|
+
import type { DB, DBFactory } from '@_tc/template-core'
|
|
605
692
|
|
|
606
|
-
|
|
693
|
+
interface DataOptions {
|
|
694
|
+
namespace?: string
|
|
695
|
+
}
|
|
607
696
|
|
|
608
|
-
|
|
697
|
+
const store = new Map<string, unknown>()
|
|
609
698
|
|
|
610
|
-
|
|
699
|
+
const getDB = ((_app) => {
|
|
700
|
+
const toKey = (key: string, namespace = 'framework') => `${namespace}:${key}`
|
|
611
701
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
702
|
+
const db = {
|
|
703
|
+
dbPath: 'memory',
|
|
704
|
+
getDBConnection: () => {
|
|
705
|
+
throw new Error('memory db 没有底层 SQLite connection')
|
|
706
|
+
},
|
|
707
|
+
execDB: () => undefined,
|
|
708
|
+
runDB: () => ({ changes: 0, lastInsertRowid: 0 }),
|
|
709
|
+
queryDB: <T extends object = Record<string, unknown>>() => [] as T[],
|
|
710
|
+
getDBFirst: <T extends object = Record<string, unknown>>() => undefined as T | undefined,
|
|
711
|
+
transactionDB: <T>(handler: (db: DB) => T) => handler(db),
|
|
712
|
+
getDBData: <T = unknown>(key: string, options?: DataOptions) => {
|
|
713
|
+
return store.get(toKey(key, options?.namespace)) as T | undefined
|
|
714
|
+
},
|
|
715
|
+
getDBDataRecord: <T = unknown>(key: string, options?: DataOptions) => {
|
|
716
|
+
const value = store.get(toKey(key, options?.namespace)) as T | undefined
|
|
717
|
+
if (value === undefined) return undefined
|
|
615
718
|
|
|
616
|
-
|
|
719
|
+
return {
|
|
720
|
+
key,
|
|
721
|
+
value,
|
|
722
|
+
namespace: options?.namespace ?? 'framework',
|
|
723
|
+
createdAt: '',
|
|
724
|
+
updatedAt: '',
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
setDBData: <T = unknown>(key: string, value: T, options?: DataOptions) => {
|
|
728
|
+
store.set(toKey(key, options?.namespace), value)
|
|
729
|
+
return { changes: 1, lastInsertRowid: 0 }
|
|
730
|
+
},
|
|
731
|
+
hasDBData: (key: string, options?: DataOptions) => {
|
|
732
|
+
return store.has(toKey(key, options?.namespace))
|
|
733
|
+
},
|
|
734
|
+
deleteDBData: (key: string, options?: DataOptions) => {
|
|
735
|
+
return store.delete(toKey(key, options?.namespace))
|
|
736
|
+
},
|
|
737
|
+
listDBData: <T = unknown>(options?: DataOptions) => {
|
|
738
|
+
const namespace = options?.namespace ?? 'framework'
|
|
617
739
|
|
|
618
|
-
|
|
740
|
+
return Array.from(store.entries())
|
|
741
|
+
.filter(([key]) => key.startsWith(`${namespace}:`))
|
|
742
|
+
.map(([key, value]) => ({
|
|
743
|
+
key: key.slice(namespace.length + 1),
|
|
744
|
+
value: value as T,
|
|
745
|
+
namespace,
|
|
746
|
+
createdAt: '',
|
|
747
|
+
updatedAt: '',
|
|
748
|
+
}))
|
|
749
|
+
},
|
|
750
|
+
countDBData: () => store.size,
|
|
751
|
+
clearDBData: () => {
|
|
752
|
+
const size = store.size
|
|
753
|
+
store.clear()
|
|
754
|
+
return size
|
|
755
|
+
},
|
|
756
|
+
closeDB: () => undefined,
|
|
757
|
+
} satisfies DB
|
|
619
758
|
|
|
620
|
-
|
|
621
|
-
|
|
759
|
+
return db
|
|
760
|
+
}) satisfies DBFactory
|
|
622
761
|
|
|
623
|
-
export
|
|
624
|
-
return <ThemeSwitch />
|
|
625
|
-
}
|
|
762
|
+
export default getDB
|
|
626
763
|
```
|
|
627
764
|
|
|
628
|
-
|
|
765
|
+
Controller 示例:
|
|
629
766
|
|
|
630
|
-
|
|
767
|
+
```ts
|
|
768
|
+
import { baseFn, type ControllerFN, type Ctx } from '@_tc/template-core'
|
|
631
769
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
| `value` / `onChange` | 受控模式,值为 `light` 或 `dark`。 |
|
|
635
|
-
| `defaultValue` | 非受控初始主题。 |
|
|
636
|
-
| `showLabel` | 是否展示当前主题文本。 |
|
|
637
|
-
| `persist` | 是否写入 localStorage,默认 `true`。 |
|
|
638
|
-
| `storageKey` | 自定义本地存储 key,默认 `tc_theme`。 |
|
|
639
|
-
|
|
640
|
-
如需在自定义入口或组件外手动初始化/切换主题,可以从 `@_tc/template-core/fe` 导入主题工具:
|
|
770
|
+
const getProductController = ((app) => {
|
|
771
|
+
const BaseController = baseFn.baseControllerFn(app)
|
|
641
772
|
|
|
642
|
-
|
|
643
|
-
|
|
773
|
+
return class ProductController extends BaseController {
|
|
774
|
+
list = async (ctx: Ctx) => {
|
|
775
|
+
this.success(ctx, {
|
|
776
|
+
data: [],
|
|
777
|
+
page: 1,
|
|
778
|
+
pageSize: 10,
|
|
779
|
+
total: 0,
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}) satisfies ControllerFN
|
|
644
784
|
|
|
645
|
-
|
|
646
|
-
applyThemeMode('dark', true)
|
|
785
|
+
export default getProductController
|
|
647
786
|
```
|
|
648
787
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
`@_tc/template-core/fe/rc` 和 `@_tc/template-core/fe/rc/hooks` 也会随发布包提供对应 UI 与 hooks 入口;hooks 汇总入口包含 `useBreadcrumb`、`useExecuteOnce`、`useInit`、`useLanguage`、`usePagination`、`useRefState`、`useWatch`。
|
|
652
|
-
|
|
653
|
-
`useRefState` 支持可选的 `delayTiming` 参数,传入大于 0 的值时会延迟 state 提交,并复用 `@tc/common/rafTimer` 的浏览器优先、Node/SSR 自动降级计时器实现。
|
|
654
|
-
|
|
655
|
-
### 前端请求封装详解
|
|
656
|
-
|
|
657
|
-
框架内置的请求封装基于 `@tc/common/http`,底层使用原生 `fetch` API,实现了类似 Axios 的接口和拦截器机制,提供了签名、鉴权、错误处理等能力。
|
|
658
|
-
|
|
659
|
-
**基础配置**:
|
|
660
|
-
- `BASE_URL`:固定为 `/api`
|
|
661
|
-
- `timeout`:15000ms(15秒)
|
|
662
|
-
- `credentials`:`'include'`,支持跨域携带 cookie
|
|
663
|
-
- `window._signKey`:由服务端页面模板从 `app.config.signKey` 注入
|
|
664
|
-
- 默认会先设置 `Content-Type: application/json`;如果是上传文件、`FormData` 或其他内容类型,请在请求配置里显式覆盖
|
|
665
|
-
|
|
666
|
-
**请求拦截器**:
|
|
667
|
-
|
|
668
|
-
每个请求会自动添加以下 headers:
|
|
788
|
+
Router 示例:
|
|
669
789
|
|
|
670
790
|
```ts
|
|
671
|
-
{
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
'
|
|
675
|
-
'Authorization': 'Bearer token(如果已登录)'
|
|
791
|
+
import type { KoaApp, Router } from '@_tc/template-core'
|
|
792
|
+
|
|
793
|
+
export default (app: KoaApp, router: Router) => {
|
|
794
|
+
router.get('/api/product/list', app.controller.product.list)
|
|
676
795
|
}
|
|
677
796
|
```
|
|
678
797
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
**签名机制**:
|
|
682
|
-
- 签名密钥:服务端从 `app.config.signKey` 读取,并在页面渲染时注入为 `window._signKey`
|
|
683
|
-
- 签名算法:`md5(签名密钥_${时间戳})`
|
|
684
|
-
- 服务端会在非 local 环境下校验签名和时间戳
|
|
685
|
-
|
|
686
|
-
**响应拦截器**:
|
|
687
|
-
|
|
688
|
-
- **401 处理**:自动清空 token 并跳转到 `/login`
|
|
689
|
-
- **错误提取**:自动从响应中提取 `message`、`msg` 或 `errors` 字段
|
|
690
|
-
- **超时处理**:请求超时会返回当前语言的超时提示
|
|
691
|
-
- **网络错误**:网络异常会返回当前语言的网络错误提示
|
|
798
|
+
路由挂载顺序:
|
|
692
799
|
|
|
693
|
-
|
|
800
|
+
- 每个 `app/router/*.ts` 文件都会获得独立的 `koa-router` 实例。
|
|
801
|
+
- 可以通过 `router.level = number` 控制挂载顺序,数值越小越早挂载。
|
|
802
|
+
- 默认 `level` 是 `0`,框架兜底 router 是 `99`。
|
|
803
|
+
- 通配、兜底、重定向类路由建议设置较大的 `level`,避免抢先匹配项目 API。
|
|
694
804
|
|
|
695
805
|
```ts
|
|
696
|
-
import {
|
|
697
|
-
import type { BaseResponse, RequestConfig, ResponseConfig } from '@_tc/template-core/fe'
|
|
698
|
-
|
|
699
|
-
// GET 请求
|
|
700
|
-
const data = await get('/product/list', { page: 1, pageSize: 10 })
|
|
701
|
-
|
|
702
|
-
// POST 请求
|
|
703
|
-
const result = await post('/product', { name: '商品名称', price: 100 })
|
|
704
|
-
|
|
705
|
-
// PUT 请求
|
|
706
|
-
await put('/product', { id: 1, name: '新名称' })
|
|
806
|
+
import type { KoaApp, Router } from '@_tc/template-core'
|
|
707
807
|
|
|
708
|
-
|
|
709
|
-
|
|
808
|
+
export default (app: KoaApp, router: Router) => {
|
|
809
|
+
router.level = 10
|
|
810
|
+
router.get('/api/fallback/:id', app.controller.fallback.detail)
|
|
811
|
+
}
|
|
710
812
|
```
|
|
711
813
|
|
|
712
|
-
|
|
814
|
+
## 前端公共能力
|
|
815
|
+
|
|
816
|
+
项目侧需要 TemplateCore 前端能力时,优先从 `@_tc/template-core/fe` 导入,不要直接依赖 `frontend/src/common/...`、`frontend/src/defaultPages/...`、`frontend/src/stores/...` 这类内部路径。
|
|
713
817
|
|
|
714
818
|
```ts
|
|
715
819
|
import {
|
|
716
820
|
api,
|
|
821
|
+
apiFreezerStore,
|
|
822
|
+
addLanguageResources,
|
|
823
|
+
AsyncSelect,
|
|
824
|
+
clearAuthToken,
|
|
825
|
+
eventsInfo,
|
|
826
|
+
getCurrentLanguage,
|
|
827
|
+
getFallbackLanguage,
|
|
828
|
+
FreezeState,
|
|
717
829
|
get,
|
|
830
|
+
getSupportedLanguages,
|
|
831
|
+
getAuthToken,
|
|
832
|
+
LanguageSwitch,
|
|
833
|
+
localKeyMap,
|
|
834
|
+
merge,
|
|
835
|
+
modeStore,
|
|
718
836
|
post,
|
|
837
|
+
renderImportComponent,
|
|
838
|
+
registerFrontendI18nResources,
|
|
719
839
|
request,
|
|
720
|
-
|
|
840
|
+
schemaEventBus,
|
|
841
|
+
schemaStore,
|
|
842
|
+
setFallbackLanguage,
|
|
843
|
+
setLanguage,
|
|
844
|
+
setResources,
|
|
845
|
+
setAuthToken,
|
|
846
|
+
ThemeSwitch,
|
|
847
|
+
t,
|
|
848
|
+
useApiFreezer,
|
|
849
|
+
useModeStore,
|
|
850
|
+
useSchemaStore,
|
|
851
|
+
useText,
|
|
852
|
+
} from '@_tc/template-core/fe'
|
|
853
|
+
|
|
721
854
|
import type {
|
|
722
855
|
BaseResponse,
|
|
856
|
+
CallComComponentsMap,
|
|
857
|
+
CallComRenderer,
|
|
858
|
+
DashComponentsMap,
|
|
859
|
+
DashRouteGuard,
|
|
860
|
+
DashRoutesExtender,
|
|
861
|
+
FormFieldSchema,
|
|
862
|
+
ApiFreezerRequestMatcher,
|
|
863
|
+
PageParams,
|
|
864
|
+
PageResponse,
|
|
723
865
|
RequestConfig,
|
|
724
866
|
ResponseConfig,
|
|
725
|
-
|
|
867
|
+
SchemaTableRenderComponent,
|
|
868
|
+
SchemaTableRenderComponentsMap,
|
|
869
|
+
SchemaFormComponentsMap,
|
|
870
|
+
SchemaFormNamespace,
|
|
871
|
+
SelectProps,
|
|
872
|
+
} from '@_tc/template-core/fe'
|
|
726
873
|
```
|
|
727
874
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
请求冻结器用于在某段流程中暂停普通 API 请求,典型场景是 RT 刷新 token:刷新期间冻结其它请求,刷新接口本身走白名单放行,刷新完成后再解冻并回放队列。
|
|
875
|
+
当前公共入口包含:
|
|
731
876
|
|
|
732
|
-
|
|
877
|
+
| 类型 | 导出内容 |
|
|
878
|
+
| --- | --- |
|
|
879
|
+
| 应用启动 | `initApp` |
|
|
880
|
+
| 内置前端组件 | `AsyncSelect`、`LanguageSwitch`、`ThemeSwitch` |
|
|
881
|
+
| 组件加载辅助 | `renderImportComponent` |
|
|
882
|
+
| Dash 扩展类型 | `DashRoutesExtender`、`DashRoutesContext`、`DashComponentsMap`、`DashHeaderUserAreaProps`、`DashRouteGuard` |
|
|
883
|
+
| SchemaPage 类型 | `CallComComponentsMap`、`CallComRenderer`、`SchemaTableRenderComponent`、`SchemaTableRenderComponentsMap` |
|
|
884
|
+
| SchemaForm 类型 | `FormFieldSchema`、`SchemaFormComponentsMap`、`SchemaFormNamespace`、`SelectProps` |
|
|
885
|
+
| 请求方法 | `api`、`request`、`get`、`post`、`put`、`patch`、`del` |
|
|
886
|
+
| 请求类型 | `BaseResponse`、`PageParams`、`PageResponse`、`RequestConfig`、`ResponseConfig`、`AxiosError` |
|
|
887
|
+
| RAF 风格计时器 | `rafSetTimeout`、`rafSetInterval`、`rafClearTimeout`、`rafClearInterval`、`clearRafTimer`、`RafTimerId`、`RafTimerCallback` |
|
|
888
|
+
| Token 工具 | `getAuthToken`、`setAuthToken`、`clearAuthToken`、`localKeyMap` |
|
|
889
|
+
| 多语言工具 | `useText`、`getText`、`t`、`registerFrontendI18nResources`、`addLanguageResources`、`setLanguage`、`setFallbackLanguage`、`setResources`、`getCurrentLanguage`、`getFallbackLanguage`、`getSupportedLanguages` |
|
|
890
|
+
| 请求冻结 | `apiFreezerStore`、`useApiFreezer`、`FreezeState`、`ApiFreezerRequestMatcher` |
|
|
891
|
+
| 事件工具 | `eventsInfo`、`merge` |
|
|
892
|
+
| 共享状态 | `modeStore`、`useModeStore`、`schemaStore`、`useSchemaStore`、`schemaEventBus` |
|
|
733
893
|
|
|
734
|
-
|
|
735
|
-
import { apiFreezerStore } from '@_tc/template-core/fe'
|
|
894
|
+
`modeStore` 管项目模型和当前项目数据,`schemaStore` 管当前 Schema 页面的 schema/cache,`schemaEventBus` 用来在 Search、Table、CallCom 之间通信。
|
|
736
895
|
|
|
737
|
-
|
|
896
|
+
`renderImportComponent` 封装了 `React.lazy` + `Suspense`,默认会用页面骨架屏作为 fallback。`getAuthToken()`、`setAuthToken()` 和 `clearAuthToken()` 用于读写或清理本地短 token,`localKeyMap` 暴露对应的本地存储 key 常量。
|
|
738
897
|
|
|
739
|
-
|
|
740
|
-
'/auth/refresh',
|
|
741
|
-
/^\/auth\//,
|
|
742
|
-
(api) => api.includes('/refresh-token'),
|
|
743
|
-
])
|
|
898
|
+
只使用请求层时,也可以从 `@_tc/template-core/fe/common/request` 导入 `api`、请求方法和请求类型;这是发布包显式子路径,不要下钻到 `frontend/src/common/request`。
|
|
744
899
|
|
|
745
|
-
|
|
900
|
+
### 前端多语言
|
|
746
901
|
|
|
747
|
-
|
|
748
|
-
await refreshToken()
|
|
749
|
-
} finally {
|
|
750
|
-
thawing()
|
|
751
|
-
}
|
|
752
|
-
```
|
|
902
|
+
TemplateCore 前端通过 `@_tc/template-core/fe` 暴露 i18n 能力。框架内置中文、英文资源,但不会在普通自定义页面 import 前端入口时自动注册,避免非 Dashboard 页面把框架文案整包带入运行时资源。
|
|
753
903
|
|
|
754
|
-
|
|
904
|
+
默认 Dashboard 入口会自动调用 `registerFrontendI18nResources()`,所以 Dashboard、Schema CRUD、内置页头、语言切换等框架页面可以直接使用框架内置文案。自定义入口如果也要复用这些框架文案,需要在渲染前手动注册:
|
|
755
905
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
- `(api, params) => boolean`:自定义判断,`params` 是请求函数收到的完整参数
|
|
906
|
+
```ts
|
|
907
|
+
import { initApp, registerFrontendI18nResources } from '@_tc/template-core/fe'
|
|
759
908
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
909
|
+
initApp(App, {
|
|
910
|
+
beforeRender: async () => {
|
|
911
|
+
await registerFrontendI18nResources()
|
|
912
|
+
},
|
|
913
|
+
})
|
|
914
|
+
```
|
|
765
915
|
|
|
766
|
-
|
|
916
|
+
项目可以继续追加自己的资源或新增语种。
|
|
767
917
|
|
|
768
|
-
|
|
918
|
+
**追加自定义语言资源**:
|
|
769
919
|
|
|
770
|
-
|
|
920
|
+
```ts
|
|
921
|
+
import { addLanguageResources } from '@_tc/template-core/fe'
|
|
771
922
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
| `DatePicker` | 日期选择器 | `@_tc/template-core/fe/rc` |
|
|
783
|
-
| `Upload` | 文件上传组件 | `@_tc/template-core/fe/rc` |
|
|
784
|
-
| `FileUpload` | 通用文件上传组件 | `@_tc/template-core/fe/rc` |
|
|
785
|
-
| `ImageUpload` | 图片上传组件 | `@_tc/template-core/fe/rc` |
|
|
786
|
-
| `ImagePreview` | 图片预览组件 | `@_tc/template-core/fe/rc` |
|
|
787
|
-
| `Checkbox` | 复选框组件 | `@_tc/template-core/fe/rc` |
|
|
788
|
-
| `Radio` | 单选框组件 | `@_tc/template-core/fe/rc` |
|
|
789
|
-
| `Switch` | 开关组件 | `@_tc/template-core/fe/rc` |
|
|
923
|
+
addLanguageResources('zh-CN', {
|
|
924
|
+
app: {
|
|
925
|
+
product: {
|
|
926
|
+
menu: '商品管理',
|
|
927
|
+
name: '商品名称',
|
|
928
|
+
create: '新增商品',
|
|
929
|
+
createTitle: '新增商品',
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
})
|
|
790
933
|
|
|
791
|
-
|
|
934
|
+
addLanguageResources('en-US', {
|
|
935
|
+
app: {
|
|
936
|
+
product: {
|
|
937
|
+
menu: 'Products',
|
|
938
|
+
name: 'Product name',
|
|
939
|
+
create: 'Create product',
|
|
940
|
+
createTitle: 'Create product',
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
})
|
|
944
|
+
```
|
|
792
945
|
|
|
793
|
-
|
|
794
|
-
| --- | --- | --- |
|
|
795
|
-
| `Form` | 表单容器组件 | `@_tc/template-core/fe/rc` |
|
|
796
|
-
| `SchemaForm` | 配置驱动的表单组件,根据 schema 自动生成表单 | `@_tc/template-core/fe/rc` |
|
|
797
|
-
| `FormItem` | 表单项组件 | `@_tc/template-core/fe/rc` |
|
|
946
|
+
**新增语种并切换**:
|
|
798
947
|
|
|
799
|
-
|
|
948
|
+
```ts
|
|
949
|
+
import { addLanguageResources, setLanguage } from '@_tc/template-core/fe'
|
|
800
950
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
| `Calendar` | 日历组件 | `@_tc/template-core/fe/rc` |
|
|
809
|
-
| `TimePicker` | 时间选择器组件 | `@_tc/template-core/fe/rc` |
|
|
810
|
-
| `Skeleton` | 骨架屏加载占位组件 | `@_tc/template-core/fe/rc` |
|
|
811
|
-
|
|
812
|
-
### 反馈组件
|
|
813
|
-
|
|
814
|
-
| 组件 | 用途 | 导入路径 |
|
|
815
|
-
| --- | --- | --- |
|
|
816
|
-
| `Modal` | 模态框组件 | `@_tc/template-core/fe/rc` |
|
|
817
|
-
| `Drawer` | 抽屉组件 | `@_tc/template-core/fe/rc` |
|
|
818
|
-
| `message` | 消息提示方法 | `@_tc/template-core/fe/rc` |
|
|
819
|
-
| `Notification` | 通知组件 | `@_tc/template-core/fe/rc` |
|
|
820
|
-
| `ConfirmDialog` | 确认对话框组件 | `@_tc/template-core/fe/rc` |
|
|
821
|
-
| `Popup` | 弹出层组件 | `@_tc/template-core/fe/rc` |
|
|
822
|
-
| `Tooltip` | 文字提示组件 | `@_tc/template-core/fe/rc` |
|
|
823
|
-
| `Overlay` | 页面遮罩层组件 | `@_tc/template-core/fe/rc` |
|
|
824
|
-
| `Loading` | 加载状态组件 | `@_tc/template-core/fe/rc` |
|
|
825
|
-
|
|
826
|
-
### 布局组件
|
|
827
|
-
|
|
828
|
-
| 组件 | 用途 | 导入路径 |
|
|
829
|
-
| --- | --- | --- |
|
|
830
|
-
| `Layout` | 页面布局组件 | `@_tc/template-core/fe/rc` |
|
|
831
|
-
| `Card` | 卡片容器组件 | `@_tc/template-core/fe/rc` |
|
|
832
|
-
| `Tabs` | 标签页组件 | `@_tc/template-core/fe/rc` |
|
|
833
|
-
| `Menu` | 菜单组件(含 MenuItem 等子组件) | `@_tc/template-core/fe/rc` |
|
|
834
|
-
| `Label` | 标签组件 | `@_tc/template-core/fe/rc` |
|
|
835
|
-
|
|
836
|
-
### 按钮组件
|
|
951
|
+
addLanguageResources('ja-JP', {
|
|
952
|
+
app: {
|
|
953
|
+
product: {
|
|
954
|
+
menu: '商品管理',
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
})
|
|
837
958
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
| `Button` | 通用按钮组件 | `@_tc/template-core/fe/rc` |
|
|
841
|
-
| `AsynchronousButton` | 异步按钮组件,支持加载状态 | `@_tc/template-core/fe/rc` |
|
|
842
|
-
| `SubmitButton` | 表单提交按钮组件 | `@_tc/template-core/fe/rc` |
|
|
843
|
-
| `ActionBtn` | 操作按钮组件 | `@_tc/template-core/fe/rc` |
|
|
959
|
+
setLanguage('ja-JP')
|
|
960
|
+
```
|
|
844
961
|
|
|
845
|
-
|
|
962
|
+
`addLanguageResources()` 用于追加或新增某个语种的文案,默认会深合并同语种资源;传 `{ merge: false }` 可以替换该语种资源。`setResources(resources, { merge: true })` 可批量合并多语种资源。`LanguageSwitch` 默认会把已注册语种加入选项;需要自定义展示文案时仍可传入 `options`。
|
|
846
963
|
|
|
847
|
-
|
|
848
|
-
import { Button, Input, Select, Modal, DataTable } from '@_tc/template-core/fe/rc'
|
|
964
|
+
**覆盖组件内置文案**:
|
|
849
965
|
|
|
850
|
-
|
|
851
|
-
return (
|
|
852
|
-
<div>
|
|
853
|
-
<Button variant="primary" onClick={() => console.log('clicked')}>
|
|
854
|
-
点击我
|
|
855
|
-
</Button>
|
|
966
|
+
UI 组件库内置文案也在同一份资源里,统一放在 `components` 命名空间下。项目侧可以覆盖已有文案,也可以给新增语种补齐组件文案:
|
|
856
967
|
|
|
857
|
-
|
|
858
|
-
|
|
968
|
+
```ts
|
|
969
|
+
import { addLanguageResources } from '@_tc/template-core/fe'
|
|
859
970
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
971
|
+
addLanguageResources('zh-CN', {
|
|
972
|
+
components: {
|
|
973
|
+
date: {
|
|
974
|
+
placeholder: '选择日期',
|
|
975
|
+
placeholderRange: '选择日期区间',
|
|
976
|
+
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
|
977
|
+
monthFormat: 'yyyy年 M月',
|
|
978
|
+
},
|
|
979
|
+
tableSearch: {
|
|
980
|
+
search: '筛选',
|
|
981
|
+
reset: '清空',
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
})
|
|
869
985
|
```
|
|
870
986
|
|
|
871
|
-
|
|
872
|
-
- 所有组件都支持 TypeScript 类型提示
|
|
873
|
-
- 组件样式基于 Tailwind CSS V4
|
|
874
|
-
- 详细的组件 API 和示例可以访问 `/ui-components` 页面查看
|
|
875
|
-
|
|
876
|
-
## 后端约定
|
|
987
|
+
Date 组件除了文案,还依赖 `date-fns` locale 做月份格式化和周起止计算。新增非内置语种时同步注册:
|
|
877
988
|
|
|
878
|
-
|
|
989
|
+
```ts
|
|
990
|
+
import { registerDateFnsLocale } from '@_tc/template-core/fe/rc/components/Date'
|
|
991
|
+
import { ja } from 'date-fns/locale'
|
|
879
992
|
|
|
880
|
-
|
|
881
|
-
app/controller/**/*.(js|ts) -> app.controller
|
|
882
|
-
app/service/**/*.(js|ts) -> app.service
|
|
883
|
-
app/middlewares/**/*.(js|ts) -> app.middlewares
|
|
884
|
-
app/middleware.(js|ts) -> 全局中间件编排,按框架 -> 项目顺序执行
|
|
885
|
-
app/router/**/*.(js|ts) -> Koa router
|
|
886
|
-
app/router-schema/**/*.(js|ts) -> app.routerSchema
|
|
887
|
-
app/extends/*.(js|ts) -> app.extends
|
|
888
|
-
config/config.default.(js|ts) -> app.config
|
|
889
|
-
config/config.{env}.(js|ts) -> app.config
|
|
890
|
-
model/**/*.(js|ts) -> 内置 project service 按需读取的项目模型配置
|
|
993
|
+
registerDateFnsLocale('ja-JP', ja)
|
|
891
994
|
```
|
|
892
995
|
|
|
893
|
-
|
|
996
|
+
React 组件 render 中使用 `useText()` 读取文案。它会订阅语言和资源变化,调用 `setLanguage()` 后组件会自动重渲染。
|
|
894
997
|
|
|
895
|
-
|
|
896
|
-
- `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
|
|
897
|
-
- `app.extends.db`:默认 SQLite 框架数据库,提供 `getDBData/setDBData` 等通用数据方法。
|
|
998
|
+
`useText()` / `getText()` 遇到 `$i18n::` 前缀会先去掉前缀再查语言资源;没有前缀时会直接把传入字符串作为 key 查询,查不到才原样返回:
|
|
898
999
|
|
|
899
|
-
|
|
1000
|
+
```tsx
|
|
1001
|
+
import { useText } from '@_tc/template-core/fe'
|
|
900
1002
|
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1003
|
+
function ProductTitle() {
|
|
1004
|
+
const text = useText()
|
|
1005
|
+
|
|
1006
|
+
return (
|
|
1007
|
+
<>
|
|
1008
|
+
<h1>{text('$i18n::app.product.menu')}</h1>
|
|
1009
|
+
<span>{text('Plain title')}</span>
|
|
1010
|
+
</>
|
|
1011
|
+
)
|
|
907
1012
|
}
|
|
908
1013
|
```
|
|
909
1014
|
|
|
910
|
-
|
|
911
|
-
- `body` 来自 `ctx.request.body`。
|
|
912
|
-
- `headers` 来自 `ctx.headers`。
|
|
913
|
-
- `data` 是便捷合并层,当前按 `query -> body` 顺序合并;字段冲突时 body 覆盖 query。
|
|
1015
|
+
`getText()` 是普通函数,适合请求错误、日志、校验工具或事件回调等非 React 场景;它不会主动触发 React 组件重渲染。
|
|
914
1016
|
|
|
915
|
-
|
|
1017
|
+
资源注册时不写 `$i18n::` 前缀,配置中使用时才写:
|
|
916
1018
|
|
|
917
|
-
|
|
1019
|
+
```ts
|
|
1020
|
+
const title = '$i18n::app.product.createTitle'
|
|
1021
|
+
```
|
|
918
1022
|
|
|
919
|
-
|
|
1023
|
+
**在 model / Schema 配置中使用**:
|
|
920
1024
|
|
|
921
1025
|
```ts
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1026
|
+
{
|
|
1027
|
+
name: '$i18n::app.product.menu',
|
|
1028
|
+
moduleType: 'schema',
|
|
1029
|
+
schemaConfig: {
|
|
1030
|
+
schema: {
|
|
1031
|
+
type: 'object',
|
|
1032
|
+
properties: {
|
|
1033
|
+
name: {
|
|
1034
|
+
type: 'string',
|
|
1035
|
+
label: '$i18n::app.product.name',
|
|
1036
|
+
minLength: 2,
|
|
1037
|
+
createFormOption: {
|
|
1038
|
+
comType: 'input',
|
|
1039
|
+
},
|
|
1040
|
+
tableOption: {},
|
|
1041
|
+
searchOption: {},
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
required: ['name'],
|
|
1045
|
+
},
|
|
1046
|
+
tableConfig: {
|
|
1047
|
+
headerButtons: [
|
|
1048
|
+
{
|
|
1049
|
+
label: '$i18n::app.product.create',
|
|
1050
|
+
eventKey: 'callComponent',
|
|
1051
|
+
eventOption: { comName: 'createForm' },
|
|
1052
|
+
},
|
|
1053
|
+
],
|
|
1054
|
+
},
|
|
1055
|
+
componentConfig: {
|
|
1056
|
+
createForm: {
|
|
1057
|
+
title: '$i18n::app.product.createTitle',
|
|
1058
|
+
saveBtnText: '$i18n::common.submit',
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
926
1061
|
},
|
|
927
1062
|
}
|
|
928
1063
|
```
|
|
929
1064
|
|
|
930
|
-
|
|
1065
|
+
已支持 `$i18n::...` 的常用位置:
|
|
931
1066
|
|
|
932
|
-
|
|
933
|
-
|
|
1067
|
+
- 菜单名:`menu.name`
|
|
1068
|
+
- 项目标题:`projectInfo.desc` 或回退的 `projectInfo.name`
|
|
1069
|
+
- Schema 字段名:`schema.properties.*.label`
|
|
1070
|
+
- 表格按钮和行按钮:`headerButtons.*.label`、`rowButtons.*.label`
|
|
1071
|
+
- 弹窗、抽屉标题:`componentConfig.*.title`
|
|
1072
|
+
- 表单保存按钮:`componentConfig.*.saveBtnText`
|
|
1073
|
+
- 接口错误提示:`BaseResponse.message`
|
|
934
1074
|
|
|
935
|
-
|
|
936
|
-
const BaseController = baseFn.baseControllerFn(app)
|
|
1075
|
+
**切换语言**:
|
|
937
1076
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const data = app.extends.db.getDBData('site-setting', {
|
|
941
|
-
namespace: 'setting',
|
|
942
|
-
})
|
|
1077
|
+
```ts
|
|
1078
|
+
import { i18nStore } from '@_tc/template-core/fe'
|
|
943
1079
|
|
|
944
|
-
|
|
945
|
-
|
|
1080
|
+
i18nStore.getState().setLanguage('en-US')
|
|
1081
|
+
i18nStore.getState().setLanguage('zh-CN')
|
|
1082
|
+
```
|
|
946
1083
|
|
|
947
|
-
|
|
948
|
-
app.extends.db.setDBData('site-setting', ctx.request.body, {
|
|
949
|
-
namespace: 'setting',
|
|
950
|
-
})
|
|
1084
|
+
默认会写入 localStorage,key 是 `tc_language`。刷新页面后会继续使用上次语言。
|
|
951
1085
|
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}) satisfies ControllerFN
|
|
1086
|
+
项目页面可以直接使用语言选择组件:
|
|
956
1087
|
|
|
957
|
-
|
|
1088
|
+
```tsx
|
|
1089
|
+
import { LanguageSwitch } from '@_tc/template-core/fe'
|
|
1090
|
+
|
|
1091
|
+
export function Toolbar() {
|
|
1092
|
+
return <LanguageSwitch />
|
|
1093
|
+
}
|
|
958
1094
|
```
|
|
959
1095
|
|
|
960
|
-
|
|
1096
|
+
`LanguageSwitch` 默认提供 `zh-CN` 和 `en-US`,也支持传入 `options` 覆盖语言列表。组件当前是按钮 + 下拉菜单形态,`option.label` 支持 ReactNode,`buttonClassName` 可自定义触发按钮样式。组件内部会调用 `i18nStore.getState().setLanguage()`,因此会沿用 `tc_language` 的持久化行为。
|
|
961
1097
|
|
|
962
|
-
|
|
963
|
-
- `setDBData(key, value, options)`:写入 JSON 数据。
|
|
964
|
-
- `hasDBData(key, options)`:判断数据是否存在。
|
|
965
|
-
- `deleteDBData(key, options)`:删除单条数据。
|
|
966
|
-
- `listDBData(options)`:分页列出当前 namespace 下的数据。
|
|
967
|
-
- `countDBData(options)` / `clearDBData(options)`:统计和清空当前 namespace。
|
|
968
|
-
- `queryDB(sql, params)` / `runDB(sql, params)`:直接执行 SQLite SQL。
|
|
969
|
-
- `transactionDB(handler)`:事务执行。
|
|
970
|
-
- `closeDB()`:关闭连接。
|
|
1098
|
+
**AJV 校验文案**:
|
|
971
1099
|
|
|
972
|
-
|
|
1100
|
+
Schema 表单校验会把 AJV keyword 映射到内置语言 key,覆盖 `required`、`type`、`format`、`minimum`、`maximum`、`minLength`、`maxLength`、`pattern`、`enum`、`oneOf`、`anyOf` 等常见规则。
|
|
973
1101
|
|
|
974
|
-
|
|
975
|
-
- `queryDB`、`runDB`、`getDBFirst` 是原始 SQL 入口,用户输入必须放到 `params`,不要拼接到 SQL 字符串里。
|
|
976
|
-
- `execDB` 没有参数绑定能力,只建议用于固定 SQL,例如建表或迁移。
|
|
1102
|
+
非必填字段为空时会跳过 AJV 校验,空值包括 `undefined`、`null`、空字符串、空数组和空对象。
|
|
977
1103
|
|
|
978
|
-
|
|
1104
|
+
如果需要扩展更多 AJV 文案,在源码项目中同步增加:
|
|
979
1105
|
|
|
980
|
-
`
|
|
1106
|
+
1. `frontend/src/defaultPages/SchemaPage/utils/validator.ts` 的 `formatError()` 映射。
|
|
1107
|
+
2. `frontend/src/language/zh-CN.ts` 和 `frontend/src/language/en-US.ts` 文案。
|
|
1108
|
+
3. `frontend/src/language/index.ts` 的 `frontendLangKeys`。
|
|
981
1109
|
|
|
982
|
-
|
|
1110
|
+
### 前端主题切换
|
|
983
1111
|
|
|
984
|
-
|
|
985
|
-
- `base64urlDecode(value)`:把 base64url 解码回 UTF-8 字符串。
|
|
986
|
-
- `base64urlDecodeToBuffer(value)`:把 base64url 解码回 Buffer。
|
|
987
|
-
- `hmacSign(payload, options)`:对文本做 HMAC 签名,默认算法 `sha256`。
|
|
988
|
-
- `safeEqual(left, right)`:常量时间比较字符串。
|
|
989
|
-
- `createSignedPayload(payload, options)`:生成 `payload.signature` 形式的签名串。
|
|
990
|
-
- `verifySignedPayload(token, options)`:验证并解析签名串,失败返回 `null`。
|
|
991
|
-
|
|
992
|
-
```ts
|
|
993
|
-
const token = app.extends.crypto.createSignedPayload({
|
|
994
|
-
account: 'admin',
|
|
995
|
-
exp: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
|
996
|
-
iat: Date.now(),
|
|
997
|
-
})
|
|
1112
|
+
项目页面可以使用 `ThemeSwitch` 切换浅色和深色模式:
|
|
998
1113
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
exp: number
|
|
1002
|
-
iat: number
|
|
1003
|
-
}>(token)
|
|
1114
|
+
```tsx
|
|
1115
|
+
import { ThemeSwitch } from '@_tc/template-core/fe'
|
|
1004
1116
|
|
|
1005
|
-
|
|
1117
|
+
export function Toolbar() {
|
|
1118
|
+
return <ThemeSwitch />
|
|
1119
|
+
}
|
|
1006
1120
|
```
|
|
1007
1121
|
|
|
1008
|
-
|
|
1009
|
-
// 推荐:参数绑定
|
|
1010
|
-
const users = app.extends.db.queryDB(
|
|
1011
|
-
'SELECT * FROM user WHERE name = ?',
|
|
1012
|
-
[ctx.query.name as string]
|
|
1013
|
-
)
|
|
1122
|
+
`ThemeSwitch` 会切换 `document.documentElement` 上的 `dark` class,并同步 `color-scheme`。默认写入 localStorage,key 是 `tc_theme`。刷新页面后会优先读取本地主题;没有本地主题时,读取当前根节点是否已有 `dark` class。默认 Dashboard 会在渲染前调用 `initThemeMode()`,让已保存主题尽早生效。
|
|
1014
1123
|
|
|
1015
|
-
|
|
1016
|
-
const unsafeUsers = app.extends.db.queryDB(
|
|
1017
|
-
`SELECT * FROM user WHERE name = '${ctx.query.name}'`
|
|
1018
|
-
)
|
|
1019
|
-
```
|
|
1124
|
+
常用参数:
|
|
1020
1125
|
|
|
1021
|
-
|
|
1126
|
+
| 参数 | 说明 |
|
|
1127
|
+
| --- | --- |
|
|
1128
|
+
| `value` / `onChange` | 受控模式,值为 `light` 或 `dark`。 |
|
|
1129
|
+
| `defaultValue` | 非受控初始主题。 |
|
|
1130
|
+
| `showLabel` | 是否展示当前主题文本。 |
|
|
1131
|
+
| `persist` | 是否写入 localStorage,默认 `true`。 |
|
|
1132
|
+
| `storageKey` | 自定义本地存储 key,默认 `tc_theme`。 |
|
|
1133
|
+
|
|
1134
|
+
如需在自定义入口或组件外手动初始化/切换主题,可以从 `@_tc/template-core/fe` 导入主题工具:
|
|
1022
1135
|
|
|
1023
1136
|
```ts
|
|
1024
|
-
|
|
1025
|
-
import type { DB, DBFactory } from '@_tc/template-core'
|
|
1137
|
+
import { applyThemeMode, getCurrentThemeMode, initThemeMode, themeSwitchStorageKey } from '@_tc/template-core/fe'
|
|
1026
1138
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1139
|
+
initThemeMode()
|
|
1140
|
+
applyThemeMode('dark', true)
|
|
1141
|
+
```
|
|
1030
1142
|
|
|
1031
|
-
|
|
1143
|
+
RAF 风格计时器由 `@tc/common/rafTimer` 提供,浏览器优先使用 `requestAnimationFrame`,缺少 RAF 时自动降级到 `setTimeout`。
|
|
1032
1144
|
|
|
1033
|
-
|
|
1034
|
-
const toKey = (key: string, namespace = 'framework') => `${namespace}:${key}`
|
|
1145
|
+
`@_tc/template-core/fe/rc` 和 `@_tc/template-core/fe/rc/hooks` 也会随发布包提供对应 UI 与 hooks 入口;hooks 汇总入口包含 `useBreadcrumb`、`useExecuteOnce`、`useInit`、`useLanguage`、`usePagination`、`useRefState`、`useWatch`。
|
|
1035
1146
|
|
|
1036
|
-
|
|
1037
|
-
dbPath: 'memory',
|
|
1038
|
-
getDBConnection: () => {
|
|
1039
|
-
throw new Error('memory db 没有底层 SQLite connection')
|
|
1040
|
-
},
|
|
1041
|
-
execDB: () => undefined,
|
|
1042
|
-
runDB: () => ({ changes: 0, lastInsertRowid: 0 }),
|
|
1043
|
-
queryDB: <T extends object = Record<string, unknown>>() => [] as T[],
|
|
1044
|
-
getDBFirst: <T extends object = Record<string, unknown>>() => undefined as T | undefined,
|
|
1045
|
-
transactionDB: <T>(handler: (db: DB) => T) => handler(db),
|
|
1046
|
-
getDBData: <T = unknown>(key: string, options?: DataOptions) => {
|
|
1047
|
-
return store.get(toKey(key, options?.namespace)) as T | undefined
|
|
1048
|
-
},
|
|
1049
|
-
getDBDataRecord: <T = unknown>(key: string, options?: DataOptions) => {
|
|
1050
|
-
const value = store.get(toKey(key, options?.namespace)) as T | undefined
|
|
1051
|
-
if (value === undefined) return undefined
|
|
1147
|
+
`useRefState` 支持可选的 `delayTiming` 参数,传入大于 0 的值时会延迟 state 提交,并复用 `@tc/common/rafTimer` 的浏览器优先、Node/SSR 自动降级计时器实现。
|
|
1052
1148
|
|
|
1053
|
-
|
|
1054
|
-
key,
|
|
1055
|
-
value,
|
|
1056
|
-
namespace: options?.namespace ?? 'framework',
|
|
1057
|
-
createdAt: '',
|
|
1058
|
-
updatedAt: '',
|
|
1059
|
-
}
|
|
1060
|
-
},
|
|
1061
|
-
setDBData: <T = unknown>(key: string, value: T, options?: DataOptions) => {
|
|
1062
|
-
store.set(toKey(key, options?.namespace), value)
|
|
1063
|
-
return { changes: 1, lastInsertRowid: 0 }
|
|
1064
|
-
},
|
|
1065
|
-
hasDBData: (key: string, options?: DataOptions) => {
|
|
1066
|
-
return store.has(toKey(key, options?.namespace))
|
|
1067
|
-
},
|
|
1068
|
-
deleteDBData: (key: string, options?: DataOptions) => {
|
|
1069
|
-
return store.delete(toKey(key, options?.namespace))
|
|
1070
|
-
},
|
|
1071
|
-
listDBData: <T = unknown>(options?: DataOptions) => {
|
|
1072
|
-
const namespace = options?.namespace ?? 'framework'
|
|
1149
|
+
### 前端请求封装详解
|
|
1073
1150
|
|
|
1074
|
-
|
|
1075
|
-
.filter(([key]) => key.startsWith(`${namespace}:`))
|
|
1076
|
-
.map(([key, value]) => ({
|
|
1077
|
-
key: key.slice(namespace.length + 1),
|
|
1078
|
-
value: value as T,
|
|
1079
|
-
namespace,
|
|
1080
|
-
createdAt: '',
|
|
1081
|
-
updatedAt: '',
|
|
1082
|
-
}))
|
|
1083
|
-
},
|
|
1084
|
-
countDBData: () => store.size,
|
|
1085
|
-
clearDBData: () => {
|
|
1086
|
-
const size = store.size
|
|
1087
|
-
store.clear()
|
|
1088
|
-
return size
|
|
1089
|
-
},
|
|
1090
|
-
closeDB: () => undefined,
|
|
1091
|
-
} satisfies DB
|
|
1151
|
+
框架内置的请求封装基于 `@tc/common/http`,底层使用原生 `fetch` API,实现了类似 Axios 的接口和拦截器机制,提供了签名、鉴权、错误处理等能力。
|
|
1092
1152
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1153
|
+
**基础配置**:
|
|
1154
|
+
- `BASE_URL`:固定为 `/api`
|
|
1155
|
+
- `timeout`:15000ms(15秒)
|
|
1156
|
+
- `credentials`:`'include'`,支持跨域携带 cookie
|
|
1157
|
+
- `window._signKey`:由服务端页面模板从 `app.config.signKey` 注入
|
|
1158
|
+
- 默认会先设置 `Content-Type: application/json`;如果是上传文件、`FormData` 或其他内容类型,请在请求配置里显式覆盖
|
|
1095
1159
|
|
|
1096
|
-
|
|
1160
|
+
**请求拦截器**:
|
|
1161
|
+
|
|
1162
|
+
每个请求会自动添加以下 headers:
|
|
1163
|
+
|
|
1164
|
+
```ts
|
|
1165
|
+
{
|
|
1166
|
+
's_t': '当前时间戳',
|
|
1167
|
+
's_sign': 'md5(签名密钥_时间戳)',
|
|
1168
|
+
'projk': '当前项目 key(从 localStorage 读取)',
|
|
1169
|
+
'Authorization': 'Bearer token(如果已登录)'
|
|
1170
|
+
}
|
|
1097
1171
|
```
|
|
1098
1172
|
|
|
1099
|
-
|
|
1173
|
+
`Authorization` 默认对应服务端配置 `config.auth.ATKey`。前端本地 token 存在 `localStorage.auth_token`,项目 key 存在 `localStorage.p_J_k`;刷新 token header 名预留为 `config.auth.RTKey`,默认是 `RT`。
|
|
1174
|
+
|
|
1175
|
+
**签名机制**:
|
|
1176
|
+
- 签名密钥:服务端从 `app.config.signKey` 读取,并在页面渲染时注入为 `window._signKey`
|
|
1177
|
+
- 签名算法:`md5(签名密钥_${时间戳})`
|
|
1178
|
+
- 服务端会在非 local 环境下校验签名和时间戳
|
|
1179
|
+
|
|
1180
|
+
**响应拦截器**:
|
|
1181
|
+
|
|
1182
|
+
- **401 处理**:自动清空 token 并跳转到 `/login`
|
|
1183
|
+
- **错误提取**:自动从响应中提取 `message`、`msg` 或 `errors` 字段
|
|
1184
|
+
- **超时处理**:请求超时会返回当前语言的超时提示
|
|
1185
|
+
- **网络错误**:网络异常会返回当前语言的网络错误提示
|
|
1186
|
+
|
|
1187
|
+
**使用示例**:
|
|
1100
1188
|
|
|
1101
1189
|
```ts
|
|
1102
|
-
import {
|
|
1190
|
+
import { api, get, post, put, del } from '@_tc/template-core/fe'
|
|
1191
|
+
import type { BaseResponse, RequestConfig, ResponseConfig } from '@_tc/template-core/fe'
|
|
1103
1192
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1193
|
+
// GET 请求
|
|
1194
|
+
const data = await get('/product/list', { page: 1, pageSize: 10 })
|
|
1106
1195
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
this.success(ctx, {
|
|
1110
|
-
data: [],
|
|
1111
|
-
page: 1,
|
|
1112
|
-
pageSize: 10,
|
|
1113
|
-
total: 0,
|
|
1114
|
-
})
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}) satisfies ControllerFN
|
|
1196
|
+
// POST 请求
|
|
1197
|
+
const result = await post('/product', { name: '商品名称', price: 100 })
|
|
1118
1198
|
|
|
1119
|
-
|
|
1199
|
+
// PUT 请求
|
|
1200
|
+
await put('/product', { id: 1, name: '新名称' })
|
|
1201
|
+
|
|
1202
|
+
// DELETE 请求
|
|
1203
|
+
await del('/product', { id: 1 })
|
|
1120
1204
|
```
|
|
1121
1205
|
|
|
1122
|
-
|
|
1206
|
+
如果希望把请求能力单独拆出来导入:
|
|
1123
1207
|
|
|
1124
1208
|
```ts
|
|
1125
|
-
import
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1209
|
+
import {
|
|
1210
|
+
api,
|
|
1211
|
+
get,
|
|
1212
|
+
post,
|
|
1213
|
+
request,
|
|
1214
|
+
} from '@_tc/template-core/fe/common/request'
|
|
1215
|
+
import type {
|
|
1216
|
+
BaseResponse,
|
|
1217
|
+
RequestConfig,
|
|
1218
|
+
ResponseConfig,
|
|
1219
|
+
} from '@_tc/template-core/fe/common/request'
|
|
1130
1220
|
```
|
|
1131
1221
|
|
|
1132
|
-
|
|
1222
|
+
**请求冻结器**:
|
|
1133
1223
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
- 通配、兜底、重定向类路由建议设置较大的 `level`,避免抢先匹配项目 API。
|
|
1224
|
+
请求冻结器用于在某段流程中暂停普通 API 请求,典型场景是 RT 刷新 token:刷新期间冻结其它请求,刷新接口本身走白名单放行,刷新完成后再解冻并回放队列。
|
|
1225
|
+
|
|
1226
|
+
冻结器只在 `Freeze` 状态拦截请求;`Hibernation` 和 `Thawing` 状态都会直接放行。解冻时会按最多 4 个并发回放已冻结的请求;相同序列化参数的请求只会实际发送一次,多个调用方共享同一份响应或错误。
|
|
1138
1227
|
|
|
1139
1228
|
```ts
|
|
1140
|
-
import
|
|
1229
|
+
import { apiFreezerStore } from '@_tc/template-core/fe'
|
|
1141
1230
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1231
|
+
const { freezing, thawing, setRequestWhitelist } = apiFreezerStore.getState()
|
|
1232
|
+
|
|
1233
|
+
setRequestWhitelist([
|
|
1234
|
+
'/auth/refresh',
|
|
1235
|
+
/^\/auth\//,
|
|
1236
|
+
(api) => api.includes('/refresh-token'),
|
|
1237
|
+
])
|
|
1238
|
+
|
|
1239
|
+
freezing()
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
await refreshToken()
|
|
1243
|
+
} finally {
|
|
1244
|
+
thawing()
|
|
1145
1245
|
}
|
|
1146
1246
|
```
|
|
1147
1247
|
|
|
1148
|
-
|
|
1248
|
+
白名单支持三种 matcher:
|
|
1149
1249
|
|
|
1150
|
-
`
|
|
1250
|
+
- `string`:精确匹配 API 路径
|
|
1251
|
+
- `RegExp`:正则匹配 API 路径
|
|
1252
|
+
- `(api, params) => boolean`:自定义判断,`params` 是请求函数收到的完整参数
|
|
1151
1253
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1254
|
+
**注意事项**:
|
|
1255
|
+
- `get` 和 `del` 方法的参数会自动转为 query string
|
|
1256
|
+
- `post`、`put`、`patch` 方法的参数会作为 request body
|
|
1257
|
+
- 所有请求方法都会自动处理错误,无需手动 try-catch(除非需要自定义错误处理)
|
|
1258
|
+
- 底层使用原生 `fetch` API,但提供了类似 Axios 的拦截器和配置接口
|
|
1156
1259
|
|
|
1157
|
-
|
|
1260
|
+
## 样式与 Tailwind V4
|
|
1261
|
+
|
|
1262
|
+
使用内置前端或 UI 组件时,需要引入包内样式。默认 `@_tc/template-core/fe` 的 `initApp` 会经过源码入口 `frontend/src/main.tsx` 引入 `frontend/src/main.css`;如果项目侧自定义入口、只使用 `fe/rc` 组件,或希望样式入口更明确,建议显式引入:
|
|
1158
1263
|
|
|
1159
1264
|
```ts
|
|
1160
|
-
|
|
1265
|
+
// frontend/xxx/xxx.entry.tsx
|
|
1266
|
+
import '@_tc/template-core/fe/tailwind_ui.css'
|
|
1267
|
+
```
|
|
1161
1268
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
tableOption: {},
|
|
1194
|
-
searchOption: { comType: 'input' },
|
|
1195
|
-
createFormOption: { comType: 'input' },
|
|
1196
|
-
editFormOption: { comType: 'input' },
|
|
1197
|
-
detailPanelOption: {},
|
|
1198
|
-
},
|
|
1199
|
-
price: {
|
|
1200
|
-
type: 'number',
|
|
1201
|
-
label: '价格',
|
|
1202
|
-
tableOption: {},
|
|
1203
|
-
createFormOption: { comType: 'inputNumber' },
|
|
1204
|
-
editFormOption: { comType: 'inputNumber' },
|
|
1205
|
-
detailPanelOption: {},
|
|
1206
|
-
},
|
|
1207
|
-
},
|
|
1208
|
-
required: ['product_name'],
|
|
1209
|
-
},
|
|
1210
|
-
componentConfig: {
|
|
1211
|
-
createForm: {
|
|
1212
|
-
title: '新增商品',
|
|
1213
|
-
saveBtnText: '创建',
|
|
1214
|
-
},
|
|
1215
|
-
editForm: {
|
|
1216
|
-
title: '编辑商品',
|
|
1217
|
-
saveBtnText: '保存',
|
|
1218
|
-
fetchKey: 'product_id',
|
|
1219
|
-
},
|
|
1220
|
-
detailPanel: {
|
|
1221
|
-
title: '商品详情',
|
|
1222
|
-
fetchKey: 'product_id',
|
|
1223
|
-
},
|
|
1224
|
-
},
|
|
1225
|
-
tableConfig: {
|
|
1226
|
-
headerButtons: [
|
|
1227
|
-
{
|
|
1228
|
-
label: '新增',
|
|
1229
|
-
eventKey: 'callComponent',
|
|
1230
|
-
variant: 'primary',
|
|
1231
|
-
eventOption: { comName: 'createForm' },
|
|
1232
|
-
},
|
|
1233
|
-
],
|
|
1234
|
-
rowButtons: [
|
|
1235
|
-
{
|
|
1236
|
-
label: '编辑',
|
|
1237
|
-
eventKey: 'callComponent',
|
|
1238
|
-
eventOption: { comName: 'editForm' },
|
|
1239
|
-
},
|
|
1240
|
-
{
|
|
1241
|
-
label: '详情',
|
|
1242
|
-
eventKey: 'callComponent',
|
|
1243
|
-
eventOption: { comName: 'detailPanel' },
|
|
1244
|
-
},
|
|
1245
|
-
{
|
|
1246
|
-
label: '删除',
|
|
1247
|
-
eventKey: 'remove',
|
|
1248
|
-
eventOption: {
|
|
1249
|
-
params: {
|
|
1250
|
-
product_id: '$schema::product_id',
|
|
1251
|
-
},
|
|
1252
|
-
},
|
|
1253
|
-
},
|
|
1254
|
-
],
|
|
1255
|
-
},
|
|
1256
|
-
},
|
|
1257
|
-
},
|
|
1258
|
-
],
|
|
1269
|
+
如果项目有自己的全局样式入口,也可以在 CSS 中引入:
|
|
1270
|
+
|
|
1271
|
+
```css
|
|
1272
|
+
/* frontend/main.css */
|
|
1273
|
+
@import "@_tc/template-core/fe/tailwind_ui.css";
|
|
1274
|
+
|
|
1275
|
+
/* 扫描项目源码 */
|
|
1276
|
+
@source "./**/*.{js,ts,jsx,tsx}";
|
|
1277
|
+
@source "../model/**/*.{js,ts,jsx,tsx}";
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
Tailwind V4 默认会忽略 `node_modules` 和 `.gitignore` 中的文件。如果没有使用上面的 `tailwind_ui.css`,或者项目侧构建链路绕过了 TemplateCore 的样式入口,需要手动把包加入 Tailwind 扫描:
|
|
1281
|
+
|
|
1282
|
+
```css
|
|
1283
|
+
/* frontend/main.css */
|
|
1284
|
+
@import "tailwindcss";
|
|
1285
|
+
|
|
1286
|
+
@source "./**/*.{js,ts,jsx,tsx}";
|
|
1287
|
+
@source "../model/**/*.{js,ts,jsx,tsx}";
|
|
1288
|
+
@source "../node_modules/@_tc/template-core";
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
VS Code 建议安装官方 Tailwind CSS IntelliSense 插件,并在工作区 `.vscode/settings.json` 指向项目 CSS 入口:
|
|
1292
|
+
|
|
1293
|
+
```json
|
|
1294
|
+
{
|
|
1295
|
+
"tailwindCSS.experimental.configFile": "frontend/main.css",
|
|
1296
|
+
"files.associations": {
|
|
1297
|
+
"*.css": "tailwindcss"
|
|
1298
|
+
},
|
|
1299
|
+
"css.lint.unknownAtRules": "ignore"
|
|
1259
1300
|
}
|
|
1301
|
+
```
|
|
1260
1302
|
|
|
1261
|
-
|
|
1303
|
+
多入口项目可以写成对象:
|
|
1304
|
+
|
|
1305
|
+
```json
|
|
1306
|
+
{
|
|
1307
|
+
"tailwindCSS.experimental.configFile": {
|
|
1308
|
+
"frontend/main.css": "frontend/**/*.{ts,tsx,js,jsx}",
|
|
1309
|
+
"frontend/admin/admin.css": "frontend/admin/**/*.{ts,tsx,js,jsx}"
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1262
1312
|
```
|
|
1263
1313
|
|
|
1264
|
-
|
|
1314
|
+
## 内置 UI 组件
|
|
1315
|
+
|
|
1316
|
+
框架通过 `@_tc/template-core/fe/rc` 提供了丰富的 React 组件库,可在项目中直接使用。
|
|
1317
|
+
|
|
1318
|
+
### 基础组件
|
|
1319
|
+
|
|
1320
|
+
| 组件 | 用途 | 导入路径 |
|
|
1321
|
+
| --- | --- | --- |
|
|
1322
|
+
| `Button` | 按钮组件,支持多种样式和尺寸 | `@_tc/template-core/fe/rc` |
|
|
1323
|
+
| `Input` | 输入框组件,支持清空、右侧附加内容和密码显隐 | `@_tc/template-core/fe/rc` |
|
|
1324
|
+
| `InputNumber` | 数字输入框组件 | `@_tc/template-core/fe/rc` |
|
|
1325
|
+
| `Textarea` | 多行文本输入框组件 | `@_tc/template-core/fe/rc` |
|
|
1326
|
+
| `Select` | 下拉选择组件,支持单选和多选 | `@_tc/template-core/fe/rc` |
|
|
1327
|
+
| `Search` | 搜索输入组件 | `@_tc/template-core/fe/rc` |
|
|
1328
|
+
| `TreeSelect` | 树形选择组件 | `@_tc/template-core/fe/rc` |
|
|
1329
|
+
| `Dropdown` | 下拉菜单组件 | `@_tc/template-core/fe/rc` |
|
|
1330
|
+
| `DatePicker` | 日期选择器 | `@_tc/template-core/fe/rc` |
|
|
1331
|
+
| `Upload` | 文件上传组件 | `@_tc/template-core/fe/rc` |
|
|
1332
|
+
| `FileUpload` | 通用文件上传组件 | `@_tc/template-core/fe/rc` |
|
|
1333
|
+
| `ImageUpload` | 图片上传组件 | `@_tc/template-core/fe/rc` |
|
|
1334
|
+
| `ImagePreview` | 图片预览组件 | `@_tc/template-core/fe/rc` |
|
|
1335
|
+
| `Checkbox` | 复选框组件 | `@_tc/template-core/fe/rc` |
|
|
1336
|
+
| `Radio` | 单选框组件 | `@_tc/template-core/fe/rc` |
|
|
1337
|
+
| `Switch` | 开关组件 | `@_tc/template-core/fe/rc` |
|
|
1338
|
+
|
|
1339
|
+
### 表单组件
|
|
1340
|
+
|
|
1341
|
+
| 组件 | 用途 | 导入路径 |
|
|
1342
|
+
| --- | --- | --- |
|
|
1343
|
+
| `Form` | 表单容器组件 | `@_tc/template-core/fe/rc` |
|
|
1344
|
+
| `SchemaForm` | 配置驱动的表单组件,根据 schema 自动生成表单 | `@_tc/template-core/fe/rc` |
|
|
1345
|
+
| `FormItem` | 表单项组件 | `@_tc/template-core/fe/rc` |
|
|
1346
|
+
|
|
1347
|
+
### 数据展示组件
|
|
1348
|
+
|
|
1349
|
+
| 组件 | 用途 | 导入路径 |
|
|
1350
|
+
| --- | --- | --- |
|
|
1351
|
+
| `DataTable` | 数据表格组件,支持分页、排序、筛选 | `@_tc/template-core/fe/rc` |
|
|
1352
|
+
| `Table` | 表格组件(含表头、列配置等子组件) | `@_tc/template-core/fe/rc` |
|
|
1353
|
+
| `TableSearch` | 表格搜索组件,配合 DataTable 使用 | `@_tc/template-core/fe/rc` |
|
|
1354
|
+
| `Pagination` | 分页组件 | `@_tc/template-core/fe/rc` |
|
|
1355
|
+
| `Breadcrumb` | 面包屑导航组件 | `@_tc/template-core/fe/rc` |
|
|
1356
|
+
| `Calendar` | 日历组件 | `@_tc/template-core/fe/rc` |
|
|
1357
|
+
| `TimePicker` | 时间选择器组件 | `@_tc/template-core/fe/rc` |
|
|
1358
|
+
| `Skeleton` | 骨架屏加载占位组件 | `@_tc/template-core/fe/rc` |
|
|
1265
1359
|
|
|
1266
|
-
|
|
1360
|
+
### 反馈组件
|
|
1267
1361
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1362
|
+
| 组件 | 用途 | 导入路径 |
|
|
1363
|
+
| --- | --- | --- |
|
|
1364
|
+
| `Modal` | 模态框组件 | `@_tc/template-core/fe/rc` |
|
|
1365
|
+
| `Drawer` | 抽屉组件 | `@_tc/template-core/fe/rc` |
|
|
1366
|
+
| `message` | 消息提示方法 | `@_tc/template-core/fe/rc` |
|
|
1367
|
+
| `Notification` | 通知组件 | `@_tc/template-core/fe/rc` |
|
|
1368
|
+
| `ConfirmDialog` | 确认对话框组件 | `@_tc/template-core/fe/rc` |
|
|
1369
|
+
| `Popup` | 弹出层组件 | `@_tc/template-core/fe/rc` |
|
|
1370
|
+
| `Tooltip` | 文字提示组件 | `@_tc/template-core/fe/rc` |
|
|
1371
|
+
| `Overlay` | 页面遮罩层组件 | `@_tc/template-core/fe/rc` |
|
|
1372
|
+
| `Loading` | 加载状态组件 | `@_tc/template-core/fe/rc` |
|
|
1275
1373
|
|
|
1276
|
-
|
|
1374
|
+
### 布局组件
|
|
1277
1375
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1376
|
+
| 组件 | 用途 | 导入路径 |
|
|
1377
|
+
| --- | --- | --- |
|
|
1378
|
+
| `Layout` | 页面布局组件 | `@_tc/template-core/fe/rc` |
|
|
1379
|
+
| `Card` | 卡片容器组件 | `@_tc/template-core/fe/rc` |
|
|
1380
|
+
| `Tabs` | 标签页组件 | `@_tc/template-core/fe/rc` |
|
|
1381
|
+
| `Menu` | 菜单组件(含 MenuItem 等子组件) | `@_tc/template-core/fe/rc` |
|
|
1382
|
+
| `Label` | 标签组件 | `@_tc/template-core/fe/rc` |
|
|
1282
1383
|
|
|
1283
|
-
###
|
|
1384
|
+
### 按钮组件
|
|
1284
1385
|
|
|
1285
|
-
|
|
1386
|
+
| 组件 | 用途 | 导入路径 |
|
|
1387
|
+
| --- | --- | --- |
|
|
1388
|
+
| `Button` | 通用按钮组件 | `@_tc/template-core/fe/rc` |
|
|
1389
|
+
| `AsynchronousButton` | 异步按钮组件,支持加载状态 | `@_tc/template-core/fe/rc` |
|
|
1390
|
+
| `SubmitButton` | 表单提交按钮组件 | `@_tc/template-core/fe/rc` |
|
|
1391
|
+
| `ActionBtn` | 操作按钮组件 | `@_tc/template-core/fe/rc` |
|
|
1286
1392
|
|
|
1287
|
-
|
|
1288
|
-
// model/product/mode.ts
|
|
1289
|
-
export default {
|
|
1290
|
-
name: '商品后台',
|
|
1291
|
-
menu: [
|
|
1292
|
-
{
|
|
1293
|
-
key: 'product',
|
|
1294
|
-
name: '商品列表',
|
|
1295
|
-
menuType: 'module',
|
|
1296
|
-
moduleType: 'schema',
|
|
1297
|
-
schemaConfig: {
|
|
1298
|
-
api: '/api/product',
|
|
1299
|
-
// ... 省略 schema 配置
|
|
1300
|
-
},
|
|
1301
|
-
},
|
|
1302
|
-
{
|
|
1303
|
-
key: 'category',
|
|
1304
|
-
name: '分类管理',
|
|
1305
|
-
menuType: 'module',
|
|
1306
|
-
moduleType: 'schema',
|
|
1307
|
-
schemaConfig: {
|
|
1308
|
-
api: '/api/category',
|
|
1309
|
-
// ... 省略 schema 配置
|
|
1310
|
-
},
|
|
1311
|
-
},
|
|
1312
|
-
],
|
|
1313
|
-
}
|
|
1314
|
-
```
|
|
1393
|
+
### 使用示例
|
|
1315
1394
|
|
|
1316
|
-
|
|
1395
|
+
```tsx
|
|
1396
|
+
import { Button, Input, Select, Modal, DataTable } from '@_tc/template-core/fe/rc'
|
|
1317
1397
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
{
|
|
1325
|
-
key: 'product',
|
|
1326
|
-
name: 'Demo 商品', // 覆盖 name
|
|
1327
|
-
schemaConfig: {
|
|
1328
|
-
api: '/api/demo/product', // 覆盖 api
|
|
1329
|
-
},
|
|
1330
|
-
},
|
|
1331
|
-
// 2. 新增:添加 brand 菜单
|
|
1332
|
-
{
|
|
1333
|
-
key: 'brand',
|
|
1334
|
-
name: '品牌管理',
|
|
1335
|
-
menuType: 'module',
|
|
1336
|
-
moduleType: 'schema',
|
|
1337
|
-
schemaConfig: {
|
|
1338
|
-
api: '/api/brand',
|
|
1339
|
-
},
|
|
1340
|
-
},
|
|
1341
|
-
// 3. 继承:category 菜单未在 project 中定义,会完整继承 mode 的配置
|
|
1342
|
-
],
|
|
1343
|
-
}
|
|
1344
|
-
```
|
|
1398
|
+
function MyComponent() {
|
|
1399
|
+
return (
|
|
1400
|
+
<div>
|
|
1401
|
+
<Button variant="primary" onClick={() => console.log('clicked')}>
|
|
1402
|
+
点击我
|
|
1403
|
+
</Button>
|
|
1345
1404
|
|
|
1346
|
-
|
|
1405
|
+
<Input placeholder="请输入内容" />
|
|
1406
|
+
<Input type="password" placeholder="请输入密码" />
|
|
1347
1407
|
|
|
1348
|
-
|
|
1349
|
-
{
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
moduleType: 'schema', // mode 继承
|
|
1357
|
-
schemaConfig: {
|
|
1358
|
-
api: '/api/demo/product', // project 覆盖
|
|
1359
|
-
// schema 配置会递归合并
|
|
1360
|
-
},
|
|
1361
|
-
},
|
|
1362
|
-
{
|
|
1363
|
-
key: 'category', // mode 完整继承
|
|
1364
|
-
name: '分类管理',
|
|
1365
|
-
menuType: 'module',
|
|
1366
|
-
moduleType: 'schema',
|
|
1367
|
-
schemaConfig: {
|
|
1368
|
-
api: '/api/category',
|
|
1369
|
-
},
|
|
1370
|
-
},
|
|
1371
|
-
{
|
|
1372
|
-
key: 'brand', // project 新增
|
|
1373
|
-
name: '品牌管理',
|
|
1374
|
-
menuType: 'module',
|
|
1375
|
-
moduleType: 'schema',
|
|
1376
|
-
schemaConfig: {
|
|
1377
|
-
api: '/api/brand',
|
|
1378
|
-
},
|
|
1379
|
-
},
|
|
1380
|
-
],
|
|
1408
|
+
<Select
|
|
1409
|
+
options={[
|
|
1410
|
+
{ label: '选项1', value: '1' },
|
|
1411
|
+
{ label: '选项2', value: '2' },
|
|
1412
|
+
]}
|
|
1413
|
+
/>
|
|
1414
|
+
</div>
|
|
1415
|
+
)
|
|
1381
1416
|
}
|
|
1382
1417
|
```
|
|
1383
1418
|
|
|
1384
|
-
|
|
1385
|
-
-
|
|
1386
|
-
-
|
|
1387
|
-
-
|
|
1388
|
-
|
|
1389
|
-
## model 数据如何引用
|
|
1390
|
-
|
|
1391
|
-
后端可以直接调用 loader:
|
|
1392
|
-
|
|
1393
|
-
```ts
|
|
1394
|
-
import { modelLoader, type KoaApp } from '@_tc/template-core'
|
|
1419
|
+
**注意**:
|
|
1420
|
+
- 所有组件都支持 TypeScript 类型提示
|
|
1421
|
+
- 组件样式基于 Tailwind CSS V4
|
|
1422
|
+
- 详细的组件 API 和示例可以访问 `/ui-components` 页面查看
|
|
1395
1423
|
|
|
1396
|
-
|
|
1397
|
-
const modelList = modelLoader(app)
|
|
1398
|
-
return modelList
|
|
1399
|
-
}
|
|
1400
|
-
```
|
|
1424
|
+
## 扩展自定义前端页面
|
|
1401
1425
|
|
|
1402
|
-
|
|
1426
|
+
新增应用入口:
|
|
1403
1427
|
|
|
1404
1428
|
```text
|
|
1405
|
-
|
|
1406
|
-
GET /api/project/list?projk=demo
|
|
1407
|
-
GET /api/project/demo
|
|
1429
|
+
frontend/admin/admin.entry.tsx
|
|
1408
1430
|
```
|
|
1409
1431
|
|
|
1410
|
-
|
|
1432
|
+
入口文件名必须包含 `.entry.`,构建器才会扫描。
|
|
1411
1433
|
|
|
1412
|
-
|
|
1434
|
+
最小入口示例:
|
|
1413
1435
|
|
|
1414
|
-
```
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
PUT {api} -> 编辑
|
|
1419
|
-
DELETE {api} -> 删除
|
|
1420
|
-
```
|
|
1436
|
+
```tsx
|
|
1437
|
+
// frontend/admin/admin.entry.tsx
|
|
1438
|
+
import { initApp } from '@_tc/template-core/fe'
|
|
1439
|
+
import type { ReactNode } from 'react'
|
|
1421
1440
|
|
|
1422
|
-
|
|
1441
|
+
const AdminApp = ({ children }: { children: ReactNode }) => (
|
|
1442
|
+
<main>{children}</main>
|
|
1443
|
+
)
|
|
1423
1444
|
|
|
1424
|
-
|
|
1445
|
+
initApp(AdminApp, {
|
|
1446
|
+
initModeData: false,
|
|
1447
|
+
})
|
|
1448
|
+
```
|
|
1425
1449
|
|
|
1426
|
-
|
|
1450
|
+
如果需要给页面加额外 `meta`、预连接资源或启动脚本,可以在入口同目录放 `.html` 插槽文件。这个文件不是完整 HTML,只能包含 `tc-slot` 块:
|
|
1427
1451
|
|
|
1428
|
-
```
|
|
1429
|
-
|
|
1452
|
+
```html
|
|
1453
|
+
<!-- frontend/admin/admin.html -->
|
|
1454
|
+
<!-- tc-slot:head -->
|
|
1455
|
+
<meta name="description" content="Admin Console" />
|
|
1456
|
+
<link rel="preconnect" href="https://example.com" />
|
|
1457
|
+
<!-- /tc-slot:head -->
|
|
1430
1458
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
return {
|
|
1435
|
-
status: 404,
|
|
1436
|
-
body: { code: 404, message: `${ctx.path} not found` },
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1459
|
+
<!-- tc-slot:body-before-root -->
|
|
1460
|
+
<div id="boot-mask"></div>
|
|
1461
|
+
<!-- /tc-slot:body-before-root -->
|
|
1439
1462
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1463
|
+
<!-- tc-slot:body-after-root -->
|
|
1464
|
+
<script>
|
|
1465
|
+
window.__ADMIN_BOOT_TIME__ = Date.now()
|
|
1466
|
+
</script>
|
|
1467
|
+
<!-- /tc-slot:body-after-root -->
|
|
1443
1468
|
```
|
|
1444
1469
|
|
|
1445
|
-
|
|
1470
|
+
命名规则:
|
|
1446
1471
|
|
|
1447
|
-
|
|
1472
|
+
```text
|
|
1473
|
+
frontend/admin/admin.entry.tsx
|
|
1474
|
+
frontend/admin/admin.html yes
|
|
1448
1475
|
|
|
1449
|
-
|
|
1476
|
+
frontend/report/index.entry.tsx
|
|
1477
|
+
frontend/report/index.html yes
|
|
1478
|
+
frontend/report/report.html yes
|
|
1479
|
+
```
|
|
1450
1480
|
|
|
1451
|
-
|
|
1481
|
+
不要在插槽文件里写 `<!DOCTYPE html>`、`<html>`、`<body>` 或 `<div id="root"></div>`。页面外壳由 TemplateCore 的 `app/view/entry.tpl` 统一生成,`window._basePath`、`window._projKey`、`window._signKey`、`window.appOptions` 也由它注入。
|
|
1482
|
+
|
|
1483
|
+
扩展 Dashboard 路由:
|
|
1452
1484
|
|
|
1453
1485
|
```tsx
|
|
1454
|
-
// frontend/extended/dash/
|
|
1455
|
-
import type {
|
|
1486
|
+
// frontend/extended/dash/customRoutes.tsx
|
|
1487
|
+
import type { DashRoutesExtender } from '@_tc/template-core/fe'
|
|
1488
|
+
|
|
1489
|
+
const extendDashRoutes: DashRoutesExtender = ({ topRoutes, sidebarRoutes }) => {
|
|
1490
|
+
topRoutes.push({
|
|
1491
|
+
path: 'custom-page',
|
|
1492
|
+
component: <div>Custom Page</div>,
|
|
1493
|
+
})
|
|
1456
1494
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1495
|
+
sidebarRoutes.push({
|
|
1496
|
+
path: 'custom-sidebar-page',
|
|
1497
|
+
component: <div>Custom Sidebar Page</div>,
|
|
1498
|
+
})
|
|
1499
|
+
}
|
|
1462
1500
|
|
|
1463
|
-
export default
|
|
1501
|
+
export default extendDashRoutes
|
|
1464
1502
|
```
|
|
1465
1503
|
|
|
1466
|
-
|
|
1504
|
+
**路径规则说明**:
|
|
1467
1505
|
|
|
1468
|
-
-
|
|
1506
|
+
- **topRoutes**:追加到 Dashboard 顶层路由,路径相对于 `/dash`
|
|
1507
|
+
- 示例:`path: 'custom-page'` → 访问路径为 `/dash/custom-page`
|
|
1508
|
+
- 不要写成 `/custom-page`,使用相对路径即可
|
|
1469
1509
|
|
|
1470
|
-
|
|
1510
|
+
- **sidebarRoutes**:追加到侧边栏容器的子路由,路径相对于 `/dash/_sidebar_`
|
|
1511
|
+
- 示例:`path: 'custom-sidebar-page'` → 访问路径为 `/dash/_sidebar_/custom-sidebar-page`
|
|
1512
|
+
- 适用于 `menuLayout: 'left'` 的项目
|
|
1471
1513
|
|
|
1472
|
-
|
|
1514
|
+
model 中用 `custom` 菜单指向自定义路径:
|
|
1473
1515
|
|
|
1474
1516
|
```ts
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1517
|
+
{
|
|
1518
|
+
key: 'custom',
|
|
1519
|
+
name: '自定义页面',
|
|
1520
|
+
menuType: 'module',
|
|
1521
|
+
moduleType: 'custom',
|
|
1522
|
+
customConfig: {
|
|
1523
|
+
path: '/custom-page', // 注意:这里使用完整路径,以 / 开头
|
|
1524
|
+
},
|
|
1480
1525
|
}
|
|
1481
|
-
|
|
1482
|
-
export default dashRouteGuard
|
|
1483
1526
|
```
|
|
1484
1527
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
- 返回 `string`:框架使用 `window.location.href` 跳转,登录页不需要在 Dash 路由下。
|
|
1488
|
-
- 返回非 `string`:中断 Dashboard 内置重定向,使用方可自行跳转或处理提示。
|
|
1489
|
-
- `DashRouteGuardResult` 是 `string | undefined`;`undefined`(包括已登录分支的隐式返回)也会打断 Dashboard 内置重定向。
|
|
1490
|
-
- 没有自定义 `routeGuard` 文件时,框架使用兜底空对象,不影响默认 Dashboard 重定向。
|
|
1491
|
-
|
|
1492
|
-
### 扩展 SchemaForm 字段组件
|
|
1493
|
-
|
|
1494
|
-
1. 声明字段类型。
|
|
1495
|
-
2. 注册运行时组件。
|
|
1496
|
-
3. 在 `model` 的 `createFormOption`、`editFormOption`、`searchOption` 中使用。
|
|
1528
|
+
## 前端构建 buildFE
|
|
1497
1529
|
|
|
1498
|
-
|
|
1530
|
+
开发构建:
|
|
1499
1531
|
|
|
1500
1532
|
```ts
|
|
1501
|
-
|
|
1502
|
-
import '@_tc/template-core/fe/rc'
|
|
1503
|
-
import type { SelectProps } from '@_tc/template-core/fe/rc'
|
|
1504
|
-
|
|
1505
|
-
declare module '@_tc/template-core/fe/rc' {
|
|
1506
|
-
export namespace SchemaFormNamespace {
|
|
1507
|
-
interface FieldTypes {
|
|
1508
|
-
businessSelect: true
|
|
1509
|
-
}
|
|
1533
|
+
import { buildFE } from '@_tc/template-core/bundler'
|
|
1510
1534
|
|
|
1511
|
-
|
|
1512
|
-
businessSelect: SelectProps & {
|
|
1513
|
-
requestUrl?: string
|
|
1514
|
-
valueField?: string
|
|
1515
|
-
labelField?: string
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1535
|
+
await buildFE('dev')
|
|
1520
1536
|
```
|
|
1521
1537
|
|
|
1522
|
-
|
|
1538
|
+
生产构建:
|
|
1523
1539
|
|
|
1524
1540
|
```ts
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
import type { SchemaFormComponentsMap } from '@_tc/template-core/fe/rc'
|
|
1541
|
+
await buildFE('prod')
|
|
1542
|
+
```
|
|
1528
1543
|
|
|
1529
|
-
|
|
1530
|
-
businessSelect: React.lazy(async () => ({
|
|
1531
|
-
default: (await import('../../components/BusinessSelect')).BusinessSelect,
|
|
1532
|
-
})),
|
|
1533
|
-
} satisfies SchemaFormComponentsMap
|
|
1544
|
+
默认输出到当前导入格式对应的框架静态目录,例如 `esm/app/public/dist` 或 `cjs/app/public/dist`。如果要输出到项目:
|
|
1534
1545
|
|
|
1535
|
-
|
|
1546
|
+
```ts
|
|
1547
|
+
await buildFE('prod', {
|
|
1548
|
+
output: 'run',
|
|
1549
|
+
})
|
|
1536
1550
|
```
|
|
1537
1551
|
|
|
1538
|
-
|
|
1552
|
+
也可以压缩最终模板输出:
|
|
1539
1553
|
|
|
1540
1554
|
```ts
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
searchOption: {
|
|
1545
|
-
comType: 'businessSelect',
|
|
1546
|
-
requestUrl: '/api/status/options',
|
|
1547
|
-
valueField: 'value',
|
|
1548
|
-
labelField: 'label',
|
|
1549
|
-
},
|
|
1550
|
-
}
|
|
1555
|
+
await buildFE('prod', {
|
|
1556
|
+
minifyHtml: true,
|
|
1557
|
+
})
|
|
1551
1558
|
```
|
|
1552
1559
|
|
|
1553
|
-
|
|
1560
|
+
构建完成后会在输出目录写入 `FEBuildKey`,服务端渲染 HTML 时会用它判断本地 HTML ETag 缓存是否需要清空。
|
|
1554
1561
|
|
|
1555
|
-
|
|
1562
|
+
## Node/backend 构建 buildBE
|
|
1556
1563
|
|
|
1557
|
-
|
|
1558
|
-
- `editForm`
|
|
1559
|
-
- `detailPanel`
|
|
1564
|
+
消费方 Node/backend 侧构建使用 `buildBE()`。它复用发布包内的 `scripts/vite-build/buildEntries`,只是提供消费方默认配置。
|
|
1560
1565
|
|
|
1561
|
-
|
|
1566
|
+
最小用法:
|
|
1562
1567
|
|
|
1563
1568
|
```ts
|
|
1564
|
-
|
|
1565
|
-
import '@_tc/template-core/model'
|
|
1569
|
+
import { buildBE } from '@_tc/template-core/bundler'
|
|
1566
1570
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
interface ConfigMap {
|
|
1570
|
-
auditPanel: {
|
|
1571
|
-
title: string
|
|
1572
|
-
fetchKey: string
|
|
1573
|
-
approveApi?: string
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1571
|
+
await buildBE()
|
|
1572
|
+
```
|
|
1576
1573
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1574
|
+
消费方脚本示例:
|
|
1575
|
+
|
|
1576
|
+
```js
|
|
1577
|
+
// scripts/build-be.mjs
|
|
1578
|
+
import { buildBE } from '@_tc/template-core/bundler'
|
|
1579
|
+
|
|
1580
|
+
await buildBE({
|
|
1581
|
+
rootDir: process.cwd(),
|
|
1582
|
+
input: ['index.ts', 'index.js', 'app', 'config', 'model'],
|
|
1583
|
+
outDir: 'dist',
|
|
1584
|
+
format: 'cjs',
|
|
1585
|
+
alias: {
|
|
1586
|
+
'@app': './app',
|
|
1587
|
+
'@model': './model',
|
|
1588
|
+
},
|
|
1589
|
+
})
|
|
1590
|
+
```
|
|
1591
|
+
|
|
1592
|
+
```json
|
|
1593
|
+
{
|
|
1594
|
+
"scripts": {
|
|
1595
|
+
"build:be": "node scripts/build-be.mjs"
|
|
1582
1596
|
}
|
|
1583
1597
|
}
|
|
1584
1598
|
```
|
|
1585
1599
|
|
|
1586
|
-
|
|
1600
|
+
监听文件变化并重新构建:
|
|
1587
1601
|
|
|
1588
|
-
```
|
|
1589
|
-
//
|
|
1590
|
-
import
|
|
1591
|
-
import
|
|
1602
|
+
```js
|
|
1603
|
+
// scripts/watch-be.mjs
|
|
1604
|
+
import chokidar from 'chokidar'
|
|
1605
|
+
import { buildBE } from '@_tc/template-core/bundler'
|
|
1592
1606
|
|
|
1593
|
-
const
|
|
1594
|
-
auditPanel: lazy(() => import('../../components/AuditPanel')),
|
|
1595
|
-
} satisfies CallComComponentsMap
|
|
1607
|
+
const input = ['index.ts', 'index.js', 'app', 'config', 'model']
|
|
1596
1608
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1609
|
+
let building = false
|
|
1610
|
+
let pending = false
|
|
1599
1611
|
|
|
1600
|
-
|
|
1612
|
+
async function runBuild() {
|
|
1613
|
+
if (building) {
|
|
1614
|
+
pending = true
|
|
1615
|
+
return
|
|
1616
|
+
}
|
|
1601
1617
|
|
|
1602
|
-
|
|
1603
|
-
// frontend/src/components/AuditPanel.tsx
|
|
1604
|
-
import { eventsInfo, merge, schemaEventBus } from '@_tc/template-core/fe'
|
|
1618
|
+
building = true
|
|
1605
1619
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1620
|
+
try {
|
|
1621
|
+
console.log('[buildBE] building...')
|
|
1622
|
+
await buildBE({
|
|
1623
|
+
rootDir: process.cwd(),
|
|
1624
|
+
input,
|
|
1625
|
+
outDir: 'dist',
|
|
1626
|
+
format: 'cjs',
|
|
1627
|
+
alias: {
|
|
1628
|
+
'@app': './app',
|
|
1629
|
+
'@model': './model',
|
|
1630
|
+
},
|
|
1613
1631
|
})
|
|
1614
|
-
|
|
1632
|
+
console.log('[buildBE] done')
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
console.error('[buildBE] failed')
|
|
1635
|
+
console.error(error)
|
|
1636
|
+
} finally {
|
|
1637
|
+
building = false
|
|
1615
1638
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
)
|
|
1639
|
+
if (pending) {
|
|
1640
|
+
pending = false
|
|
1641
|
+
await runBuild()
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1622
1644
|
}
|
|
1623
|
-
```
|
|
1624
|
-
|
|
1625
|
-
model 中使用:
|
|
1626
1645
|
|
|
1627
|
-
|
|
1628
|
-
componentConfig: {
|
|
1629
|
-
auditPanel: {
|
|
1630
|
-
title: '审核',
|
|
1631
|
-
fetchKey: 'id',
|
|
1632
|
-
approveApi: '/api/audit/approve',
|
|
1633
|
-
},
|
|
1634
|
-
},
|
|
1635
|
-
tableConfig: {
|
|
1636
|
-
rowButtons: [
|
|
1637
|
-
{
|
|
1638
|
-
label: '审核',
|
|
1639
|
-
eventKey: 'callComponent',
|
|
1640
|
-
eventOption: {
|
|
1641
|
-
comName: 'auditPanel',
|
|
1642
|
-
},
|
|
1643
|
-
},
|
|
1644
|
-
],
|
|
1645
|
-
},
|
|
1646
|
-
```
|
|
1646
|
+
await runBuild()
|
|
1647
1647
|
|
|
1648
|
-
|
|
1648
|
+
chokidar
|
|
1649
|
+
.watch(input, {
|
|
1650
|
+
ignored: ['dist/**', 'node_modules/**'],
|
|
1651
|
+
ignoreInitial: true,
|
|
1652
|
+
})
|
|
1653
|
+
.on('all', async (_event, filePath) => {
|
|
1654
|
+
console.log(`[buildBE] changed: ${filePath}`)
|
|
1655
|
+
await runBuild()
|
|
1656
|
+
})
|
|
1657
|
+
```
|
|
1649
1658
|
|
|
1650
|
-
```
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
visible: true,
|
|
1659
|
+
```json
|
|
1660
|
+
{
|
|
1661
|
+
"scripts": {
|
|
1662
|
+
"build:be": "node scripts/build-be.mjs",
|
|
1663
|
+
"build:be:watch": "node scripts/watch-be.mjs"
|
|
1656
1664
|
},
|
|
1665
|
+
"devDependencies": {
|
|
1666
|
+
"chokidar": "^4.0.3"
|
|
1667
|
+
}
|
|
1657
1668
|
}
|
|
1658
1669
|
```
|
|
1659
1670
|
|
|
1660
|
-
|
|
1671
|
+
默认会在当前工作目录构建这些入口:
|
|
1661
1672
|
|
|
1662
|
-
|
|
1673
|
+
```ts
|
|
1674
|
+
['index.ts', 'index.js', 'app', 'config', 'model']
|
|
1675
|
+
```
|
|
1663
1676
|
|
|
1664
|
-
|
|
1677
|
+
不存在的默认入口会自动跳过;如果没有任何匹配源码,构建仍会失败。输出到 `dist`,格式为 `cjs`。默认 `outputStructure: "preserve"` 按源路径输出,`bundleDependencies: false` 会 external Node 内置模块和 npm 包,不会把依赖打进产物。`buildBE` 会按同一组 input 扫描并复制内置白名单资源扩展,主要覆盖 `app`、`config`、`model` 目录。
|
|
1665
1678
|
|
|
1666
|
-
|
|
1679
|
+
常见配置:
|
|
1667
1680
|
|
|
1668
1681
|
```ts
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
height: 48,
|
|
1677
|
-
},
|
|
1682
|
+
await buildBE({
|
|
1683
|
+
input: ['app', 'config', 'model'],
|
|
1684
|
+
outDir: 'dist',
|
|
1685
|
+
format: ['es', 'cjs'],
|
|
1686
|
+
alias: {
|
|
1687
|
+
'@app': './app',
|
|
1688
|
+
'@model': './model',
|
|
1678
1689
|
},
|
|
1679
|
-
}
|
|
1690
|
+
})
|
|
1680
1691
|
```
|
|
1681
1692
|
|
|
1682
|
-
|
|
1693
|
+
如果需要声明文件:
|
|
1683
1694
|
|
|
1684
1695
|
```ts
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
PriceCell: {
|
|
1692
|
-
value?: unknown
|
|
1693
|
-
record?: Record<string, unknown>
|
|
1694
|
-
rowIndex?: number
|
|
1695
|
-
fieldKey?: string
|
|
1696
|
-
currency?: string
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1696
|
+
await buildBE({
|
|
1697
|
+
dts: {
|
|
1698
|
+
outDir: 'types',
|
|
1699
|
+
tsconfig: 'tsconfig.json',
|
|
1700
|
+
},
|
|
1701
|
+
})
|
|
1701
1702
|
```
|
|
1702
1703
|
|
|
1703
|
-
|
|
1704
|
+
`buildEntries()` 底层默认生成 d.ts,但 `buildBE()` 默认关闭声明文件;需要时按上面这样显式传 `dts`。
|
|
1705
|
+
|
|
1706
|
+
如果传入自定义 `input`,缺失路径默认会报错;需要跳过时显式打开:
|
|
1704
1707
|
|
|
1705
1708
|
```ts
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
+
await buildBE({
|
|
1710
|
+
input: ['app', 'config', 'model'],
|
|
1711
|
+
allowMissingInput: true,
|
|
1712
|
+
})
|
|
1713
|
+
```
|
|
1709
1714
|
|
|
1710
|
-
|
|
1711
|
-
PriceCell: lazy(() => import('../../components/PriceCell')),
|
|
1712
|
-
} satisfies SchemaTableRenderComponentsMap
|
|
1715
|
+
## 如何扩展组件
|
|
1713
1716
|
|
|
1714
|
-
|
|
1715
|
-
```
|
|
1717
|
+
### 扩展 Dashboard 顶部用户区域
|
|
1716
1718
|
|
|
1717
|
-
|
|
1719
|
+
项目可以创建 `frontend/extended/dash/components.tsx`,通过组件映射填充内置 Dashboard 页头右侧区域。
|
|
1718
1720
|
|
|
1719
1721
|
```tsx
|
|
1720
|
-
// frontend/components
|
|
1721
|
-
|
|
1722
|
-
value?: unknown
|
|
1723
|
-
currency?: string
|
|
1724
|
-
}) {
|
|
1725
|
-
return <span>{props.currency ?? '¥'}{Number(props.value ?? 0).toFixed(2)}</span>
|
|
1726
|
-
}
|
|
1727
|
-
```
|
|
1728
|
-
|
|
1729
|
-
model 中使用:
|
|
1722
|
+
// frontend/extended/dash/components.tsx
|
|
1723
|
+
import type { DashComponentsMap } from '@_tc/template-core/fe'
|
|
1730
1724
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
label: '价格',
|
|
1735
|
-
tableOption: {
|
|
1736
|
-
renderComponent: 'PriceCell',
|
|
1737
|
-
renderComponentProps: {
|
|
1738
|
-
currency: '¥',
|
|
1739
|
-
},
|
|
1725
|
+
const components = {
|
|
1726
|
+
'HeaderView.userArea': ({ projectInfo }) => {
|
|
1727
|
+
return <div>{projectInfo?.name}</div>
|
|
1740
1728
|
},
|
|
1741
|
-
}
|
|
1742
|
-
```
|
|
1743
|
-
|
|
1744
|
-
## 如何配置类型扩展
|
|
1729
|
+
} satisfies DashComponentsMap
|
|
1745
1730
|
|
|
1746
|
-
|
|
1731
|
+
export default components
|
|
1732
|
+
```
|
|
1747
1733
|
|
|
1748
|
-
|
|
1734
|
+
当前支持的 Dashboard 组件扩展槽位:
|
|
1749
1735
|
|
|
1750
|
-
|
|
1751
|
-
{
|
|
1752
|
-
"include": ["app/**/*", "model/**/*", "frontend/**/*", "typing/**/*.d.ts"]
|
|
1753
|
-
}
|
|
1754
|
-
```
|
|
1736
|
+
- `HeaderView.userArea`:Dashboard 顶部右侧区域。
|
|
1755
1737
|
|
|
1756
|
-
###
|
|
1738
|
+
### 扩展 Dashboard 登录守卫
|
|
1757
1739
|
|
|
1758
|
-
|
|
1740
|
+
项目可以创建 `frontend/extended/dash/routeGuard.ts`,用于在 Dashboard 内置 `homePage` / 首菜单重定向前做登录判断。
|
|
1759
1741
|
|
|
1760
1742
|
```ts
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1743
|
+
// frontend/extended/dash/routeGuard.ts
|
|
1744
|
+
import type { DashRouteGuard } from '@_tc/template-core/fe'
|
|
1745
|
+
|
|
1746
|
+
const dashRouteGuard: DashRouteGuard = ({ isLoggedIn }) => {
|
|
1747
|
+
if (!isLoggedIn) return '/login'
|
|
1766
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
export default dashRouteGuard
|
|
1767
1751
|
```
|
|
1768
1752
|
|
|
1769
|
-
|
|
1770
|
-
- `PropsMap` 用来绑定该字段的 props 类型。
|
|
1771
|
-
- 类型声明只负责 TS 校验,运行时还必须在 `frontend/extended/SchemaForm/data.ts` 注册组件。
|
|
1753
|
+
返回值规则:
|
|
1772
1754
|
|
|
1773
|
-
|
|
1755
|
+
- 返回 `string`:框架使用 `window.location.href` 跳转,登录页不需要在 Dash 路由下。
|
|
1756
|
+
- 返回非 `string`:中断 Dashboard 内置重定向,使用方可自行跳转或处理提示。
|
|
1757
|
+
- `DashRouteGuardResult` 是 `string | undefined`;`undefined`(包括已登录分支的隐式返回)也会打断 Dashboard 内置重定向。
|
|
1758
|
+
- 没有自定义 `routeGuard` 文件时,框架使用兜底空对象,不影响默认 Dashboard 重定向。
|
|
1774
1759
|
|
|
1775
|
-
|
|
1760
|
+
### 扩展 SchemaForm 字段组件
|
|
1761
|
+
|
|
1762
|
+
1. 声明字段类型。
|
|
1763
|
+
2. 注册运行时组件。
|
|
1764
|
+
3. 在 `model` 的 `createFormOption`、`editFormOption`、`searchOption` 中使用。
|
|
1765
|
+
|
|
1766
|
+
类型声明:
|
|
1776
1767
|
|
|
1777
1768
|
```ts
|
|
1778
1769
|
// typing/template-core.d.ts
|
|
1779
|
-
import '@_tc/template-core/
|
|
1770
|
+
import '@_tc/template-core/fe/rc'
|
|
1771
|
+
import type { SelectProps } from '@_tc/template-core/fe/rc'
|
|
1780
1772
|
|
|
1781
|
-
declare module '@_tc/template-core/
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1773
|
+
declare module '@_tc/template-core/fe/rc' {
|
|
1774
|
+
export namespace SchemaFormNamespace {
|
|
1775
|
+
interface FieldTypes {
|
|
1776
|
+
businessSelect: true
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
interface PropsMap {
|
|
1780
|
+
businessSelect: SelectProps & {
|
|
1781
|
+
requestUrl?: string
|
|
1782
|
+
valueField?: string
|
|
1783
|
+
labelField?: string
|
|
1787
1784
|
}
|
|
1788
1785
|
}
|
|
1789
1786
|
}
|
|
1790
1787
|
}
|
|
1791
1788
|
```
|
|
1792
1789
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
```ts
|
|
1796
|
-
import type { ModelDataType } from '@_tc/template-core/model'
|
|
1797
|
-
|
|
1798
|
-
const model: ModelDataType<'portal'> = {
|
|
1799
|
-
mode: 'portal',
|
|
1800
|
-
name: '门户',
|
|
1801
|
-
desc: '门户配置',
|
|
1802
|
-
homePage: '/',
|
|
1803
|
-
portalConfig: {
|
|
1804
|
-
title: 'Portal',
|
|
1805
|
-
theme: 'light',
|
|
1806
|
-
},
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
export default model
|
|
1810
|
-
```
|
|
1811
|
-
|
|
1812
|
-
继续使用内置管理后台时:
|
|
1790
|
+
运行时注册:
|
|
1813
1791
|
|
|
1814
1792
|
```ts
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
mode: 'MB',
|
|
1819
|
-
name: '商品后台',
|
|
1820
|
-
desc: '商品管理',
|
|
1821
|
-
homePage: '/_sidebar_/product?projk=demo',
|
|
1822
|
-
menu: [],
|
|
1823
|
-
}
|
|
1824
|
-
```
|
|
1825
|
-
|
|
1826
|
-
### CallCom 类型扩展
|
|
1793
|
+
// frontend/extended/SchemaForm/data.ts
|
|
1794
|
+
import React from 'react'
|
|
1795
|
+
import type { SchemaFormComponentsMap } from '@_tc/template-core/fe/rc'
|
|
1827
1796
|
|
|
1828
|
-
|
|
1797
|
+
const componentsMap = {
|
|
1798
|
+
businessSelect: React.lazy(async () => ({
|
|
1799
|
+
default: (await import('../../components/BusinessSelect')).BusinessSelect,
|
|
1800
|
+
})),
|
|
1801
|
+
} satisfies SchemaFormComponentsMap
|
|
1829
1802
|
|
|
1830
|
-
|
|
1831
|
-
declare module '@_tc/template-core/model' {
|
|
1832
|
-
export namespace CallComNamespace {
|
|
1833
|
-
interface ConfigMap {}
|
|
1834
|
-
interface OptionMap {}
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1803
|
+
export default componentsMap
|
|
1837
1804
|
```
|
|
1838
1805
|
|
|
1839
|
-
|
|
1840
|
-
- `OptionMap` 对应字段上的 `xxxOption`。
|
|
1841
|
-
- 运行时组件注册在 `frontend/extended/SchemaPage/CallCom/data.ts`。
|
|
1842
|
-
|
|
1843
|
-
### SchemaTable 渲染组件类型扩展
|
|
1844
|
-
|
|
1845
|
-
扩展目标:
|
|
1806
|
+
model 中使用:
|
|
1846
1807
|
|
|
1847
1808
|
```ts
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1809
|
+
status: {
|
|
1810
|
+
type: 'string',
|
|
1811
|
+
label: '状态',
|
|
1812
|
+
searchOption: {
|
|
1813
|
+
comType: 'businessSelect',
|
|
1814
|
+
requestUrl: '/api/status/options',
|
|
1815
|
+
valueField: 'value',
|
|
1816
|
+
labelField: 'label',
|
|
1817
|
+
},
|
|
1852
1818
|
}
|
|
1853
1819
|
```
|
|
1854
1820
|
|
|
1855
|
-
|
|
1856
|
-
- 运行时组件注册在 `frontend/extended/SchemaPage/SchemaTable/data.ts`。
|
|
1857
|
-
- 组件运行时会自动收到 `value`、`record`、`rowIndex`、`fieldKey`;`renderComponentProps` 只配置自定义参数。
|
|
1821
|
+
### 扩展 SchemaPage CallCom
|
|
1858
1822
|
|
|
1859
|
-
|
|
1823
|
+
CallCom 是 SchemaPage 中的弹窗、抽屉、面板类组件。内置组件有:
|
|
1860
1824
|
|
|
1861
|
-
|
|
1825
|
+
- `createForm`
|
|
1826
|
+
- `editForm`
|
|
1827
|
+
- `detailPanel`
|
|
1828
|
+
|
|
1829
|
+
声明配置类型:
|
|
1862
1830
|
|
|
1863
1831
|
```ts
|
|
1864
1832
|
// typing/template-core.d.ts
|
|
1865
|
-
import '@_tc/template-core'
|
|
1866
|
-
|
|
1867
|
-
declare module '@_tc/template-core' {
|
|
1868
|
-
namespace FrameworkAugment {
|
|
1869
|
-
interface IServiceAugmented {
|
|
1870
|
-
product: {
|
|
1871
|
-
list(): Promise<unknown[]>
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1833
|
+
import '@_tc/template-core/model'
|
|
1874
1834
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1835
|
+
declare module '@_tc/template-core/model' {
|
|
1836
|
+
export namespace CallComNamespace {
|
|
1837
|
+
interface ConfigMap {
|
|
1838
|
+
auditPanel: {
|
|
1839
|
+
title: string
|
|
1840
|
+
fetchKey: string
|
|
1841
|
+
approveApi?: string
|
|
1878
1842
|
}
|
|
1879
1843
|
}
|
|
1880
1844
|
|
|
1881
|
-
interface
|
|
1882
|
-
|
|
1883
|
-
|
|
1845
|
+
interface OptionMap {
|
|
1846
|
+
auditPanel: {
|
|
1847
|
+
visible?: boolean
|
|
1884
1848
|
}
|
|
1885
1849
|
}
|
|
1886
|
-
|
|
1887
|
-
interface IMiddlewaresAugmented {
|
|
1888
|
-
auth: import('koa').Middleware
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
interface AppConfigAugmented {
|
|
1892
|
-
authSecret: string
|
|
1893
|
-
}
|
|
1894
1850
|
}
|
|
1895
1851
|
}
|
|
1896
1852
|
```
|
|
1897
1853
|
|
|
1898
|
-
|
|
1854
|
+
注册运行时组件:
|
|
1899
1855
|
|
|
1900
1856
|
```ts
|
|
1901
|
-
|
|
1857
|
+
// frontend/extended/SchemaPage/CallCom/data.ts
|
|
1858
|
+
import { lazy } from 'react'
|
|
1859
|
+
import type { CallComComponentsMap } from '@_tc/template-core/fe'
|
|
1902
1860
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1861
|
+
const componentsMap = {
|
|
1862
|
+
auditPanel: lazy(() => import('../../components/AuditPanel')),
|
|
1863
|
+
} satisfies CallComComponentsMap
|
|
1864
|
+
|
|
1865
|
+
export default componentsMap
|
|
1909
1866
|
```
|
|
1910
1867
|
|
|
1911
|
-
|
|
1868
|
+
组件示例:
|
|
1912
1869
|
|
|
1913
|
-
|
|
1870
|
+
```tsx
|
|
1871
|
+
// frontend/src/components/AuditPanel.tsx
|
|
1872
|
+
import { eventsInfo, merge, schemaEventBus } from '@_tc/template-core/fe'
|
|
1914
1873
|
|
|
1915
|
-
|
|
1874
|
+
export default function AuditPanel(props: {
|
|
1875
|
+
data: Record<string, unknown>
|
|
1876
|
+
comName: 'auditPanel'
|
|
1877
|
+
}) {
|
|
1878
|
+
const closeAndRefresh = () => {
|
|
1879
|
+
schemaEventBus.getState().emitCom({
|
|
1880
|
+
type: merge(eventsInfo.closeCom, eventsInfo.initTable),
|
|
1881
|
+
})
|
|
1882
|
+
}
|
|
1916
1883
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1884
|
+
return (
|
|
1885
|
+
<div>
|
|
1886
|
+
<div>审核对象:{String(props.data.id ?? '')}</div>
|
|
1887
|
+
<button onClick={closeAndRefresh}>完成</button>
|
|
1888
|
+
</div>
|
|
1889
|
+
)
|
|
1890
|
+
}
|
|
1919
1891
|
```
|
|
1920
1892
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
最小入口示例:
|
|
1893
|
+
model 中使用:
|
|
1924
1894
|
|
|
1925
|
-
```
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1895
|
+
```ts
|
|
1896
|
+
componentConfig: {
|
|
1897
|
+
auditPanel: {
|
|
1898
|
+
title: '审核',
|
|
1899
|
+
fetchKey: 'id',
|
|
1900
|
+
approveApi: '/api/audit/approve',
|
|
1901
|
+
},
|
|
1902
|
+
},
|
|
1903
|
+
tableConfig: {
|
|
1904
|
+
rowButtons: [
|
|
1905
|
+
{
|
|
1906
|
+
label: '审核',
|
|
1907
|
+
eventKey: 'callComponent',
|
|
1908
|
+
eventOption: {
|
|
1909
|
+
comName: 'auditPanel',
|
|
1910
|
+
},
|
|
1911
|
+
},
|
|
1912
|
+
],
|
|
1913
|
+
},
|
|
1914
|
+
```
|
|
1929
1915
|
|
|
1930
|
-
|
|
1931
|
-
<main>{children}</main>
|
|
1932
|
-
)
|
|
1916
|
+
字段级配置使用 `${组件名}Option`:
|
|
1933
1917
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1918
|
+
```ts
|
|
1919
|
+
remark: {
|
|
1920
|
+
type: 'string',
|
|
1921
|
+
label: '备注',
|
|
1922
|
+
auditPanelOption: {
|
|
1923
|
+
visible: true,
|
|
1924
|
+
},
|
|
1925
|
+
}
|
|
1937
1926
|
```
|
|
1938
1927
|
|
|
1939
|
-
|
|
1928
|
+
### 扩展 SchemaTable 单元格渲染组件
|
|
1940
1929
|
|
|
1941
|
-
|
|
1942
|
-
<!-- frontend/admin/admin.html -->
|
|
1943
|
-
<!-- tc-slot:head -->
|
|
1944
|
-
<meta name="description" content="Admin Console" />
|
|
1945
|
-
<link rel="preconnect" href="https://example.com" />
|
|
1946
|
-
<!-- /tc-slot:head -->
|
|
1930
|
+
SchemaPage 表格列支持通过 `tableOption.renderComponent` 使用内置或自定义注册的渲染组件。内置组件:
|
|
1947
1931
|
|
|
1948
|
-
|
|
1949
|
-
<div id="boot-mask"></div>
|
|
1950
|
-
<!-- /tc-slot:body-before-root -->
|
|
1932
|
+
- `PreviewImage`:把当前单元格值渲染成图片预览;值是数组时直接作为图片列表,值是单个字符串时自动转成单图数组。
|
|
1951
1933
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1934
|
+
直接使用内置组件:
|
|
1935
|
+
|
|
1936
|
+
```ts
|
|
1937
|
+
cover: {
|
|
1938
|
+
type: 'string',
|
|
1939
|
+
label: '封面',
|
|
1940
|
+
tableOption: {
|
|
1941
|
+
renderComponent: 'PreviewImage',
|
|
1942
|
+
renderComponentProps: {
|
|
1943
|
+
width: 48,
|
|
1944
|
+
height: 48,
|
|
1945
|
+
},
|
|
1946
|
+
},
|
|
1947
|
+
}
|
|
1957
1948
|
```
|
|
1958
1949
|
|
|
1959
|
-
|
|
1950
|
+
自定义渲染组件时,先声明 props 类型:
|
|
1960
1951
|
|
|
1961
|
-
```
|
|
1962
|
-
|
|
1963
|
-
|
|
1952
|
+
```ts
|
|
1953
|
+
// typing/template-core.d.ts
|
|
1954
|
+
import '@_tc/template-core/model'
|
|
1964
1955
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1956
|
+
declare module '@_tc/template-core/model' {
|
|
1957
|
+
export namespace SchemaTableNamespace {
|
|
1958
|
+
interface RenderComponentPropsMap {
|
|
1959
|
+
PriceCell: {
|
|
1960
|
+
value?: unknown
|
|
1961
|
+
record?: Record<string, unknown>
|
|
1962
|
+
rowIndex?: number
|
|
1963
|
+
fieldKey?: string
|
|
1964
|
+
currency?: string
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1968
1969
|
```
|
|
1969
1970
|
|
|
1970
|
-
|
|
1971
|
+
注册运行时组件:
|
|
1971
1972
|
|
|
1972
|
-
|
|
1973
|
+
```ts
|
|
1974
|
+
// frontend/extended/SchemaPage/SchemaTable/data.ts
|
|
1975
|
+
import { lazy } from 'react'
|
|
1976
|
+
import type { SchemaTableRenderComponentsMap } from '@_tc/template-core/fe'
|
|
1973
1977
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1978
|
+
const componentsMap = {
|
|
1979
|
+
PriceCell: lazy(() => import('../../components/PriceCell')),
|
|
1980
|
+
} satisfies SchemaTableRenderComponentsMap
|
|
1977
1981
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
path: 'custom-page',
|
|
1981
|
-
component: <div>Custom Page</div>,
|
|
1982
|
-
})
|
|
1982
|
+
export default componentsMap
|
|
1983
|
+
```
|
|
1983
1984
|
|
|
1984
|
-
|
|
1985
|
-
path: 'custom-sidebar-page',
|
|
1986
|
-
component: <div>Custom Sidebar Page</div>,
|
|
1987
|
-
})
|
|
1988
|
-
}
|
|
1985
|
+
组件会收到 `value`、`record`、`rowIndex`、`fieldKey`,以及 `renderComponentProps` 中的自定义参数:
|
|
1989
1986
|
|
|
1990
|
-
|
|
1987
|
+
```tsx
|
|
1988
|
+
// frontend/components/PriceCell.tsx
|
|
1989
|
+
export default function PriceCell(props: {
|
|
1990
|
+
value?: unknown
|
|
1991
|
+
currency?: string
|
|
1992
|
+
}) {
|
|
1993
|
+
return <span>{props.currency ?? '¥'}{Number(props.value ?? 0).toFixed(2)}</span>
|
|
1994
|
+
}
|
|
1991
1995
|
```
|
|
1992
1996
|
|
|
1993
|
-
|
|
1997
|
+
model 中使用:
|
|
1994
1998
|
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1999
|
+
```ts
|
|
2000
|
+
price: {
|
|
2001
|
+
type: 'number',
|
|
2002
|
+
label: '价格',
|
|
2003
|
+
tableOption: {
|
|
2004
|
+
renderComponent: 'PriceCell',
|
|
2005
|
+
renderComponentProps: {
|
|
2006
|
+
currency: '¥',
|
|
2007
|
+
},
|
|
2008
|
+
},
|
|
2009
|
+
}
|
|
2010
|
+
```
|
|
1998
2011
|
|
|
1999
|
-
|
|
2000
|
-
- 示例:`path: 'custom-sidebar-page'` → 访问路径为 `/dash/_sidebar_/custom-sidebar-page`
|
|
2001
|
-
- 适用于 `menuLayout: 'left'` 的项目
|
|
2012
|
+
## 如何配置类型扩展
|
|
2002
2013
|
|
|
2003
|
-
|
|
2014
|
+
### tsconfig
|
|
2004
2015
|
|
|
2005
|
-
|
|
2016
|
+
如果项目分了后端、前端、model 多个 TS 上下文,确保它们都 include 同一份声明文件:
|
|
2017
|
+
|
|
2018
|
+
```json
|
|
2006
2019
|
{
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2020
|
+
"include": ["app/**/*", "model/**/*", "frontend/**/*", "typing/**/*.d.ts"]
|
|
2021
|
+
}
|
|
2022
|
+
```
|
|
2023
|
+
|
|
2024
|
+
### SchemaForm 类型扩展
|
|
2025
|
+
|
|
2026
|
+
扩展目标:
|
|
2027
|
+
|
|
2028
|
+
```ts
|
|
2029
|
+
declare module '@_tc/template-core/fe/rc' {
|
|
2030
|
+
export namespace SchemaFormNamespace {
|
|
2031
|
+
interface FieldTypes {}
|
|
2032
|
+
interface PropsMap {}
|
|
2033
|
+
}
|
|
2014
2034
|
}
|
|
2015
2035
|
```
|
|
2016
2036
|
|
|
2017
|
-
|
|
2037
|
+
- `FieldTypes` 用来增加字段类型名。
|
|
2038
|
+
- `PropsMap` 用来绑定该字段的 props 类型。
|
|
2039
|
+
- 类型声明只负责 TS 校验,运行时还必须在 `frontend/extended/SchemaForm/data.ts` 注册组件。
|
|
2018
2040
|
|
|
2019
|
-
|
|
2041
|
+
### Model mode 类型扩展
|
|
2042
|
+
|
|
2043
|
+
`mode` 和对应的数据结构一起扩展。内置 `MB` 对应管理后台菜单结构 `MBMenuType`。
|
|
2020
2044
|
|
|
2021
2045
|
```ts
|
|
2022
|
-
|
|
2046
|
+
// typing/template-core.d.ts
|
|
2047
|
+
import '@_tc/template-core/model'
|
|
2023
2048
|
|
|
2024
|
-
|
|
2049
|
+
declare module '@_tc/template-core/model' {
|
|
2050
|
+
interface ModelModeMap {
|
|
2051
|
+
portal: {
|
|
2052
|
+
portalConfig: {
|
|
2053
|
+
title: string
|
|
2054
|
+
theme?: 'light' | 'dark'
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2025
2059
|
```
|
|
2026
2060
|
|
|
2027
|
-
|
|
2061
|
+
使用新 `mode`:
|
|
2028
2062
|
|
|
2029
2063
|
```ts
|
|
2030
|
-
|
|
2031
|
-
```
|
|
2064
|
+
import type { ModelDataType } from '@_tc/template-core/model'
|
|
2032
2065
|
|
|
2033
|
-
|
|
2066
|
+
const model: ModelDataType<'portal'> = {
|
|
2067
|
+
mode: 'portal',
|
|
2068
|
+
name: '门户',
|
|
2069
|
+
desc: '门户配置',
|
|
2070
|
+
homePage: '/',
|
|
2071
|
+
portalConfig: {
|
|
2072
|
+
title: 'Portal',
|
|
2073
|
+
theme: 'light',
|
|
2074
|
+
},
|
|
2075
|
+
}
|
|
2034
2076
|
|
|
2035
|
-
|
|
2036
|
-
await buildFE('prod', {
|
|
2037
|
-
output: 'run',
|
|
2038
|
-
})
|
|
2077
|
+
export default model
|
|
2039
2078
|
```
|
|
2040
2079
|
|
|
2041
|
-
|
|
2080
|
+
继续使用内置管理后台时:
|
|
2042
2081
|
|
|
2043
2082
|
```ts
|
|
2044
|
-
|
|
2045
|
-
minifyHtml: true,
|
|
2046
|
-
})
|
|
2047
|
-
```
|
|
2048
|
-
|
|
2049
|
-
构建完成后会在输出目录写入 `FEBuildKey`,服务端渲染 HTML 时会用它判断本地 HTML ETag 缓存是否需要清空。
|
|
2083
|
+
import type { ModelDataType } from '@_tc/template-core/model'
|
|
2050
2084
|
|
|
2051
|
-
|
|
2085
|
+
const model: ModelDataType<'MB'> = {
|
|
2086
|
+
mode: 'MB',
|
|
2087
|
+
name: '商品后台',
|
|
2088
|
+
desc: '商品管理',
|
|
2089
|
+
homePage: '/_sidebar_/product?projk=demo',
|
|
2090
|
+
menu: [],
|
|
2091
|
+
}
|
|
2092
|
+
```
|
|
2052
2093
|
|
|
2053
|
-
|
|
2094
|
+
### CallCom 类型扩展
|
|
2054
2095
|
|
|
2055
|
-
|
|
2096
|
+
扩展目标:
|
|
2056
2097
|
|
|
2057
2098
|
```ts
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2099
|
+
declare module '@_tc/template-core/model' {
|
|
2100
|
+
export namespace CallComNamespace {
|
|
2101
|
+
interface ConfigMap {}
|
|
2102
|
+
interface OptionMap {}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2061
2105
|
```
|
|
2062
2106
|
|
|
2063
|
-
|
|
2107
|
+
- `ConfigMap` 对应 `schemaConfig.componentConfig.xxx`。
|
|
2108
|
+
- `OptionMap` 对应字段上的 `xxxOption`。
|
|
2109
|
+
- 运行时组件注册在 `frontend/extended/SchemaPage/CallCom/data.ts`。
|
|
2064
2110
|
|
|
2065
|
-
|
|
2066
|
-
// scripts/build-be.mjs
|
|
2067
|
-
import { buildBE } from '@_tc/template-core/bundler'
|
|
2111
|
+
### SchemaTable 渲染组件类型扩展
|
|
2068
2112
|
|
|
2069
|
-
|
|
2070
|
-
rootDir: process.cwd(),
|
|
2071
|
-
input: ['index.ts', 'index.js', 'app', 'config', 'model'],
|
|
2072
|
-
outDir: 'dist',
|
|
2073
|
-
format: 'cjs',
|
|
2074
|
-
alias: {
|
|
2075
|
-
'@app': './app',
|
|
2076
|
-
'@model': './model',
|
|
2077
|
-
},
|
|
2078
|
-
})
|
|
2079
|
-
```
|
|
2113
|
+
扩展目标:
|
|
2080
2114
|
|
|
2081
|
-
```
|
|
2082
|
-
{
|
|
2083
|
-
|
|
2084
|
-
|
|
2115
|
+
```ts
|
|
2116
|
+
declare module '@_tc/template-core/model' {
|
|
2117
|
+
export namespace SchemaTableNamespace {
|
|
2118
|
+
interface RenderComponentPropsMap {}
|
|
2085
2119
|
}
|
|
2086
2120
|
}
|
|
2087
2121
|
```
|
|
2088
2122
|
|
|
2089
|
-
|
|
2123
|
+
- `RenderComponentPropsMap` 的 key 对应 `tableOption.renderComponent`。
|
|
2124
|
+
- 运行时组件注册在 `frontend/extended/SchemaPage/SchemaTable/data.ts`。
|
|
2125
|
+
- 组件运行时会自动收到 `value`、`record`、`rowIndex`、`fieldKey`;`renderComponentProps` 只配置自定义参数。
|
|
2090
2126
|
|
|
2091
|
-
|
|
2092
|
-
// scripts/watch-be.mjs
|
|
2093
|
-
import chokidar from 'chokidar'
|
|
2094
|
-
import { buildBE } from '@_tc/template-core/bundler'
|
|
2127
|
+
### KoaApp 类型扩展
|
|
2095
2128
|
|
|
2096
|
-
|
|
2129
|
+
使用方推荐增强根包 `@_tc/template-core`:
|
|
2097
2130
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2131
|
+
```ts
|
|
2132
|
+
// typing/template-core.d.ts
|
|
2133
|
+
import '@_tc/template-core'
|
|
2100
2134
|
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2135
|
+
declare module '@_tc/template-core' {
|
|
2136
|
+
namespace FrameworkAugment {
|
|
2137
|
+
interface IServiceAugmented {
|
|
2138
|
+
product: {
|
|
2139
|
+
list(): Promise<unknown[]>
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2106
2142
|
|
|
2107
|
-
|
|
2143
|
+
interface IControllerAugmented {
|
|
2144
|
+
product: {
|
|
2145
|
+
list(ctx: import('@_tc/template-core').Ctx): Promise<void>
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2108
2148
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
outDir: 'dist',
|
|
2115
|
-
format: 'cjs',
|
|
2116
|
-
alias: {
|
|
2117
|
-
'@app': './app',
|
|
2118
|
-
'@model': './model',
|
|
2119
|
-
},
|
|
2120
|
-
})
|
|
2121
|
-
console.log('[buildBE] done')
|
|
2122
|
-
} catch (error) {
|
|
2123
|
-
console.error('[buildBE] failed')
|
|
2124
|
-
console.error(error)
|
|
2125
|
-
} finally {
|
|
2126
|
-
building = false
|
|
2149
|
+
interface IExtendsAugmented {
|
|
2150
|
+
redis: {
|
|
2151
|
+
get(key: string): Promise<string | null>
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2127
2154
|
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2155
|
+
interface IMiddlewaresAugmented {
|
|
2156
|
+
auth: import('koa').Middleware
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
interface AppConfigAugmented {
|
|
2160
|
+
authSecret: string
|
|
2131
2161
|
}
|
|
2132
2162
|
}
|
|
2133
2163
|
}
|
|
2164
|
+
```
|
|
2134
2165
|
|
|
2135
|
-
|
|
2166
|
+
使用:
|
|
2136
2167
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
ignored: ['dist/**', 'node_modules/**'],
|
|
2140
|
-
ignoreInitial: true,
|
|
2141
|
-
})
|
|
2142
|
-
.on('all', async (_event, filePath) => {
|
|
2143
|
-
console.log(`[buildBE] changed: ${filePath}`)
|
|
2144
|
-
await runBuild()
|
|
2145
|
-
})
|
|
2146
|
-
```
|
|
2168
|
+
```ts
|
|
2169
|
+
import type { KoaApp } from '@_tc/template-core'
|
|
2147
2170
|
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
},
|
|
2154
|
-
"devDependencies": {
|
|
2155
|
-
"chokidar": "^4.0.3"
|
|
2156
|
-
}
|
|
2171
|
+
export default (app: KoaApp) => {
|
|
2172
|
+
app.config.authSecret
|
|
2173
|
+
app.service.product.list()
|
|
2174
|
+
app.extends.$fetch.get('https://example.com/api')
|
|
2175
|
+
app.extends.redis.get('token')
|
|
2157
2176
|
}
|
|
2158
2177
|
```
|
|
2159
2178
|
|
|
2160
|
-
|
|
2179
|
+
如果在 TemplateCore 仓库源码内部扩展,声明目标通常是内部别名 `@tc/core`;npm 使用方不要依赖 `@tc/*` 内部路径。
|
|
2161
2180
|
|
|
2162
|
-
|
|
2163
|
-
['index.ts', 'index.js', 'app', 'config', 'model']
|
|
2164
|
-
```
|
|
2181
|
+
## 可选:给项目 AI 助手的文档与 Skill
|
|
2165
2182
|
|
|
2166
|
-
|
|
2183
|
+
如果你在项目中让 AI 助手协助开发,可以让它优先读取 npm 包内的 `AGENT_README.md`。安装后的路径通常是 `node_modules/@_tc/template-core/AGENT_README.md`。这份文档面向“协助使用 TemplateCore 的 AI 助手”,覆盖公开入口、最小启动、目录约定、model 配置、前端使用和构建方式。
|
|
2167
2184
|
|
|
2168
|
-
|
|
2185
|
+
包内附带两份 Agent Skill,本质是可复用的 Agent 指令包,采用渐进式披露(SKILL.md + reference/)结构。
|
|
2169
2186
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
format: ['es', 'cjs'],
|
|
2175
|
-
alias: {
|
|
2176
|
-
'@app': './app',
|
|
2177
|
-
'@model': './model',
|
|
2178
|
-
},
|
|
2179
|
-
})
|
|
2180
|
-
```
|
|
2187
|
+
| Skill | 用途 |
|
|
2188
|
+
| --- | --- |
|
|
2189
|
+
| `tc-generator` | 按 TemplateCore 约定生成项目、model 配置、Schema CRUD、controller 和 router。 |
|
|
2190
|
+
| `tc-component-usage-skills` | 指导 Agent 选用和正确使用内置 React 组件库(Button、Form、DataTable、Modal 等)。 |
|
|
2181
2191
|
|
|
2182
|
-
|
|
2192
|
+
如果你的 Agent 支持本地 skills 目录,可以从已安装的 npm 包复制安装。以 Codex 为例:
|
|
2183
2193
|
|
|
2184
|
-
```
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
tsconfig: 'tsconfig.json',
|
|
2189
|
-
},
|
|
2190
|
-
})
|
|
2194
|
+
```bash
|
|
2195
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
|
2196
|
+
cp -R node_modules/@_tc/template-core/.skills/tc-generator "${CODEX_HOME:-$HOME/.codex}/skills/tc-generator"
|
|
2197
|
+
cp -R node_modules/@_tc/template-core/.skills/tc-component-usage-skills "${CODEX_HOME:-$HOME/.codex}/skills/tc-component-usage-skills"
|
|
2191
2198
|
```
|
|
2192
2199
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
如果传入自定义 `input`,缺失路径默认会报错;需要跳过时显式打开:
|
|
2200
|
+
安装后重启对应 Agent,让新 skill 生效。使用时可以直接说”用 tc-generator 生成一个商品管理模块”或”用 tc-component-usage-skills 帮我写一个带搜索的分页表格”。
|
|
2196
2201
|
|
|
2197
|
-
|
|
2198
|
-
await buildBE({
|
|
2199
|
-
input: ['app', 'config', 'model'],
|
|
2200
|
-
allowMissingInput: true,
|
|
2201
|
-
})
|
|
2202
|
-
```
|
|
2202
|
+
其他 Agent 如果不支持自动安装 skill,也可以直接读取 `.skills/tc-generator/SKILL.md` 或 `.skills/tc-component-usage-skills/SKILL.md`,再按需读取 `reference/` 下的参考文件。
|
|
2203
2203
|
|
|
2204
2204
|
## 注意事项
|
|
2205
2205
|
|