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.
Files changed (55) hide show
  1. package/README.md +53 -0
  2. package/bin/create-simpleadmin-ui.mjs +2 -0
  3. package/package.json +35 -0
  4. package/src/index.mjs +557 -0
  5. package/src/sync-template.mjs +64 -0
  6. package/template/.env.development +4 -0
  7. package/template/.env.production.example +4 -0
  8. package/template/.vscode/extensions.json +3 -0
  9. package/template/README.md +26 -0
  10. package/template/index.html +19 -0
  11. package/template/package.json +30 -0
  12. package/template/public/vite.svg +1 -0
  13. package/template/src/App.vue +3 -0
  14. package/template/src/components/AppBreadcrumb.vue +132 -0
  15. package/template/src/components/AppPage.vue +23 -0
  16. package/template/src/components/ChangePasswordDialog.vue +97 -0
  17. package/template/src/components/ProfileEditDialog.vue +142 -0
  18. package/template/src/components/RichTextEditor.vue +121 -0
  19. package/template/src/components/SidebarMenuItem.vue +56 -0
  20. package/template/src/components/TagsView.vue +172 -0
  21. package/template/src/constants/menuIcons.ts +205 -0
  22. package/template/src/directives/permission.ts +13 -0
  23. package/template/src/env.d.ts +22 -0
  24. package/template/src/layouts/MainLayout.vue +410 -0
  25. package/template/src/main.ts +27 -0
  26. package/template/src/router/index.ts +198 -0
  27. package/template/src/stores/tagsView.ts +161 -0
  28. package/template/src/stores/theme.ts +68 -0
  29. package/template/src/stores/user.ts +191 -0
  30. package/template/src/styles/global.css +314 -0
  31. package/template/src/utils/datetime.ts +34 -0
  32. package/template/src/utils/request.ts +324 -0
  33. package/template/src/utils/requestSign.ts +162 -0
  34. package/template/src/views/error/NotFound.vue +49 -0
  35. package/template/src/views/home/HomeView.vue +1177 -0
  36. package/template/src/views/login/LoginView.vue +576 -0
  37. package/template/src/views/monitor/loginlog/index.vue +366 -0
  38. package/template/src/views/monitor/online/index.vue +298 -0
  39. package/template/src/views/monitor/operlog/index.vue +492 -0
  40. package/template/src/views/system/config/index.vue +407 -0
  41. package/template/src/views/system/dept/index.vue +517 -0
  42. package/template/src/views/system/dict/index.vue +747 -0
  43. package/template/src/views/system/file/index.vue +1031 -0
  44. package/template/src/views/system/menu/index.vue +822 -0
  45. package/template/src/views/system/message/index.vue +422 -0
  46. package/template/src/views/system/notice/index.vue +578 -0
  47. package/template/src/views/system/openApp/index.vue +596 -0
  48. package/template/src/views/system/post/index.vue +495 -0
  49. package/template/src/views/system/role/index.vue +546 -0
  50. package/template/src/views/system/user/index.vue +373 -0
  51. package/template/src/vite-env.d.ts +1 -0
  52. package/template/tsconfig.app.json +30 -0
  53. package/template/tsconfig.json +7 -0
  54. package/template/tsconfig.node.json +24 -0
  55. 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)。
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.mjs'
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}`)