@_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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  TemplateCore 是一个基于 TypeScript、Koa、React 的后台框架包。它把后端约定式加载、前端 Dashboard、Schema CRUD 页面、model 类型和类型扩展放到同一个 npm 包里,项目只需要按目录补充自己的 `app/`、`model/`、`frontend/`。
4
4
 
5
- 组件预览地址:https://t-c-s-p.allomg.qzz.io/ui-components
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 >= 18
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
- ## 样式与 Tailwind V4
243
+ ## model 数据如何配置
249
244
 
250
- 使用内置前端或 UI 组件时,需要引入包内样式。默认 `@_tc/template-core/fe``initApp` 会经过源码入口 `frontend/src/main.tsx` 引入 `frontend/src/main.css`;如果项目侧自定义入口、只使用 `fe/rc` 组件,或希望样式入口更明确,建议显式引入:
245
+ `model` 是后台菜单、项目和 Schema 页面的数据源。框架会读取:
251
246
 
252
- ```ts
253
- // frontend/xxx/xxx.entry.tsx
254
- import '@_tc/template-core/fe/tailwind_ui.css'
247
+ ```text
248
+ model/{modelKey}/mode.ts
249
+ model/{modelKey}/project/{projectKey}.ts
255
250
  ```
256
251
 
257
- 如果项目有自己的全局样式入口,也可以在 CSS 中引入:
252
+ `mode.ts` 定义模型模板:
258
253
 
259
- ```css
260
- /* frontend/main.css */
261
- @import "@_tc/template-core/fe/tailwind_ui.css";
254
+ ```ts
255
+ import type { ModelDataType } from '@_tc/template-core/model'
262
256
 
263
- /* 扫描项目源码 */
264
- @source "./**/*.{js,ts,jsx,tsx}";
265
- @source "../model/**/*.{js,ts,jsx,tsx}";
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
- Tailwind V4 默认会忽略 `node_modules` `.gitignore` 中的文件。如果没有使用上面的 `tailwind_ui.css`,或者项目侧构建链路绕过了 TemplateCore 的样式入口,需要手动把包加入 Tailwind 扫描:
359
+ `name`、`desc`、`icon`、`homePage` 都是可选项。Dashboard 页头标题优先使用 `desc`,没有时回退到 `name`;`icon` 可以是图片地址、图片路径、data image 或普通文本。未配置 `homePage` 时会自动跳到第一个可用菜单;配置时必须命中菜单生成的真实路由,左侧布局通常是 `/_sidebar_/{menuKey}`,顶部布局通常是 `/{menuKey}`。
269
360
 
270
- ```css
271
- /* frontend/main.css */
272
- @import "tailwindcss";
361
+ `project/demo.ts` 定义项目覆盖:
273
362
 
274
- @source "./**/*.{js,ts,jsx,tsx}";
275
- @source "../model/**/*.{js,ts,jsx,tsx}";
276
- @source "../node_modules/@_tc/template-core";
363
+ ```ts
364
+ export default {
365
+ name: 'Demo 企业',
366
+ desc: 'Demo 企业商品后台',
367
+ homePage: '/_sidebar_/product?projk=demo',
368
+ }
277
369
  ```
278
370
 
279
- VS Code 建议安装官方 Tailwind CSS IntelliSense 插件,并在工作区 `.vscode/settings.json` 指向项目 CSS 入口:
371
+ 合并规则:
280
372
 
