create-simpleadmin-ui 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/bin/create-simpleadmin-ui.mjs +2 -0
- package/package.json +35 -0
- package/src/index.mjs +557 -0
- package/src/sync-template.mjs +64 -0
- package/template/.env.development +4 -0
- package/template/.env.production.example +4 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/README.md +26 -0
- package/template/index.html +19 -0
- package/template/package.json +30 -0
- package/template/public/vite.svg +1 -0
- package/template/src/App.vue +3 -0
- package/template/src/components/AppBreadcrumb.vue +132 -0
- package/template/src/components/AppPage.vue +23 -0
- package/template/src/components/ChangePasswordDialog.vue +97 -0
- package/template/src/components/ProfileEditDialog.vue +142 -0
- package/template/src/components/RichTextEditor.vue +121 -0
- package/template/src/components/SidebarMenuItem.vue +56 -0
- package/template/src/components/TagsView.vue +172 -0
- package/template/src/constants/menuIcons.ts +205 -0
- package/template/src/directives/permission.ts +13 -0
- package/template/src/env.d.ts +22 -0
- package/template/src/layouts/MainLayout.vue +410 -0
- package/template/src/main.ts +27 -0
- package/template/src/router/index.ts +198 -0
- package/template/src/stores/tagsView.ts +161 -0
- package/template/src/stores/theme.ts +68 -0
- package/template/src/stores/user.ts +191 -0
- package/template/src/styles/global.css +314 -0
- package/template/src/utils/datetime.ts +34 -0
- package/template/src/utils/request.ts +324 -0
- package/template/src/utils/requestSign.ts +162 -0
- package/template/src/views/error/NotFound.vue +49 -0
- package/template/src/views/home/HomeView.vue +1177 -0
- package/template/src/views/login/LoginView.vue +576 -0
- package/template/src/views/monitor/loginlog/index.vue +366 -0
- package/template/src/views/monitor/online/index.vue +298 -0
- package/template/src/views/monitor/operlog/index.vue +492 -0
- package/template/src/views/system/config/index.vue +407 -0
- package/template/src/views/system/dept/index.vue +517 -0
- package/template/src/views/system/dict/index.vue +747 -0
- package/template/src/views/system/file/index.vue +1031 -0
- package/template/src/views/system/menu/index.vue +822 -0
- package/template/src/views/system/message/index.vue +422 -0
- package/template/src/views/system/notice/index.vue +578 -0
- package/template/src/views/system/openApp/index.vue +596 -0
- package/template/src/views/system/post/index.vue +495 -0
- package/template/src/views/system/role/index.vue +546 -0
- package/template/src/views/system/user/index.vue +373 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +30 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +24 -0
- package/template/vite.config.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# create-simpleadmin-ui
|
|
2
|
+
|
|
3
|
+
交互式创建 **SimpleAdmin Vue3(Element Plus)** 管理前端脚手架。模板源为同级 [`Temp.SimpleAdmin.UI.Vue3`](../Temp.SimpleAdmin.UI.Vue3)。
|
|
4
|
+
|
|
5
|
+
## 用法(对外)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create simpleadmin-ui@latest
|
|
9
|
+
# 或指定目录名
|
|
10
|
+
npm create simpleadmin-ui@latest my-admin
|
|
11
|
+
|
|
12
|
+
# 等价
|
|
13
|
+
npx create-simpleadmin-ui@latest
|
|
14
|
+
pnpm create simpleadmin-ui
|
|
15
|
+
yarn create simpleadmin-ui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
交互项:项目目录名、品牌名、开发代理 API、端口、`VITE_*` 签名配置、包管理器。
|
|
19
|
+
|
|
20
|
+
非交互(CI / 脚本):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx create-simpleadmin-ui my-admin -y --title MyAdmin --pm none
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 本地开发(本仓库)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd ProjectTemplates/DotNetCore/Temp.SimpleAdmin/create-simpleadmin-ui
|
|
30
|
+
npm install
|
|
31
|
+
npm run sync-template # 从 ../Temp.SimpleAdmin.UI.Vue3 同步到 template/
|
|
32
|
+
node ./bin/create-simpleadmin-ui.mjs demo-ui
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
未执行 sync 时,CLI 会回退读取同级 `Temp.SimpleAdmin.UI.Vue3`(便于改模板后立刻试跑)。
|
|
36
|
+
|
|
37
|
+
## 发布到 npm
|
|
38
|
+
|
|
39
|
+
1. 确认 `Temp.SimpleAdmin.UI.Vue3` 为期望内容(`node_modules` / `dist` 不会打进包)。
|
|
40
|
+
2. bump `package.json` 的 `version`。
|
|
41
|
+
3. `npm publish`(`prepack` 会自动 `sync-template`)。
|
|
42
|
+
|
|
43
|
+
包内 `files` 仅含 `bin` / `src` / `template`。
|
|
44
|
+
|
|
45
|
+
## 生成后
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd <项目名>
|
|
49
|
+
npm run dev # 默认 http://localhost:5173,代理到后端
|
|
50
|
+
npm run build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`VITE_SIGN_APP_ID` / `VITE_SIGN_APP_SECRET` 须与后端表 `SysOpenApp` 一致。完整后台说明见 [Api README](../Temp.SimpleAdmin.Api/README.md)。
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-simpleadmin-ui",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "交互式创建 SimpleAdmin Vue3 管理前端脚手架",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-simpleadmin-ui": "./bin/create-simpleadmin-ui.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"template"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": "^18.0.0 || >=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"sync-template": "node ./src/sync-template.mjs",
|
|
19
|
+
"prepack": "node ./src/sync-template.mjs",
|
|
20
|
+
"start": "node ./bin/create-simpleadmin-ui.mjs"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"simpleadmin",
|
|
24
|
+
"vue3",
|
|
25
|
+
"element-plus",
|
|
26
|
+
"scaffold",
|
|
27
|
+
"create",
|
|
28
|
+
"admin"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^0.10.0",
|
|
33
|
+
"picocolors": "^1.1.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-simpleadmin-ui:交互式生成 SimpleAdmin Vue3 管理前端工程。
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import * as p from '@clack/prompts'
|
|
8
|
+
import pc from 'picocolors'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const pkgRoot = path.resolve(__dirname, '..')
|
|
13
|
+
|
|
14
|
+
/** 文本文件扩展名(需做占位替换) */
|
|
15
|
+
const TEXT_EXTS = new Set([
|
|
16
|
+
'.ts',
|
|
17
|
+
'.tsx',
|
|
18
|
+
'.js',
|
|
19
|
+
'.mjs',
|
|
20
|
+
'.cjs',
|
|
21
|
+
'.vue',
|
|
22
|
+
'.json',
|
|
23
|
+
'.html',
|
|
24
|
+
'.css',
|
|
25
|
+
'.scss',
|
|
26
|
+
'.md',
|
|
27
|
+
'.svg',
|
|
28
|
+
'.env',
|
|
29
|
+
'.txt',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/** 拷贝时跳过的目录/文件名 */
|
|
33
|
+
const SKIP_NAMES = new Set([
|
|
34
|
+
'node_modules',
|
|
35
|
+
'dist',
|
|
36
|
+
'dist-ssr',
|
|
37
|
+
'.git',
|
|
38
|
+
'.DS_Store',
|
|
39
|
+
'package-lock.json',
|
|
40
|
+
'pnpm-lock.yaml',
|
|
41
|
+
'yarn.lock',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} CliFlags
|
|
46
|
+
* @property {string|undefined} projectName
|
|
47
|
+
* @property {boolean} yes
|
|
48
|
+
* @property {string|undefined} title
|
|
49
|
+
* @property {string|undefined} apiProxy
|
|
50
|
+
* @property {string|undefined} port
|
|
51
|
+
* @property {string|undefined} apiBaseUrl
|
|
52
|
+
* @property {string|undefined} signAppId
|
|
53
|
+
* @property {string|undefined} signAppSecret
|
|
54
|
+
* @property {string|undefined} packageManager
|
|
55
|
+
* @property {boolean} help
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {object} ScaffoldAnswers
|
|
60
|
+
* @property {string} projectName 目录名
|
|
61
|
+
* @property {string} packageName package.json name
|
|
62
|
+
* @property {string} title 界面品牌名
|
|
63
|
+
* @property {string} apiProxy 开发代理目标
|
|
64
|
+
* @property {number} port 开发端口
|
|
65
|
+
* @property {string} apiBaseUrl VITE_API_BASE_URL
|
|
66
|
+
* @property {string} signAppId VITE_SIGN_APP_ID
|
|
67
|
+
* @property {string} signAppSecret VITE_SIGN_APP_SECRET
|
|
68
|
+
* @property {'npm'|'pnpm'|'yarn'|'bun'|'none'} packageManager 包管理器
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 将目录名规范为合法 npm package name。
|
|
73
|
+
* @param {string} name 原始名称
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function toPackageName(name) {
|
|
77
|
+
return (
|
|
78
|
+
name
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
82
|
+
.replace(/^-+|-+$/g, '')
|
|
83
|
+
.replace(/^\.+/, '') || 'simpleadmin-ui'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 校验项目目录名。
|
|
89
|
+
* @param {string} value 输入
|
|
90
|
+
* @returns {string|undefined} 错误信息
|
|
91
|
+
*/
|
|
92
|
+
function validateProjectName(value) {
|
|
93
|
+
if (!value?.trim()) return '请输入项目名称'
|
|
94
|
+
if (/[<>:"|?*\u0000-\u001F]/.test(value)) return '名称含非法字符'
|
|
95
|
+
if (value === '.' || value === '..') return '名称无效'
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 解析 CLI 参数。
|
|
101
|
+
* @param {string[]} argv process.argv
|
|
102
|
+
* @returns {CliFlags}
|
|
103
|
+
*/
|
|
104
|
+
function parseArgs(argv) {
|
|
105
|
+
/** @type {CliFlags} */
|
|
106
|
+
const flags = {
|
|
107
|
+
projectName: undefined,
|
|
108
|
+
yes: false,
|
|
109
|
+
title: undefined,
|
|
110
|
+
apiProxy: undefined,
|
|
111
|
+
port: undefined,
|
|
112
|
+
apiBaseUrl: undefined,
|
|
113
|
+
signAppId: undefined,
|
|
114
|
+
signAppSecret: undefined,
|
|
115
|
+
packageManager: undefined,
|
|
116
|
+
help: false,
|
|
117
|
+
}
|
|
118
|
+
const args = argv.slice(2)
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
const a = args[i]
|
|
121
|
+
if (a === '-h' || a === '--help') {
|
|
122
|
+
flags.help = true
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
if (a === '-y' || a === '--yes') {
|
|
126
|
+
flags.yes = true
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
const take = () => {
|
|
130
|
+
const v = args[++i]
|
|
131
|
+
if (!v || v.startsWith('-')) throw new Error(`参数 ${a} 需要值`)
|
|
132
|
+
return v
|
|
133
|
+
}
|
|
134
|
+
if (a === '--title') {
|
|
135
|
+
flags.title = take()
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (a === '--api-proxy') {
|
|
139
|
+
flags.apiProxy = take()
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (a === '--port') {
|
|
143
|
+
flags.port = take()
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (a === '--api-base-url') {
|
|
147
|
+
flags.apiBaseUrl = take()
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (a === '--sign-app-id') {
|
|
151
|
+
flags.signAppId = take()
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (a === '--sign-app-secret') {
|
|
155
|
+
flags.signAppSecret = take()
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (a === '--pm') {
|
|
159
|
+
flags.packageManager = take()
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (a.startsWith('-')) throw new Error(`未知参数: ${a}`)
|
|
163
|
+
if (!flags.projectName) flags.projectName = a
|
|
164
|
+
else throw new Error(`多余参数: ${a}`)
|
|
165
|
+
}
|
|
166
|
+
return flags
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 打印帮助。
|
|
171
|
+
* @returns {void}
|
|
172
|
+
*/
|
|
173
|
+
function printHelp() {
|
|
174
|
+
console.log(`
|
|
175
|
+
Usage:
|
|
176
|
+
npm create simpleadmin-ui@latest [project-name] [options]
|
|
177
|
+
npx create-simpleadmin-ui [project-name] [options]
|
|
178
|
+
|
|
179
|
+
Options:
|
|
180
|
+
-y, --yes 使用默认值,跳过交互
|
|
181
|
+
--title <name> 界面品牌名(默认 SimpleAdmin)
|
|
182
|
+
--api-proxy <url> 开发代理目标(默认 http://localhost:5080)
|
|
183
|
+
--port <n> 开发端口(默认 5173)
|
|
184
|
+
--api-base-url <path> VITE_API_BASE_URL(默认 /api/v1)
|
|
185
|
+
--sign-app-id <id> VITE_SIGN_APP_ID
|
|
186
|
+
--sign-app-secret <sec> VITE_SIGN_APP_SECRET
|
|
187
|
+
--pm <npm|pnpm|yarn|bun|none> 包管理器(默认 npm;-y 时可用 none)
|
|
188
|
+
-h, --help 显示帮助
|
|
189
|
+
`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 解析模板目录:优先本包 template/,否则回退同级源工程(本地开发)。
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function resolveTemplateDir() {
|
|
197
|
+
const bundled = path.join(pkgRoot, 'template')
|
|
198
|
+
const sibling = path.resolve(pkgRoot, '../Temp.SimpleAdmin.UI.Vue3')
|
|
199
|
+
if (fs.existsSync(path.join(bundled, 'package.json'))) return bundled
|
|
200
|
+
if (fs.existsSync(path.join(sibling, 'package.json'))) return sibling
|
|
201
|
+
throw new Error(
|
|
202
|
+
'找不到模板目录。请先在 create-simpleadmin-ui 下执行 npm run sync-template,或确保同级存在 Temp.SimpleAdmin.UI.Vue3。',
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 判断相对路径是否跳过。
|
|
208
|
+
* @param {string} rel 相对路径
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
function shouldSkip(rel) {
|
|
212
|
+
const parts = rel.split(/[/\\]/).filter(Boolean)
|
|
213
|
+
return parts.some((p) => SKIP_NAMES.has(p) || p.endsWith('.local'))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 是否按文本处理并替换。
|
|
218
|
+
* @param {string} filePath 文件路径
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
function isTextFile(filePath) {
|
|
222
|
+
const base = path.basename(filePath)
|
|
223
|
+
if (base.startsWith('.env')) return true
|
|
224
|
+
if (base === '.gitignore') return true
|
|
225
|
+
return TEXT_EXTS.has(path.extname(filePath).toLowerCase())
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 对模板文本做占位替换(品牌与包名;环境变量文件单独重写)。
|
|
230
|
+
* @param {string} content 原文
|
|
231
|
+
* @param {ScaffoldAnswers} answers 答案
|
|
232
|
+
* @param {string} rel 相对路径
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
function transformContent(content, answers, rel) {
|
|
236
|
+
const normalized = rel.replace(/\\/g, '/')
|
|
237
|
+
if (normalized === 'package.json') {
|
|
238
|
+
const pkg = JSON.parse(content)
|
|
239
|
+
pkg.name = answers.packageName
|
|
240
|
+
return `${JSON.stringify(pkg, null, 2)}\n`
|
|
241
|
+
}
|
|
242
|
+
if (normalized === '.env.development') {
|
|
243
|
+
return [
|
|
244
|
+
`VITE_API_BASE_URL=${answers.apiBaseUrl}`,
|
|
245
|
+
'# 须与 SysOpenApp 中对应客户端一致;生产勿把真实密钥提交仓库',
|
|
246
|
+
`VITE_SIGN_APP_ID=${answers.signAppId}`,
|
|
247
|
+
`VITE_SIGN_APP_SECRET=${answers.signAppSecret}`,
|
|
248
|
+
'',
|
|
249
|
+
].join('\n')
|
|
250
|
+
}
|
|
251
|
+
if (normalized === '.env.production.example') {
|
|
252
|
+
return [
|
|
253
|
+
'# 生产构建示例:复制为 .env.production.local 并由 CI 注入真实值,勿提交密钥',
|
|
254
|
+
'VITE_API_BASE_URL=https://api.example.com/api/v1',
|
|
255
|
+
`VITE_SIGN_APP_ID=${answers.signAppId}`,
|
|
256
|
+
'VITE_SIGN_APP_SECRET=REPLACE_WITH_SAME_AS_SysOpenApp_AppSecret',
|
|
257
|
+
'',
|
|
258
|
+
].join('\n')
|
|
259
|
+
}
|
|
260
|
+
if (normalized === 'vite.config.ts') {
|
|
261
|
+
return content
|
|
262
|
+
.replace(/port:\s*\d+/, `port: ${answers.port}`)
|
|
263
|
+
.replace(/target:\s*['"][^'"]+['"]/, `target: '${answers.apiProxy}'`)
|
|
264
|
+
}
|
|
265
|
+
if (normalized === 'README.md') {
|
|
266
|
+
return [
|
|
267
|
+
`# ${answers.title}`,
|
|
268
|
+
'',
|
|
269
|
+
'由 `create-simpleadmin-ui` 生成的 SimpleAdmin Vue3 管理前端。',
|
|
270
|
+
'',
|
|
271
|
+
'```bash',
|
|
272
|
+
'npm install && npm run dev',
|
|
273
|
+
'npm run build',
|
|
274
|
+
'```',
|
|
275
|
+
'',
|
|
276
|
+
`开发默认:http://localhost:${answers.port} → 代理 ${answers.apiProxy}`,
|
|
277
|
+
'',
|
|
278
|
+
'环境变量:`VITE_API_BASE_URL`、`VITE_SIGN_APP_ID`、`VITE_SIGN_APP_SECRET`(须与后端 `SysOpenApp` 一致)。',
|
|
279
|
+
'',
|
|
280
|
+
].join('\n')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let next = content.replaceAll('temp-simpleadmin-ui', answers.packageName)
|
|
284
|
+
next = next.replaceAll('SimpleAdmin_Dev_Sign_Secret_Change_Me_32', answers.signAppSecret)
|
|
285
|
+
next = next.replaceAll('SimpleAdmin', answers.title)
|
|
286
|
+
return next
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 递归拷贝并变换模板到目标目录。
|
|
291
|
+
* @param {string} from 源
|
|
292
|
+
* @param {string} to 目标
|
|
293
|
+
* @param {ScaffoldAnswers} answers 答案
|
|
294
|
+
* @param {string} [rel=''] 相对路径
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
function copyTemplate(from, to, answers, rel = '') {
|
|
298
|
+
if (shouldSkip(rel)) return
|
|
299
|
+
const stat = fs.statSync(from)
|
|
300
|
+
if (stat.isDirectory()) {
|
|
301
|
+
fs.mkdirSync(to, { recursive: true })
|
|
302
|
+
for (const name of fs.readdirSync(from)) {
|
|
303
|
+
copyTemplate(path.join(from, name), path.join(to, name), answers, path.join(rel, name))
|
|
304
|
+
}
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
308
|
+
if (isTextFile(from)) {
|
|
309
|
+
const raw = fs.readFileSync(from, 'utf8')
|
|
310
|
+
fs.writeFileSync(to, transformContent(raw, answers, rel), 'utf8')
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
fs.copyFileSync(from, to)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 安装依赖。
|
|
318
|
+
* @param {string} cwd 项目目录
|
|
319
|
+
* @param {ScaffoldAnswers['packageManager']} pm 包管理器
|
|
320
|
+
* @returns {boolean} 是否成功
|
|
321
|
+
*/
|
|
322
|
+
function installDeps(cwd, pm) {
|
|
323
|
+
if (pm === 'none') return true
|
|
324
|
+
const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm'
|
|
325
|
+
const args = pm === 'yarn' ? [] : ['install']
|
|
326
|
+
const result = spawnSync(cmd, args, {
|
|
327
|
+
cwd,
|
|
328
|
+
stdio: 'inherit',
|
|
329
|
+
shell: process.platform === 'win32',
|
|
330
|
+
})
|
|
331
|
+
return result.status === 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 校验并规范化包管理器。
|
|
336
|
+
* @param {string} value 输入
|
|
337
|
+
* @returns {ScaffoldAnswers['packageManager']}
|
|
338
|
+
*/
|
|
339
|
+
function normalizePm(value) {
|
|
340
|
+
const allowed = /** @type {const} */ (['npm', 'pnpm', 'yarn', 'bun', 'none'])
|
|
341
|
+
if (!allowed.includes(/** @type {*} */ (value))) {
|
|
342
|
+
throw new Error(`--pm 仅支持: ${allowed.join('|')}`)
|
|
343
|
+
}
|
|
344
|
+
return /** @type {ScaffoldAnswers['packageManager']} */ (value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 由 flags 拼出默认答案(可再被交互覆盖)。
|
|
349
|
+
* @param {CliFlags} flags CLI 参数
|
|
350
|
+
* @returns {Omit<ScaffoldAnswers, never>}
|
|
351
|
+
*/
|
|
352
|
+
function defaultsFromFlags(flags) {
|
|
353
|
+
const projectName = (flags.projectName || 'simpleadmin-ui').trim()
|
|
354
|
+
const err = validateProjectName(projectName)
|
|
355
|
+
if (err) throw new Error(err)
|
|
356
|
+
const port = Number(flags.port || '5173')
|
|
357
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
358
|
+
throw new Error('端口须为 1–65535 的整数')
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
projectName,
|
|
362
|
+
packageName: toPackageName(projectName),
|
|
363
|
+
title: (flags.title || 'SimpleAdmin').trim(),
|
|
364
|
+
apiProxy: (flags.apiProxy || 'http://localhost:5080').trim().replace(/\/$/, ''),
|
|
365
|
+
port,
|
|
366
|
+
apiBaseUrl: (flags.apiBaseUrl || '/api/v1').trim(),
|
|
367
|
+
signAppId: (flags.signAppId || 'simple-admin-web').trim(),
|
|
368
|
+
signAppSecret: (flags.signAppSecret || 'SimpleAdmin_Dev_Sign_Secret_Change_Me_32').trim(),
|
|
369
|
+
packageManager: normalizePm(flags.packageManager || (flags.yes ? 'none' : 'npm')),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 交互收集参数;`--yes` 时直接使用 flags 默认值。
|
|
375
|
+
* @param {CliFlags} flags CLI 参数
|
|
376
|
+
* @returns {Promise<ScaffoldAnswers|null>}
|
|
377
|
+
*/
|
|
378
|
+
async function collectAnswers(flags) {
|
|
379
|
+
const base = defaultsFromFlags(flags)
|
|
380
|
+
if (flags.yes) return base
|
|
381
|
+
|
|
382
|
+
const projectName = await p.text({
|
|
383
|
+
message: '项目目录名',
|
|
384
|
+
placeholder: base.projectName,
|
|
385
|
+
initialValue: base.projectName,
|
|
386
|
+
validate: validateProjectName,
|
|
387
|
+
})
|
|
388
|
+
if (p.isCancel(projectName)) return null
|
|
389
|
+
|
|
390
|
+
const title = await p.text({
|
|
391
|
+
message: '界面品牌名(登录页 / 侧栏标题)',
|
|
392
|
+
placeholder: base.title,
|
|
393
|
+
initialValue: flags.title || base.title,
|
|
394
|
+
validate: (v) => (!v?.trim() ? '不能为空' : undefined),
|
|
395
|
+
})
|
|
396
|
+
if (p.isCancel(title)) return null
|
|
397
|
+
|
|
398
|
+
const apiProxy = await p.text({
|
|
399
|
+
message: '开发代理 API 地址(vite server.proxy)',
|
|
400
|
+
initialValue: flags.apiProxy || base.apiProxy,
|
|
401
|
+
validate: (v) => (!v?.trim() ? '不能为空' : undefined),
|
|
402
|
+
})
|
|
403
|
+
if (p.isCancel(apiProxy)) return null
|
|
404
|
+
|
|
405
|
+
const portRaw = await p.text({
|
|
406
|
+
message: '开发端口',
|
|
407
|
+
initialValue: String(flags.port || base.port),
|
|
408
|
+
validate: (v) => {
|
|
409
|
+
const n = Number(v)
|
|
410
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return '请输入 1–65535 的端口'
|
|
411
|
+
return undefined
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
if (p.isCancel(portRaw)) return null
|
|
415
|
+
|
|
416
|
+
const apiBaseUrl = await p.text({
|
|
417
|
+
message: 'VITE_API_BASE_URL',
|
|
418
|
+
initialValue: flags.apiBaseUrl || base.apiBaseUrl,
|
|
419
|
+
validate: (v) => (!v?.trim() ? '不能为空' : undefined),
|
|
420
|
+
})
|
|
421
|
+
if (p.isCancel(apiBaseUrl)) return null
|
|
422
|
+
|
|
423
|
+
const signAppId = await p.text({
|
|
424
|
+
message: 'VITE_SIGN_APP_ID(须与 SysOpenApp 一致)',
|
|
425
|
+
initialValue: flags.signAppId || base.signAppId,
|
|
426
|
+
validate: (v) => (!v?.trim() ? '不能为空' : undefined),
|
|
427
|
+
})
|
|
428
|
+
if (p.isCancel(signAppId)) return null
|
|
429
|
+
|
|
430
|
+
const signAppSecret = await p.text({
|
|
431
|
+
message: 'VITE_SIGN_APP_SECRET(开发密钥,勿用于生产)',
|
|
432
|
+
initialValue: flags.signAppSecret || base.signAppSecret,
|
|
433
|
+
validate: (v) => (!v?.trim() ? '不能为空' : undefined),
|
|
434
|
+
})
|
|
435
|
+
if (p.isCancel(signAppSecret)) return null
|
|
436
|
+
|
|
437
|
+
const packageManager = await p.select({
|
|
438
|
+
message: '安装依赖',
|
|
439
|
+
options: [
|
|
440
|
+
{ value: 'npm', label: 'npm' },
|
|
441
|
+
{ value: 'pnpm', label: 'pnpm' },
|
|
442
|
+
{ value: 'yarn', label: 'yarn' },
|
|
443
|
+
{ value: 'bun', label: 'bun' },
|
|
444
|
+
{ value: 'none', label: '跳过安装' },
|
|
445
|
+
],
|
|
446
|
+
initialValue: flags.packageManager ? normalizePm(flags.packageManager) : 'npm',
|
|
447
|
+
})
|
|
448
|
+
if (p.isCancel(packageManager)) return null
|
|
449
|
+
|
|
450
|
+
const name = projectName.trim()
|
|
451
|
+
return {
|
|
452
|
+
projectName: name,
|
|
453
|
+
packageName: toPackageName(name),
|
|
454
|
+
title: title.trim(),
|
|
455
|
+
apiProxy: apiProxy.trim().replace(/\/$/, ''),
|
|
456
|
+
port: Number(portRaw),
|
|
457
|
+
apiBaseUrl: apiBaseUrl.trim(),
|
|
458
|
+
signAppId: signAppId.trim(),
|
|
459
|
+
signAppSecret: signAppSecret.trim(),
|
|
460
|
+
packageManager,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 入口。
|
|
466
|
+
* @returns {Promise<void>}
|
|
467
|
+
*/
|
|
468
|
+
async function main() {
|
|
469
|
+
let flags
|
|
470
|
+
try {
|
|
471
|
+
flags = parseArgs(process.argv)
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
474
|
+
printHelp()
|
|
475
|
+
process.exit(1)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (flags.help) {
|
|
479
|
+
printHelp()
|
|
480
|
+
process.exit(0)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log()
|
|
484
|
+
p.intro(pc.bgCyan(pc.black(' create-simpleadmin-ui ')))
|
|
485
|
+
|
|
486
|
+
let answers
|
|
487
|
+
try {
|
|
488
|
+
answers = await collectAnswers(flags)
|
|
489
|
+
} catch (err) {
|
|
490
|
+
p.cancel(err instanceof Error ? err.message : String(err))
|
|
491
|
+
process.exit(1)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!answers) {
|
|
495
|
+
p.cancel('已取消')
|
|
496
|
+
process.exit(0)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const targetDir = path.resolve(process.cwd(), answers.projectName)
|
|
500
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
501
|
+
p.cancel(`目录非空:${targetDir}`)
|
|
502
|
+
process.exit(1)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const templateDir = resolveTemplateDir()
|
|
506
|
+
const spin = p.spinner()
|
|
507
|
+
spin.start('正在生成项目…')
|
|
508
|
+
try {
|
|
509
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
510
|
+
copyTemplate(templateDir, targetDir, answers)
|
|
511
|
+
spin.stop('项目文件已写入')
|
|
512
|
+
} catch (err) {
|
|
513
|
+
spin.stop('生成失败')
|
|
514
|
+
p.cancel(err instanceof Error ? err.message : String(err))
|
|
515
|
+
process.exit(1)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (answers.packageManager !== 'none') {
|
|
519
|
+
const installSpin = p.spinner()
|
|
520
|
+
installSpin.start(`使用 ${answers.packageManager} 安装依赖…`)
|
|
521
|
+
const ok = installDeps(targetDir, answers.packageManager)
|
|
522
|
+
if (ok) installSpin.stop('依赖安装完成')
|
|
523
|
+
else installSpin.stop('依赖安装失败,可稍后手动执行')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const cd = answers.projectName === '.' ? '' : `cd ${answers.projectName}`
|
|
527
|
+
const runDev =
|
|
528
|
+
answers.packageManager === 'yarn'
|
|
529
|
+
? 'yarn dev'
|
|
530
|
+
: answers.packageManager === 'pnpm'
|
|
531
|
+
? 'pnpm dev'
|
|
532
|
+
: answers.packageManager === 'bun'
|
|
533
|
+
? 'bun run dev'
|
|
534
|
+
: 'npm run dev'
|
|
535
|
+
|
|
536
|
+
p.note(
|
|
537
|
+
[
|
|
538
|
+
cd,
|
|
539
|
+
answers.packageManager === 'none' ? 'npm install' : null,
|
|
540
|
+
runDev,
|
|
541
|
+
'',
|
|
542
|
+
`浏览器:http://localhost:${answers.port}`,
|
|
543
|
+
`代理 → ${answers.apiProxy}`,
|
|
544
|
+
'请确认签名配置与后端 SysOpenApp 一致。',
|
|
545
|
+
]
|
|
546
|
+
.filter((line) => line !== null)
|
|
547
|
+
.join('\n'),
|
|
548
|
+
'下一步',
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
p.outro(pc.green(`完成:${answers.title} → ${targetDir}`))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
main().catch((err) => {
|
|
555
|
+
console.error(err)
|
|
556
|
+
process.exit(1)
|
|
557
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将同级 Temp.SimpleAdmin.UI.Vue3 同步到本包 template/,供 npm pack / 发布使用。
|
|
3
|
+
* 源目录仍是开发真相;发布前由 prepack 自动执行本脚本。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const root = path.resolve(__dirname, '..')
|
|
11
|
+
const source = path.resolve(root, '../Temp.SimpleAdmin.UI.Vue3')
|
|
12
|
+
const target = path.resolve(root, 'template')
|
|
13
|
+
|
|
14
|
+
/** 不同步到发布模板的路径片段 */
|
|
15
|
+
const SKIP_NAMES = new Set([
|
|
16
|
+
'node_modules',
|
|
17
|
+
'dist',
|
|
18
|
+
'dist-ssr',
|
|
19
|
+
'.git',
|
|
20
|
+
'.DS_Store',
|
|
21
|
+
'package-lock.json',
|
|
22
|
+
'pnpm-lock.yaml',
|
|
23
|
+
'yarn.lock',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断相对路径是否应跳过。
|
|
28
|
+
* @param {string} rel 相对源根的路径
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function shouldSkip(rel) {
|
|
32
|
+
const parts = rel.split(/[/\\]/).filter(Boolean)
|
|
33
|
+
return parts.some((p) => SKIP_NAMES.has(p) || p.endsWith('.local'))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 递归复制目录。
|
|
38
|
+
* @param {string} from 源
|
|
39
|
+
* @param {string} to 目标
|
|
40
|
+
* @param {string} [rel=''] 相对路径(用于跳过判断)
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
function copyRecursive(from, to, rel = '') {
|
|
44
|
+
if (shouldSkip(rel)) return
|
|
45
|
+
const stat = fs.statSync(from)
|
|
46
|
+
if (stat.isDirectory()) {
|
|
47
|
+
fs.mkdirSync(to, { recursive: true })
|
|
48
|
+
for (const name of fs.readdirSync(from)) {
|
|
49
|
+
copyRecursive(path.join(from, name), path.join(to, name), path.join(rel, name))
|
|
50
|
+
}
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
54
|
+
fs.copyFileSync(from, to)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(path.join(source, 'package.json'))) {
|
|
58
|
+
console.error(`[sync-template] 找不到源模板: ${source}`)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
63
|
+
copyRecursive(source, target)
|
|
64
|
+
console.log(`[sync-template] ${source} -> ${target}`)
|