create-lumfall 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +24 -17
  2. package/cli.js +177 -22
  3. package/package.json +5 -7
  4. package/templates/basic/README.md +0 -113
  5. package/templates/basic/app/controller/demo.js +0 -28
  6. package/templates/basic/app/extend/README.md +0 -3
  7. package/templates/basic/app/middleware/README.md +0 -2
  8. package/templates/basic/app/middleware.js +0 -13
  9. package/templates/basic/app/pages/home/entry.home.js +0 -4
  10. package/templates/basic/app/pages/home/home.vue +0 -164
  11. package/templates/basic/app/router/demo.js +0 -17
  12. package/templates/basic/app/router-schema/demo.js +0 -29
  13. package/templates/basic/app/service/demo.js +0 -51
  14. package/templates/basic/app/webpack.config.js +0 -4
  15. package/templates/basic/build.js +0 -7
  16. package/templates/basic/config/config.beta.js +0 -4
  17. package/templates/basic/config/config.default.js +0 -32
  18. package/templates/basic/config/config.local.js +0 -4
  19. package/templates/basic/config/config.prod.js +0 -6
  20. package/templates/basic/package.json +0 -31
  21. package/templates/basic/server.js +0 -26
  22. package/templates/document/README.md +0 -58
  23. package/templates/document/app/extend/README.md +0 -6
  24. package/templates/document/app/middleware/README.md +0 -2
  25. package/templates/document/app/middleware.js +0 -2
  26. package/templates/document/app/pages/docs/assets/docs-logo.svg +0 -5
  27. package/templates/document/app/pages/docs/components/doc-layout.vue +0 -144
  28. package/templates/document/app/pages/docs/components/doc-navbar.vue +0 -103
  29. package/templates/document/app/pages/docs/components/doc-search.vue +0 -131
  30. package/templates/document/app/pages/docs/components/doc-sidebar.vue +0 -29
  31. package/templates/document/app/pages/docs/components/doc-toc.vue +0 -21
  32. package/templates/document/app/pages/docs/content.js +0 -28
  33. package/templates/document/app/pages/docs/docs-config.js +0 -68
  34. package/templates/document/app/pages/docs/docs.vue +0 -24
  35. package/templates/document/app/pages/docs/entry.docs.js +0 -26
  36. package/templates/document/app/pages/docs/markdown/highlight.js +0 -30
  37. package/templates/document/app/pages/docs/markdown/index.js +0 -129
  38. package/templates/document/app/pages/docs/search.js +0 -142
  39. package/templates/document/app/pages/docs/styles/docs.less +0 -1091
  40. package/templates/document/app/pages/docs/styles/vars.less +0 -87
  41. package/templates/document/app/pages/docs/theme.js +0 -47
  42. package/templates/document/app/pages/docs/utils.js +0 -58
  43. package/templates/document/app/pages/docs/views/doc-home.vue +0 -56
  44. package/templates/document/app/pages/docs/views/doc-page.vue +0 -147
  45. package/templates/document/app/webpack.config.js +0 -15
  46. package/templates/document/build.js +0 -6
  47. package/templates/document/config/config.beta.js +0 -2
  48. package/templates/document/config/config.default.js +0 -9
  49. package/templates/document/config/config.local.js +0 -2
  50. package/templates/document/config/config.prod.js +0 -2
  51. package/templates/document/docs/guide/example.md +0 -41
  52. package/templates/document/docs/guide/introduction.md +0 -31
  53. package/templates/document/package.json +0 -36
  54. package/templates/document/scripts/build-static.js +0 -129
  55. package/templates/document/server.js +0 -13
