@yuer678/create-kb 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +196 -0
  3. package/package.json +48 -0
  4. package/templates/ai/.env.example +7 -0
  5. package/templates/ai/README.md +39 -0
  6. package/templates/ai/package.json +42 -0
  7. package/templates/ai/public/index.html +117 -0
  8. package/templates/ai/src/app.ts +22 -0
  9. package/templates/ai/src/config.ts +13 -0
  10. package/templates/ai/src/index.ts +6 -0
  11. package/templates/ai/src/lib/llm.ts +24 -0
  12. package/templates/ai/src/routes/chat.ts +72 -0
  13. package/templates/ai/test/chat.spec.ts +27 -0
  14. package/templates/ai/tsconfig.json +13 -0
  15. package/templates/ai/vitest.config.ts +8 -0
  16. package/templates/api/README.md +50 -0
  17. package/templates/api/package.json +42 -0
  18. package/templates/api/src/app.ts +84 -0
  19. package/templates/api/src/data/options.ts +17 -0
  20. package/templates/api/src/data/regions.ts +71 -0
  21. package/templates/api/src/data/users.ts +76 -0
  22. package/templates/api/src/index.ts +9 -0
  23. package/templates/api/src/middleware/error.ts +28 -0
  24. package/templates/api/src/middleware/validate.ts +22 -0
  25. package/templates/api/src/query.ts +106 -0
  26. package/templates/api/src/routes/health.ts +21 -0
  27. package/templates/api/src/routes/options.ts +19 -0
  28. package/templates/api/src/routes/regions.ts +31 -0
  29. package/templates/api/src/routes/users.ts +57 -0
  30. package/templates/api/src/server.ts +31 -0
  31. package/templates/api/src/types.ts +8 -0
  32. package/templates/api/test/app.spec.ts +216 -0
  33. package/templates/api/tsconfig.json +13 -0
  34. package/templates/api/vitest.config.ts +8 -0
  35. package/templates/base/README.md +25 -0
  36. package/templates/base/index.html +12 -0
  37. package/templates/base/package.json +32 -0
  38. package/templates/base/src/App.vue +46 -0
  39. package/templates/base/src/main.ts +6 -0
  40. package/templates/base/tsconfig.json +18 -0
  41. package/templates/base/vite.config.ts +6 -0
  42. package/templates/electron/README.md +32 -0
  43. package/templates/electron/package.json +54 -0
  44. package/templates/electron/src/main/index.ts +36 -0
  45. package/templates/electron/src/preload/index.ts +10 -0
  46. package/templates/electron/src/renderer/index.html +22 -0
  47. package/templates/electron/src/renderer/renderer.ts +14 -0
  48. package/templates/electron/tsconfig.json +13 -0
  49. package/templates/fullstack/Dockerfile +16 -0
  50. package/templates/fullstack/README.md +37 -0
  51. package/templates/fullstack/docker-compose.yml +26 -0
  52. package/templates/fullstack/package.json +30 -0
  53. package/templates/fullstack/pnpm-workspace.yaml +3 -0
  54. package/templates/fullstack/server/package.json +41 -0
  55. package/templates/fullstack/server/src/app.ts +84 -0
  56. package/templates/fullstack/server/src/data/options.ts +17 -0
  57. package/templates/fullstack/server/src/data/regions.ts +71 -0
  58. package/templates/fullstack/server/src/data/users.ts +76 -0
  59. package/templates/fullstack/server/src/index.ts +9 -0
  60. package/templates/fullstack/server/src/middleware/error.ts +28 -0
  61. package/templates/fullstack/server/src/middleware/validate.ts +22 -0
  62. package/templates/fullstack/server/src/query.ts +106 -0
  63. package/templates/fullstack/server/src/routes/health.ts +21 -0
  64. package/templates/fullstack/server/src/routes/options.ts +19 -0
  65. package/templates/fullstack/server/src/routes/regions.ts +31 -0
  66. package/templates/fullstack/server/src/routes/users.ts +57 -0
  67. package/templates/fullstack/server/src/server.ts +31 -0
  68. package/templates/fullstack/server/src/types.ts +8 -0
  69. package/templates/fullstack/server/test/app.spec.ts +216 -0
  70. package/templates/fullstack/server/tsconfig.json +13 -0
  71. package/templates/fullstack/server/vitest.config.ts +8 -0
  72. package/templates/fullstack/web/index.html +12 -0
  73. package/templates/fullstack/web/package.json +30 -0
  74. package/templates/fullstack/web/src/App.vue +108 -0
  75. package/templates/fullstack/web/src/main.ts +4 -0
  76. package/templates/fullstack/web/tsconfig.json +13 -0
  77. package/templates/fullstack/web/vite.config.ts +16 -0
  78. package/templates/react/README.md +8 -0
  79. package/templates/react/index.html +12 -0
  80. package/templates/react/package.json +34 -0
  81. package/templates/react/src/App.css +32 -0
  82. package/templates/react/src/App.tsx +16 -0
  83. package/templates/react/src/main.tsx +9 -0
  84. package/templates/react/tsconfig.json +20 -0
  85. package/templates/react/vite.config.ts +9 -0
  86. package/templates/starter/README.md +28 -0
  87. package/templates/starter/index.html +12 -0
  88. package/templates/starter/package.json +32 -0
  89. package/templates/starter/src/App.vue +125 -0
  90. package/templates/starter/src/main.ts +6 -0
  91. package/templates/starter/tsconfig.json +18 -0
  92. package/templates/starter/vite.config.ts +6 -0
