@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,8 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ /** 服务自身的标识信息,由 createApp 注入,健康检查与根路由都会用到 */
5
+ export interface ApiInfo {
6
+ name: string
7
+ version: string
8
+ }
@@ -0,0 +1,216 @@
1
+ /* eslint-disable */
2
+ // ⚠️ 自动生成文件,请勿直接修改。
3
+ // 源:packages/api/src —— 改动请改源文件后运行 `pnpm sync:api-template`。
4
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
5
+ import request from 'supertest'
6
+ import type { Server } from 'node:http'
7
+ import { createApp } from '../src/app'
8
+ import { OPTIONS_TOTAL } from '../src/data/options'
9
+ import { REGION_NODE_COUNT } from '../src/data/regions'
10
+ import { USERS_TOTAL } from '../src/data/users'
11
+
12
+ let server: Server
13
+ let base: string
14
+
15
+ beforeAll(async () => {
16
+ const app = createApp({ name: 'test-api' })
17
+ await new Promise<void>((resolve) => {
18
+ server = app.listen(0, () => resolve())
19
+ })
20
+ const address = server.address()
21
+ if (address && typeof address === 'object') {
22
+ base = `http://127.0.0.1:${address.port}`
23
+ }
24
+ })
25
+
26
+ afterAll(() => {
27
+ server?.close()
28
+ })
29
+
30
+ describe('健康检查与根路由', () => {
31
+ it('GET /api/health 返回 ok 与服务标识', async () => {
32
+ const res = await request(base).get('/api/health')
33
+ expect(res.status).toBe(200)
34
+ expect(res.body.status).toBe('ok')
35
+ expect(res.body.name).toBe('test-api')
36
+ expect(typeof res.body.uptime).toBe('number')
37
+ })
38
+
39
+ it('GET /health 无前缀同样可用(容器探活)', async () => {
40
+ const res = await request(base).get('/health')
41
+ expect(res.status).toBe(200)
42
+ expect(res.body.status).toBe('ok')
43
+ })
44
+
45
+ it('GET / 列出可用接口', async () => {
46
+ const res = await request(base).get('/')
47
+ expect(res.status).toBe(200)
48
+ expect(res.body.prefix).toBe('/api')
49
+ expect(res.body.endpoints.length).toBeGreaterThan(0)
50
+ })
51
+ })
52
+
53
+ describe('GET /api/users 分页 / 排序 / 搜索', () => {
54
+ it('默认返回第 1 页 10 条,total 为全量', async () => {
55
+ const res = await request(base).get('/api/users')
56
+ expect(res.status).toBe(200)
57
+ expect(res.body.list).toHaveLength(10)
58
+ expect(res.body.total).toBe(USERS_TOTAL)
59
+ expect(res.body.page).toBe(1)
60
+ expect(res.body.pageSize).toBe(10)
61
+ expect(res.body.list[0].id).toBe(1)
62
+ })
63
+
64
+ it('翻页返回对应片段且不重叠', async () => {
65
+ const first = await request(base).get('/api/users?page=1&pageSize=20')
66
+ const second = await request(base).get('/api/users?page=2&pageSize=20')
67
+ expect(first.body.list).toHaveLength(20)
68
+ expect(second.body.list).toHaveLength(20)
69
+ expect(second.body.list[0].id).toBe(21)
70
+ const overlap = second.body.list.filter((row: { id: number }) =>
71
+ first.body.list.some((item: { id: number }) => item.id === row.id),
72
+ )
73
+ expect(overlap).toHaveLength(0)
74
+ })
75
+
76
+ it('pageSize 非法时回退默认值,page 越界返回空列表', async () => {
77
+ const fallback = await request(base).get('/api/users?pageSize=0')
78
+ expect(fallback.body.pageSize).toBe(10)
79
+
80
+ const beyond = await request(base).get('/api/users?page=999')
81
+ expect(beyond.body.list).toHaveLength(0)
82
+ expect(beyond.body.total).toBe(USERS_TOTAL)
83
+ })
84
+
85
+ it('keyword 模糊匹配姓名 / 公司 / 邮箱', async () => {
86
+ const res = await request(base).get('/api/users?keyword=%E6%98%9F%E5%B0%98')
87
+ // "星尘" 命中星尘科技
88
+ expect(res.body.total).toBeGreaterThan(0)
89
+ for (const row of res.body.list) {
90
+ expect(row.company).toContain('星尘')
91
+ }
92
+
93
+ const byEmail = await request(base).get('/api/users?keyword=user12@')
94
+ expect(byEmail.body.list[0].email).toBe('user12@example.com')
95
+ })
96
+
97
+ it('sortBy=score&order=desc 正确降序,白名单外字段被忽略', async () => {
98
+ const desc = await request(base).get('/api/users?sortBy=score&order=desc&pageSize=5')
99
+ const scores = desc.body.list.map((row: { score: number }) => row.score)
100
+ expect([...scores].sort((a: number, b: number) => b - a)).toEqual(scores)
101
+
102
+ const asc = await request(base).get('/api/users?sortBy=score&order=asc&pageSize=5')
103
+ const ascScores = asc.body.list.map((row: { score: number }) => row.score)
104
+ expect([...ascScores].sort((a: number, b: number) => a - b)).toEqual(ascScores)
105
+
106
+ // 不在白名单里 → 保持原始顺序
107
+ const ignored = await request(base).get('/api/users?sortBy=__proto__&order=desc')
108
+ expect(ignored.body.list[0].id).toBe(1)
109
+ })
110
+ })
111
+
112
+ describe('GET /api/regions 树与懒加载', () => {
113
+ it('GET /api/regions/tree 返回完整嵌套树', async () => {
114
+ const res = await request(base).get('/api/regions/tree')
115
+ expect(res.status).toBe(200)
116
+ expect(res.body.total).toBe(REGION_NODE_COUNT)
117
+ expect(res.body.list).toHaveLength(8)
118
+ expect(res.body.list[0].children).toHaveLength(6)
119
+ expect(res.body.list[0].children[0].children).toHaveLength(8)
120
+ })
121
+
122
+ it('GET /api/regions 不带 parent 返回顶层', async () => {
123
+ const res = await request(base).get('/api/regions')
124
+ expect(res.body.parent).toBeNull()
125
+ expect(res.body.list).toHaveLength(8)
126
+ expect(res.body.leaf).toBe(false)
127
+ })
128
+
129
+ it('GET /api/regions?parent=a1 只返回下一级且不带 children', async () => {
130
+ const res = await request(base).get('/api/regions?parent=a1')
131
+ expect(res.body.list).toHaveLength(6)
132
+ expect(res.body.parent).toBe('a1')
133
+ for (const node of res.body.list) {
134
+ expect(node).not.toHaveProperty('children')
135
+ expect(node.leaf).toBe(false)
136
+ }
137
+ })
138
+
139
+ it('末级节点的下一级为空且 leaf 为 true', async () => {
140
+ const res = await request(base).get('/api/regions?parent=a1-c1-b1')
141
+ expect(res.body.list).toHaveLength(0)
142
+ expect(res.body.leaf).toBe(true)
143
+ })
144
+
145
+ it('parent 不存在返回 404', async () => {
146
+ const res = await request(base).get('/api/regions?parent=not-exist')
147
+ expect(res.status).toBe(404)
148
+ expect(res.body.code).toBe(404)
149
+ })
150
+ })
151
+
152
+ describe('GET /api/options 搜索与分页', () => {
153
+ it('默认第 1 页 10 条,total 为全量', async () => {
154
+ const res = await request(base).get('/api/options')
155
+ expect(res.body.list).toHaveLength(10)
156
+ expect(res.body.total).toBe(OPTIONS_TOTAL)
157
+ })
158
+
159
+ it('pageSize 足够大时一次取回全部', async () => {
160
+ const res = await request(base).get('/api/options?pageSize=500')
161
+ expect(res.body.list).toHaveLength(OPTIONS_TOTAL)
162
+ })
163
+
164
+ it('keyword 过滤后 total 是过滤后的条数', async () => {
165
+ const res = await request(base).get('/api/options?keyword=%E5%80%99%E9%80%89%E9%A1%B9%2001')
166
+ expect(res.body.total).toBeLessThan(OPTIONS_TOTAL)
167
+ expect(res.body.total).toBeGreaterThan(0)
168
+ })
169
+ })
170
+
171
+ describe('写操作、校验与错误处理', () => {
172
+ it('POST /api/users 参数不合法返回 400 与字段错误', async () => {
173
+ const res = await request(base).post('/api/users').send({ name: '' })
174
+ expect(res.status).toBe(400)
175
+ expect(res.body.code).toBe(400)
176
+ expect(res.body.errors.name).toBeTruthy()
177
+ })
178
+
179
+ it('POST /api/users 创建成功返回 201', async () => {
180
+ const res = await request(base).post('/api/users').send({ name: '新用户', score: 88 })
181
+ expect(res.status).toBe(201)
182
+ expect(res.body.name).toBe('新用户')
183
+ expect(res.body.score).toBe(88)
184
+ expect(res.body.id).toBeGreaterThan(USERS_TOTAL)
185
+
186
+ const detail = await request(base).get(`/api/users/${res.body.id}`)
187
+ expect(detail.status).toBe(200)
188
+ expect(detail.body.name).toBe('新用户')
189
+
190
+ const removed = await request(base).delete(`/api/users/${res.body.id}`)
191
+ expect(removed.status).toBe(204)
192
+
193
+ const again = await request(base).delete(`/api/users/${res.body.id}`)
194
+ expect(again.status).toBe(404)
195
+ })
196
+
197
+ it('GET /api/users/:id 不存在返回 404 与中文提示', async () => {
198
+ const res = await request(base).get('/api/users/999999')
199
+ expect(res.status).toBe(404)
200
+ expect(res.body.message).toContain('用户不存在')
201
+ })
202
+
203
+ it('未知路由返回 404', async () => {
204
+ const res = await request(base).get('/api/not-exist')
205
+ expect(res.status).toBe(404)
206
+ expect(res.body.code).toBe(404)
207
+ })
208
+
209
+ it('预检请求返回 204 并带 CORS 头', async () => {
210
+ const res = await request(base)
211
+ .options('/api/users')
212
+ .set('Origin', 'http://localhost:5173')
213
+ expect(res.status).toBe(204)
214
+ expect(res.headers['access-control-allow-origin']).toBe('http://localhost:5173')
215
+ })
216
+ })
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"],
10
+ "noEmit": true
11
+ },
12
+ "include": ["src", "test"]
13
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['test/**/*.spec.ts'],
7
+ },
8
+ })
@@ -0,0 +1,25 @@
1
+ # {{projectName}}
2
+
3
+ 基于 [KB UI](https://github.com/)(@kb/ui)的 Vue 3 + Vite + TypeScript 项目模板。
4
+
5
+ ## 命令
6
+
7
+ ```bash
8
+ pnpm install # 安装依赖
9
+ pnpm dev # 启动开发服务器
10
+ pnpm build # 生产构建
11
+ pnpm preview # 预览构建产物
12
+ ```
13
+
14
+ ## 组件库
15
+
16
+ 全量引入 @kb/ui(`src/main.ts`):
17
+
18
+ ```ts
19
+ import KbUi from 'kb-ui-vue'
20
+ import '@kb/ui/styles/index.css'
21
+
22
+ createApp(App).use(KbUi).mount('#app')
23
+ ```
24
+
25
+ 暗色主题:给 `<html>` 设置 `data-theme="dark"` 即可。
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{projectName}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc --noEmit && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "kb-ui-vue": "{{kbVersion}}",
13
+ "vue": "^3.5.0"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-vue": "^6.0.0",
17
+ "typescript": "^5.9.0",
18
+ "vite": "^8.0.0",
19
+ "vue-tsc": "^3.0.0"
20
+ },
21
+ "overrides": {
22
+ "js-yaml": "^4.3.2",
23
+ "fast-uri": "^3.1.0",
24
+ "electron": "^39.8.10"
25
+ },
26
+ "pnpm": {
27
+ "overrides": {
28
+ "js-yaml": "^4.1.1",
29
+ "fast-uri": "^3.1.0"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ const dark = ref(false)
5
+
6
+ function toggleTheme() {
7
+ dark.value = !dark.value
8
+ document.documentElement.dataset.theme = dark.value ? 'dark' : 'light'
9
+ }
10
+ </script>
11
+
12
+ <template>
13
+ <main class="page">
14
+ <h1>{{projectName}}</h1>
15
+ <p class="subtitle">基于 @kb/ui 的 Vue 3 + Vite + TypeScript 项目</p>
16
+
17
+ <div class="actions">
18
+ <KbButton type="primary" @click="toggleTheme">
19
+ 切换到{{ dark ? '亮色' : '暗色' }}主题
20
+ </KbButton>
21
+ <KbButton plain>普通按钮</KbButton>
22
+ </div>
23
+ </main>
24
+ </template>
25
+
26
+ <style scoped>
27
+ .page {
28
+ max-width: 640px;
29
+ margin: 0 auto;
30
+ padding: 80px 24px;
31
+ font-family: var(--kb-font-family);
32
+ text-align: center;
33
+ }
34
+
35
+ .subtitle {
36
+ color: var(--kb-color-text-3);
37
+ font-size: var(--kb-font-size-sm);
38
+ }
39
+
40
+ .actions {
41
+ display: flex;
42
+ justify-content: center;
43
+ gap: var(--kb-space-2);
44
+ margin-top: 32px;
45
+ }
46
+ </style>
@@ -0,0 +1,6 @@
1
+ import { createApp } from 'vue'
2
+ import KbUi from 'kb-ui-vue'
3
+ import '@kb/ui/styles/index.css'
4
+ import App from './App.vue'
5
+
6
+ createApp(App).use(KbUi).mount('#app')
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "isolatedModules": true,
13
+ "verbatimModuleSyntax": true,
14
+ "jsx": "preserve",
15
+ "types": ["vite/client"]
16
+ },
17
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
18
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ })
@@ -0,0 +1,32 @@
1
+ # {{projectName}}
2
+
3
+ Electron 桌面应用模板(TypeScript + contextIsolation 安全模型)。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev # 开发模式启动 Electron
10
+ ```
11
+
12
+ ## 构建
13
+
14
+ ```bash
15
+ pnpm build # 打包 Windows 安装包(NSIS + 便携版)→ release/
16
+ pnpm pack # 仅生成未打包目录(调试用)
17
+ ```
18
+
19
+ ## 结构
20
+
21
+ ```
22
+ src/
23
+ ├── main/ # 主进程(窗口生命周期、IPC)
24
+ ├── preload/ # 白名单 API 桥(contextBridge)
25
+ └── renderer/ # 渲染进程页面
26
+ ```
27
+
28
+ ## 安全模型
29
+
30
+ - `contextIsolation: true` + `nodeIntegration: false`
31
+ - 渲染进程通过 `window.desktop`(preload 暴露的白名单 API)与主进程通信
32
+ - 新增能力:在 `src/main` 注册 `ipcMain.handle`,并在 `src/preload` 对应暴露
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "{{packageName}}-desktop",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "main": "dist/main/index.js",
7
+ "scripts": {
8
+ "dev": "electron .",
9
+ "start": "electron .",
10
+ "typecheck": "tsc --noEmit",
11
+ "pack": "electron-builder --dir",
12
+ "build": "electron-builder --win"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^24.0.0",
16
+ "electron": "^39.8.10",
17
+ "electron-builder": "^26.0.0",
18
+ "typescript": "^5.9.0"
19
+ },
20
+ "build": {
21
+ "appId": "com.example.{{projectName}}",
22
+ "productName": "{{projectName}}",
23
+ "directories": {
24
+ "output": "release"
25
+ },
26
+ "files": [
27
+ "dist/**/*"
28
+ ],
29
+ "win": {
30
+ "target": [
31
+ "nsis",
32
+ "portable"
33
+ ]
34
+ },
35
+ "nsis": {
36
+ "oneClick": false,
37
+ "allowToChangeInstallationDirectory": true
38
+ }
39
+ },
40
+ "overrides": {
41
+ "js-yaml": "^4.3.2",
42
+ "fast-uri": "^3.1.0",
43
+ "vite": "^6.3.5",
44
+ "electron": "^39.8.10"
45
+ },
46
+ "pnpm": {
47
+ "overrides": {
48
+ "js-yaml": "^4.1.1",
49
+ "fast-uri": "^3.1.0",
50
+ "vite": "^6.3.5"
51
+ }
52
+ },
53
+ "dependencies": {}
54
+ }
@@ -0,0 +1,36 @@
1
+ import { app, BrowserWindow, ipcMain } from 'electron'
2
+ import { join } from 'node:path'
3
+
4
+ function createWindow() {
5
+ const win = new BrowserWindow({
6
+ width: 960,
7
+ height: 640,
8
+ title: '{{projectName}}',
9
+ webPreferences: {
10
+ preload: join(import.meta.dirname, '../preload/index.js'),
11
+ contextIsolation: true,
12
+ nodeIntegration: false,
13
+ },
14
+ })
15
+
16
+ // 开发时可加载远程地址,打包后加载本地文件
17
+ win.loadFile(join(import.meta.dirname, '../renderer/index.html'))
18
+ }
19
+
20
+ /** 主进程示例 IPC:返回平台信息 */
21
+ ipcMain.handle('app:info', () => ({
22
+ platform: process.platform,
23
+ version: app.getVersion(),
24
+ cwd: process.cwd(),
25
+ }))
26
+
27
+ app.whenReady().then(() => {
28
+ createWindow()
29
+ app.on('activate', () => {
30
+ if (BrowserWindow.getAllWindows().length === 0) createWindow()
31
+ })
32
+ })
33
+
34
+ app.on('window-all-closed', () => {
35
+ if (process.platform !== 'darwin') app.quit()
36
+ })
@@ -0,0 +1,10 @@
1
+ import { contextBridge, ipcRenderer } from 'electron'
2
+
3
+ /** 白名单 API 桥:渲染进程只能通过这里访问主进程能力 */
4
+ const api = {
5
+ getAppInfo: () => ipcRenderer.invoke('app:info'),
6
+ }
7
+
8
+ contextBridge.exposeInMainWorld('desktop', api)
9
+
10
+ export type DesktopApi = typeof api
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>{{projectName}}</title>
6
+ <style>
7
+ body { margin: 0; font-family: system-ui, sans-serif; display: grid; place-items: center; height: 100vh; background: #f6f7f9; }
8
+ .card { padding: 32px 40px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgb(0 0 0 / 8%); text-align: center; }
9
+ h1 { margin: 0 0 8px; }
10
+ p { color: #666; }
11
+ code { background: #eef; padding: 2px 6px; border-radius: 4px; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <div class="card">
16
+ <h1>{{projectName}}</h1>
17
+ <p>Electron 桌面应用模板</p>
18
+ <p id="info">正在读取主进程信息…</p>
19
+ </div>
20
+ <script src="./renderer.js"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,14 @@
1
+ // 渲染进程脚本(通过 preload 暴露的 window.desktop 访问主进程)
2
+ declare global {
3
+ interface Window {
4
+ desktop: {
5
+ getAppInfo(): Promise<{ platform: string; version: string; cwd: string }>
6
+ }
7
+ }
8
+ }
9
+
10
+ window.desktop?.getAppInfo().then((info) => {
11
+ document.getElementById('info')!.textContent = `平台:${info.platform} · 版本:${info.version}`
12
+ })
13
+
14
+ export {}
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"],
10
+ "noEmit": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,16 @@
1
+ # Web 前端镜像
2
+ FROM node:26-alpine AS web
3
+ WORKDIR /app
4
+ COPY web/package.json ./
5
+ RUN npm install --no-audit --no-fund
6
+ COPY web/ ./
7
+ RUN npm run build
8
+
9
+ # API 服务镜像
10
+ FROM node:26-alpine AS api
11
+ WORKDIR /app
12
+ COPY server/package.json ./
13
+ RUN npm install --no-audit --no-fund
14
+ COPY server/ ./
15
+ EXPOSE 3000
16
+ CMD ["npm", "start"]
@@ -0,0 +1,37 @@
1
+ # {{projectName}}
2
+
3
+ 全栈项目模板:**Vue 3 前端 + Express API + Docker Compose**。
4
+
5
+ ## 目录
6
+
7
+ ```
8
+ ├── web/ # Vue 3 + Vite 前端(:5173)
9
+ ├── server/ # Express + TypeScript + Zod API(:3000)
10
+ └── docker-compose.yml
11
+ ```
12
+
13
+ ## 开发模式
14
+
15
+ ```bash
16
+ pnpm install
17
+ pnpm dev # 同时启动 web + api(concurrently)
18
+ ```
19
+
20
+ - 前端 http://localhost:5173(/api 已代理到 3000)
21
+ - API 文档见 server/README 或直接访问 /health
22
+
23
+ ## 一键容器化部署
24
+
25
+ ```bash
26
+ pnpm docker:up # docker compose up -d --build
27
+ pnpm docker:down
28
+ ```
29
+
30
+ - Web http://localhost:8080
31
+ - API http://localhost:3000
32
+
33
+ ## 测试
34
+
35
+ ```bash
36
+ pnpm test # API 集成测试(vitest + supertest)
37
+ ```
@@ -0,0 +1,26 @@
1
+ version: "3"
2
+
3
+ services:
4
+ api:
5
+ build:
6
+ context: .
7
+ target: api
8
+ ports:
9
+ - "3000:3000"
10
+ environment:
11
+ - PORT=3000
12
+ healthcheck:
13
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
14
+ interval: 10s
15
+ timeout: 3s
16
+ retries: 5
17
+
18
+ web:
19
+ build:
20
+ context: .
21
+ target: web
22
+ ports:
23
+ - "8080:80"
24
+ depends_on:
25
+ api:
26
+ condition: service_healthy