create-lumfall 1.0.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 (82) hide show
  1. package/README.md +44 -0
  2. package/cli.js +218 -0
  3. package/package.json +29 -0
  4. package/templates/basic/README.md +113 -0
  5. package/templates/basic/app/controller/demo.js +28 -0
  6. package/templates/basic/app/extend/README.md +3 -0
  7. package/templates/basic/app/middleware/README.md +2 -0
  8. package/templates/basic/app/middleware.js +13 -0
  9. package/templates/basic/app/pages/home/entry.home.js +4 -0
  10. package/templates/basic/app/pages/home/home.vue +164 -0
  11. package/templates/basic/app/router/demo.js +17 -0
  12. package/templates/basic/app/router-schema/demo.js +29 -0
  13. package/templates/basic/app/service/demo.js +51 -0
  14. package/templates/basic/app/webpack.config.js +4 -0
  15. package/templates/basic/build.js +7 -0
  16. package/templates/basic/config/config.beta.js +4 -0
  17. package/templates/basic/config/config.default.js +32 -0
  18. package/templates/basic/config/config.local.js +4 -0
  19. package/templates/basic/config/config.prod.js +6 -0
  20. package/templates/basic/package.json +31 -0
  21. package/templates/basic/server.js +26 -0
  22. package/templates/document/README.md +145 -0
  23. package/templates/document/app/extend/README.md +6 -0
  24. package/templates/document/app/middleware/README.md +2 -0
  25. package/templates/document/app/middleware.js +2 -0
  26. package/templates/document/app/pages/docs/assets/docs-logo.svg +5 -0
  27. package/templates/document/app/pages/docs/components/doc-layout.vue +144 -0
  28. package/templates/document/app/pages/docs/components/doc-navbar.vue +103 -0
  29. package/templates/document/app/pages/docs/components/doc-search.vue +131 -0
  30. package/templates/document/app/pages/docs/components/doc-sidebar.vue +29 -0
  31. package/templates/document/app/pages/docs/components/doc-toc.vue +21 -0
  32. package/templates/document/app/pages/docs/content.js +28 -0
  33. package/templates/document/app/pages/docs/docs-config.js +154 -0
  34. package/templates/document/app/pages/docs/docs.vue +24 -0
  35. package/templates/document/app/pages/docs/entry.docs.js +26 -0
  36. package/templates/document/app/pages/docs/markdown/highlight.js +30 -0
  37. package/templates/document/app/pages/docs/markdown/index.js +129 -0
  38. package/templates/document/app/pages/docs/search.js +142 -0
  39. package/templates/document/app/pages/docs/styles/docs.less +1091 -0
  40. package/templates/document/app/pages/docs/styles/vars.less +87 -0
  41. package/templates/document/app/pages/docs/theme.js +47 -0
  42. package/templates/document/app/pages/docs/utils.js +58 -0
  43. package/templates/document/app/pages/docs/views/doc-home.vue +56 -0
  44. package/templates/document/app/pages/docs/views/doc-page.vue +147 -0
  45. package/templates/document/app/webpack.config.js +15 -0
  46. package/templates/document/build.js +6 -0
  47. package/templates/document/config/config.beta.js +2 -0
  48. package/templates/document/config/config.default.js +9 -0
  49. package/templates/document/config/config.local.js +2 -0
  50. package/templates/document/config/config.prod.js +2 -0
  51. package/templates/document/docs/advanced/dashboard.md +66 -0
  52. package/templates/document/docs/advanced/health.md +72 -0
  53. package/templates/document/docs/advanced/monitoring.md +60 -0
  54. package/templates/document/docs/advanced/security.md +88 -0
  55. package/templates/document/docs/core/app-instance.md +114 -0
  56. package/templates/document/docs/core/controller-service.md +113 -0
  57. package/templates/document/docs/core/lifecycle.md +60 -0
  58. package/templates/document/docs/core/middleware.md +83 -0
  59. package/templates/document/docs/core/plugins.md +77 -0
  60. package/templates/document/docs/core/router-schema.md +102 -0
  61. package/templates/document/docs/dsl/api-contract.md +88 -0
  62. package/templates/document/docs/dsl/extend.md +311 -0
  63. package/templates/document/docs/dsl/menu.md +101 -0
  64. package/templates/document/docs/dsl/model-project.md +122 -0
  65. package/templates/document/docs/dsl/overview.md +116 -0
  66. package/templates/document/docs/dsl/reference.md +175 -0
  67. package/templates/document/docs/dsl/schema-actions.md +135 -0
  68. package/templates/document/docs/dsl/schema.md +121 -0
  69. package/templates/document/docs/frontend/build.md +118 -0
  70. package/templates/document/docs/frontend/curl.md +75 -0
  71. package/templates/document/docs/frontend/page.md +99 -0
  72. package/templates/document/docs/frontend/widgets.md +151 -0
  73. package/templates/document/docs/guide/config.md +108 -0
  74. package/templates/document/docs/guide/deployment.md +115 -0
  75. package/templates/document/docs/guide/getting-started.md +199 -0
  76. package/templates/document/docs/guide/introduction.md +55 -0
  77. package/templates/document/docs/guide/structure.md +98 -0
  78. package/templates/document/docs/reference/commands.md +70 -0
  79. package/templates/document/docs/reference/faq.md +94 -0
  80. package/templates/document/package.json +36 -0
  81. package/templates/document/scripts/build-static.js +129 -0
  82. package/templates/document/server.js +13 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # create-lumfall
