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.
- package/README.md +121 -0
- package/bin/create-vue-template.js +565 -0
- package/package.json +28 -0
- package/templates/vue-project/.browserslistrc +3 -0
- package/templates/vue-project/.editorconfig +28 -0
- package/templates/vue-project/.env.development +2 -0
- package/templates/vue-project/.env.production +2 -0
- package/templates/vue-project/.eslintrc.cjs +76 -0
- package/templates/vue-project/.keep +0 -0
- package/templates/vue-project/.node-version +1 -0
- package/templates/vue-project/.prettierignore +13 -0
- package/templates/vue-project/.prettierrc +20 -0
- package/templates/vue-project/.prettierrc.txt +130 -0
- package/templates/vue-project/.stylelintrc.json +94 -0
- package/templates/vue-project/README.md +24 -0
- package/templates/vue-project/babel.config.js +5 -0
- package/templates/vue-project/index.html +34 -0
- package/templates/vue-project/package.json +75 -0
- package/templates/vue-project/public/favicon.ico +0 -0
- package/templates/vue-project/public/static/img/ai-default.jpg +0 -0
- package/templates/vue-project/public/static/img/image.png +0 -0
- package/templates/vue-project/public/static/img/ppt1.png +0 -0
- package/templates/vue-project/public/static/img/ppt2.png +0 -0
- package/templates/vue-project/public/static/img/ppt3.png +0 -0
- package/templates/vue-project/public/static/js/config.js +11 -0
- package/templates/vue-project/public/static/js/dataConfig.js +1143 -0
- package/templates/vue-project/src/App.vue +10 -0
- package/templates/vue-project/src/api/error-handler.ts +60 -0
- package/templates/vue-project/src/api/http.ts +254 -0
- package/templates/vue-project/src/api/services/aicebd.ts +47 -0
- package/templates/vue-project/src/api/services/base.ts +18 -0
- package/templates/vue-project/src/api/services/umse.ts +17 -0
- package/templates/vue-project/src/assets/font/Alibaba-PuHuiTi-Medium.otf +0 -0
- package/templates/vue-project/src/assets/font/Alibaba-PuHuiTi-Regular.otf +0 -0
- package/templates/vue-project/src/assets/font/DOUYINSANSBOLD.OTF +0 -0
- package/templates/vue-project/src/assets/font/Pangmen-Title.TTF +0 -0
- package/templates/vue-project/src/assets/font/font.css +25 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.css +402 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.js +66 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.json +688 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.ttf +0 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.woff +0 -0
- package/templates/vue-project/src/assets/iconfont/iconfont.woff2 +0 -0
- package/templates/vue-project/src/assets/images/Click-tap.png +0 -0
- package/templates/vue-project/src/assets/images/Effects.png +0 -0
- package/templates/vue-project/src/assets/images/bg.png +0 -0
- package/templates/vue-project/src/assets/images/erCode.png +0 -0
- package/templates/vue-project/src/assets/images/header-bg.png +0 -0
- package/templates/vue-project/src/assets/images/logo.png +0 -0
- package/templates/vue-project/src/assets/scss/common.scss +530 -0
- package/templates/vue-project/src/assets/styles/element-overrides.css +53 -0
- package/templates/vue-project/src/assets/styles/reset.css +186 -0
- package/templates/vue-project/src/assets/styles/theme.css +100 -0
- package/templates/vue-project/src/components/BarChart.vue +238 -0
- package/templates/vue-project/src/components/echarts/EChart.vue +140 -0
- package/templates/vue-project/src/composables/useTheme.ts +84 -0
- package/templates/vue-project/src/main.ts +111 -0
- package/templates/vue-project/src/mocks/base.ts +37 -0
- package/templates/vue-project/src/mocks/umse.ts +31 -0
- package/templates/vue-project/src/router/index.ts +32 -0
- package/templates/vue-project/src/shims-vue.d.ts +19 -0
- package/templates/vue-project/src/store/index.ts +18 -0
- package/templates/vue-project/src/store/modules/user.ts +85 -0
- package/templates/vue-project/src/types/DTO/aicebd.d.ts +60 -0
- package/templates/vue-project/src/types/DTO/base.d.ts +26 -0
- package/templates/vue-project/src/types/DTO/global.d.ts +48 -0
- package/templates/vue-project/src/types/VO/teachingLog.d.ts +15 -0
- package/templates/vue-project/src/types/auto-imports.d.ts +73 -0
- package/templates/vue-project/src/types/components.d.ts +17 -0
- package/templates/vue-project/src/types/element-plus.d.ts +15 -0
- package/templates/vue-project/src/types/js-cookie.d.ts +1 -0
- package/templates/vue-project/src/types/unocss.d.ts +2 -0
- package/templates/vue-project/src/types/vite-plugins.d.ts +3 -0
- package/templates/vue-project/src/types/vue-router.d.ts +1 -0
- package/templates/vue-project/src/types/window-config.d.ts +12 -0
- package/templates/vue-project/src/utils/com-methods.ts +307 -0
- package/templates/vue-project/src/utils/echarts.ts +111 -0
- package/templates/vue-project/src/utils/number.ts +99 -0
- package/templates/vue-project/src/utils/rem.ts +82 -0
- package/templates/vue-project/src/utils/responsive.ts +103 -0
- package/templates/vue-project/src/utils/time.ts +314 -0
- package/templates/vue-project/src/utils/tracker.ts +527 -0
- package/templates/vue-project/src/utils/validators.ts +85 -0
- package/templates/vue-project/src/utils/window.ts +132 -0
- package/templates/vue-project/src/views/home/Home.vue +60 -0
- package/templates/vue-project/src/views/home/composables/useUserAuth.ts +13 -0
- package/templates/vue-project/src/views/teachingLog/TeachingLog.vue +40 -0
- package/templates/vue-project/src/views/teachingLog/__tests__/TeachingEffect.test.ts +96 -0
- package/templates/vue-project/src/views/teachingLog/__tests__/TeachingHighlight.test.ts +66 -0
- package/templates/vue-project/src/views/teachingLog/__tests__/TeachingLog.test.ts +34 -0
- package/templates/vue-project/src/views/teachingLog/components/TeachingEffect.vue +458 -0
- package/templates/vue-project/src/views/teachingLog/components/TeachingHighlight.vue +181 -0
- package/templates/vue-project/src/views/teachingLog/composables/useEffectTooltip.ts +88 -0
- package/templates/vue-project/src/views/teachingLog/composables/useEffectTrendChart.ts +160 -0
- package/templates/vue-project/tests/setup.ts +27 -0
- package/templates/vue-project/tsconfig.json +24 -0
- package/templates/vue-project/tsconfig.node.json +41 -0
- package/templates/vue-project/uno.config.ts +84 -0
- package/templates/vue-project/vite.config.ts +216 -0
- package/templates/vue-project/vue3_ai_prompt.md +652 -0
- package/templates/vue-project/vue3_ai_prompt_basic.md +722 -0
- package/templates/vue-project/vue3_ai_prompt_full.md +1021 -0
- package/templates/vue-project/vue3_ai_prompt_unocss.md +768 -0
- 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
|
+
}
|