@@ -1,164 +0,0 @@
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>
@@ -1,17 +0,0 @@
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
- };
@@ -1,29 +0,0 @@
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
- };
@@ -1,51 +0,0 @@
1
- // 示例 service:演示「工厂返回 class、继承 BaseService」的约定。
2
- // 文件名 demo.js + 无子目录 → 挂载到 app.services.demo
3
- // 数据放在内存里,重启即清空;真实业务请换成数据库 / 远程服务调用。
4
- module.exports = (app) => {
5
- const BaseService = require("lumfall").Service.Base(app);
6
-
7
- // 内存数据池
8
- const notePool = [
9
- { id: 1, content: "第一条示例笔记:这是一个基于 lumfall 的基础项目骨架", createdAt: "2026-10-02 10:00:00" },
10
- { id: 2, content: "在 app/service 下新建文件即可被自动加载", createdAt: "2026-10-02 10:01:00" },
11
- { id: 3, content: "配置通过 config/config.<env>.js 按 _ENV 合并", createdAt: "2026-10-02 10:02:00" },
12
- ];
13
- let nextId = notePool.length + 1;
14
-
15
- return class DemoService extends BaseService {
16
- // 请求阶段读 this.config(= app.config)是安全的;
17
- // 不要在工厂执行期或构造期读,那时 configLoader 还没跑
18
- getInfo() {
19
- return {
20
- appName: this.config.name,
21
- demoMessage: this.config.demoMessage,
22
- serverTime: new Date().toISOString(),
23
- };
24
- }
25
-
26
- /**
27
- * 分页查询笔记
28
- * @param {{ page: number, size: number }} param0
29
- */
30
- getNoteList({ page = 1, size = 10 }) {
31
- const start = (page - 1) * size;
32
- const data = notePool.slice(start, start + size).map((note) => ({ ...note }));
33
-
34
- return { data, total: notePool.length, page, size };
35
- }
36
-
37
- /**
38
- * 新建笔记
39
- * @param {{ content: string }} param0
40
- */
41
- createNote({ content }) {
42
- const note = {
43
- id: nextId++,
44
- content,
45
- createdAt: new Date().toISOString().slice(0, 19).replace("T", " "),
46
- };
47
- notePool.unshift(note);
48
- return note;
49
- }
50
- };
51
- };
@@ -1,4 +0,0 @@
1
- // 业务 Webpack 扩展配置:与框架基础配置 webpack-merge.merge.smart 合并。
2
- // 例如给 .md 文件加 raw 导入:
3
- // module.exports = { module: { rules: [{ test: /\.md$/, type: "asset/source" }] } };
4
- module.exports = {};
@@ -1,7 +0,0 @@
1
- const { frontendBuild } = require("lumfall");
2
-
3
- // 环境由 _ENV 决定(不是 NODE_ENV):
4
- // _ENV=local 启动 Webpack dev server(HMR 热更新,默认 127.0.0.1:9002)
5
- // _ENV=prod 产物构建到 app/public/dist/prod/,同时产出各页面 .tpl 模板
6
- // 其他值(如 beta)不执行任何构建
7
- frontendBuild(process.env._ENV);
@@ -1,4 +0,0 @@
1
- // _ENV=beta 时加载,覆盖 config.default.js 的同名键(浅合并)
2
- module.exports = {
3
- demoMessage: "hello from beta config",
4
- };
@@ -1,32 +0,0 @@
1
- // 所有环境共享的基础配置。合并顺序(后者覆盖前者同名键):
2
- // 框架 config.default -> 业务 config.default -> 框架 config.<env> -> 业务 config.<env>
3
- // 环境由 _ENV 决定(local / beta / prod,缺省 local),不是 NODE_ENV。
4
- module.exports = {
5
- // 应用名,与 server.js 的 name 保持一致
6
- name: "lumfall-basic-project",
7
-
8
- // API 路径前缀约定:框架的参数校验(apiParamsVerify)与安全策略(securityPolicy)
9
- // 只作用于 /api 开头的请求
10
- apiBasePath: "/api",
11
-
12
- // 示例配置:演示「配置如何从服务端流动到接口/页面」,各环境文件里都有同名键
13
- demoMessage: "hello from config.default",
14
-
15
- // 安全策略(可选)。不配置这段时,框架等价于「接口签名关闭 + projectKey 开启」
16
- security: {
17
- // 接口签名校验:开启后 /api 请求必须携带 s_sign = md5(secret + "_" + st)、
18
- // st = 毫秒时间戳(前端 $lumfallCurl 默认带的是 s_sign / s_t,均可识别),
19
- // 时间差超过 maxAgeMs 或时间戳在未来都会返回 code 445。
20
- // 生产开启时 secret 建议走环境变量 / 配置中心,不要提交到仓库。
21
- apiSignature: {
22
- enabled: false,
23
- maxAgeMs: 600000,
24
- },
25
- // project_key 校验:只作用于 /api/project/ 路径,缺 header 返回 code 446。
26
- // /api/project/model_list、/api/project/list 内置豁免,可用 freePaths 追加。
27
- projectKey: {
28
- enabled: true,
29
- headerName: "project_key",
30
- },
31
- },
32
- };
@@ -1,4 +0,0 @@
1
- // _ENV=local 时加载,覆盖 config.default.js 的同名键(浅合并)
2
- module.exports = {
3
- demoMessage: "hello from local config",
4
- };
@@ -1,6 +0,0 @@
1
- // _ENV=prod 时加载,覆盖 config.default.js 的同名键(浅合并)。
2
- // 生产相关的密钥、连接串建议从环境变量读取:
3
- // module.exports = { dbPassword: process.env.DB_PASSWORD };
4
- module.exports = {
5
- demoMessage: "hello from prod config",
6
- };
@@ -1,31 +0,0 @@
1
- {
2
- "name": "lumfall-basic-project",
3
- "version": "1.0.0",
4
- "description": "基于 lumfall 的基础业务项目骨架:目录约定、示例 API 与示例页面,开箱即用",
5
- "main": "server.js",
6
- "scripts": {
7
- "dev": "_ENV='local' nodemon --exitcrash server.js",
8
- "beta": "_ENV='beta' nodemon --exitcrash server.js",
9
- "prod": "_ENV='prod' node server.js",
10
- "build:dev": "_ENV='local' node --max_old_space_size=4096 ./build.js",
11
- "build:prod": "_ENV='prod' node ./build.js",
12
- "start:dev": "concurrently --kill-others-on-fail -n webpack,server -c cyan,green \"pnpm build:dev\" \"pnpm dev\"",
13
- "start:prod": "pnpm build:prod && pnpm prod",
14
- "new-page": "node ./node_modules/lumfall/scripts/generate-page.js"
15
- },
16
- "dependencies": {
17
- "lumfall": "^1.2.1"
18
- },
19
- "devDependencies": {
20
- "concurrently": "^10.0.5",
21
- "nodemon": "^3.1.14"
22
- },
23
- "packageManager": "pnpm@10.30.0",
24
- "keywords": [
25
- "lumfall",
26
- "koa",
27
- "vue",
28
- "fullstack"
29
- ],
30
- "license": "ISC"
31
- }
@@ -1,26 +0,0 @@
1
- const { serviceStart } = require("lumfall");
2
-
3
- const app = serviceStart({
4
- // 应用名:渲染页面模板 <title> 时使用
5
- name: "lumfall-basic-project",
6
-
7
- // 未命中任何路由时的 302 兜底目标。
8
- // 注意:serviceStart 一旦传入 options 对象,就不再套用框架默认值,
9
- // homePath 必须在这里显式声明,否则兜底重定向会退化为 "/"
10
- homePath: "/view/home",
11
-
12
- // 可选:配置强校验。传入 JSON Schema,合并后的配置不匹配会直接启动失败
13
- // configSchema: require("./config/config.schema.js"),
14
-
15
- // 可选:启动 / 停止 hook(beforeStart / beforeRouteLoad / afterRouteLoad /
16
- // afterStart / onError / beforeStop / afterStop),启动期 hook 必须同步
17
- // lifecycle: {},
18
-
19
- // 可选:插件(数据库、缓存等先于业务中间件/路由初始化的能力)
20
- // plugins: [],
21
-
22
- // 可选:请求级观测 hook(traceId / onRequestStart / onRequestEnd / onRequestError)
23
- // monitoring: {},
24
- });
25
-
26
- module.exports = app;
@@ -1,58 +0,0 @@
1
- # lumfall-document(空模板)
2
-
3
- 基于 [lumfall](https://www.npmjs.com/package/lumfall) 的技术文档站**空模板**:
4
- 导航、侧边栏、页面目录(TOC)、站内搜索(Ctrl/Cmd+K)、代码高亮与复制、
5
- 亮 / 暗主题、上一篇/下一篇、移动端适配全部就绪,生成后只需要写内容。
6
-
7
- > 这个分支(`template/empty`)是 create-lumfall 脚手架 `document` 模板的来源;
8
- > `main` 分支是自举的完整示例(内置 lumfall 技术文档)。
9
-
10
- ## 开始
11
-
12
- ```sh
13
- pnpm install
14
- pnpm start:dev # http://localhost:3000/view/docs
15
-
16
- pnpm build:static # 产出 dist-static/(Vercel 等静态托管可直接部署)
17
- ```
18
-
19
- ## 写内容
20
-
21
- 1. **内容**:`docs/` 下的 markdown 就是文档,目录结构随意(两级以内体验最佳)。
22
- 文档里可以互相引用,支持相对链接与锚点:
23
- ```markdown
24
- [其他页](./other.md) [上级目录](../advanced/x.md) [某节](./other.md#锚点)
25
- ```
26
- 2. **站点配置**:`app/pages/docs/docs-config.js` 是唯一需要动的配置文件——
27
- 站点名、导航、侧栏分组、首页 hero、页脚。侧栏条目的 `path` 对应
28
- `docs/` 下的文件路径(`/view/docs/<path>` ↔ `docs/<path>.md`)
29
- 3. **可选**:改主题色(`app/pages/docs/styles/vars.less` 的 `--doc-brand`
30
- 等变量)、换 logo(`app/pages/docs/assets/docs-logo.svg`)
31
-
32
- ## 功能清单
33
-
34
- | 功能 | 说明 |
35
- | --- | --- |
36
- | 首页 hero | 大标题 + 标语 + 按钮 + 特性卡片(配置驱动) |
37
- | 侧边栏 | 分组导航,当前页高亮,移动端抽屉 |
38
- | 页面目录 | 正文右侧 TOC,滚动联动高亮(<1280px 隐藏) |
39
- | 站内搜索 | `Ctrl/Cmd + K` 或 `/` 唤起;标题 / 小节 / 正文加权匹配,关键词高亮摘要,键盘导航 |
40
- | 代码块 | highlight.js 按需注册语言、语言标签、一键复制、亮暗配色 |
41
- | markdown 扩展 | `::: tip / info / warning / danger <可选标题>` 提示块、标题锚点、外链新窗口 |
42
- | 主题 | 亮 / 暗(右上角切换),首次进站跟随系统,localStorage 记忆 |
43
- | 上一篇/下一篇 | 按侧边栏顺序自动生成 |
44
- | 静态部署 | `pnpm build:static` 一键产出 `dist-static/`(vercel.json rewrite + 长缓存) |
45
-
46
- ## 写作约定
47
-
48
- - 每篇文档以 `# 一级标题` 开头(渲染为页面标题),章节用 `##` / `###`
49
- (自动进入右侧 TOC 与搜索索引)
50
- - 代码块标注语言(`js` / `vue` / `bash` / `json`…)才会高亮
51
- - 不支持数学公式、mermaid 图(markdown-it 未接插件,需要时可在
52
- `app/pages/docs/markdown/index.js` 扩展)
53
-
54
- ## 相关项目
55
-
56
- - `lumfall-basic-project/`:基础业务工程骨架
57
- - `lumfall-business/`:B 端全栈模板
58
- - `create-lumfall/`:应用脚手架(本模板通过它的 `-t document` 生成)
@@ -1,6 +0,0 @@
1
- // 扩展点目录:每个文件导出 (app) => object,返回值直接挂到 app 上。
2
- // 例如注册业务健康探针(文档站默认没有外部依赖,无需探针):
3
- // module.exports = (app) => {
4
- // app.health.register("database", async () => { /* ping */ });
5
- // return {};
6
- // };
@@ -1,2 +0,0 @@
1
- // 可复用中间件目录:每个文件导出 (app) => (ctx, next) => {},
2
- // 自动挂载到 app.middlewares.<目录名>.<文件名>(kebab/snake-case 自动转 camelCase)。
@@ -1,2 +0,0 @@
1
- // 业务全局中间件注册入口。文档站是纯静态站点,默认不需要额外中间件。
2
- module.exports = (app) => {};
@@ -1,5 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect x="2" y="2" width="28" height="28" rx="7" fill="#165dff"/>
3
- <path d="M10 9v11a3 3 0 0 0 3 3h9v-2h-9a1 1 0 0 1-1-1V9h-2z" fill="#fff"/>
4
- <path d="M13 9h9a1 1 0 0 1 1 1v8h-8a2 2 0 0 0-2 2V9z" fill="#9fd4ff"/>
5
- </svg>
@@ -1,144 +0,0 @@
1
- <template>
2
- <div class="doc-shell">
3
- <DocNavbar
4
- :show-sidebar-toggle="sidebar"
5
- @toggle-sidebar="drawerOpen = !drawerOpen"
6
- @open-search="searchOpen = true"
7
- />
8
-
9
- <div class="doc-body" @click="handleBodyClick">
10
- <DocSidebar v-if="sidebar" class="doc-desktop-sidebar" />
11
-
12
- <!-- 移动端抽屉侧栏 -->
13
- <Transition name="doc-fade">
14
- <div v-if="sidebar && drawerOpen" class="doc-drawer-mask" @click.self="drawerOpen = false">
15
- <DocSidebar class="doc-drawer-sidebar" @navigate="drawerOpen = false" />
16
- </div>
17
- </Transition>
18
-
19
- <main class="doc-main">
20
- <slot />
21
- </main>
22
- </div>
23
-
24
- <DocSearch v-model="searchOpen" />
25
- </div>
26
- </template>
27
-
28
- <script setup>
29
- import { onBeforeUnmount, onMounted, ref, watch } from "vue";
30
- import { useRoute, useRouter } from "vue-router";
31
- import DocNavbar from "./doc-navbar.vue";
32
- import DocSidebar from "./doc-sidebar.vue";
33
- import DocSearch from "./doc-search.vue";
34
-
35
- defineProps({
36
- // 是否显示侧栏(首页不显示)
37
- sidebar: { type: Boolean, default: true },
38
- });
39
-
40
- const route = useRoute();
41
- const router = useRouter();
42
- const drawerOpen = ref(false);
43
- const searchOpen = ref(false);
44
-
45
- // 路由切换时收起移动端抽屉
46
- watch(
47
- () => route.fullPath,
48
- () => {
49
- drawerOpen.value = false;
50
- }
51
- );
52
-
53
- // ---- 全局点击处理:复制代码 / 站内路由 / 锚点平滑滚动 ----
54
- const copyText = (text) => {
55
- if (navigator.clipboard && window.isSecureContext) {
56
- navigator.clipboard.writeText(text);
57
- return;
58
- }
59
- // 非安全上下文(http 部署)回退方案
60
- const textarea = document.createElement("textarea");
61
- textarea.value = text;
62
- textarea.style.position = "fixed";
63
- textarea.style.opacity = "0";
64
- document.body.appendChild(textarea);
65
- textarea.select();
66
- try {
67
- document.execCommand("copy");
68
- } catch (err) {
69
- // 忽略复制失败
70
- }
71
- document.body.removeChild(textarea);
72
- };
73
-
74
- const handleBodyClick = (e) => {
75
- // 1. 代码块复制按钮
76
- const copyBtn = e.target.closest(".doc-code-copy");
77
- if (copyBtn) {
78
- const block = copyBtn.closest(".doc-code-block");
79
- const pre = block && block.querySelector("pre");
80
- if (pre) {
81
- copyText(pre.textContent || "");
82
- copyBtn.textContent = "已复制";
83
- setTimeout(() => {
84
- copyBtn.textContent = "复制";
85
- }, 1500);
86
- }
87
- return;
88
- }
89
-
90
- // 2. 站内文档链接走 SPA 路由,避免整页刷新
91
- const link = e.target.closest("a");
92
- if (!link) {
93
- return;
94
- }
95
- const href = link.getAttribute("href") || "";
96
-
97
- if (href.startsWith("/view/docs")) {
98
- e.preventDefault();
99
- router.push(href);
100
- return;
101
- }
102
-
103
- // 3. 页内锚点:平滑滚动,滚动结束后再更新 URL。
104
- // 注意:带 fragment 的 history.replaceState 会取消 Chrome 正在进行的
105
- // 平滑滚动(无论先后调用),所以必须等 scrollend 之后再写 URL。
106
- if (href.startsWith("#") && href.length > 1) {
107
- const target = document.getElementById(decodeURIComponent(href.slice(1)));
108
- if (target) {
109
- e.preventDefault();
110
- let urlUpdated = false;
111
- const updateUrl = () => {
112
- if (urlUpdated) return;
113
- urlUpdated = true;
114
- history.replaceState(null, "", href);
115
- };
116
- target.scrollIntoView({ behavior: "smooth" });
117
- // 平滑滚动结束(scrollend 支持前用定时器兜底;目标已在视口内时不会滚动)
118
- window.addEventListener("scrollend", updateUrl, { once: true });
119
- setTimeout(updateUrl, 800);
120
- }
121
- }
122
- };
123
-
124
- // 全局快捷键:Ctrl/Cmd + K 或 / 唤起搜索(输入框内按 / 不触发)
125
- const onGlobalKeydown = (e) => {
126
- const isCtrlK = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k";
127
- const isSlash =
128
- e.key === "/" &&
129
- !["INPUT", "TEXTAREA"].includes(e.target.tagName) &&
130
- !e.target.isContentEditable;
131
- if (isCtrlK || isSlash) {
132
- e.preventDefault();
133
- searchOpen.value = true;
134
- }
135
- };
136
-
137
- onMounted(() => {
138
- window.addEventListener("keydown", onGlobalKeydown);
139
- });
140
-
141
- onBeforeUnmount(() => {
142
- window.removeEventListener("keydown", onGlobalKeydown);
143
- });
144
- </script>