281
- ```json
282
- {
283
- "tailwindCSS.experimental.configFile": "frontend/main.css",
284
- "files.associations": {
285
- "*.css": "tailwindcss"
286
- },
287
- "css.lint.unknownAtRules": "ignore"
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
- ```json
294
- {
295
- "tailwindCSS.experimental.configFile": {
296
- "frontend/main.css": "frontend/**/*.{ts,tsx,js,jsx}",
297
- "frontend/admin/admin.css": "frontend/admin/**/*.{ts,tsx,js,jsx}"
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
- - Koa 服务启动:`serverStart(options)`。
305
- - 约定式加载:自动加载 `config`、`extends`、`router-schema`、`service`、`controller`、`middlewares`、根级 `middleware`、`router`。
306
- - 双层 app:框架内置 `app/` 和项目 `app/` 一起加载。
307
- - 默认项目接口:
308
- - `GET /api/project/model_list`
309
- - `GET /api/project/list`
310
- - `GET /api/project/:key`
311
- - React Dashboard:内置 `/dash` 页面入口。
312
- - 组件预览页面:内置 `/ui-components` 页面,用于查看和测试框架内置的 UI 组件。
313
- - 菜单渲染:支持 `schema`、`custom`、`iframe`、`sidebar`。
314
- - Schema CRUD:根据 `model` 中的 `schemaConfig` 生成搜索、表格、新增、编辑、详情。
315
- - 内置 UI 组件:提供丰富的 React 组件库,详见下方"内置 UI 组件"章节。
316
- - API 中间件:错误处理、静态资源、body parser、项目上下文、接口签名、参数校验。
317
- - 前端构建:扫描框架和项目的 `frontend/**/*.entry.{ts,tsx,js,jsx}`,输出服务端可渲染的 `.entry.tpl`,并写入 `FEBuildKey` 供 HTML ETag 缓存失效使用。
318
- - 类型扩展:支持扩展 SchemaForm、SchemaTable 单元格渲染组件、CallCom、KoaApp。
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
- 项目侧需要 TemplateCore 前端能力时,优先从 `@_tc/template-core/fe` 导入,不要直接依赖 `frontend/src/common/...`、`frontend/src/defaultPages/...`、`frontend/src/stores/...` 这类内部路径。
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
- import type {
361
- BaseResponse,
362
- CallComComponentsMap,
363
- CallComRenderer,
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
- | 应用启动 | `initApp` |
386
- | 内置前端组件 | `AsyncSelect`、`LanguageSwitch`、`ThemeSwitch` |
387
- | 组件加载辅助 | `renderImportComponent` |
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
- `modeStore` 管项目模型和当前项目数据,`schemaStore` 管当前 Schema 页面的 schema/cache,`schemaEventBus` 用来在 Search、Table、CallCom 之间通信。
505
+ Dashboard 会根据 `projk` 请求 `/api/project/:key`,再按返回的 `menu` 渲染页面。
401
506
 
402
- `renderImportComponent` 封装了 `React.lazy` + `Suspense`,默认会用页面骨架屏作为 fallback。`getAuthToken()`、`setAuthToken()` 和 `clearAuthToken()` 用于读写或清理本地短 token,`localKeyMap` 暴露对应的本地存储 key 常量。
507
+ Schema 页面的接口约定:
403
508
 
404
- 只使用请求层时,也可以从 `@_tc/template-core/fe/common/request` 导入 `api`、请求方法和请求类型;这是发布包显式子路径,不要下钻到 `frontend/src/common/request`。
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
- TemplateCore 前端通过 `@_tc/template-core/fe` 暴露 i18n 能力。框架内置中文、英文资源,但不会在普通自定义页面 import 前端入口时自动注册,避免非 Dashboard 页面把框架文案整包带入运行时资源。
519
+ ### 服务端兜底路由守卫
409
520
 
410
- 默认 Dashboard 入口会自动调用 `registerFrontendI18nResources()`,所以 Dashboard、Schema CRUD、内置页头、语言切换等框架页面可以直接使用框架内置文案。自定义入口如果也要复用这些框架文案,需要在渲染前手动注册:
521
+ 项目可以创建 `app/router-guard.ts` `app/router-guard.js`。它只在所有正常 `app/router/*` 路由都没有命中时执行。
411
522
 
412
523
  ```ts
413
- import { initApp, registerFrontendI18nResources } from '@_tc/template-core/fe'
524
+ import type { Ctx, KoaApp } from '@_tc/template-core'
414
525
 
415
- initApp(App, {
416
- beforeRender: async () => {
417
- await registerFrontendI18nResources()
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
- ```ts
427
- import { addLanguageResources } from '@_tc/template-core/fe'
542
+ ## 后端约定
428
543
 
429
- addLanguageResources('zh-CN', {
430
- app: {
431
- product: {
432
- menu: '商品管理',
433
- name: '商品名称',
434
- create: '新增商品',
435
- createTitle: '新增商品',
436
- },
437
- },
438
- })
544
+ 项目目录会按下面规则加载:
439
545
 
440
- addLanguageResources('en-US', {
441
- app: {
442
- product: {
443
- menu: 'Products',
444
- name: 'Product name',
445
- create: 'Create product',
446
- createTitle: 'Create product',
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
- ```ts
455
- import { addLanguageResources, setLanguage } from '@_tc/template-core/fe'
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
- addLanguageResources('ja-JP', {
458
- app: {
459
- product: {
460
- menu: '商品管理',
461
- },
462
- },
463
- })
565
+ API controller 可以通过 `ctx.reqData` 读取统一请求参数:
464
566
 
465
- setLanguage('ja-JP')
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
- `addLanguageResources()` 用于追加或新增某个语种的文案,默认会深合并同语种资源;传 `{ merge: false }` 可以替换该语种资源。`setResources(resources, { merge: true })` 可批量合并多语种资源。`LanguageSwitch` 默认会把已注册语种加入选项;需要自定义展示文案时仍可传入 `options`。
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
- UI 组件库内置文案也在同一份资源里,统一放在 `components` 命名空间下。项目侧可以覆盖已有文案,也可以给新增语种补齐组件文案:
583
+ ### 数据库扩展
473
584
 
474
- ```ts
475
- import { addLanguageResources } from '@_tc/template-core/fe'
585
+ 默认 `app.extends.db` 使用 SQLite 保存数据,数据库文件位于项目根目录 `.template-core/template-core.sqlite`。可以在配置中调整路径:
476
586
 
477
- addLanguageResources('zh-CN', {
478
- components: {
479
- date: {
480
- placeholder: '选择日期',
481
- placeholderRange: '选择日期区间',
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
- Date 组件除了文案,还依赖 `date-fns` locale 做月份格式化和周起止计算。新增非内置语种时同步注册:
596
+ Controller 中使用:
494
597
 
495
598
  ```ts
496
- import { registerDateFnsLocale } from '@_tc/template-core/fe/rc/components/Date'
497
- import { ja } from 'date-fns/locale'
599
+ import { baseFn, type ControllerFN, type Ctx } from '@_tc/template-core'
498
600
 
499
- registerDateFnsLocale('ja-JP', ja)
500
- ```
601
+ const getSettingController = ((app) => {
602
+ const BaseController = baseFn.baseControllerFn(app)
501
603
 
502
- React 组件 render 中使用 `useText()` 读取文案。它会订阅语言和资源变化,调用 `setLanguage()` 后组件会自动重渲染。
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
- `useText()` / `getText()` 遇到 `$i18n::` 前缀会先去掉前缀再查语言资源;没有前缀时会直接把传入字符串作为 key 查询,查不到才原样返回:
610
+ this.success(ctx, data ?? {})
611
+ }
505
612
 
506
- ```tsx
507
- import { useText } from '@_tc/template-core/fe'
613
+ save = async (ctx: Ctx) => {
614
+ app.extends.db.setDBData('site-setting', ctx.request.body, {
615
+ namespace: 'setting',
616
+ })
508
617
 
509
- function ProductTitle() {
510
- const text = useText()
618
+ this.success(ctx, true)
619
+ }
620
+ }
621
+ }) satisfies ControllerFN
511
622
 
512
- return (
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
- `getText()` 是普通函数,适合请求错误、日志、校验工具或事件回调等非 React 场景;它不会主动触发 React 组件重渲染。
626
+ 常用方法:
522
627
 
523
- 资源注册时不写 `$i18n::` 前缀,配置中使用时才写:
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
- ```ts
526
- const title = '$i18n::app.product.createTitle'
527
- ```
638
+ SQL 注入边界:
528
639
 
529
- **在 model / Schema 配置中使用**:
640
+ - `getDBData`、`setDBData`、`deleteDBData`、`listDBData` 这类通用方法内部使用参数绑定,不需要手写 SQL。
641
+ - `queryDB`、`runDB`、`getDBFirst` 是原始 SQL 入口,用户输入必须放到 `params`,不要拼接到 SQL 字符串里。
642
+ - `execDB` 没有参数绑定能力,只建议用于固定 SQL,例如建表或迁移。
530
643
 
531
- ```ts
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
- 已支持 `$i18n::...` 的常用位置:
646
+ `app.extends.crypto` 默认使用 `app.config.signKey` 作为签名密钥,适合生成登录 token、接口签名和安全比较。
572
647
 
573
- - 菜单名:`menu.name`
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
- import { i18nStore } from '@_tc/template-core/fe'
585
-
586
- i18nStore.getState().setLanguage('en-US')
587
- i18nStore.getState().setLanguage('zh-CN')
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
- 默认会写入 localStorage,key `tc_language`。刷新页面后会继续使用上次语言。
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
- ```tsx
595
- import { LanguageSwitch } from '@_tc/template-core/fe'
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
- export function Toolbar() {
598
- return <LanguageSwitch />
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
- `LanguageSwitch` 默认提供 `zh-CN` 和 `en-US`,也支持传入 `options` 覆盖语言列表。组件当前是按钮 + 下拉菜单形态,`option.label` 支持 ReactNode,`buttonClassName` 可自定义触发按钮样式。组件内部会调用 `i18nStore.getState().setLanguage()`,因此会沿用 `tc_language` 的持久化行为。
687
+ 项目可以用自己的 `app/extends/db.ts` 覆盖默认实现。建议保留同一组方法名,这样 controller/service 调用方不用调整。
603
688
 
604
- **AJV 校验文案**:
689
+ ```ts
690
+ // app/extends/db.ts
691
+ import type { DB, DBFactory } from '@_tc/template-core'
605
692
 
606
- Schema 表单校验会把 AJV keyword 映射到内置语言 key,覆盖 `required`、`type`、`format`、`minimum`、`maximum`、`minLength`、`maxLength`、`pattern`、`enum`、`oneOf`、`anyOf` 等常见规则。
693
+ interface DataOptions {
694
+ namespace?: string
695
+ }
607
696
 
608
- 非必填字段为空时会跳过 AJV 校验,空值包括 `undefined`、`null`、空字符串、空数组和空对象。
697
+ const store = new Map<string, unknown>()
609
698
 
610
- 如果需要扩展更多 AJV 文案,在源码项目中同步增加:
699
+ const getDB = ((_app) => {
700
+ const toKey = (key: string, namespace = 'framework') => `${namespace}:${key}`
611
701
 
612
- 1. `frontend/src/defaultPages/SchemaPage/utils/validator.ts` `formatError()` 映射。
613
- 2. `frontend/src/language/zh-CN.ts` 和 `frontend/src/language/en-US.ts` 文案。
614
- 3. `frontend/src/language/index.ts` `frontendLangKeys`。
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
- 项目页面可以使用 `ThemeSwitch` 切换浅色和深色模式:
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
- ```tsx
621
- import { ThemeSwitch } from '@_tc/template-core/fe'
759
+ return db
760
+ }) satisfies DBFactory
622
761
 
623
- export function Toolbar() {
624
- return <ThemeSwitch />
625
- }
762
+ export default getDB
626
763
  ```
627
764
 
628
- `ThemeSwitch` 会切换 `document.documentElement` 上的 `dark` class,并同步 `color-scheme`。默认写入 localStorage,key 是 `tc_theme`。刷新页面后会优先读取本地主题;没有本地主题时,读取当前根节点是否已有 `dark` class。默认 Dashboard 会在渲染前调用 `initThemeMode()`,让已保存主题尽早生效。
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
- ```ts
643
- import { applyThemeMode, getCurrentThemeMode, initThemeMode, themeSwitchStorageKey } from '@_tc/template-core/fe'
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
- initThemeMode()
646
- applyThemeMode('dark', true)
785
+ export default getProductController
647
786
  ```
648
787
 
649
- RAF 风格计时器由 `@tc/common/rafTimer` 提供,浏览器优先使用 `requestAnimationFrame`,缺少 RAF 时自动降级到 `setTimeout`。
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
- 's_t': '当前时间戳',
673
- 's_sign': 'md5(签名密钥_时间戳)',
674
- 'projk': '当前项目 key(从 localStorage 读取)',
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
- `Authorization` 默认对应服务端配置 `config.auth.ATKey`。前端本地 token 存在 `localStorage.auth_token`,项目 key 存在 `localStorage.p_J_k`;刷新 token header 名预留为 `config.auth.RTKey`,默认是 `RT`。
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 { api, get, post, put, del } from '@_tc/template-core/fe'
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
- // DELETE 请求
709
- await del('/product', { id: 1 })
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
- } from '@_tc/template-core/fe/common/request'
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
- } from '@_tc/template-core/fe/common/request'
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
- 冻结器只在 `Freeze` 状态拦截请求;`Hibernation` `Thawing` 状态都会直接放行。解冻时会按最多 4 个并发回放已冻结的请求;相同序列化参数的请求只会实际发送一次,多个调用方共享同一份响应或错误。
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
- ```ts
735
- import { apiFreezerStore } from '@_tc/template-core/fe'
894
+ `modeStore` 管项目模型和当前项目数据,`schemaStore` 管当前 Schema 页面的 schema/cache,`schemaEventBus` 用来在 Search、Table、CallCom 之间通信。
736
895
 
737
- const { freezing, thawing, setRequestWhitelist } = apiFreezerStore.getState()
896
+ `renderImportComponent` 封装了 `React.lazy` + `Suspense`,默认会用页面骨架屏作为 fallback。`getAuthToken()`、`setAuthToken()` `clearAuthToken()` 用于读写或清理本地短 token,`localKeyMap` 暴露对应的本地存储 key 常量。
738
897
 
739
- setRequestWhitelist([
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
- freezing()
900
+ ### 前端多语言
746
901
 
747
- try {
748
- await refreshToken()
749
- } finally {
750
- thawing()
751
- }
752
- ```
902
+ TemplateCore 前端通过 `@_tc/template-core/fe` 暴露 i18n 能力。框架内置中文、英文资源,但不会在普通自定义页面 import 前端入口时自动注册,避免非 Dashboard 页面把框架文案整包带入运行时资源。
753
903
 
754
- 白名单支持三种 matcher:
904
+ 默认 Dashboard 入口会自动调用 `registerFrontendI18nResources()`,所以 Dashboard、Schema CRUD、内置页头、语言切换等框架页面可以直接使用框架内置文案。自定义入口如果也要复用这些框架文案,需要在渲染前手动注册:
755
905
 
756
- - `string`:精确匹配 API 路径
757
- - `RegExp`:正则匹配 API 路径
758
- - `(api, params) => boolean`:自定义判断,`params` 是请求函数收到的完整参数
906
+ ```ts
907
+ import { initApp, registerFrontendI18nResources } from '@_tc/template-core/fe'
759
908
 
760
- **注意事项**:
761
- - `get` `del` 方法的参数会自动转为 query string
762
- - `post`、`put`、`patch` 方法的参数会作为 request body
763
- - 所有请求方法都会自动处理错误,无需手动 try-catch(除非需要自定义错误处理)
764
- - 底层使用原生 `fetch` API,但提供了类似 Axios 的拦截器和配置接口
909
+ initApp(App, {
910
+ beforeRender: async () => {
911
+ await registerFrontendI18nResources()
912
+ },
913
+ })
914
+ ```
765
915
 
766
- ## 内置 UI 组件
916
+ 项目可以继续追加自己的资源或新增语种。
767
917
 
768
- 框架通过 `@_tc/template-core/fe/rc` 提供了丰富的 React 组件库,可在项目中直接使用。
918
+ **追加自定义语言资源**:
769
919
 
770
- ### 基础组件
920
+ ```ts
921
+ import { addLanguageResources } from '@_tc/template-core/fe'
771
922
 
772
- | 组件 | 用途 | 导入路径 |
773
- | --- | --- | --- |
774
- | `Button` | 按钮组件,支持多种样式和尺寸 | `@_tc/template-core/fe/rc` |
775
- | `Input` | 输入框组件,支持清空、右侧附加内容和密码显隐 | `@_tc/template-core/fe/rc` |
776
- | `InputNumber` | 数字输入框组件 | `@_tc/template-core/fe/rc` |
777
- | `Textarea` | 多行文本输入框组件 | `@_tc/template-core/fe/rc` |
778
- | `Select` | 下拉选择组件,支持单选和多选 | `@_tc/template-core/fe/rc` |
779
- | `Search` | 搜索输入组件 | `@_tc/template-core/fe/rc` |
780
- | `TreeSelect` | 树形选择组件 | `@_tc/template-core/fe/rc` |
781
- | `Dropdown` | 下拉菜单组件 | `@_tc/template-core/fe/rc` |
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
- | `DataTable` | 数据表格组件,支持分页、排序、筛选 | `@_tc/template-core/fe/rc` |
804
- | `Table` | 表格组件(含表头、列配置等子组件) | `@_tc/template-core/fe/rc` |
805
- | `TableSearch` | 表格搜索组件,配合 DataTable 使用 | `@_tc/template-core/fe/rc` |
806
- | `Pagination` | 分页组件 | `@_tc/template-core/fe/rc` |
807
- | `Breadcrumb` | 面包屑导航组件 | `@_tc/template-core/fe/rc` |
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
- ```tsx
848
- import { Button, Input, Select, Modal, DataTable } from '@_tc/template-core/fe/rc'
964
+ **覆盖组件内置文案**:
849
965
 
850
- function MyComponent() {
851
- return (
852
- <div>
853
- <Button variant="primary" onClick={() => console.log('clicked')}>
854
- 点击我
855
- </Button>
966
+ UI 组件库内置文案也在同一份资源里,统一放在 `components` 命名空间下。项目侧可以覆盖已有文案,也可以给新增语种补齐组件文案:
856
967
 
857
- <Input placeholder="请输入内容" />
858
- <Input type="password" placeholder="请输入密码" />
968
+ ```ts
969
+ import { addLanguageResources } from '@_tc/template-core/fe'
859
970
 
860
- <Select
861
- options={[
862
- { label: '选项1', value: '1' },
863
- { label: '选项2', value: '2' },
864
- ]}
865
- />
866
- </div>
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
- ```text
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
- - `app.extends.$fetch`:Node 侧基于 `fetch` axios 风格请求实例,支持 `get/post/put/patch/delete` 和 `create(config)`。
896
- - `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
897
- - `app.extends.db`:默认 SQLite 框架数据库,提供 `getDBData/setDBData` 等通用数据方法。
998
+ `useText()` / `getText()` 遇到 `$i18n::` 前缀会先去掉前缀再查语言资源;没有前缀时会直接把传入字符串作为 key 查询,查不到才原样返回:
898
999
 
899
- API controller 可以通过 `ctx.reqData` 读取统一请求参数:
1000
+ ```tsx
1001
+ import { useText } from '@_tc/template-core/fe'
900
1002
 
901
- ```ts
902
- const { query, body, headers, data } = ctx.reqData ?? {
903
- query: {},
904
- body: undefined,
905
- headers: {},
906
- data: {},
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
- - `query` 来自 `ctx.query`。
911
- - `body` 来自 `ctx.request.body`。
912
- - `headers` 来自 `ctx.headers`。
913
- - `data` 是便捷合并层,当前按 `query -> body` 顺序合并;字段冲突时 body 覆盖 query。
1015
+ `getText()` 是普通函数,适合请求错误、日志、校验工具或事件回调等非 React 场景;它不会主动触发 React 组件重渲染。
914
1016
 
915
- `ctx.reqData` 只在 API 请求中由内置中间件注入,类型上是可选字段。router params 不在 `reqData` 里,仍然从 `ctx.params` 获取。
1017
+ 资源注册时不写 `$i18n::` 前缀,配置中使用时才写:
916
1018
 
917
- ### 数据库扩展
1019
+ ```ts
1020
+ const title = '$i18n::app.product.createTitle'
1021
+ ```
918
1022
 
919
- 默认 `app.extends.db` 使用 SQLite 保存数据,数据库文件位于项目根目录 `.template-core/template-core.sqlite`。可以在配置中调整路径:
1023
+ **在 model / Schema 配置中使用**:
920
1024
 
921
1025
  ```ts
922
- // config/config.default.ts
923
- export default {
924
- db: {
925
- path: 'data/app.sqlite',
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
- Controller 中使用:
1065
+ 已支持 `$i18n::...` 的常用位置:
931
1066
 
932
- ```ts
933
- import { baseFn, type ControllerFN, type Ctx } from '@_tc/template-core'
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
- const getSettingController = ((app) => {
936
- const BaseController = baseFn.baseControllerFn(app)
1075
+ **切换语言**:
937
1076
 
938
- return class SettingController extends BaseController {
939
- detail = async (ctx: Ctx) => {
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
- this.success(ctx, data ?? {})
945
- }
1080
+ i18nStore.getState().setLanguage('en-US')
1081
+ i18nStore.getState().setLanguage('zh-CN')
1082
+ ```
946
1083
 
947
- save = async (ctx: Ctx) => {
948
- app.extends.db.setDBData('site-setting', ctx.request.body, {
949
- namespace: 'setting',
950
- })
1084
+ 默认会写入 localStorage,key `tc_language`。刷新页面后会继续使用上次语言。
951
1085
 
952
- this.success(ctx, true)
953
- }
954
- }
955
- }) satisfies ControllerFN
1086
+ 项目页面可以直接使用语言选择组件:
956
1087
 
957
- export default getSettingController
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
- - `getDBData(key, options)`:读取 JSON 数据。
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
- SQL 注入边界:
1100
+ Schema 表单校验会把 AJV keyword 映射到内置语言 key,覆盖 `required`、`type`、`format`、`minimum`、`maximum`、`minLength`、`maxLength`、`pattern`、`enum`、`oneOf`、`anyOf` 等常见规则。
973
1101
 
974
- - `getDBData`、`setDBData`、`deleteDBData`、`listDBData` 这类通用方法内部使用参数绑定,不需要手写 SQL。
975
- - `queryDB`、`runDB`、`getDBFirst` 是原始 SQL 入口,用户输入必须放到 `params`,不要拼接到 SQL 字符串里。
976
- - `execDB` 没有参数绑定能力,只建议用于固定 SQL,例如建表或迁移。
1102
+ 非必填字段为空时会跳过 AJV 校验,空值包括 `undefined`、`null`、空字符串、空数组和空对象。
977
1103
 
978
- ### 加密扩展
1104
+ 如果需要扩展更多 AJV 文案,在源码项目中同步增加:
979
1105
 
980
- `app.extends.crypto` 默认使用 `app.config.signKey` 作为签名密钥,适合生成登录 token、接口签名和安全比较。
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
- - `base64urlEncode(value)`:把字符串或二进制数据编码为 base64url。
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
- const payload = app.extends.crypto.verifySignedPayload<{
1000
- account: string
1001
- exp: number
1002
- iat: number
1003
- }>(token)
1114
+ ```tsx
1115
+ import { ThemeSwitch } from '@_tc/template-core/fe'
1004
1116
 
1005
- const same = app.extends.crypto.safeEqual('abc', 'abc')
1117
+ export function Toolbar() {
1118
+ return <ThemeSwitch />
1119
+ }
1006
1120
  ```
1007
1121
 
1008
- ```ts
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
- // 不推荐:拼接用户输入,有 SQL 注入风险
1016
- const unsafeUsers = app.extends.db.queryDB(
1017
- `SELECT * FROM user WHERE name = '${ctx.query.name}'`
1018
- )
1019
- ```
1124
+ 常用参数:
1020
1125
 
1021
- 项目可以用自己的 `app/extends/db.ts` 覆盖默认实现。建议保留同一组方法名,这样 controller/service 调用方不用调整。
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
- // app/extends/db.ts
1025
- import type { DB, DBFactory } from '@_tc/template-core'
1137
+ import { applyThemeMode, getCurrentThemeMode, initThemeMode, themeSwitchStorageKey } from '@_tc/template-core/fe'
1026
1138
 
1027
- interface DataOptions {
1028
- namespace?: string
1029
- }
1139
+ initThemeMode()
1140
+ applyThemeMode('dark', true)
1141
+ ```
1030
1142
 
1031
- const store = new Map<string, unknown>()
1143
+ RAF 风格计时器由 `@tc/common/rafTimer` 提供,浏览器优先使用 `requestAnimationFrame`,缺少 RAF 时自动降级到 `setTimeout`。
1032
1144
 
1033
- const getDB = ((_app) => {
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
- const db = {
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
- return {
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
- return Array.from(store.entries())
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
- return db
1094
- }) satisfies DBFactory
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
- export default getDB
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
- Controller 示例:
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 { baseFn, type ControllerFN, type Ctx } from '@_tc/template-core'
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
- const getProductController = ((app) => {
1105
- const BaseController = baseFn.baseControllerFn(app)
1193
+ // GET 请求
1194
+ const data = await get('/product/list', { page: 1, pageSize: 10 })
1106
1195
 
1107
- return class ProductController extends BaseController {
1108
- list = async (ctx: Ctx) => {
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
- export default getProductController
1199
+ // PUT 请求
1200
+ await put('/product', { id: 1, name: '新名称' })
1201
+
1202
+ // DELETE 请求
1203
+ await del('/product', { id: 1 })
1120
1204
  ```
1121
1205
 
1122
- Router 示例:
1206
+ 如果希望把请求能力单独拆出来导入:
1123
1207
 
1124
1208
  ```ts
1125
- import type { KoaApp, Router } from '@_tc/template-core'
1126
-
1127
- export default (app: KoaApp, router: Router) => {
1128
- router.get('/api/product/list', app.controller.product.list)
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
- - 每个 `app/router/*.ts` 文件都会获得独立的 `koa-router` 实例。
1135
- - 可以通过 `router.level = number` 控制挂载顺序,数值越小越早挂载。
1136
- - 默认 `level` `0`,框架兜底 router `99`。
1137
- - 通配、兜底、重定向类路由建议设置较大的 `level`,避免抢先匹配项目 API。
1224
+ 请求冻结器用于在某段流程中暂停普通 API 请求,典型场景是 RT 刷新 token:刷新期间冻结其它请求,刷新接口本身走白名单放行,刷新完成后再解冻并回放队列。
1225
+
1226
+ 冻结器只在 `Freeze` 状态拦截请求;`Hibernation` `Thawing` 状态都会直接放行。解冻时会按最多 4 个并发回放已冻结的请求;相同序列化参数的请求只会实际发送一次,多个调用方共享同一份响应或错误。
1138
1227
 
1139
1228
  ```ts
1140
- import type { KoaApp, Router } from '@_tc/template-core'
1229
+ import { apiFreezerStore } from '@_tc/template-core/fe'
1141
1230
 
1142
- export default (app: KoaApp, router: Router) => {
1143
- router.level = 10
1144
- router.get('/api/fallback/:id', app.controller.fallback.detail)
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
- ## model 数据如何配置
1248
+ 白名单支持三种 matcher:
1149
1249
 
1150
- `model` 是后台菜单、项目和 Schema 页面的数据源。框架会读取:
1250
+ - `string`:精确匹配 API 路径
1251
+ - `RegExp`:正则匹配 API 路径
1252
+ - `(api, params) => boolean`:自定义判断,`params` 是请求函数收到的完整参数
1151
1253
 
1152
- ```text
1153
- model/{modelKey}/mode.ts
1154
- model/{modelKey}/project/{projectKey}.ts
1155
- ```
1254
+ **注意事项**:
1255
+ - `get` 和 `del` 方法的参数会自动转为 query string
1256
+ - `post`、`put`、`patch` 方法的参数会作为 request body
1257
+ - 所有请求方法都会自动处理错误,无需手动 try-catch(除非需要自定义错误处理)
1258
+ - 底层使用原生 `fetch` API,但提供了类似 Axios 的拦截器和配置接口
1156
1259
 
1157
- `mode.ts` 定义模型模板:
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
- import type { ModelDataType } from '@_tc/template-core/model'
1265
+ // frontend/xxx/xxx.entry.tsx
1266
+ import '@_tc/template-core/fe/tailwind_ui.css'
1267
+ ```
1161
1268
 
1162
- const model: ModelDataType = {
1163
- mode: 'MB',
1164
- name: '商品后台',
1165
- desc: '商品管理',
1166
- icon: '',
1167
- homePage: '/_sidebar_/product?projk=demo',
1168
- menuLayout: 'left',
1169
- menu: [
1170
- {
1171
- key: 'product',
1172
- name: '商品列表',
1173
- menuType: 'module',
1174
- moduleType: 'schema',
1175
- schemaConfig: {
1176
- api: '/api/product',
1177
- schema: {
1178
- type: 'object',
1179
- properties: {
1180
- product_id: {
1181
- type: 'string',
1182
- label: '商品 ID',
1183
- tableOption: { width: 160 },
1184
- detailPanelOption: {},
1185
- editFormOption: {
1186
- comType: 'input',
1187
- disabled: true,
1188
- },
1189
- },
1190
- product_name: {
1191
- type: 'string',
1192
- label: '商品名称',
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
- export default model
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
- `name`、`desc`、`icon`、`homePage` 都是可选项。Dashboard 页头标题优先使用 `desc`,没有时回退到 `name`;`icon` 可以是图片地址、图片路径、data image 或普通文本。未配置 `homePage` 时会自动跳到第一个可用菜单;配置时必须命中菜单生成的真实路由,左侧布局通常是 `/_sidebar_/{menuKey}`,顶部布局通常是 `/{menuKey}`。
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
- `project/demo.ts` 定义项目覆盖:
1360
+ ### 反馈组件
1267
1361
 
1268
- ```ts
1269
- export default {
1270
- name: 'Demo 企业',
1271
- desc: 'Demo 企业商品后台',
1272
- homePage: '/_sidebar_/product?projk=demo',
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
- - `project` 会继承同目录上层 `mode`。
1279
- - 对象递归合并,`project` 覆盖 `mode`。
1280
- - 数组按 `key` 合并:同 key 覆盖,不同 key 新增。
1281
- - `model/index.ts` `model/index.js` 会被 loader 跳过。
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
- ### model 数组合并示例
1384
+ ### 按钮组件
1284
1385
 
1285
- 假设 `mode.ts` 定义了两个菜单项:
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
- ```ts
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
- `project/demo.ts` 可以:
1395
+ ```tsx
1396
+ import { Button, Input, Select, Modal, DataTable } from '@_tc/template-core/fe/rc'
1317
1397
 
1318
- ```ts
1319
- // model/product/project/demo.ts
1320
- export default {
1321
- name: 'Demo 企业商品后台',
1322
- menu: [
1323
- // 1. 覆盖:修改 product 菜单的名称和配置
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
- ```ts
1349
- {
1350
- name: 'Demo 企业商品后台', // project 覆盖
1351
- menu: [
1352
- {
1353
- key: 'product',
1354
- name: 'Demo 商品', // project 覆盖
1355
- menuType: 'module', // mode 继承
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
- - 数组合并依赖 `key` 字段,确保每个菜单项都有唯一的 `key`
1386
- - `key` 的项会递归合并,不是简单替换
1387
- - `project` 中未定义的项会完整继承 `mode` 的配置
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
- export default (app: KoaApp) => {
1397
- const modelList = modelLoader(app)
1398
- return modelList
1399
- }
1400
- ```
1424
+ ## 扩展自定义前端页面
1401
1425
 
1402
- 前端默认不直接读文件,而是通过内置接口获取:
1426
+ 新增应用入口:
1403
1427
 
1404
1428
  ```text
1405
- GET /api/project/model_list
1406
- GET /api/project/list?projk=demo
1407
- GET /api/project/demo
1429
+ frontend/admin/admin.entry.tsx
1408
1430
  ```
1409
1431
 
1410
- Dashboard 会根据 `projk` 请求 `/api/project/:key`,再按返回的 `menu` 渲染页面。
1432
+ 入口文件名必须包含 `.entry.`,构建器才会扫描。
1411
1433
 
1412
- Schema 页面的接口约定:
1434
+ 最小入口示例:
1413
1435
 
1414
- ```text
1415
- GET {api}/list -> 表格列表
1416
- GET {api} -> 编辑/详情拉取,参数由 fetchKey 决定
1417
- POST {api} -> 新增
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
- `$schema::字段名` 表示从当前表格行读取字段值,常用于删除按钮参数。
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
- 项目可以创建 `app/router-guard.ts` `app/router-guard.js`。它只在所有正常 `app/router/*` 路由都没有命中时执行。
1450
+ 如果需要给页面加额外 `meta`、预连接资源或启动脚本,可以在入口同目录放 `.html` 插槽文件。这个文件不是完整 HTML,只能包含 `tc-slot` 块:
1427
1451
 
1428
- ```ts
1429
- import type { Ctx, KoaApp } from '@_tc/template-core'
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
- export default (_app: KoaApp) => {
1432
- return (ctx: Ctx) => {
1433
- if (ctx.path.startsWith('/api/')) {
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
- return '/dash'
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
- 返回字符串会 `ctx.redirect()`;返回 `{ status, body }` 会直接设置响应;无返回则使用框架默认兜底:API 路径返回 HTTP 200 + `{ code: 404, message }`,非 API 路径返回 HTTP 404 + `notFound`。
1470
+ 命名规则:
1446
1471
 
1447
- ## 如何扩展组件
1472
+ ```text
1473
+ frontend/admin/admin.entry.tsx
1474
+ frontend/admin/admin.html yes
1448
1475
 
1449
- ### 扩展 Dashboard 顶部用户区域
1476
+ frontend/report/index.entry.tsx
1477
+ frontend/report/index.html yes
1478
+ frontend/report/report.html yes
1479
+ ```
1450
1480
 
1451
- 项目可以创建 `frontend/extended/dash/components.tsx`,通过组件映射填充内置 Dashboard 页头右侧区域。
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/components.tsx
1455
- import type { DashComponentsMap } from '@_tc/template-core/fe'
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
- const components = {
1458
- 'HeaderView.userArea': ({ projectInfo }) => {
1459
- return <div>{projectInfo?.name}</div>
1460
- },
1461
- } satisfies DashComponentsMap
1495
+ sidebarRoutes.push({
1496
+ path: 'custom-sidebar-page',
1497
+ component: <div>Custom Sidebar Page</div>,
1498
+ })
1499
+ }
1462
1500
 
1463
- export default components
1501
+ export default extendDashRoutes
1464
1502
  ```
1465
1503
 
1466
- 当前支持的 Dashboard 组件扩展槽位:
1504
+ **路径规则说明**:
1467
1505
 
1468
- - `HeaderView.userArea`:Dashboard 顶部右侧区域。
1506
+ - **topRoutes**:追加到 Dashboard 顶层路由,路径相对于 `/dash`
1507
+ - 示例:`path: 'custom-page'` → 访问路径为 `/dash/custom-page`
1508
+ - 不要写成 `/custom-page`,使用相对路径即可
1469
1509
 
1470
- ### 扩展 Dashboard 登录守卫
1510
+ - **sidebarRoutes**:追加到侧边栏容器的子路由,路径相对于 `/dash/_sidebar_`
1511
+ - 示例:`path: 'custom-sidebar-page'` → 访问路径为 `/dash/_sidebar_/custom-sidebar-page`
1512
+ - 适用于 `menuLayout: 'left'` 的项目
1471
1513
 
1472
- 项目可以创建 `frontend/extended/dash/routeGuard.ts`,用于在 Dashboard 内置 `homePage` / 首菜单重定向前做登录判断。
1514
+ model 中用 `custom` 菜单指向自定义路径:
1473
1515
 
1474
1516
  ```ts
1475
- // frontend/extended/dash/routeGuard.ts
1476
- import type { DashRouteGuard } from '@_tc/template-core/fe'
1477
-
1478
- const dashRouteGuard: DashRouteGuard = ({ isLoggedIn }) => {
1479
- if (!isLoggedIn) return '/login'
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
- // typing/template-core.d.ts
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
- interface PropsMap {
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
- // frontend/extended/SchemaForm/data.ts
1526
- import React from 'react'
1527
- import type { SchemaFormComponentsMap } from '@_tc/template-core/fe/rc'
1541
+ await buildFE('prod')
1542
+ ```
1528
1543
 
1529
- const componentsMap = {
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
- export default componentsMap
1546
+ ```ts
1547
+ await buildFE('prod', {
1548
+ output: 'run',
1549
+ })
1536
1550
  ```
1537
1551
 
1538
- model 中使用:
1552
+ 也可以压缩最终模板输出:
1539
1553
 
1540
1554
  ```ts
1541
- status: {
1542
- type: 'string',
1543
- label: '状态',
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
- ### 扩展 SchemaPage CallCom
1560
+ 构建完成后会在输出目录写入 `FEBuildKey`,服务端渲染 HTML 时会用它判断本地 HTML ETag 缓存是否需要清空。
1554
1561
 
1555
- CallCom SchemaPage 中的弹窗、抽屉、面板类组件。内置组件有:
1562
+ ## Node/backend 构建 buildBE
1556
1563
 
1557
- - `createForm`
1558
- - `editForm`
1559
- - `detailPanel`
1564
+ 消费方 Node/backend 侧构建使用 `buildBE()`。它复用发布包内的 `scripts/vite-build/buildEntries`,只是提供消费方默认配置。
1560
1565
 
1561
- 声明配置类型:
1566
+ 最小用法:
1562
1567
 
1563
1568
  ```ts
1564
- // typing/template-core.d.ts
1565
- import '@_tc/template-core/model'
1569
+ import { buildBE } from '@_tc/template-core/bundler'
1566
1570
 
1567
- declare module '@_tc/template-core/model' {
1568
- export namespace CallComNamespace {
1569
- interface ConfigMap {
1570
- auditPanel: {
1571
- title: string
1572
- fetchKey: string
1573
- approveApi?: string
1574
- }
1575
- }
1571
+ await buildBE()
1572
+ ```
1576
1573
 
1577
- interface OptionMap {
1578
- auditPanel: {
1579
- visible?: boolean
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
- ```ts
1589
- // frontend/extended/SchemaPage/CallCom/data.ts
1590
- import { lazy } from 'react'
1591
- import type { CallComComponentsMap } from '@_tc/template-core/fe'
1602
+ ```js
1603
+ // scripts/watch-be.mjs
1604
+ import chokidar from 'chokidar'
1605
+ import { buildBE } from '@_tc/template-core/bundler'
1592
1606
 
1593
- const componentsMap = {
1594
- auditPanel: lazy(() => import('../../components/AuditPanel')),
1595
- } satisfies CallComComponentsMap
1607
+ const input = ['index.ts', 'index.js', 'app', 'config', 'model']
1596
1608
 
1597
- export default componentsMap
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
- ```tsx
1603
- // frontend/src/components/AuditPanel.tsx
1604
- import { eventsInfo, merge, schemaEventBus } from '@_tc/template-core/fe'
1618
+ building = true
1605
1619
 
1606
- export default function AuditPanel(props: {
1607
- data: Record<string, unknown>
1608
- comName: 'auditPanel'
1609
- }) {
1610
- const closeAndRefresh = () => {
1611
- schemaEventBus.getState().emitCom({
1612
- type: merge(eventsInfo.closeCom, eventsInfo.initTable),
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
- return (
1617
- <div>
1618
- <div>审核对象:{String(props.data.id ?? '')}</div>
1619
- <button onClick={closeAndRefresh}>完成</button>
1620
- </div>
1621
- )
1639
+ if (pending) {
1640
+ pending = false
1641
+ await runBuild()
1642
+ }
1643
+ }
1622
1644
  }
1623
- ```
1624
-
1625
- model 中使用:
1626
1645
 
1627
- ```ts
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
- 字段级配置使用 `${组件名}Option`:
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
- ```ts
1651
- remark: {
1652
- type: 'string',
1653
- label: '备注',
1654
- auditPanelOption: {
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
- ### 扩展 SchemaTable 单元格渲染组件
1671
+ 默认会在当前工作目录构建这些入口:
1661
1672
 
1662
- SchemaPage 表格列支持通过 `tableOption.renderComponent` 使用内置或自定义注册的渲染组件。内置组件:
1673
+ ```ts
1674
+ ['index.ts', 'index.js', 'app', 'config', 'model']
1675
+ ```
1663
1676
 
1664
- - `PreviewImage`:把当前单元格值渲染成图片预览;值是数组时直接作为图片列表,值是单个字符串时自动转成单图数组。
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
- cover: {
1670
- type: 'string',
1671
- label: '封面',
1672
- tableOption: {
1673
- renderComponent: 'PreviewImage',
1674
- renderComponentProps: {
1675
- width: 48,
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
- 自定义渲染组件时,先声明 props 类型:
1693
+ 如果需要声明文件:
1683
1694
 
1684
1695
  ```ts
1685
- // typing/template-core.d.ts
1686
- import '@_tc/template-core/model'
1687
-
1688
- declare module '@_tc/template-core/model' {
1689
- export namespace SchemaTableNamespace {
1690
- interface RenderComponentPropsMap {
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
- // frontend/extended/SchemaPage/SchemaTable/data.ts
1707
- import { lazy } from 'react'
1708
- import type { SchemaTableRenderComponentsMap } from '@_tc/template-core/fe'
1709
+ await buildBE({
1710
+ input: ['app', 'config', 'model'],
1711
+ allowMissingInput: true,
1712
+ })
1713
+ ```
1709
1714
 
1710
- const componentsMap = {
1711
- PriceCell: lazy(() => import('../../components/PriceCell')),
1712
- } satisfies SchemaTableRenderComponentsMap
1715
+ ## 如何扩展组件
1713
1716
 
1714
- export default componentsMap
1715
- ```
1717
+ ### 扩展 Dashboard 顶部用户区域
1716
1718
 
1717
- 组件会收到 `value`、`record`、`rowIndex`、`fieldKey`,以及 `renderComponentProps` 中的自定义参数:
1719
+ 项目可以创建 `frontend/extended/dash/components.tsx`,通过组件映射填充内置 Dashboard 页头右侧区域。
1718
1720
 
1719
1721
  ```tsx
1720
- // frontend/components/PriceCell.tsx
1721
- export default function PriceCell(props: {
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
- ```ts
1732
- price: {
1733
- type: 'number',
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
- ### tsconfig
1731
+ export default components
1732
+ ```
1747
1733
 
1748
- 如果项目分了后端、前端、model 多个 TS 上下文,确保它们都 include 同一份声明文件:
1734
+ 当前支持的 Dashboard 组件扩展槽位:
1749
1735
 
1750
- ```json
1751
- {
1752
- "include": ["app/**/*", "model/**/*", "frontend/**/*", "typing/**/*.d.ts"]
1753
- }
1754
- ```
1736
+ - `HeaderView.userArea`:Dashboard 顶部右侧区域。
1755
1737
 
1756
- ### SchemaForm 类型扩展
1738
+ ### 扩展 Dashboard 登录守卫
1757
1739
 
1758
- 扩展目标:
1740
+ 项目可以创建 `frontend/extended/dash/routeGuard.ts`,用于在 Dashboard 内置 `homePage` / 首菜单重定向前做登录判断。
1759
1741
 
1760
1742
  ```ts
1761
- declare module '@_tc/template-core/fe/rc' {
1762
- export namespace SchemaFormNamespace {
1763
- interface FieldTypes {}
1764
- interface PropsMap {}
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
- - `FieldTypes` 用来增加字段类型名。
1770
- - `PropsMap` 用来绑定该字段的 props 类型。
1771
- - 类型声明只负责 TS 校验,运行时还必须在 `frontend/extended/SchemaForm/data.ts` 注册组件。
1753
+ 返回值规则:
1772
1754
 
1773
- ### Model mode 类型扩展
1755
+ - 返回 `string`:框架使用 `window.location.href` 跳转,登录页不需要在 Dash 路由下。
1756
+ - 返回非 `string`:中断 Dashboard 内置重定向,使用方可自行跳转或处理提示。
1757
+ - `DashRouteGuardResult` 是 `string | undefined`;`undefined`(包括已登录分支的隐式返回)也会打断 Dashboard 内置重定向。
1758
+ - 没有自定义 `routeGuard` 文件时,框架使用兜底空对象,不影响默认 Dashboard 重定向。
1774
1759
 
1775
- `mode` 和对应的数据结构一起扩展。内置 `MB` 对应管理后台菜单结构 `MBMenuType`。
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/model'
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/model' {
1782
- interface ModelModeMap {
1783
- portal: {
1784
- portalConfig: {
1785
- title: string
1786
- theme?: 'light' | 'dark'
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
- 使用新 `mode`:
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
- import type { ModelDataType } from '@_tc/template-core/model'
1816
-
1817
- const model: ModelDataType<'MB'> = {
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
- ```ts
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
- - `ConfigMap` 对应 `schemaConfig.componentConfig.xxx`。
1840
- - `OptionMap` 对应字段上的 `xxxOption`。
1841
- - 运行时组件注册在 `frontend/extended/SchemaPage/CallCom/data.ts`。
1842
-
1843
- ### SchemaTable 渲染组件类型扩展
1844
-
1845
- 扩展目标:
1806
+ model 中使用:
1846
1807
 
1847
1808
  ```ts
1848
- declare module '@_tc/template-core/model' {
1849
- export namespace SchemaTableNamespace {
1850
- interface RenderComponentPropsMap {}
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
- - `RenderComponentPropsMap` key 对应 `tableOption.renderComponent`。
1856
- - 运行时组件注册在 `frontend/extended/SchemaPage/SchemaTable/data.ts`。
1857
- - 组件运行时会自动收到 `value`、`record`、`rowIndex`、`fieldKey`;`renderComponentProps` 只配置自定义参数。
1821
+ ### 扩展 SchemaPage CallCom
1858
1822
 
1859
- ### KoaApp 类型扩展
1823
+ CallCom SchemaPage 中的弹窗、抽屉、面板类组件。内置组件有:
1860
1824
 
1861
- 使用方推荐增强根包 `@_tc/template-core`:
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
- interface IControllerAugmented {
1876
- product: {
1877
- list(ctx: import('@_tc/template-core').Ctx): Promise<void>
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 IExtendsAugmented {
1882
- redis: {
1883
- get(key: string): Promise<string | null>
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
- import type { KoaApp } from '@_tc/template-core'
1857
+ // frontend/extended/SchemaPage/CallCom/data.ts
1858
+ import { lazy } from 'react'
1859
+ import type { CallComComponentsMap } from '@_tc/template-core/fe'
1902
1860
 
1903
- export default (app: KoaApp) => {
1904
- app.config.authSecret
1905
- app.service.product.list()
1906
- app.extends.$fetch.get('https://example.com/api')
1907
- app.extends.redis.get('token')
1908
- }
1861
+ const componentsMap = {
1862
+ auditPanel: lazy(() => import('../../components/AuditPanel')),
1863
+ } satisfies CallComComponentsMap
1864
+
1865
+ export default componentsMap
1909
1866
  ```
1910
1867
 
1911
- 如果在 TemplateCore 仓库源码内部扩展,声明目标通常是内部别名 `@tc/core`;npm 使用方不要依赖 `@tc/*` 内部路径。
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
- ```text
1918
- frontend/admin/admin.entry.tsx
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
- 入口文件名必须包含 `.entry.`,构建器才会扫描。
1922
-
1923
- 最小入口示例:
1893
+ model 中使用:
1924
1894
 
1925
- ```tsx
1926
- // frontend/admin/admin.entry.tsx
1927
- import { initApp } from '@_tc/template-core/fe'
1928
- import type { ReactNode } from 'react'
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
- const AdminApp = ({ children }: { children: ReactNode }) => (
1931
- <main>{children}</main>
1932
- )
1916
+ 字段级配置使用 `${组件名}Option`:
1933
1917
 
1934
- initApp(AdminApp, {
1935
- initModeData: false,
1936
- })
1918
+ ```ts
1919
+ remark: {
1920
+ type: 'string',
1921
+ label: '备注',
1922
+ auditPanelOption: {
1923
+ visible: true,
1924
+ },
1925
+ }
1937
1926
  ```
1938
1927
 
1939
- 如果需要给页面加额外 `meta`、预连接资源或启动脚本,可以在入口同目录放 `.html` 插槽文件。这个文件不是完整 HTML,只能包含 `tc-slot` 块:
1928
+ ### 扩展 SchemaTable 单元格渲染组件
1940
1929
 
1941
- ```html
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
- <!-- tc-slot:body-before-root -->
1949
- <div id="boot-mask"></div>
1950
- <!-- /tc-slot:body-before-root -->
1932
+ - `PreviewImage`:把当前单元格值渲染成图片预览;值是数组时直接作为图片列表,值是单个字符串时自动转成单图数组。
1951
1933
 
1952
- <!-- tc-slot:body-after-root -->
1953
- <script>
1954
- window.__ADMIN_BOOT_TIME__ = Date.now()
1955
- </script>
1956
- <!-- /tc-slot:body-after-root -->
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
- ```text
1962
- frontend/admin/admin.entry.tsx
1963
- frontend/admin/admin.html yes
1952
+ ```ts
1953
+ // typing/template-core.d.ts
1954
+ import '@_tc/template-core/model'
1964
1955
 
1965
- frontend/report/index.entry.tsx
1966
- frontend/report/index.html yes
1967
- frontend/report/report.html yes
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
- 不要在插槽文件里写 `<!DOCTYPE html>`、`<html>`、`<body>` 或 `<div id="root"></div>`。页面外壳由 TemplateCore 的 `app/view/entry.tpl` 统一生成,`window._basePath`、`window._projKey`、`window._signKey`、`window.appOptions` 也由它注入。
1971
+ 注册运行时组件:
1971
1972
 
1972
- 扩展 Dashboard 路由:
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
- ```tsx
1975
- // frontend/extended/dash/customRoutes.tsx
1976
- import type { DashRoutesExtender } from '@_tc/template-core/fe'
1978
+ const componentsMap = {
1979
+ PriceCell: lazy(() => import('../../components/PriceCell')),
1980
+ } satisfies SchemaTableRenderComponentsMap
1977
1981
 
1978
- const extendDashRoutes: DashRoutesExtender = ({ topRoutes, sidebarRoutes }) => {
1979
- topRoutes.push({
1980
- path: 'custom-page',
1981
- component: <div>Custom Page</div>,
1982
- })
1982
+ export default componentsMap
1983
+ ```
1983
1984
 
1984
- sidebarRoutes.push({
1985
- path: 'custom-sidebar-page',
1986
- component: <div>Custom Sidebar Page</div>,
1987
- })
1988
- }
1985
+ 组件会收到 `value`、`record`、`rowIndex`、`fieldKey`,以及 `renderComponentProps` 中的自定义参数:
1989
1986
 
1990
- export default extendDashRoutes
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
- - **topRoutes**:追加到 Dashboard 顶层路由,路径相对于 `/dash`
1996
- - 示例:`path: 'custom-page'` → 访问路径为 `/dash/custom-page`
1997
- - 不要写成 `/custom-page`,使用相对路径即可
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
- - **sidebarRoutes**:追加到侧边栏容器的子路由,路径相对于 `/dash/_sidebar_`
2000
- - 示例:`path: 'custom-sidebar-page'` → 访问路径为 `/dash/_sidebar_/custom-sidebar-page`
2001
- - 适用于 `menuLayout: 'left'` 的项目
2012
+ ## 如何配置类型扩展
2002
2013
 
2003
- model 中用 `custom` 菜单指向自定义路径:
2014
+ ### tsconfig
2004
2015
 
2005
- ```ts
2016
+ 如果项目分了后端、前端、model 多个 TS 上下文,确保它们都 include 同一份声明文件:
2017
+
2018
+ ```json
2006
2019
  {
2007
- key: 'custom',
2008
- name: '自定义页面',
2009
- menuType: 'module',
2010
- moduleType: 'custom',
2011
- customConfig: {
2012
- path: '/custom-page', // 注意:这里使用完整路径,以 / 开头
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
- ## 前端构建 buildFE
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
- import { buildFE } from '@_tc/template-core/bundler'
2046
+ // typing/template-core.d.ts
2047
+ import '@_tc/template-core/model'
2023
2048
 
2024
- await buildFE('dev')
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
- await buildFE('prod')
2031
- ```
2064
+ import type { ModelDataType } from '@_tc/template-core/model'
2032
2065
 
2033
- 默认输出到当前导入格式对应的框架静态目录,例如 `esm/app/public/dist` `cjs/app/public/dist`。如果要输出到项目:
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
- ```ts
2036
- await buildFE('prod', {
2037
- output: 'run',
2038
- })
2077
+ export default model
2039
2078
  ```
2040
2079
 
2041
- 也可以压缩最终模板输出:
2080
+ 继续使用内置管理后台时:
2042
2081
 
2043
2082
  ```ts
2044
- await buildFE('prod', {
2045
- minifyHtml: true,
2046
- })
2047
- ```
2048
-
2049
- 构建完成后会在输出目录写入 `FEBuildKey`,服务端渲染 HTML 时会用它判断本地 HTML ETag 缓存是否需要清空。
2083
+ import type { ModelDataType } from '@_tc/template-core/model'
2050
2084
 
2051
- ## Node/backend 构建 buildBE
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
- 消费方 Node/backend 侧构建使用 `buildBE()`。它复用发布包内的 `scripts/vite-build/buildEntries`,只是提供消费方默认配置。
2094
+ ### CallCom 类型扩展
2054
2095
 
2055
- 最小用法:
2096
+ 扩展目标:
2056
2097
 
2057
2098
  ```ts
2058
- import { buildBE } from '@_tc/template-core/bundler'
2059
-
2060
- await buildBE()
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
- ```js
2066
- // scripts/build-be.mjs
2067
- import { buildBE } from '@_tc/template-core/bundler'
2111
+ ### SchemaTable 渲染组件类型扩展
2068
2112
 
2069
- await buildBE({
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
- ```json
2082
- {
2083
- "scripts": {
2084
- "build:be": "node scripts/build-be.mjs"
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
- ```js
2092
- // scripts/watch-be.mjs
2093
- import chokidar from 'chokidar'
2094
- import { buildBE } from '@_tc/template-core/bundler'
2127
+ ### KoaApp 类型扩展
2095
2128
 
2096
- const input = ['index.ts', 'index.js', 'app', 'config', 'model']
2129
+ 使用方推荐增强根包 `@_tc/template-core`:
2097
2130
 
2098
- let building = false
2099
- let pending = false
2131
+ ```ts
2132
+ // typing/template-core.d.ts
2133
+ import '@_tc/template-core'
2100
2134
 
2101
- async function runBuild() {
2102
- if (building) {
2103
- pending = true
2104
- return
2105
- }
2135
+ declare module '@_tc/template-core' {
2136
+ namespace FrameworkAugment {
2137
+ interface IServiceAugmented {
2138
+ product: {
2139
+ list(): Promise<unknown[]>
2140
+ }
2141
+ }
2106
2142
 
2107
- building = true
2143
+ interface IControllerAugmented {
2144
+ product: {
2145
+ list(ctx: import('@_tc/template-core').Ctx): Promise<void>
2146
+ }
2147
+ }
2108
2148
 
2109
- try {
2110
- console.log('[buildBE] building...')
2111
- await buildBE({
2112
- rootDir: process.cwd(),
2113
- input,
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
- if (pending) {
2129
- pending = false
2130
- await runBuild()
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
- await runBuild()
2166
+ 使用:
2136
2167
 
2137
- chokidar
2138
- .watch(input, {
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
- ```json
2149
- {
2150
- "scripts": {
2151
- "build:be": "node scripts/build-be.mjs",
2152
- "build:be:watch": "node scripts/watch-be.mjs"
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
- ```ts
2163
- ['index.ts', 'index.js', 'app', 'config', 'model']
2164
- ```
2181
+ ## 可选:给项目 AI 助手的文档与 Skill
2165
2182
 
2166
- 不存在的默认入口会自动跳过;如果没有任何匹配源码,构建仍会失败。输出到 `dist`,格式为 `cjs`。默认 `outputStructure: "preserve"` 按源路径输出,`bundleDependencies: false` 会 external Node 内置模块和 npm 包,不会把依赖打进产物。`buildBE` 会按同一组 input 扫描并复制内置白名单资源扩展,主要覆盖 `app`、`config`、`model` 目录。
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
- ```ts
2171
- await buildBE({
2172
- input: ['app', 'config', 'model'],
2173
- outDir: 'dist',
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
- ```ts
2185
- await buildBE({
2186
- dts: {
2187
- outDir: 'types',
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
- `buildEntries()` 底层默认生成 d.ts,但 `buildBE()` 默认关闭声明文件;需要时按上面这样显式传 `dts`。
2194
-
2195
- 如果传入自定义 `input`,缺失路径默认会报错;需要跳过时显式打开:
2200
+ 安装后重启对应 Agent,让新 skill 生效。使用时可以直接说”用 tc-generator 生成一个商品管理模块”或”用 tc-component-usage-skills 帮我写一个带搜索的分页表格”。
2196
2201
 
2197
- ```ts
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