bohui-vue 1.0.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 (104) hide show
  1. package/README.md +121 -0
  2. package/bin/create-vue-template.js +565 -0
  3. package/package.json +28 -0
  4. package/templates/vue-project/.browserslistrc +3 -0
  5. package/templates/vue-project/.editorconfig +28 -0
  6. package/templates/vue-project/.env.development +2 -0
  7. package/templates/vue-project/.env.production +2 -0
  8. package/templates/vue-project/.eslintrc.cjs +76 -0
  9. package/templates/vue-project/.keep +0 -0
  10. package/templates/vue-project/.node-version +1 -0
  11. package/templates/vue-project/.prettierignore +13 -0
  12. package/templates/vue-project/.prettierrc +20 -0
  13. package/templates/vue-project/.prettierrc.txt +130 -0
  14. package/templates/vue-project/.stylelintrc.json +94 -0
  15. package/templates/vue-project/README.md +24 -0
  16. package/templates/vue-project/babel.config.js +5 -0
  17. package/templates/vue-project/index.html +34 -0
  18. package/templates/vue-project/package.json +75 -0
  19. package/templates/vue-project/public/favicon.ico +0 -0
  20. package/templates/vue-project/public/static/img/ai-default.jpg +0 -0
  21. package/templates/vue-project/public/static/img/image.png +0 -0
  22. package/templates/vue-project/public/static/img/ppt1.png +0 -0
  23. package/templates/vue-project/public/static/img/ppt2.png +0 -0
  24. package/templates/vue-project/public/static/img/ppt3.png +0 -0
  25. package/templates/vue-project/public/static/js/config.js +11 -0
  26. package/templates/vue-project/public/static/js/dataConfig.js +1143 -0
  27. package/templates/vue-project/src/App.vue +10 -0
  28. package/templates/vue-project/src/api/error-handler.ts +60 -0
  29. package/templates/vue-project/src/api/http.ts +254 -0
  30. package/templates/vue-project/src/api/services/aicebd.ts +47 -0
  31. package/templates/vue-project/src/api/services/base.ts +18 -0
  32. package/templates/vue-project/src/api/services/umse.ts +17 -0
  33. package/templates/vue-project/src/assets/font/Alibaba-PuHuiTi-Medium.otf +0 -0
  34. package/templates/vue-project/src/assets/font/Alibaba-PuHuiTi-Regular.otf +0 -0
  35. package/templates/vue-project/src/assets/font/DOUYINSANSBOLD.OTF +0 -0
  36. package/templates/vue-project/src/assets/font/Pangmen-Title.TTF +0 -0
  37. package/templates/vue-project/src/assets/font/font.css +25 -0
  38. package/templates/vue-project/src/assets/iconfont/iconfont.css +402 -0
  39. package/templates/vue-project/src/assets/iconfont/iconfont.js +66 -0
  40. package/templates/vue-project/src/assets/iconfont/iconfont.json +688 -0
  41. package/templates/vue-project/src/assets/iconfont/iconfont.ttf +0 -0
  42. package/templates/vue-project/src/assets/iconfont/iconfont.woff +0 -0
  43. package/templates/vue-project/src/assets/iconfont/iconfont.woff2 +0 -0
  44. package/templates/vue-project/src/assets/images/Click-tap.png +0 -0
  45. package/templates/vue-project/src/assets/images/Effects.png +0 -0
  46. package/templates/vue-project/src/assets/images/bg.png +0 -0
  47. package/templates/vue-project/src/assets/images/erCode.png +0 -0
  48. package/templates/vue-project/src/assets/images/header-bg.png +0 -0
  49. package/templates/vue-project/src/assets/images/logo.png +0 -0
  50. package/templates/vue-project/src/assets/scss/common.scss +530 -0
  51. package/templates/vue-project/src/assets/styles/element-overrides.css +53 -0
  52. package/templates/vue-project/src/assets/styles/reset.css +186 -0
  53. package/templates/vue-project/src/assets/styles/theme.css +100 -0
  54. package/templates/vue-project/src/components/BarChart.vue +238 -0
  55. package/templates/vue-project/src/components/echarts/EChart.vue +140 -0
  56. package/templates/vue-project/src/composables/useTheme.ts +84 -0
  57. package/templates/vue-project/src/main.ts +111 -0
  58. package/templates/vue-project/src/mocks/base.ts +37 -0
  59. package/templates/vue-project/src/mocks/umse.ts +31 -0
  60. package/templates/vue-project/src/router/index.ts +32 -0
  61. package/templates/vue-project/src/shims-vue.d.ts +19 -0
  62. package/templates/vue-project/src/store/index.ts +18 -0
  63. package/templates/vue-project/src/store/modules/user.ts +85 -0
  64. package/templates/vue-project/src/types/DTO/aicebd.d.ts +60 -0
  65. package/templates/vue-project/src/types/DTO/base.d.ts +26 -0
  66. package/templates/vue-project/src/types/DTO/global.d.ts +48 -0
  67. package/templates/vue-project/src/types/VO/teachingLog.d.ts +15 -0
  68. package/templates/vue-project/src/types/auto-imports.d.ts +73 -0
  69. package/templates/vue-project/src/types/components.d.ts +17 -0
  70. package/templates/vue-project/src/types/element-plus.d.ts +15 -0
  71. package/templates/vue-project/src/types/js-cookie.d.ts +1 -0
  72. package/templates/vue-project/src/types/unocss.d.ts +2 -0
  73. package/templates/vue-project/src/types/vite-plugins.d.ts +3 -0
  74. package/templates/vue-project/src/types/vue-router.d.ts +1 -0
  75. package/templates/vue-project/src/types/window-config.d.ts +12 -0
  76. package/templates/vue-project/src/utils/com-methods.ts +307 -0
  77. package/templates/vue-project/src/utils/echarts.ts +111 -0
  78. package/templates/vue-project/src/utils/number.ts +99 -0
  79. package/templates/vue-project/src/utils/rem.ts +82 -0
  80. package/templates/vue-project/src/utils/responsive.ts +103 -0
  81. package/templates/vue-project/src/utils/time.ts +314 -0
  82. package/templates/vue-project/src/utils/tracker.ts +527 -0
  83. package/templates/vue-project/src/utils/validators.ts +85 -0
  84. package/templates/vue-project/src/utils/window.ts +132 -0
  85. package/templates/vue-project/src/views/home/Home.vue +60 -0
  86. package/templates/vue-project/src/views/home/composables/useUserAuth.ts +13 -0
  87. package/templates/vue-project/src/views/teachingLog/TeachingLog.vue +40 -0
  88. package/templates/vue-project/src/views/teachingLog/__tests__/TeachingEffect.test.ts +96 -0
  89. package/templates/vue-project/src/views/teachingLog/__tests__/TeachingHighlight.test.ts +66 -0
  90. package/templates/vue-project/src/views/teachingLog/__tests__/TeachingLog.test.ts +34 -0
  91. package/templates/vue-project/src/views/teachingLog/components/TeachingEffect.vue +458 -0
  92. package/templates/vue-project/src/views/teachingLog/components/TeachingHighlight.vue +181 -0
  93. package/templates/vue-project/src/views/teachingLog/composables/useEffectTooltip.ts +88 -0
  94. package/templates/vue-project/src/views/teachingLog/composables/useEffectTrendChart.ts +160 -0
  95. package/templates/vue-project/tests/setup.ts +27 -0
  96. package/templates/vue-project/tsconfig.json +24 -0
  97. package/templates/vue-project/tsconfig.node.json +41 -0
  98. package/templates/vue-project/uno.config.ts +84 -0
  99. package/templates/vue-project/vite.config.ts +216 -0
  100. package/templates/vue-project/vue3_ai_prompt.md +652 -0
  101. package/templates/vue-project/vue3_ai_prompt_basic.md +722 -0
  102. package/templates/vue-project/vue3_ai_prompt_full.md +1021 -0
  103. package/templates/vue-project/vue3_ai_prompt_unocss.md +768 -0
  104. package/templates/vue-project//345/267/245/347/250/213/345/214/226/346/250/241/346/235/277/344/273/213/347/273/215.md +463 -0
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # bohui-vue
2
+
3
+ 前端工程化项目模板脚手架(Vue 3 + TypeScript + Vite + Element Plus),用于通过命令快速生成标准化的业务项目。
4
+
5
+ 特点:
6
+
7
+ - 交互式选择功能模块:ECharts / UnoCSS / 数据埋点(Tracker)
8
+ - 未选择的功能会在生成结果中被真实移除(依赖、配置、源码都会裁剪)
9
+ - 生成后的项目默认使用 pnpm(模板内置 `only-allow pnpm`)
10
+
11
+ ## 本地运行(开发/调试脚手架)
12
+
13
+ 在本仓库根目录执行:
14
+
15
+ ```bash
16
+ node ./bin/create-vue-template.js
17
+ ```
18
+
19
+ 也可以带项目名(避免先询问项目名):
20
+
21
+ ```bash
22
+ node ./bin/create-vue-template.js my-app
23
+ ```
24
+
25
+ 调试“无交互”模式(适合 CI 或批量生成):
26
+
27
+ ```bash
28
+ node ./bin/create-vue-template.js my-app --no-echarts --no-unocss --no-tracker --force --description "demo" --author "team"
29
+ ```
30
+
31
+ 参数说明:
32
+
33
+ - `--echarts` / `--no-echarts`:是否包含 ECharts 相关能力(默认:交互模式按选择;无交互默认 true,可用 `--no-echarts` 关闭)
34
+ - `--unocss` / `--no-unocss`:是否包含 UnoCSS(同上)
35
+ - `--tracker` / `--no-tracker`:是否包含数据埋点 Tracker(同上)
36
+ - `--description "<text>"`:项目描述(用于写入生成项目的 package.json)
37
+ - `--author "<name>"`:作者(用于写入生成项目的 package.json)
38
+ - `--force`:目标目录存在时强制覆盖(无交互模式必备)
39
+
40
+ 生成成功后,在新项目目录执行:
41
+
42
+ ```bash
43
+ pnpm install
44
+ pnpm serve
45
+ ```
46
+
47
+ ## 发布 npm 包
48
+
49
+ ### 1) 检查 package.json
50
+
51
+ 确认根目录 `package.json` 信息正确:
52
+
53
+ - `name`:包名(例如 `bohui-vue` 或 `@org/bohui-vue`)
54
+ - `version`:版本号(每次发布需递增)
55
+ - `bin`:命令名映射(本项目为 `bohui-vue` -> `./bin/create-vue-template.js`)
56
+ - `files`:发布时包含 `bin/` 与 `templates/`
57
+
58
+ ### 2) 本地预验证(可选但推荐)
59
+
60
+ 打包检查发布内容:
61
+
62
+ ```bash
63
+ npm pack
64
+ ```
65
+
66
+ 会生成一个 `.tgz` 文件,安装验证:
67
+
68
+ ```bash
69
+ npm i -g ./bohui-vue-*.tgz
70
+ bohui-vue my-app
71
+ ```
72
+
73
+ ### 3) 登录与发布
74
+
75
+ ```bash
76
+ npm login
77
+ npm publish
78
+ ```
79
+
80
+ 如果是 scope 包并需要公开发布:
81
+
82
+ ```bash
83
+ npm publish --access public
84
+ ```
85
+
86
+ ## 发布后如何使用
87
+
88
+ ### 方式 A:npx(推荐,无需全局安装)
89
+
90
+ ```bash
91
+ npx bohui-vue
92
+ ```
93
+
94
+ 带项目名:
95
+
96
+ ```bash
97
+ npx bohui-vue my-app
98
+ ```
99
+
100
+ 全参数示例(关闭三个可选模块):
101
+
102
+ ```bash
103
+ npx bohui-vue my-app --no-echarts --no-unocss --no-tracker --force --description "demo" --author "team"
104
+ ```
105
+
106
+ ### 方式 B:全局安装
107
+
108
+ ```bash
109
+ npm i -g bohui-vue
110
+ bohui-vue my-app
111
+ ```
112
+
113
+ ## 模板目录约定
114
+
115
+ 模板位于:
116
+
117
+ ```text
118
+ templates/vue-project/
119
+ ```
120
+
121
+ 脚手架会复制该目录到目标目录,并根据用户选择对生成项目做裁剪与替换。
@@ -0,0 +1,565 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline");
6
+
7
+ // 创建命令行交互界面
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ terminal: Boolean(process.stdin.isTTY),
12
+ });
13
+
14
+ // 询问问题
15
+ function question(query) {
16
+ return new Promise((resolve) => rl.question(query, resolve));
17
+ }
18
+
19
+ function normalizeYesNo(input, defaultValue) {
20
+ const v = String(input ?? "")
21
+ .trim()
22
+ .toLowerCase();
23
+ if (!v) return defaultValue;
24
+ if (["y", "yes", "true", "1"].includes(v)) return true;
25
+ if (["n", "no", "false", "0"].includes(v)) return false;
26
+ return defaultValue;
27
+ }
28
+
29
+ async function questionYesNo(query, defaultValue = true) {
30
+ const suffix = defaultValue ? " (Y/n): " : " (y/N): ";
31
+ const ans = await question(`${query}${suffix}`);
32
+ return normalizeYesNo(ans, defaultValue);
33
+ }
34
+
35
+ function getArgValue(args, key) {
36
+ const idx = args.indexOf(key);
37
+ if (idx === -1) return null;
38
+ const v = args[idx + 1];
39
+ if (v == null) return null;
40
+ if (String(v).startsWith("-")) return null;
41
+ return String(v);
42
+ }
43
+
44
+ function parseFlags(args) {
45
+ const flags = new Set(args.filter((a) => String(a).startsWith("--")));
46
+ return {
47
+ echarts: flags.has("--no-echarts")
48
+ ? false
49
+ : flags.has("--echarts")
50
+ ? true
51
+ : null,
52
+ unocss: flags.has("--no-unocss")
53
+ ? false
54
+ : flags.has("--unocss")
55
+ ? true
56
+ : null,
57
+ tracker: flags.has("--no-tracker")
58
+ ? false
59
+ : flags.has("--tracker")
60
+ ? true
61
+ : null,
62
+ test:
63
+ flags.has("--no-test") || flags.has("--no-tests")
64
+ ? false
65
+ : flags.has("--test") || flags.has("--tests")
66
+ ? true
67
+ : null,
68
+ force: flags.has("--force"),
69
+ description: getArgValue(args, "--description"),
70
+ author: getArgValue(args, "--author"),
71
+ };
72
+ }
73
+
74
+ // 复制目录
75
+ function copyDir(src, dest) {
76
+ if (!fs.existsSync(dest)) {
77
+ fs.mkdirSync(dest, { recursive: true });
78
+ }
79
+
80
+ const entries = fs.readdirSync(src, { withFileTypes: true });
81
+
82
+ for (let entry of entries) {
83
+ const srcPath = path.join(src, entry.name);
84
+ const destPath = path.join(dest, entry.name);
85
+
86
+ if (entry.isDirectory()) {
87
+ copyDir(srcPath, destPath);
88
+ } else {
89
+ fs.copyFileSync(srcPath, destPath);
90
+ }
91
+ }
92
+ }
93
+
94
+ // 替换文件内容
95
+ function replaceInFile(filePath, replacements) {
96
+ let content = fs.readFileSync(filePath, "utf8");
97
+
98
+ for (const [key, value] of Object.entries(replacements)) {
99
+ const regex = new RegExp(key, "g");
100
+ content = content.replace(regex, value);
101
+ }
102
+
103
+ fs.writeFileSync(filePath, content, "utf8");
104
+ }
105
+
106
+ // 递归替换目录下所有文件
107
+ function replaceInDir(dirPath, replacements) {
108
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
109
+
110
+ for (let entry of entries) {
111
+ const fullPath = path.join(dirPath, entry.name);
112
+
113
+ if (entry.isDirectory()) {
114
+ // 跳过 node_modules 等目录
115
+ if (!["node_modules", ".git", "dist", "build"].includes(entry.name)) {
116
+ replaceInDir(fullPath, replacements);
117
+ }
118
+ } else {
119
+ // 只处理文本文件
120
+ const ext = path.extname(entry.name);
121
+ const textExtensions = [
122
+ ".js",
123
+ ".vue",
124
+ ".ts",
125
+ ".json",
126
+ ".md",
127
+ ".html",
128
+ ".css",
129
+ ".scss",
130
+ ".less",
131
+ ".txt",
132
+ ".yml",
133
+ ".yaml",
134
+ ];
135
+
136
+ if (textExtensions.includes(ext) || entry.name.includes(".") === false) {
137
+ try {
138
+ replaceInFile(fullPath, replacements);
139
+ } catch (err) {
140
+ // 忽略无法读取的文件
141
+ console.warn(`警告: 无法处理文件 ${fullPath}: ${err.message}`);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ function readTextIfExists(filePath) {
149
+ if (!fs.existsSync(filePath)) return null;
150
+ return fs.readFileSync(filePath, "utf8");
151
+ }
152
+
153
+ function writeText(filePath, content) {
154
+ fs.writeFileSync(filePath, content, "utf8");
155
+ }
156
+
157
+ function rmIfExists(targetPath) {
158
+ if (!fs.existsSync(targetPath)) return;
159
+ fs.rmSync(targetPath, { recursive: true, force: true });
160
+ }
161
+
162
+ function readJsonIfExists(filePath) {
163
+ if (!fs.existsSync(filePath)) return null;
164
+ try {
165
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
166
+ } catch (_e) {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function writeJson(filePath, data) {
172
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
173
+ }
174
+
175
+ function removeDeps(obj, section, depsToRemove) {
176
+ if (!obj || typeof obj !== "object") return;
177
+ const target = obj[section];
178
+ if (!target || typeof target !== "object") return;
179
+ for (const dep of depsToRemove) {
180
+ if (dep in target) delete target[dep];
181
+ }
182
+ if (Object.keys(target).length === 0) delete obj[section];
183
+ }
184
+
185
+ function removeScripts(obj, scriptsToRemove) {
186
+ if (!obj || typeof obj !== "object") return;
187
+ const target = obj.scripts;
188
+ if (!target || typeof target !== "object") return;
189
+ for (const key of scriptsToRemove) {
190
+ if (key in target) delete target[key];
191
+ }
192
+ if (Object.keys(target).length === 0) delete obj.scripts;
193
+ }
194
+
195
+ function applyFeatureSelection(targetDir, features) {
196
+ const { echarts, unocss, tracker, test } = features;
197
+
198
+ const packageJsonPath = path.join(targetDir, "package.json");
199
+ const pkg = readJsonIfExists(packageJsonPath);
200
+ if (pkg) {
201
+ if (!echarts) {
202
+ removeDeps(pkg, "dependencies", ["echarts"]);
203
+ }
204
+ if (!unocss) {
205
+ removeDeps(pkg, "devDependencies", ["unocss", "@unocss/reset"]);
206
+ }
207
+ if (!test) {
208
+ removeDeps(pkg, "devDependencies", [
209
+ "vitest",
210
+ "@vitest/ui",
211
+ "@vitest/coverage-v8",
212
+ "@vue/test-utils",
213
+ "jsdom",
214
+ ]);
215
+ removeScripts(pkg, ["test", "test:run", "test:ui", "test:coverage"]);
216
+ }
217
+ writeJson(packageJsonPath, pkg);
218
+ }
219
+
220
+ if (!unocss) {
221
+ rmIfExists(path.join(targetDir, "uno.config.ts"));
222
+ rmIfExists(path.join(targetDir, "src", "types", "unocss.d.ts"));
223
+
224
+ const viteConfigPath = path.join(targetDir, "vite.config.ts");
225
+ const viteConfig = readTextIfExists(viteConfigPath);
226
+ if (viteConfig != null) {
227
+ let next = viteConfig;
228
+ next = next.replace(
229
+ /^\s*import\s+UnoCSS\s+from\s+"unocss\/vite";\s*\r?\n/m,
230
+ "",
231
+ );
232
+ next = next.replace(/^\s*UnoCSS\(\),\s*\r?\n/m, "");
233
+ writeText(viteConfigPath, next);
234
+ }
235
+ }
236
+
237
+ if (!tracker) {
238
+ rmIfExists(path.join(targetDir, "src", "utils", "tracker.ts"));
239
+ }
240
+
241
+ if (!test) {
242
+ rmIfExists(path.join(targetDir, "tests"));
243
+ rmIfExists(
244
+ path.join(targetDir, "src", "views", "teachingLog", "__tests__"),
245
+ );
246
+
247
+ const viteConfigPath = path.join(targetDir, "vite.config.ts");
248
+ const viteConfig = readTextIfExists(viteConfigPath);
249
+ if (viteConfig != null) {
250
+ let next = viteConfig;
251
+ next = next.replace(
252
+ /^\s*import\s+\{\s*loadEnv\s*\}\s+from\s+"vite";\s*\r?\n\s*import\s+\{\s*defineConfig\s*\}\s+from\s+"vitest\/config";\s*\r?\n/m,
253
+ 'import { defineConfig, loadEnv } from "vite";\n',
254
+ );
255
+ next = next.replace(
256
+ /^\s*import\s+\{\s*defineConfig\s*\}\s+from\s+"vitest\/config";\s*\r?\n/m,
257
+ 'import { defineConfig } from "vite";\n',
258
+ );
259
+ next = next.replace(/^\s*test:\s*\{[\s\S]*?\r?\n\s*\},\s*\r?\n/m, "");
260
+ writeText(viteConfigPath, next);
261
+ }
262
+ }
263
+
264
+ if (!echarts) {
265
+ rmIfExists(path.join(targetDir, "src", "utils", "echarts.ts"));
266
+ rmIfExists(path.join(targetDir, "src", "components", "echarts"));
267
+ rmIfExists(path.join(targetDir, "src", "components", "BarChart.vue"));
268
+ rmIfExists(
269
+ path.join(
270
+ targetDir,
271
+ "src",
272
+ "views",
273
+ "teachingLog",
274
+ "composables",
275
+ "useEffectTrendChart.ts",
276
+ ),
277
+ );
278
+
279
+ const homePath = path.join(targetDir, "src", "views", "home", "Home.vue");
280
+ const homeContent = readTextIfExists(homePath);
281
+ if (homeContent != null) {
282
+ let next = homeContent;
283
+ next = next.replace(/^\s*<BarChart\s+[^>]*\/>\s*\r?\n/m, "");
284
+ next = next.replace(
285
+ /^\s*import\s+BarChart\s+from\s+"@\/components\/BarChart\.vue";\s*\r?\n/m,
286
+ "",
287
+ );
288
+ next = next.replace(
289
+ /^\s*\/\/\s*柱状图测试数据[\s\S]*?^\s*};\s*\r?\n/m,
290
+ "",
291
+ );
292
+ writeText(homePath, next);
293
+ }
294
+
295
+ const teachingEffectPath = path.join(
296
+ targetDir,
297
+ "src",
298
+ "views",
299
+ "teachingLog",
300
+ "components",
301
+ "TeachingEffect.vue",
302
+ );
303
+ const teachingEffectContent = readTextIfExists(teachingEffectPath);
304
+ if (teachingEffectContent != null) {
305
+ let next = teachingEffectContent;
306
+ next = next.replace(
307
+ /\r?\n\s*<div class="effect-trend">[\s\S]*?<\/div>\r?\n\s*<div class="decline-section">/m,
308
+ '\n <div class="decline-section">',
309
+ );
310
+ next = next.replace(
311
+ /^\s*import\s+\{\s*useEffectTrendChart\s*\}\s+from\s+"\.\.\/composables\/useEffectTrendChart";\s*\r?\n/m,
312
+ "",
313
+ );
314
+ next = next.replace(
315
+ /^\s*import\s+\{\s*studentListenFocusListAPI,\s*\r?\n\s*getAILessonAttendAPI,\s*\r?\n\s*getStudentListenOverviewAPI,\s*\r?\n\s*\}\s+from\s+"@\/api\/services\/aicebd";\s*\r?\n/m,
316
+ 'import { getAILessonAttendAPI, getStudentListenOverviewAPI } from "@/api/services/aicebd";\n',
317
+ );
318
+ next = next.replace(
319
+ /import\s+\{\s*ref,\s*computed,\s*onMounted,\s*onUnmounted\s*\}\s+from\s+"vue";/m,
320
+ 'import { ref, computed, onMounted } from "vue";',
321
+ );
322
+ next = next.replace(
323
+ /^\s*\/\/ 学生专注度趋势数据[\s\S]*?\r?\n(\s*onMounted\(\(\)\s*=>)/m,
324
+ "$1",
325
+ );
326
+ next = next.replace(
327
+ /^\s*getStudentListenFocusListData\(\);\s*\r?\n/m,
328
+ "",
329
+ );
330
+ next = next.replace(
331
+ /^\s*onUnmounted\(\(\)\s*=>\s*\{\s*\r?\n\s*disposer\?\.dispose\(\);\s*\r?\n\s*\}\);\s*\r?\n/m,
332
+ "",
333
+ );
334
+ writeText(teachingEffectPath, next);
335
+ }
336
+
337
+ const globalComponentsPath = path.join(
338
+ targetDir,
339
+ "src",
340
+ "types",
341
+ "components.d.ts",
342
+ );
343
+ const globalComponentsContent = readTextIfExists(globalComponentsPath);
344
+ if (globalComponentsContent != null) {
345
+ let next = globalComponentsContent;
346
+ next = next.replace(/^\s*BarChart:.*\r?\n/m, "");
347
+ next = next.replace(/^\s*EChart:.*\r?\n/m, "");
348
+ writeText(globalComponentsPath, next);
349
+ }
350
+ }
351
+
352
+ const mainPath = path.join(targetDir, "src", "main.ts");
353
+ const mainContent = readTextIfExists(mainPath);
354
+ if (mainContent != null) {
355
+ let next = mainContent;
356
+
357
+ if (!unocss) {
358
+ next = next.replace(
359
+ /^\s*import\s+"@unocss\/reset\/tailwind\.css";\s*\r?\n/m,
360
+ "",
361
+ );
362
+ next = next.replace(/^\s*import\s+"uno\.css";\s*\r?\n/m, "");
363
+ }
364
+
365
+ if (!tracker) {
366
+ next = next.replace(
367
+ /^\s*import\s+\{\s*createTrackerPlugin,\s*getTracker\s*\}\s+from\s+"@\/utils\/tracker";\s*\r?\n/m,
368
+ "",
369
+ );
370
+ next = next.replace(
371
+ /^\s*import\s+\{\s*abortAllRequests,\s*http,\s*formDataHttp\s*\}\s+from\s+"@\/api\/http";\s*\r?\n/m,
372
+ 'import { abortAllRequests } from "@/api/http";\n',
373
+ );
374
+ next = next.replace(
375
+ /^\s*\.use\(createTrackerPlugin\(\{ appId: "vue3-template" \}\)\)\s*\r?\n/m,
376
+ "",
377
+ );
378
+ next = next.replace(
379
+ /\r?\n\s*\/\/ 监听用户信息变化,更新 tracker 用户 ID[\s\S]*?\r?\n\s*\{ immediate: true \}\r?\n\s*\);\r?\n/m,
380
+ "\n",
381
+ );
382
+ next = next.replace(
383
+ /^\s*getTracker\(\)\.bindRouter\(router\);\s*\/\/ 绑定路由\s*\r?\n/m,
384
+ "",
385
+ );
386
+ next = next.replace(
387
+ /^\s*getTracker\(\)\.bindRouterExposure\(router\);\s*\/\/ 绑定路由曝光事件\s*\r?\n/m,
388
+ "",
389
+ );
390
+ next = next.replace(
391
+ /^\s*getTracker\(\)\.attachAxios\(\[http,\s*formDataHttp\]\);\s*\/\/ 绑定 axios 实例\s*\r?\n/m,
392
+ "",
393
+ );
394
+ }
395
+
396
+ writeText(mainPath, next);
397
+ }
398
+ }
399
+
400
+ async function main() {
401
+ console.log("\n✨ Vue 项目模板创建工具\n");
402
+
403
+ const args = process.argv.slice(2);
404
+ const nonInteractive = !process.stdin.isTTY;
405
+ const parsed = parseFlags(args);
406
+ const positionalName =
407
+ args.find((a) => a && !String(a).startsWith("-")) || "";
408
+
409
+ // 获取项目名称
410
+ const projectName = positionalName
411
+ ? positionalName
412
+ : nonInteractive
413
+ ? ""
414
+ : await question("请输入项目名称: ");
415
+
416
+ if (!projectName || !projectName.trim()) {
417
+ console.error("❌ 项目名称不能为空");
418
+ process.exit(1);
419
+ }
420
+
421
+ const projectNameTrimmed = projectName.trim();
422
+
423
+ // 验证项目名称(只允许字母、数字、连字符、下划线)
424
+ if (!/^[a-zA-Z0-9_-]+$/.test(projectNameTrimmed)) {
425
+ console.error("❌ 项目名称只能包含字母、数字、连字符和下划线");
426
+ process.exit(1);
427
+ }
428
+
429
+ // 获取项目描述
430
+ const projectDescription = parsed.description
431
+ ? parsed.description
432
+ : nonInteractive
433
+ ? "一个基于 Vue 的项目"
434
+ : (await question("请输入项目描述 (可选,直接回车跳过): ")) ||
435
+ "一个基于 Vue 的项目";
436
+
437
+ // 获取作者信息
438
+ const author = parsed.author
439
+ ? parsed.author
440
+ : nonInteractive
441
+ ? "前端团队"
442
+ : (await question("请输入作者名称 (可选,直接回车跳过): ")) || "前端团队";
443
+
444
+ const useECharts =
445
+ parsed.echarts != null
446
+ ? parsed.echarts
447
+ : nonInteractive
448
+ ? true
449
+ : await questionYesNo("是否需要 ECharts 图表能力", true);
450
+ const useUnoCSS =
451
+ parsed.unocss != null
452
+ ? parsed.unocss
453
+ : nonInteractive
454
+ ? true
455
+ : await questionYesNo("是否需要 UnoCSS 原子化 CSS 能力", true);
456
+ const useTracker =
457
+ parsed.tracker != null
458
+ ? parsed.tracker
459
+ : nonInteractive
460
+ ? true
461
+ : await questionYesNo("是否需要数据埋点(Tracker)能力", true);
462
+ const useTest =
463
+ parsed.test != null
464
+ ? parsed.test
465
+ : nonInteractive
466
+ ? true
467
+ : await questionYesNo("是否需要自动化测试(Vitest)能力", true);
468
+
469
+ // 获取目标目录
470
+ const targetDir = path.resolve(process.cwd(), projectNameTrimmed);
471
+
472
+ // 检查目录是否已存在
473
+ if (fs.existsSync(targetDir)) {
474
+ if (parsed.force) {
475
+ fs.rmSync(targetDir, { recursive: true, force: true });
476
+ } else if (nonInteractive) {
477
+ if (!parsed.force) {
478
+ console.error(
479
+ `❌ 目录 ${projectNameTrimmed} 已存在。使用 --force 覆盖。`,
480
+ );
481
+ process.exit(1);
482
+ }
483
+ fs.rmSync(targetDir, { recursive: true, force: true });
484
+ } else {
485
+ const overwrite = await question(
486
+ `目录 ${projectNameTrimmed} 已存在,是否覆盖? (y/N): `,
487
+ );
488
+ if (
489
+ overwrite.toLowerCase() !== "y" &&
490
+ overwrite.toLowerCase() !== "yes"
491
+ ) {
492
+ console.log("❌ 已取消创建");
493
+ process.exit(0);
494
+ }
495
+ fs.rmSync(targetDir, { recursive: true, force: true });
496
+ }
497
+ }
498
+
499
+ // 获取模板目录
500
+ const templateDir = path.join(__dirname, "..", "templates", "vue-project");
501
+
502
+ if (!fs.existsSync(templateDir)) {
503
+ console.error(`❌ 模板目录不存在: ${templateDir}`);
504
+ console.error("请确保模板项目已放置在 templates/vue-project 目录下");
505
+ process.exit(1);
506
+ }
507
+
508
+ console.log("\n📦 正在创建项目...\n");
509
+
510
+ // 复制模板
511
+ copyDir(templateDir, targetDir);
512
+
513
+ // 替换占位符
514
+ const replacements = {
515
+ PROJECT_NAME: projectNameTrimmed,
516
+ PROJECT_DESCRIPTION: projectDescription,
517
+ AUTHOR_NAME: author,
518
+ };
519
+
520
+ console.log("🔄 正在替换项目配置...\n");
521
+ replaceInDir(targetDir, replacements);
522
+
523
+ applyFeatureSelection(targetDir, {
524
+ echarts: useECharts,
525
+ unocss: useUnoCSS,
526
+ tracker: useTracker,
527
+ test: useTest,
528
+ });
529
+
530
+ // 如果模板中有 package.json,更新它
531
+ const packageJsonPath = path.join(targetDir, "package.json");
532
+ if (fs.existsSync(packageJsonPath)) {
533
+ try {
534
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
535
+ packageJson.name = projectNameTrimmed;
536
+ packageJson.description = projectDescription;
537
+ if (author) {
538
+ packageJson.author = author;
539
+ }
540
+ fs.writeFileSync(
541
+ packageJsonPath,
542
+ JSON.stringify(packageJson, null, 2) + "\n",
543
+ "utf8",
544
+ );
545
+ } catch (err) {
546
+ console.warn(`警告: 无法更新 package.json: ${err.message}`);
547
+ }
548
+ }
549
+
550
+ console.log("✅ 项目创建成功!\n");
551
+ console.log(`📁 项目位置: ${targetDir}\n`);
552
+
553
+ console.log("📝 下一步操作:");
554
+ console.log(` cd ${projectNameTrimmed}`);
555
+ console.log(" pnpm install");
556
+ console.log(" pnpm serve\n");
557
+
558
+ rl.close();
559
+ }
560
+
561
+ main().catch((err) => {
562
+ console.error("❌ 创建项目时出错:", err);
563
+ rl.close();
564
+ process.exit(1);
565
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "bohui-vue",
3
+ "version": "1.0.1",
4
+ "description": "Vue 项目模板创建工具,用于快速创建标准化的 Vue 项目",
5
+ "main": "bin/create-vue-template.js",
6
+ "bin": {
7
+ "bohui-vue": "./bin/create-vue-template.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node ./bin/create-vue-template.js"
11
+ },
12
+ "keywords": [
13
+ "vue",
14
+ "template",
15
+ "scaffold",
16
+ "cli",
17
+ "bohui-vue"
18
+ ],
19
+ "author": "your name",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=14.0.0"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "templates"
27
+ ]
28
+ }