2
+
3
+ [lumfall](https://www.npmjs.com/package/lumfall) 应用脚手架:一条命令生成可直接运行的
4
+ 完整工程,不用安装依赖后再手动搭目录。
5
+
6
+ ```sh
7
+ pnpm create lumfall my-app # 交互式选择模板
8
+ pnpm create lumfall my-app -t basic # 基础业务项目
9
+ pnpm create lumfall my-doc -t document # 技术文档站
10
+
11
+ # npm / npx 同样可用
12
+ npm create lumfall my-app
13
+ npx create-lumfall my-app -t basic
14
+ ```
15
+
16
+ ## 模板
17
+
18
+ | 模板 | 说明 |
19
+ | --- | --- |
20
+ | `basic` | 基础业务项目:`server.js` / `build.js` / `config/` / `app/` 全套目录约定 + 示例 API 与示例页面(来自 `lumfall-basic-project`) |
21
+ | `document` | 技术文档站:对标 VitePress(导航/侧栏/TOC/搜索/暗色模式),内置 lumfall 技术文档内容(来自 `lumfall-document`) |
22
+
23
+ 生成后:
24
+
25
+ ```sh
26
+ cd my-app
27
+ pnpm install
28
+ pnpm start:dev # 本地开发(前端 HMR + 服务)
29
+ pnpm start:prod # 生产构建 + 启动
30
+ ```
31
+
32
+ 脚手架会把模板里的自指名称(package.json / server.js / config 中的应用名)替换为你
33
+ 传入的项目名。模板内容与框架版本解耦:模板 `package.json` 里的 `lumfall` 用的是
34
+ semver 范围,安装时取最新发布版。
35
+
36
+ ## 维护
37
+
38
+ 模板源是本工作区同级的 `lumfall-basic-project/` 与 `lumfall-document/` 两个项目。
39
+ 模板内容变更后,在本目录执行:
40
+
41
+ ```sh
42
+ pnpm sync # 重新拷贝(自动排除 node_modules / 构建产物 / logs / .git / pnpm-lock.yaml)
43
+ npm publish # 发布新版
44
+ ```
package/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-lumfall —— lumfall 应用脚手架。
4
+ *
5
+ * 用法:
6
+ * pnpm create lumfall [项目名] [-t basic|document]
7
+ * npm create lumfall [项目名] [-t basic|document]
8
+ * npx create-lumfall [项目名] [-t basic|document]
9
+ *
10
+ * 零依赖:参数缺省时用 readline 交互补齐;模板内嵌在本包 templates/ 下,
11
+ * 生成即可 pnpm install && pnpm start:dev,不需要手动建目录。
12
+ */
13
+ const fs = require("fs");
14
+ const os = require("os");
15
+ const path = require("path");
16
+ const readline = require("readline");
17
+
18
+ const TEMPLATES = [
19
+ {
20
+ key: "basic",
21
+ alias: "b",
22
+ name: "基础业务项目",
23
+ desc: "目录约定 + 示例 API + 示例页面,一般业务从这里起步",
24
+ selfName: "lumfall-basic-project",
25
+ },
26
+ {
27
+ key: "document",
28
+ alias: "d",
29
+ name: "技术文档站",
30
+ desc: "对标 VitePress 的文档站模板(导航/侧栏/搜索/暗色模式),内置 lumfall 文档内容",
31
+ selfName: "lumfall-document",
32
+ },
33
+ ];
34
+
35
+ const NAME_PATTERN = /^[a-z][a-z0-9-_]*$/i;
36
+
37
+ function ask(rl, question) {
38
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
39
+ }
40
+
41
+ /**
42
+ * 统一问答入口:TTY 用 readline 逐行交互;
43
+ * 非交互(管道/CI)一次性读入 stdin,按行作为答案,避免 EOF 导致挂起。
44
+ */
45
+ function createPrompt() {
46
+ if (process.stdin.isTTY) {
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ return {
49
+ question: (question) => ask(rl, question),
50
+ close: () => rl.close(),
51
+ };
52
+ }
53
+
54
+ let buffered = null;
55
+ let cursor = 0;
56
+ return {
57
+ question: (question) => {
58
+ if (buffered === null) {
59
+ try {
60
+ buffered = fs.readFileSync(0, "utf8").split(/\r?\n/).map((line) => line.trim());
61
+ } catch (e) {
62
+ buffered = [];
63
+ }
64
+ }
65
+ process.stdout.write(question);
66
+ const answer = buffered[cursor++] ?? "";
67
+ process.stdout.write(`${answer}\n`);
68
+ return Promise.resolve(answer);
69
+ },
70
+ close: () => {},
71
+ };
72
+ }
73
+
74
+ function log(message) {
75
+ process.stdout.write(`${message}\n`);
76
+ }
77
+
78
+ function printHelp() {
79
+ log(`create-lumfall —— lumfall 应用脚手架
80
+
81
+ 用法:
82
+ pnpm create lumfall [项目名] [选项]
83
+ npx create-lumfall [项目名] [选项]
84
+
85
+ 选项:
86
+ -t, --template <basic|document> 模板类型(basic=基础业务项目, document=技术文档站)
87
+ -h, --help 显示帮助
88
+ -v, --version 显示版本
89
+
90
+ 示例:
91
+ pnpm create lumfall my-app # 交互式选择模板
92
+ pnpm create lumfall my-app -t document # 直接生成文档站
93
+ npx create-lumfall admin -t basic`);
94
+ }
95
+
96
+ function resolveTemplate(flagValue) {
97
+ if (!flagValue) return null;
98
+ const value = String(flagValue).toLowerCase();
99
+ return TEMPLATES.find((t) => t.key === value || t.alias === value) || null;
100
+ }
101
+
102
+ /** 目录非空时覆盖确认;返回是否继续 */
103
+ async function confirmOverwrite(prompt, targetDir) {
104
+ const entries = fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : [];
105
+ if (entries.length === 0) return true;
106
+
107
+ const answer = await prompt.question(`目录 "${targetDir}" 非空,是否覆盖写入?(y/N) `);
108
+ return answer.toLowerCase() === "y";
109
+ }
110
+
111
+ /** 把模板内的自指名称替换为用户项目名(package.json / server.js / config / README) */
112
+ function rewriteSelfName(targetDir, template, projectName) {
113
+ const files = [
114
+ "package.json",
115
+ "server.js",
116
+ path.join("config", "config.default.js"),
117
+ "README.md",
118
+ ];
119
+ for (const relative of files) {
120
+ const file = path.join(targetDir, relative);
121
+ if (!fs.existsSync(file)) continue;
122
+ const content = fs.readFileSync(file, "utf8");
123
+ fs.writeFileSync(file, content.split(template.selfName).join(projectName));
124
+ }
125
+ }
126
+
127
+ async function main() {
128
+ const argv = process.argv.slice(2);
129
+ const flags = { template: null };
130
+
131
+ const positional = [];
132
+ for (let i = 0; i < argv.length; i++) {
133
+ const arg = argv[i];
134
+ if (arg === "-h" || arg === "--help") {
135
+ printHelp();
136
+ return;
137
+ }
138
+ if (arg === "-v" || arg === "--version") {
139
+ log(require("./package.json").version);
140
+ return;
141
+ }
142
+ if (arg === "-t" || arg === "--template") {
143
+ flags.template = argv[++i];
144
+ continue;
145
+ }
146
+ if (arg.startsWith("-")) {
147
+ throw new Error(`未知选项 "${arg}"(--help 查看用法)`);
148
+ }
149
+ positional.push(arg);
150
+ }
151
+
152
+ const prompt = createPrompt();
153
+ try { // 1. 项目名
154
+ let projectName = positional[0];
155
+ if (!projectName) {
156
+ projectName = await prompt.question("项目名称(用作目录名,如 my-app): ");
157
+ }
158
+ projectName = projectName.replace(/\s+/g, "-").toLowerCase();
159
+ if (!NAME_PATTERN.test(projectName)) {
160
+ throw new Error(`项目名 "${projectName}" 不合法:字母开头,只能含字母/数字/-/_`);
161
+ }
162
+
163
+ // 2. 模板
164
+ let template = resolveTemplate(flags.template);
165
+ if (!template) {
166
+ log("\n选择模板:");
167
+ TEMPLATES.forEach((item, index) => {
168
+ log(` ${index + 1}. ${item.name}(${item.key})—— ${item.desc}`);
169
+ });
170
+ const choice = await prompt.question("输入编号 [1]: ");
171
+ const index = (parseInt(choice, 10) || 1) - 1;
172
+ template = TEMPLATES[index];
173
+ if (!template) {
174
+ throw new Error(`无效的模板编号 "${choice}"`);
175
+ }
176
+ }
177
+
178
+ // 3. 目标目录
179
+ const targetDir = path.resolve(process.cwd(), projectName);
180
+ if (!(await confirmOverwrite(prompt, targetDir))) {
181
+ log("已取消。");
182
+ return;
183
+ }
184
+
185
+ // 4. 拷贝模板 + 重写项目名
186
+ const templateDir = path.join(__dirname, "templates", template.key);
187
+ if (!fs.existsSync(templateDir)) {
188
+ throw new Error(`模板缺失: ${template.key}(包可能未完整发布)`);
189
+ }
190
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
191
+ fs.cpSync(templateDir, targetDir, { recursive: true });
192
+ rewriteSelfName(targetDir, template, projectName);
193
+
194
+ log(`
195
+ ✔ 已创建 ${template.name} 项目: ${projectName}
196
+ 模板: ${template.key} 位置: ${path.relative(os.homedir(), targetDir) || targetDir}
197
+
198
+ 下一步:
199
+ cd ${projectName}
200
+ pnpm install
201
+ pnpm start:dev # 本地开发(前端 HMR + 服务)
202
+ pnpm start:prod # 生产构建 + 启动
203
+ ${template.key === "document"
204
+ ? `
205
+ 文档站入口: http://localhost:3000/view/docs(站点配置在 app/pages/docs/docs-config.js)`
206
+ : `
207
+ 示例页面: http://localhost:3000/view/home(示例 API 在 app/ 下按目录约定组织)`}
208
+
209
+ 详细用法见项目内 README.md。`);
210
+ } finally {
211
+ prompt.close();
212
+ }
213
+ }
214
+
215
+ main().catch((error) => {
216
+ log(`[create-lumfall] ${error.message}`);
217
+ process.exitCode = 1;
218
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "create-lumfall",
3
+ "version": "1.0.0",
4
+ "description": "lumfall 应用脚手架:一条命令生成可运行的基础业务项目或技术文档站(pnpm create lumfall my-app)",
5
+ "bin": {
6
+ "create-lumfall": "cli.js"
7
+ },
8
+ "files": [
9
+ "cli.js",
10
+ "templates/",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "sync": "node scripts/sync-templates.js"
15
+ },
16
+ "keywords": [
17
+ "lumfall",
18
+ "create",
19
+ "scaffold",
20
+ "koa",
21
+ "vue",
22
+ "fullstack"
23
+ ],
24
+ "author": "lggbond",
25
+ "license": "ISC",
26
+ "engines": {
27
+ "node": ">=16.7.0"
28
+ }
29
+ }
@@ -0,0 +1,113 @@
1
+ # lumfall-basic-project
2
+
3
+ 基于 [lumfall](https://www.npmjs.com/package/lumfall) 的基础业务项目骨架。
4
+ 克隆或复制本目录,重命名后即可开始业务开发,不必从零搭建工程。
5
+
6
+ ## 目录结构
7
+
8
+ ```text
9
+ lumfall-basic-project/
10
+ ├── server.js # 服务端入口:serviceStart()
11
+ ├── build.js # 前端构建入口:frontendBuild(_ENV)
12
+ ├── package.json
13
+ ├── config/
14
+ │ ├── config.default.js # 全环境基础配置
15
+ │ ├── config.local.js # _ENV=local 覆盖(可选)
16
+ │ ├── config.beta.js # _ENV=beta 覆盖(可选)
17
+ │ └── config.prod.js # _ENV=prod 覆盖(可选)
18
+ └── app/
19
+ ├── middleware.js # 业务全局中间件注册入口
20
+ ├── middleware/ # 可复用中间件 → app.middlewares.<dir>.<name>
21
+ ├── controller/ # 示例:demo.js → app.controllers.demo
22
+ ├── service/ # 示例:demo.js → app.services.demo
23
+ ├── router/ # 路由注册(URL → controller 方法)
24
+ ├── router-schema/ # API 参数 JSON Schema(Ajv 校验)
25
+ ├── extend/ # 扩展点:返回值直接挂到 app 上
26
+ ├── pages/
27
+ │ └── home/ # 示例页面,访问 /view/home
28
+ │ ├── entry.home.js # 页面入口(命名必须是 entry.<page-name>.js)
29
+ │ └── home.vue
30
+ └── webpack.config.js # Webpack 扩展配置(与框架配置 merge.smart 合并)
31
+ ```
32
+
33
+ 框架通过 npm 依赖 `lumfall` 引入,业务代码只写在本目录,不要修改
34
+ `node_modules/lumfall` 里的框架本体。
35
+
36
+ ## 常用命令
37
+
38
+ ```sh
39
+ pnpm install # 安装依赖
40
+
41
+ pnpm start:dev # 本地开发:webpack dev server(HMR)+ 服务
42
+ pnpm build:prod && pnpm prod # 生产构建 + 启动
43
+ pnpm new-page user-list # 新建页面 app/pages/user-list/,访问 /view/user-list
44
+ pnpm new-page user-list --header # 新建页面并套用框架的 HeaderContainer 布局
45
+ ```
46
+
47
+ 环境由 `_ENV` 区分(`local` / `beta` / `prod`,缺省 `local`),不是 `NODE_ENV`。
48
+
49
+ ## 示例包含什么
50
+
51
+ 骨架自带一套最小但完整的示例,覆盖最常见的开发路径:
52
+
53
+ | 内容 | 位置 | 说明 |
54
+ | --- | --- | --- |
55
+ | 页面 | `app/pages/home/` | 访问 `/view/home`,演示 `$lumfallCurl` 调接口、Arco 组件 |
56
+ | 读配置的接口 | `GET /api/demo/info` | 返回 `config` 合并结果,改 `config.<env>.js` 即可看到变化 |
57
+ | 分页列表接口 | `GET /api/demo/note/list` | 演示 query 参数 + router-schema 校验 |
58
+ | 创建接口 | `POST /api/demo/note` | 演示 body 参数 + 校验失败返回 code 442 |
59
+
60
+ 新增一个 API 只需四步:`app/service/xxx.js` → `app/controller/xxx.js` →
61
+ `app/router/xxx.js` → `app/router-schema/xxx.js`,目录与挂载约定见下表。
62
+
63
+ ## 目录约定与挂载点
64
+
65
+ 文件 / 目录名用 `kebab-case` 或 `snake_case`,加载后自动转 `camelCase`。
66
+
67
+ | 业务目录 | 导出约定 | 挂载结果 |
68
+ | --- | --- | --- |
69
+ | `app/middleware/**/*.js` | `(app) => (ctx, next) => {}` | `app.middlewares.<dir>.<name>` |
70
+ | `app/controller/**/*.js` | `(app) => class` | `app.controllers.<dir>.<name>`,启动时实例化 |
71
+ | `app/service/**/*.js` | `(app) => class` | `app.services.<dir>.<name>`,启动时实例化 |
72
+ | `app/extend/**/*.js` | `(app) => object` | 直接挂到 `app`,例如 `app.logger` |
73
+ | `app/router/**/*.js` | `(app, router) => {}` | 注册路由到 `app.router` |
74
+ | `app/router-schema/**/*.js` | schema 对象或 `(app) => map` | 合并进 `app.routerSchema` |
75
+
76
+ controller / service 继承基类后可用:`this.services`(= `app.services`)、
77
+ `this.config`(= `app.config`,仅请求阶段读取),以及统一响应
78
+ `this.success(ctx, data, metadata)` / `this.fail(ctx, message, code)`。
79
+
80
+ ## 配置
81
+
82
+ 四层浅合并,后者覆盖前者同名键:
83
+
84
+ ```text
85
+ 框架 config.default -> 业务 config.default -> 框架 config.<env> -> 业务 config.<env>
86
+ ```
87
+
88
+ - 需要强约束时在 `server.js` 传 `configSchema`(JSON Schema),不匹配直接启动失败
89
+ - 需要强校验时同样可传 `lifecycle`(启动/停止 hook)、`plugins`(数据库等前置能力)、
90
+ `monitoring`(请求级观测),用法见框架文档
91
+
92
+ ## 常见注意点
93
+
94
+ 1. 服务与构建都必须在本目录(业务根目录 = `process.cwd()`)下执行
95
+ 2. 业务代码里取业务路径用 `app.businessPath`,不要用 `__dirname`
96
+ 3. `frontendBuild` 只认 `_ENV=local`(webpack dev server)和 `_ENV=prod`(产物构建)
97
+ 4. 完全未命中路由会 302 到 `server.js` 里配置的 `homePath`
98
+ 5. `/health/live`、`/health/ready` 是框架内置健康检查;业务依赖探针通过
99
+ `app/extend/` 注册到 `app.health`
100
+ 6. 生产环境建议开启 `config.security.apiSignature` 并把 `secret` 放到环境变量
101
+ 7. 框架共享依赖(`vue`、`@arco-design/web-vue`、`vue-router`、`pinia`、
102
+ `@babel/runtime`、`lodash`、`axios` 等)可直接 import,由框架
103
+ `resolve.alias` 白名单解析(需要 lumfall ≥ 1.1.1),无需重复安装,
104
+ 运行时也只有一份实例。白名单之外的库先 `pnpm add xxx` 再使用;
105
+ `_` 与 `axios` 另有 webpack 全局注入,页面代码不 import 也能用
106
+
107
+ ## 下一步
108
+
109
+ - 需要 B 端管理台(登录、Dashboard、Schema 组件、菜单管理)时,参考同工作区的
110
+ `lumfall-business/`,它演示了 `model/`(Dashboard 的 Model + Project 配置)、
111
+ schema 表格/表单/搜索栏等开箱组件的用法
112
+ - 需要技术文档站模板时,参考同工作区的 `lumfall-document/`
113
+ - 框架完整能力(安全策略、生命周期、插件、诊断清单等)见 lumfall 技术文档
@@ -0,0 +1,28 @@
1
+ // 示例 controller:演示「工厂返回 class、继承 Controller.Base」的约定。
2
+ // 文件名 demo.js + 无子目录 → 挂载到 app.controllers.demo
3
+ module.exports = (app) => {
4
+ const BaseController = require("lumfall").Controller.Base(app);
5
+
6
+ return class DemoController extends BaseController {
7
+ // 通过 this.services(= app.services)在请求阶段取 service,安全
8
+ async getInfo(ctx) {
9
+ const { demo: demoService } = this.services;
10
+ await this.success(ctx, demoService.getInfo());
11
+ }
12
+
13
+ async getNoteList(ctx) {
14
+ const { demo: demoService } = this.services;
15
+ const { data, total, page, size } = demoService.getNoteList({
16
+ page: Number(ctx.request.query.page) || 1,
17
+ size: Number(ctx.request.query.pageSize) || 10,
18
+ });
19
+ await this.success(ctx, data, { total, page, size });
20
+ }
21
+
22
+ async createNote(ctx) {
23
+ const { demo: demoService } = this.services;
24
+ const note = demoService.createNote(ctx.request.body);
25
+ await this.success(ctx, note);
26
+ }
27
+ };
28
+ };
@@ -0,0 +1,3 @@
1
+ // 扩展点目录:每个文件导出 (app) => object,返回值直接挂到 app 上。
2
+ // 例如文件 cache.js 导出 (app) => ({ get, set }),即可通过 app.cache 使用。
3
+ // 框架已占用:app.logger、app.health、app.env 等,重名会被跳过并告警。
@@ -0,0 +1,2 @@
1
+ // 可复用中间件目录:每个文件导出 (app) => (ctx, next) => {},
2
+ // 自动挂载到 app.middlewares.<目录名>.<文件名>(kebab/snake-case 自动转 camelCase)。
@@ -0,0 +1,13 @@
1
+ // 业务全局中间件注册入口。
2
+ // 执行时机在框架全局中间件之后(static → nunjucks → bodyParser → errorHandler
3
+ // → monitoring → apiParamsVerify → securityPolicy),因此这里注册的中间件
4
+ // 位于框架中间件链的内层。
5
+ // 可复用中间件放在 app/middleware/ 目录,会自动挂到 app.middlewares.<dir>.<name>。
6
+ module.exports = (app) => {
7
+ // 示例:简单请求日志(生产观测更推荐 serviceStart 的 monitoring 配置)
8
+ // app.use(async (ctx, next) => {
9
+ // const start = Date.now();
10
+ // await next();
11
+ // console.log(`${ctx.method} ${ctx.path} ${ctx.status} ${Date.now() - start}ms`);
12
+ // });
13
+ };
@@ -0,0 +1,4 @@
1
+ import boot from "$lumfallBoot";
2
+ import Home from "./home.vue";
3
+
4
+ boot(Home);
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <main class="home-page">
3
+ <section class="hero">
4
+ <h1 class="hero-title">Lumfall Basic Project</h1>
5
+ <p class="hero-desc">
6
+ 基于 lumfall 的基础业务项目骨架。这个页面演示了:页面入口约定、
7
+ $lumfallCurl 调用 /api 接口、参数校验与统一响应结构。
8
+ </p>
9
+ <div class="hero-links">
10
+ <a-button type="primary" @click="goHealth">健康检查页</a-button>
11
+ <a-button @click="loadData">刷新数据</a-button>
12
+ </div>
13
+ </section>
14
+
15
+ <a-card class="panel" title="服务端配置(GET /api/demo/info)">
16
+ <a-descriptions :column="1" size="medium" v-if="info">
17
+ <a-descriptions-item label="应用名">{{ info.appName }}</a-descriptions-item>
18
+ <a-descriptions-item label="demoMessage">
19
+ <a-tag color="arcoblue">{{ info.demoMessage }}</a-tag>
20
+ </a-descriptions-item>
21
+ <a-descriptions-item label="服务端时间">{{ info.serverTime }}</a-descriptions-item>
22
+ </a-descriptions>
23
+ </a-card>
24
+
25
+ <a-card class="panel" title="笔记列表(GET /api/demo/note/list)">
26
+ <template #extra>
27
+ <a-space>
28
+ <a-input-search
29
+ v-model="newNote"
30
+ placeholder="输入内容,回车新建笔记"
31
+ style="width: 260px"
32
+ @search="handleCreate"
33
+ allow-clear
34
+ />
35
+ </a-space>
36
+ </template>
37
+
38
+ <a-table
39
+ :data="noteList"
40
+ :loading="loading"
41
+ :pagination="pagination"
42
+ size="medium"
43
+ >
44
+ <template #columns>
45
+ <a-table-column title="ID" data-index="id" :width="80" />
46
+ <a-table-column title="内容" data-index="content" />
47
+ <a-table-column title="创建时间" data-index="createdAt" :width="200" />
48
+ </template>
49
+ </a-table>
50
+ </a-card>
51
+ </main>
52
+ </template>
53
+
54
+ <script setup>
55
+ import { computed, onMounted, ref } from "vue";
56
+ import { Message } from "@arco-design/web-vue";
57
+ import $curl from "$lumfallCurl";
58
+
59
+ const info = ref(null);
60
+ const noteList = ref([]);
61
+ const loading = ref(false);
62
+ const newNote = ref("");
63
+ const total = ref(0);
64
+ const currentPage = ref(1);
65
+ const pageSize = ref(10);
66
+
67
+ const pagination = computed(() => ({
68
+ total: total.value,
69
+ current: currentPage.value,
70
+ pageSize: pageSize.value,
71
+ showTotal: true,
72
+ }));
73
+
74
+ async function loadInfo() {
75
+ const res = await $curl({ method: "get", url: "/api/demo/info" });
76
+ if (res && res.success) {
77
+ info.value = res.data;
78
+ }
79
+ }
80
+
81
+ async function loadNoteList() {
82
+ loading.value = true;
83
+ const res = await $curl({
84
+ method: "get",
85
+ url: "/api/demo/note/list",
86
+ query: {
87
+ page: String(currentPage.value),
88
+ pageSize: String(pageSize.value),
89
+ },
90
+ });
91
+ loading.value = false;
92
+ if (res && res.success) {
93
+ noteList.value = res.data;
94
+ total.value = res.metadata.total;
95
+ }
96
+ }
97
+
98
+ async function handleCreate() {
99
+ const content = newNote.value.trim();
100
+ if (!content) {
101
+ Message.warning("请输入笔记内容");
102
+ return;
103
+ }
104
+
105
+ const res = await $curl({
106
+ method: "post",
107
+ url: "/api/demo/note",
108
+ data: { content },
109
+ });
110
+ if (res && res.success) {
111
+ newNote.value = "";
112
+ Message.success("创建成功");
113
+ currentPage.value = 1;
114
+ await loadNoteList();
115
+ }
116
+ }
117
+
118
+ function loadData() {
119
+ loadInfo();
120
+ loadNoteList();
121
+ }
122
+
123
+ function goHealth() {
124
+ window.location.href = "/view/health";
125
+ }
126
+
127
+ onMounted(loadData);
128
+ </script>
129
+
130
+ <style lang="less" scoped>
131
+ .home-page {
132
+ box-sizing: border-box;
133
+ max-width: 960px;
134
+ margin: 0 auto;
135
+ padding: 48px 24px 64px;
136
+
137
+ .hero {
138
+ margin-bottom: 32px;
139
+
140
+ .hero-title {
141
+ margin: 0 0 12px;
142
+ font-size: 32px;
143
+ font-weight: 700;
144
+ color: var(--color-text-1);
145
+ }
146
+
147
+ .hero-desc {
148
+ margin: 0 0 20px;
149
+ font-size: 15px;
150
+ line-height: 1.8;
151
+ color: var(--color-text-2);
152
+ }
153
+
154
+ .hero-links {
155
+ display: flex;
156
+ gap: 12px;
157
+ }
158
+ }
159
+
160
+ .panel {
161
+ margin-bottom: 20px;
162
+ }
163
+ }
164
+ </style>
@@ -0,0 +1,17 @@
1
+ // 路由只负责把 URL 绑到 controller 方法。
2
+ // 注意绑定时要 .bind(controller),否则方法内的 this 会丢失。
3
+ module.exports = (app, router) => {
4
+ const { demo: demoController } = app.controllers;
5
+
6
+ router.get("/api/demo/info", demoController.getInfo.bind(demoController));
7
+
8
+ router.get(
9
+ "/api/demo/note/list",
10
+ demoController.getNoteList.bind(demoController)
11
+ );
12
+
13
+ router.post(
14
+ "/api/demo/note",
15
+ demoController.createNote.bind(demoController)
16
+ );
17
+ };
@@ -0,0 +1,29 @@
1
+ // API 参数校验:key 必须是已注册路由的 path,method 必须全小写,否则启动失败。
2
+ // 只作用于 /api 开头的请求;校验失败返回 HTTP 200 + { success: false, code: 442 }。
3
+ module.exports = {
4
+ "/api/demo/info": {
5
+ get: {},
6
+ },
7
+ "/api/demo/note/list": {
8
+ get: {
9
+ query: {
10
+ type: "object",
11
+ properties: {
12
+ page: { type: "string" },
13
+ pageSize: { type: "string" },
14
+ },
15
+ },
16
+ },
17
+ },
18
+ "/api/demo/note": {
19
+ post: {
20
+ body: {
21
+ type: "object",
22
+ properties: {
23
+ content: { type: "string", minLength: 1, maxLength: 200 },
24
+ },
25
+ required: ["content"],
26
+ },
27
+ },
28
+ },
29
+ };