@@ -0,0 +1,50 @@
1
+ # {{projectName}}-api
2
+
3
+ Express 5 + TypeScript + Zod 的参考后端服务。
4
+
5
+ > 本目录的 `src/` 与 `test/` 由 `scripts/sync-api-template.mjs` 从 **`packages/api`**(`@yuer678/kb-api`)
6
+ > 自动生成,请勿直接修改;需要调整请改 `packages/api` 的源码后运行 `pnpm sync:api-template`。
7
+ > 只有 `src/index.ts`(启动入口)是模板自带的。
8
+
9
+ ## 快速开始
10
+
11
+ ```bash
12
+ pnpm install
13
+ pnpm dev # 开发模式(热重载),http://localhost:3000
14
+ pnpm test # 运行测试(vitest + supertest)
15
+ ```
16
+
17
+ 端口默认 `3000`,可用环境变量 `PORT` 覆盖。
18
+
19
+ ## 接口
20
+
21
+ | 方法 | 路径 | 说明 |
22
+ |------|------|------|
23
+ | GET | `/` | 列出可用接口 |
24
+ | GET | `/health` · `/api/health` | 健康检查(带前缀/不带前缀都有,方便容器探活) |
25
+ | GET | `/api/users` | 用户列表:`?page=&pageSize=&keyword=&sortBy=&order=` |
26
+ | GET | `/api/users/:id` | 用户详情 |
27
+ | POST | `/api/users` | 创建用户(zod 校验) |
28
+ | DELETE | `/api/users/:id` | 删除用户 |
29
+ | GET | `/api/regions` | 下一级区域:`?parent=<value>`(树形懒加载) |
30
+ | GET | `/api/regions/tree` | 完整区域树(440 节点) |
31
+ | GET | `/api/options` | 候选项列表:`?keyword=&page=&pageSize=` |
32
+
33
+ ## 结构
34
+
35
+ ```
36
+ src/
37
+ ├── app.ts # 创建 app(与启动分离,便于 supertest 直接测)
38
+ ├── server.ts # startServer():listen 并 resolve 出 Server
39
+ ├── index.ts # 模板入口(端口 3000)
40
+ ├── query.ts # 搜索 → 排序 → 分页 的统一流水线
41
+ ├── middleware/ # zod 校验 / 错误处理
42
+ ├── routes/ # health / users / regions / options
43
+ └── data/ # 内存种子数据(换成数据库时只替换这一层)
44
+ test/ # supertest 集成测试
45
+ ```
46
+
47
+ ## 换成真实数据库
48
+
49
+ `src/data/*` 暴露的是「读列表 / 查单个 / 新建 / 删除」这几个访问器,路由只依赖它们。
50
+ 把 `data/` 换成 Prisma / Drizzle / 原生 SQL 的实现即可,路由与校验层无需改动。
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch src/index.ts",
11
+ "start": "tsx src/index.ts",
12
+ "build": "tsc --noEmit",
13
+ "test": "vitest run",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "express": "^5.1.0",
18
+ "zod": "^4.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/express": "^5.0.0",
22
+ "@types/node": "^24.0.0",
23
+ "supertest": "^7.1.0",
24
+ "@types/supertest": "^6.0.0",
25
+ "tsx": "^4.20.0",
26
+ "typescript": "^5.9.0",
27
+ "vitest": "^4.1.0"
28
+ },
29
+ "overrides": {
30
+ "js-yaml": "^4.3.2",
31
+ "fast-uri": "^3.1.0",
32
+ "vite": "^6.3.5",
33
+ "electron": "^39.8.10"
34
+ },
35
+ "pnpm": {
36
+ "overrides": {
37
+ "js-yaml": "^4.1.1",
38
+ "fast-uri": "^3.1.0",
39
+ "vite": "^6.3.5"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,84 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import express from 'express'
5
+ import type { Express, RequestHandler } from 'express'
6
+ import { createHealthRouter } from './routes/health'
7
+ import { usersRouter } from './routes/users'
8
+ import { regionsRouter } from './routes/regions'
9
+ import { optionsRouter } from './routes/options'
10
+ import { errorHandler, notFoundHandler } from './middleware/error'
11
+ import type { ApiInfo } from './types'
12
+
13
+ export interface CreateAppOptions {
14
+ /** 服务名,会出现在根路由、健康检查与启动日志里 */
15
+ name?: string
16
+ version?: string
17
+ /** 插在所有业务路由前的自定义中间件,方便加日志/鉴权/耗时统计 */
18
+ middleware?: RequestHandler[]
19
+ /** 是否放开跨域,默认开启,便于本地联调 */
20
+ cors?: boolean
21
+ }
22
+
23
+ export const API_PREFIX = '/api'
24
+ export const DEFAULT_API_NAME = 'kb-api'
25
+ export const DEFAULT_API_VERSION = '0.1.0'
26
+
27
+ const ENDPOINTS = [
28
+ 'GET /api/health',
29
+ 'GET /api/users?page=&pageSize=&keyword=&sortBy=&order=',
30
+ 'GET /api/users/:id',
31
+ 'POST /api/users',
32
+ 'DELETE /api/users/:id',
33
+ 'GET /api/regions?parent=<value>',
34
+ 'GET /api/regions/tree',
35
+ 'GET /api/options?keyword=&page=&pageSize=',
36
+ ]
37
+
38
+ /** 手写 CORS,避免为一个 mock 服务再引一个依赖;预检请求直接 204 返回 */
39
+ function corsMiddleware(enabled: boolean): RequestHandler {
40
+ return (req, res, next) => {
41
+ if (!enabled) {
42
+ next()
43
+ return
44
+ }
45
+ res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '*')
46
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
47
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
48
+ res.setHeader('Vary', 'Origin')
49
+ if (req.method === 'OPTIONS') {
50
+ res.status(204).end()
51
+ return
52
+ }
53
+ next()
54
+ }
55
+ }
56
+
57
+ /** 创建 Express 应用(与启动逻辑分离,便于 supertest 直接测) */
58
+ export function createApp(options: CreateAppOptions = {}): Express {
59
+ const info: ApiInfo = {
60
+ name: options.name ?? DEFAULT_API_NAME,
61
+ version: options.version ?? DEFAULT_API_VERSION,
62
+ }
63
+
64
+ const app = express()
65
+ app.use(express.json())
66
+ app.use(corsMiddleware(options.cors ?? true))
67
+ for (const middleware of options.middleware ?? []) app.use(middleware)
68
+
69
+ app.get('/', (_req, res) => {
70
+ res.json({ name: info.name, version: info.version, prefix: API_PREFIX, endpoints: ENDPOINTS })
71
+ })
72
+
73
+ const healthRouter = createHealthRouter(info)
74
+ // 探活同时提供无前缀路径,方便容器/网关直接探测
75
+ app.use('/health', healthRouter)
76
+ app.use(`${API_PREFIX}/health`, healthRouter)
77
+ app.use(`${API_PREFIX}/users`, usersRouter)
78
+ app.use(`${API_PREFIX}/regions`, regionsRouter)
79
+ app.use(`${API_PREFIX}/options`, optionsRouter)
80
+
81
+ app.use(notFoundHandler)
82
+ app.use(errorHandler)
83
+ return app
84
+ }
@@ -0,0 +1,17 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ export interface OptionItem {
5
+ key: string
6
+ label: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ export const OPTIONS_TOTAL = 240
11
+
12
+ /** 240 条候选项,每 17 条埋一个 disabled,用来演示穿梭框的禁用态 */
13
+ export const options: OptionItem[] = Array.from({ length: OPTIONS_TOTAL }, (_, index) => ({
14
+ key: `opt-${index + 1}`,
15
+ label: `候选项 ${String(index + 1).padStart(3, '0')}`,
16
+ disabled: index % 17 === 0,
17
+ }))
@@ -0,0 +1,71 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ export interface RegionNode {
5
+ value: string
6
+ label: string
7
+ /** 末级标记:Cascader 异步模式直接消费这个字段 */
8
+ leaf: boolean
9
+ children?: RegionNode[]
10
+ }
11
+
12
+ /** 懒加载返回的节点摘要(不带 children,懒加载才有意义) */
13
+ export interface RegionSummary {
14
+ value: string
15
+ label: string
16
+ leaf: boolean
17
+ }
18
+
19
+ const AREA_COUNT = 8
20
+ const CITY_PER_AREA = 6
21
+ const BLOCK_PER_CITY = 8
22
+
23
+ /** 全合成数据(8 大区 × 6 城市 × 8 区块 = 440 个节点),刻意不带真实行政区划含义 */
24
+ function buildRegions(): RegionNode[] {
25
+ const areas: RegionNode[] = []
26
+ for (let a = 1; a <= AREA_COUNT; a += 1) {
27
+ const areaId = `a${a}`
28
+ const cities: RegionNode[] = []
29
+ for (let c = 1; c <= CITY_PER_AREA; c += 1) {
30
+ const cityId = `${areaId}-c${c}`
31
+ const blocks: RegionNode[] = []
32
+ for (let b = 1; b <= BLOCK_PER_CITY; b += 1) {
33
+ blocks.push({ value: `${cityId}-b${b}`, label: `区块 ${b}`, leaf: true })
34
+ }
35
+ cities.push({ value: cityId, label: `城市 ${c}`, leaf: false, children: blocks })
36
+ }
37
+ areas.push({ value: areaId, label: `大区 ${a}`, leaf: false, children: cities })
38
+ }
39
+ return areas
40
+ }
41
+
42
+ const store = buildRegions()
43
+
44
+ const index = new Map<string, RegionNode>()
45
+ function indexNodes(nodes: RegionNode[]): void {
46
+ for (const node of nodes) {
47
+ index.set(node.value, node)
48
+ if (node.children) indexNodes(node.children)
49
+ }
50
+ }
51
+ indexNodes(store)
52
+
53
+ /** 节点总数(8 + 48 + 384) */
54
+ export const REGION_NODE_COUNT = index.size
55
+
56
+ /** 完整嵌套树,供 Tree 的虚拟滚动演示 */
57
+ export const regionTree: RegionNode[] = store
58
+
59
+ export function toSummary(node: RegionNode): RegionSummary {
60
+ return { value: node.value, label: node.label, leaf: node.leaf }
61
+ }
62
+
63
+ /** 取某个节点的下一级;parent 为空时返回顶层。节点不存在或已是末级时返回 undefined */
64
+ export function childrenOf(parent: string | null): RegionNode[] | undefined {
65
+ if (!parent) return store
66
+ return index.get(parent)?.children
67
+ }
68
+
69
+ export function findRegion(value: string): RegionNode | undefined {
70
+ return index.get(value)
71
+ }
@@ -0,0 +1,76 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ export interface User {
5
+ id: number
6
+ name: string
7
+ company: string
8
+ city: string
9
+ email: string
10
+ role: string
11
+ score: number
12
+ createdAt: string
13
+ }
14
+
15
+ const SURNAMES = ['李', '王', '张', '刘', '陈', '杨', '黄', '赵', '周', '吴', '徐', '孙', '马', '朱', '胡', '郭']
16
+ const GIVEN_NAMES = ['明', '华', '静', '敏', '伟', '芳', '强', '磊', '洋', '艳', '勇', '军', '杰', '娟', '涛', '超']
17
+ const COMPANIES = ['星尘科技', '云图数据', '南栀软件', '澜川网络', '青梧智能', '归墟信息', '未名云', '鸣石科技']
18
+ const CITIES = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安', '南京', '苏州']
19
+ export const USER_ROLES = ['管理员', '编辑', '访客'] as const
20
+
21
+ /** 参与 keyword 模糊匹配的字段 */
22
+ export const USER_SEARCH_FIELDS = ['name', 'company', 'city', 'email'] as const
23
+ /** 允许排序的字段白名单 */
24
+ export const USER_SORT_KEYS = ['id', 'name', 'company', 'city', 'role', 'score', 'createdAt'] as const
25
+
26
+ export const USERS_TOTAL = 500
27
+
28
+ function formatDate(offsetDays: number): string {
29
+ const base = Date.UTC(2026, 0, 1)
30
+ return new Date(base + offsetDays * 86_400_000).toISOString().slice(0, 10)
31
+ }
32
+
33
+ /** 500 条确定性数据:用下标做取模而不是随机数,保证分页与测试可复现 */
34
+ const store: User[] = Array.from({ length: USERS_TOTAL }, (_, index) => ({
35
+ id: index + 1,
36
+ name: `${SURNAMES[index % SURNAMES.length]}${GIVEN_NAMES[(index * 7) % GIVEN_NAMES.length]}`,
37
+ company: COMPANIES[index % COMPANIES.length],
38
+ city: CITIES[(index * 3) % CITIES.length],
39
+ email: `user${index + 1}@example.com`,
40
+ role: USER_ROLES[index % USER_ROLES.length],
41
+ score: (index * 37) % 100,
42
+ createdAt: formatDate(index),
43
+ }))
44
+
45
+ /** 只读视图,路由层拿它做查询 */
46
+ export function listUsers(): User[] {
47
+ return store
48
+ }
49
+
50
+ export function findUser(id: number): User | undefined {
51
+ return store.find((user) => user.id === id)
52
+ }
53
+
54
+ let nextId = USERS_TOTAL + 1
55
+
56
+ export function createUser(payload: Pick<User, 'name'> & Partial<User>): User {
57
+ const user: User = {
58
+ id: nextId++,
59
+ name: payload.name,
60
+ company: payload.company ?? COMPANIES[0],
61
+ city: payload.city ?? CITIES[0],
62
+ email: payload.email ?? `user${nextId - 1}@example.com`,
63
+ role: payload.role ?? USER_ROLES[2],
64
+ score: payload.score ?? 0,
65
+ createdAt: formatDate(0),
66
+ }
67
+ store.push(user)
68
+ return user
69
+ }
70
+
71
+ export function removeUser(id: number): boolean {
72
+ const index = store.findIndex((user) => user.id === id)
73
+ if (index < 0) return false
74
+ store.splice(index, 1)
75
+ return true
76
+ }
@@ -0,0 +1,9 @@
1
+ import { startServer } from './server'
2
+
3
+ /** 模板入口:端口默认 3000(与 docker-compose、web 代理保持一致),可用 PORT 覆盖 */
4
+ const port = Number(process.env.PORT ?? 3000)
5
+
6
+ startServer({ port, name: '{{projectName}}-api' }).catch((error: unknown) => {
7
+ console.error('[api] 启动失败:', error)
8
+ process.exitCode = 1
9
+ })
@@ -0,0 +1,28 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import type { Request, Response, NextFunction } from 'express'
5
+
6
+ /** 统一的 404 响应,挂在所有路由之后 */
7
+ export function notFoundHandler(req: Request, res: Response) {
8
+ res.status(404).json({ code: 404, message: `接口不存在: ${req.method} ${req.path}` })
9
+ }
10
+
11
+ /** 统一错误响应,Express 5 会自动捕获 async 路由里抛出的异常并转到这里 */
12
+ export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {
13
+ const message = err instanceof Error ? err.message : String(err)
14
+ const status = err instanceof HttpError ? err.status : 500
15
+ if (status >= 500) console.error('[api] 未捕获异常:', err)
16
+ res.status(status).json({ code: status, message: status >= 500 ? '服务器内部错误' : message })
17
+ }
18
+
19
+ /** 路由里可以直接 throw new HttpError(404, '未找到') */
20
+ export class HttpError extends Error {
21
+ readonly status: number
22
+
23
+ constructor(status: number, message: string) {
24
+ super(message)
25
+ this.name = 'HttpError'
26
+ this.status = status
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import type { Request, Response, NextFunction, RequestHandler } from 'express'
5
+ import type { ZodType } from 'zod'
6
+
7
+ /** zod 校验中间件:校验通过后把解析结果挂回 req.body,失败返回 400 与字段级错误 */
8
+ export function validate(schema: ZodType): RequestHandler {
9
+ return (req: Request, res: Response, next: NextFunction) => {
10
+ const result = schema.safeParse(req.body)
11
+ if (!result.success) {
12
+ res.status(400).json({
13
+ code: 400,
14
+ message: '参数校验失败',
15
+ errors: result.error.flatten().fieldErrors,
16
+ })
17
+ return
18
+ }
19
+ req.body = result.data
20
+ next()
21
+ }
22
+ }
@@ -0,0 +1,106 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ export type SortOrder = 'asc' | 'desc'
5
+
6
+ export interface PageQuery {
7
+ page: number
8
+ pageSize: number
9
+ keyword: string
10
+ sortBy: string | null
11
+ order: SortOrder
12
+ }
13
+
14
+ export interface PageResult<T> {
15
+ list: T[]
16
+ total: number
17
+ page: number
18
+ pageSize: number
19
+ }
20
+
21
+ export interface ListQueryOptions {
22
+ /** 参与 keyword 模糊匹配的字段 */
23
+ searchFields?: readonly string[]
24
+ /** 允许排序的字段白名单,未列出的字段会被忽略(避免任意字段排序) */
25
+ sortKeys?: readonly string[]
26
+ }
27
+
28
+ export const DEFAULT_PAGE_SIZE = 10
29
+ export const MAX_PAGE_SIZE = 500
30
+
31
+ function toPositiveInt(value: unknown, fallback: number): number {
32
+ const num = Number(value)
33
+ if (!Number.isFinite(num)) return fallback
34
+ const int = Math.floor(num)
35
+ return int > 0 ? int : fallback
36
+ }
37
+
38
+ function readField(row: unknown, field: string): unknown {
39
+ return (row as Record<string, unknown>)[field]
40
+ }
41
+
42
+ function compareValues(a: unknown, b: unknown): number {
43
+ if (a === b) return 0
44
+ if (a === null || a === undefined) return -1
45
+ if (b === null || b === undefined) return 1
46
+ if (typeof a === 'number' && typeof b === 'number') return a - b
47
+ return String(a).localeCompare(String(b), 'zh-Hans-CN')
48
+ }
49
+
50
+ /** 解析 ?page=&pageSize=&keyword=&sortBy=&order=,非法值一律回退到默认值 */
51
+ export function parsePageQuery(query: Record<string, unknown> = {}): PageQuery {
52
+ return {
53
+ page: toPositiveInt(query.page, 1),
54
+ pageSize: Math.min(toPositiveInt(query.pageSize, DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE),
55
+ keyword: typeof query.keyword === 'string' ? query.keyword.trim() : '',
56
+ sortBy: typeof query.sortBy === 'string' && query.sortBy ? query.sortBy : null,
57
+ order: query.order === 'desc' ? 'desc' : 'asc',
58
+ }
59
+ }
60
+
61
+ export function searchRows<T>(
62
+ rows: readonly T[],
63
+ keyword: string,
64
+ fields: readonly string[],
65
+ ): T[] {
66
+ if (!keyword) return [...rows]
67
+ const needle = keyword.toLowerCase()
68
+ return rows.filter((row) =>
69
+ fields.some((field) => String(readField(row, field) ?? '').toLowerCase().includes(needle)),
70
+ )
71
+ }
72
+
73
+ export function sortRows<T>(
74
+ rows: readonly T[],
75
+ sortBy: string | null,
76
+ order: SortOrder,
77
+ allowedKeys?: readonly string[],
78
+ ): T[] {
79
+ if (!sortBy) return [...rows]
80
+ if (allowedKeys && !allowedKeys.includes(sortBy)) return [...rows]
81
+ const factor = order === 'desc' ? -1 : 1
82
+ return [...rows].sort((a, b) => compareValues(readField(a, sortBy), readField(b, sortBy)) * factor)
83
+ }
84
+
85
+ export function toPageResult<T>(rows: readonly T[], query: PageQuery): PageResult<T> {
86
+ const start = (query.page - 1) * query.pageSize
87
+ return {
88
+ list: rows.slice(start, start + query.pageSize),
89
+ total: rows.length,
90
+ page: query.page,
91
+ pageSize: query.pageSize,
92
+ }
93
+ }
94
+
95
+ /** 搜索 → 排序 → 分页 的标准流水线,列表接口都用它,保证行为一致 */
96
+ export function queryList<T>(
97
+ source: readonly T[],
98
+ query: PageQuery,
99
+ options: ListQueryOptions = {},
100
+ ): PageResult<T> {
101
+ const searched = options.searchFields
102
+ ? searchRows(source, query.keyword, options.searchFields)
103
+ : [...source]
104
+ const sorted = sortRows(searched, query.sortBy, query.order, options.sortKeys)
105
+ return toPageResult(sorted, query)
106
+ }
@@ -0,0 +1,21 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import { Router } from 'express'
5
+ import type { ApiInfo } from '../types'
6
+
7
+ /** GET /health —— 探活接口,返回服务标识与运行时长 */
8
+ export function createHealthRouter(info: ApiInfo): Router {
9
+ const router = Router()
10
+
11
+ router.get('/', (_req, res) => {
12
+ res.json({
13
+ status: 'ok',
14
+ name: info.name,
15
+ version: info.version,
16
+ uptime: Math.round(process.uptime()),
17
+ })
18
+ })
19
+
20
+ return router
21
+ }
@@ -0,0 +1,19 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import { Router } from 'express'
5
+ import { parsePageQuery, queryList } from '../query'
6
+ import { options } from '../data/options'
7
+
8
+ // 显式标注类型:否则 dts 生成会因 pnpm 的 .pnpm 路径而报 TS2742(类型不可命名)
9
+ export const optionsRouter: Router = Router()
10
+
11
+ /**
12
+ * GET /options?keyword=&page=&pageSize=
13
+ * 240 条候选项的分页 + 搜索,供 Transfer 的搜索过滤与分页演示取数。
14
+ * 返回的 total 是过滤后的条数,前端可直接用它算总页数。
15
+ */
16
+ optionsRouter.get('/', (req, res) => {
17
+ const query = parsePageQuery(req.query as Record<string, unknown>)
18
+ res.json(queryList(options, query, { searchFields: ['label'], sortKeys: ['key', 'label'] }))
19
+ })
@@ -0,0 +1,31 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import { Router } from 'express'
5
+ import { HttpError } from '../middleware/error'
6
+ import { REGION_NODE_COUNT, childrenOf, findRegion, regionTree, toSummary } from '../data/regions'
7
+
8
+ // 显式标注类型:否则 dts 生成会因 pnpm 的 .pnpm 路径而报 TS2742(类型不可命名)
9
+ export const regionsRouter: Router = Router()
10
+
11
+ /** GET /regions/tree —— 完整嵌套树(440 个节点),供 Tree 虚拟滚动使用 */
12
+ regionsRouter.get('/tree', (_req, res) => {
13
+ res.json({ list: regionTree, total: REGION_NODE_COUNT })
14
+ })
15
+
16
+ /**
17
+ * GET /regions?parent=<value>
18
+ * 只返回下一级节点摘要(不带 children),节点不存在时 404。
19
+ * Cascader 的 lazyLoad 直接消费返回的 leaf 字段判断末级。
20
+ */
21
+ regionsRouter.get('/', (req, res) => {
22
+ const parent = typeof req.query.parent === 'string' && req.query.parent ? req.query.parent : null
23
+ if (parent && !findRegion(parent)) throw new HttpError(404, `节点不存在: ${parent}`)
24
+
25
+ const children = childrenOf(parent) ?? []
26
+ res.json({
27
+ parent,
28
+ list: children.map(toSummary),
29
+ leaf: children.length === 0,
30
+ })
31
+ })
@@ -0,0 +1,57 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import { Router } from 'express'
5
+ import { z } from 'zod'
6
+ import { validate } from '../middleware/validate'
7
+ import { HttpError } from '../middleware/error'
8
+ import { parsePageQuery, queryList } from '../query'
9
+ import {
10
+ USER_SEARCH_FIELDS,
11
+ USER_SORT_KEYS,
12
+ createUser,
13
+ findUser,
14
+ listUsers,
15
+ removeUser,
16
+ } from '../data/users'
17
+
18
+ const createUserSchema = z.object({
19
+ name: z.string().min(1).max(50),
20
+ company: z.string().max(50).optional(),
21
+ city: z.string().max(30).optional(),
22
+ email: z.string().max(80).optional(),
23
+ role: z.string().max(20).optional(),
24
+ score: z.number().int().min(0).max(100).optional(),
25
+ })
26
+
27
+ // 显式标注类型:否则 dts 生成会因 pnpm 的 .pnpm 路径而报 TS2742(类型不可命名)
28
+ export const usersRouter: Router = Router()
29
+
30
+ /**
31
+ * GET /users?page=&pageSize=&keyword=&sortBy=&order=
32
+ * 标准的分页 + 排序 + 搜索,直接对接 Table 的服务端模式(sortable: 'custom')
33
+ */
34
+ usersRouter.get('/', (req, res) => {
35
+ const query = parsePageQuery(req.query as Record<string, unknown>)
36
+ res.json(
37
+ queryList(listUsers(), query, {
38
+ searchFields: USER_SEARCH_FIELDS,
39
+ sortKeys: USER_SORT_KEYS,
40
+ }),
41
+ )
42
+ })
43
+
44
+ usersRouter.get('/:id', (req, res) => {
45
+ const user = findUser(Number(req.params.id))
46
+ if (!user) throw new HttpError(404, `用户不存在: ${req.params.id}`)
47
+ res.json(user)
48
+ })
49
+
50
+ usersRouter.post('/', validate(createUserSchema), (req, res) => {
51
+ res.status(201).json(createUser(req.body))
52
+ })
53
+
54
+ usersRouter.delete('/:id', (req, res) => {
55
+ if (!removeUser(Number(req.params.id))) throw new HttpError(404, `用户不存在: ${req.params.id}`)
56
+ res.status(204).end()
57
+ })
@@ -0,0 +1,31 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import type { Server } from 'node:http'
5
+ import { createApp, DEFAULT_API_NAME } from './app'
6
+ import type { CreateAppOptions } from './app'
7
+
8
+ export interface StartServerOptions extends CreateAppOptions {
9
+ /** 监听端口,默认读环境变量 PORT,否则 8082 */
10
+ port?: number
11
+ /** 监听地址,默认 127.0.0.1(本地 mock 不对外暴露) */
12
+ host?: string
13
+ }
14
+
15
+ export const DEFAULT_PORT = 8082
16
+
17
+ /** 启动服务,listen 成功后 resolve 出 Server,便于脚本里追加逻辑或优雅退出 */
18
+ export function startServer(options: StartServerOptions = {}): Promise<Server> {
19
+ const name = options.name ?? DEFAULT_API_NAME
20
+ const port = options.port ?? Number(process.env.PORT ?? DEFAULT_PORT)
21
+ const host = options.host ?? '127.0.0.1'
22
+ const app = createApp(options)
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const server = app.listen(port, host, () => {
26
+ console.log(`[${name}] 已启动: http://${host}:${port}/api`)
27
+ resolve(server)
28
+ })
29
+ server.on('error', reject)
30
+ })
31
+ }