create-librex 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/index.mjs ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-librex — LibreX 项目脚手架
5
+ *
6
+ * 用法: npm create librex@latest [project-name]
7
+ */
8
+
9
+ import * as fs from 'node:fs'
10
+ import * as path from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import * as readline from 'node:readline'
13
+ import { execSync, spawn } from 'node:child_process'
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
+ const TEMPLATE_DIR = path.join(__dirname, 'template')
17
+
18
+ // ── ANSI ──
19
+ const c = {
20
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
21
+ green: '\x1b[32m', cyan: '\x1b[36m', yellow: '\x1b[33m',
22
+ red: '\x1b[31m', magenta: '\x1b[35m',
23
+ }
24
+
25
+ // ── 工具 ──
26
+ function ask(rl, q) { return new Promise(r => rl.question(q, r)) }
27
+
28
+ function toKebab(str) {
29
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase()
30
+ }
31
+
32
+ // ══════════════════════════════════════════
33
+ // 文件拷贝 — 跳过 _gitignore → .gitignore 重命名
34
+ // ══════════════════════════════════════════
35
+ function copyDir(src, dest) {
36
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true })
37
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
38
+ const srcPath = path.join(src, entry.name)
39
+ let destName = entry.name
40
+ if (destName === '_gitignore') destName = '.gitignore'
41
+ const destPath = path.join(dest, destName)
42
+ if (entry.isDirectory()) {
43
+ copyDir(srcPath, destPath)
44
+ } else {
45
+ fs.copyFileSync(srcPath, destPath)
46
+ }
47
+ }
48
+ }
49
+
50
+ // ══════════════════════════════════════════
51
+ // 主流程
52
+ // ══════════════════════════════════════════
53
+
54
+ async function main() {
55
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
56
+
57
+ console.log(`\n ${c.magenta}┌──────────────────────────────────────────┐${c.reset}`)
58
+ console.log(` ${c.magenta}│${c.reset} ${c.bold}LibreX${c.reset} — 积木式后台管理框架 ${c.magenta}│${c.reset}`)
59
+ console.log(` ${c.magenta}└──────────────────────────────────────────┘${c.reset}\n`)
60
+
61
+ // ── 项目名 ──
62
+ let projectName = process.argv[2]
63
+ if (!projectName) {
64
+ projectName = (await ask(rl, ` ${c.green}?${c.reset} 项目名称 ${c.dim}(librex-app):${c.reset} `)).trim()
65
+ }
66
+ if (!projectName) projectName = 'librex-app'
67
+ projectName = toKebab(projectName)
68
+
69
+ const targetDir = path.join(process.cwd(), projectName)
70
+
71
+ if (fs.existsSync(targetDir)) {
72
+ console.log(`\n ${c.yellow}⚠ 目录 "${projectName}" 已存在${c.reset}`)
73
+ const answer = (await ask(rl, ` ${c.green}?${c.reset} 是否覆盖? ${c.dim}(y/N):${c.reset} `)).trim().toLowerCase()
74
+ if (answer !== 'y') {
75
+ console.log(` ${c.dim}已取消${c.reset}\n`)
76
+ rl.close()
77
+ return
78
+ }
79
+ fs.rmSync(targetDir, { recursive: true, force: true })
80
+ }
81
+
82
+ rl.close()
83
+
84
+ // ── 拷贝模板 ──
85
+ console.log(`\n ${c.green}✧${c.reset} 创建项目 ${c.cyan}${projectName}${c.reset} ...`)
86
+ copyDir(TEMPLATE_DIR, targetDir)
87
+
88
+ // ── 写入项目名到 package.json ──
89
+ const pkgPath = path.join(targetDir, 'package.json')
90
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
91
+ pkg.name = projectName
92
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
93
+
94
+ // ── 安装依赖 ──
95
+ console.log(` ${c.green}✧${c.reset} 安装依赖 ...\n`)
96
+ try {
97
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
98
+ } catch {
99
+ console.log(`\n ${c.yellow}⚠ 依赖安装失败,请手动运行 npm install${c.reset}`)
100
+ }
101
+
102
+ console.log(`\n ${c.green}Done${c.reset} 项目创建完成\n`)
103
+ console.log(` ${c.dim}cd${c.reset} ${c.cyan}${projectName}${c.reset}`)
104
+ console.log(` ${c.dim}npm run dev${c.reset}\n`)
105
+ }
106
+
107
+ main().catch(err => {
108
+ console.error(`${c.red}创建失败:${c.reset}`, err.message)
109
+ process.exit(1)
110
+ })
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-librex",
3
+ "version": "1.0.1",
4
+ "description": "LibreX — 积木式后台管理框架项目脚手架",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-librex": "./index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs",
11
+ "template"
12
+ ],
13
+ "keywords": [
14
+ "librex",
15
+ "create-librex",
16
+ "admin",
17
+ "framework",
18
+ "vue",
19
+ "scaffold"
20
+ ],
21
+ "license": "MIT"
22
+ }
@@ -0,0 +1 @@
1
+ VITE_PROXY_TARGET=http://localhost:3000
@@ -0,0 +1,23 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build
5
+ dist/
6
+
7
+ # Dev
8
+ *.local
9
+ .env.local
10
+ .env.*.local
11
+
12
+ # IDE
13
+ .vscode/*
14
+ !.vscode/extensions.json
15
+ .idea/
16
+
17
+ # OS
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Logs
22
+ *.log
23
+ npm-debug.log*
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>LibreX</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "librex-app",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vue-tsc -b && vite build",
8
+ "preview": "vite preview",
9
+ "typecheck": "vue-tsc --noEmit",
10
+ "create": "node scripts/create-page.mjs",
11
+ "create:page": "npm run create"
12
+ },
13
+ "dependencies": {
14
+ "librex": "^1.0.0",
15
+ "pinia": "^3.0.0",
16
+ "vue": "^3.5.0",
17
+ "vue-router": "^5.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@vitejs/plugin-vue": "^6.0.0",
21
+ "typescript": "^5.7.0",
22
+ "vite": "^8.0.0",
23
+ "vue-tsc": "^2.0.0",
24
+ "less": "^4.2.0"
25
+ }
26
+ }
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Librex 页面创建器
5
+ *
6
+ * 用法:
7
+ * npm run create
8
+ * npm run create:page
9
+ */
10
+
11
+ import * as readline from 'node:readline'
12
+ import * as fs from 'node:fs'
13
+ import * as path from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
17
+ const ROOT = path.resolve(__dirname, '..')
18
+ const PAGES_DIR = path.join(ROOT, 'src', 'pages')
19
+
20
+ // ── ANSI 颜色 ──
21
+ const c = {
22
+ reset: '\x1b[0m',
23
+ bold: '\x1b[1m',
24
+ dim: '\x1b[2m',
25
+ green: '\x1b[32m',
26
+ cyan: '\x1b[36m',
27
+ yellow: '\x1b[33m',
28
+ red: '\x1b[31m',
29
+ magenta: '\x1b[35m',
30
+ }
31
+
32
+ // ── TypeScript 转 kebab-case ──
33
+ function toKebabCase(str) {
34
+ return str
35
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
36
+ .replace(/[\s_]+/g, '-')
37
+ .toLowerCase()
38
+ }
39
+
40
+ function toPascalCase(str) {
41
+ return str
42
+ .split(/[-_\s]+/)
43
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
44
+ .join('')
45
+ }
46
+
47
+ // ── 提示工具 ──
48
+ function createPrompter() {
49
+ const rl = readline.createInterface({
50
+ input: process.stdin,
51
+ output: process.stdout,
52
+ })
53
+ return {
54
+ ask(question) {
55
+ return new Promise(resolve => rl.question(question, resolve))
56
+ },
57
+ close() {
58
+ rl.close()
59
+ },
60
+ }
61
+ }
62
+
63
+ // ══════════════════════════════════════════
64
+ // 模板生成器
65
+ // ══════════════════════════════════════════
66
+
67
+ /** table 类型 — 表格列表页(最常用),带增删改查 + 导出 */
68
+ function templateTable(config) {
69
+ const { title, pagePath, icon, navGroup, navOrder, name } = config
70
+ const navOrderLine = navOrder !== undefined ? `\n navOrder: ${navOrder},` : ''
71
+ const navGroupLine = navGroup ? `\n navGroup: '${navGroup}',` : ''
72
+
73
+ return `import { definePageConfig, useContext, useConfirm } from 'librex'
74
+
75
+ export default definePageConfig({
76
+ title: '${title}',
77
+ path: '${pagePath}',
78
+ icon: '${icon}',${navOrderLine}${navGroupLine}
79
+
80
+ // 只声明 LDataTable,框架自动推论 LRowActionBar + LBatchActionBar 配套积木
81
+ builtinBricks: ['LDataTable'],
82
+
83
+ // 编辑走 overlay 覆层 — LForm 积木自动读 ctx.formFields 渲染
84
+ overlayBrick: 'LForm',
85
+
86
+ setup({ setTableData, setTableColumns, setFormFields, setPageHooks }) {
87
+ // store 引用 — 实时读取 tableData/tableColumns,用于 hooks 闭包
88
+ const store = useContext()
89
+ const confirmStore = useConfirm()
90
+
91
+ // 表格列定义
92
+ setTableColumns([
93
+ { key: 'id', label: 'ID', sortable: true, width: 60 },
94
+ { key: 'name', label: '名称', sortable: true },
95
+ { key: 'desc', label: '描述' },
96
+ { key: 'status', label: '状态' },
97
+ ])
98
+
99
+ // 表格数据(替换为实际 API 调用)
100
+ setTableData([
101
+ { id: 1, name: '示例数据 1', desc: '描述文本', status: 1 },
102
+ { id: 2, name: '示例数据 2', desc: '描述文本', status: 0 },
103
+ ])
104
+
105
+ // 表单字段 — LForm overlay 编辑时使用
106
+ setFormFields([
107
+ { key: 'name', label: '名称', fieldType: 'input', required: true },
108
+ { key: 'desc', label: '描述', fieldType: 'textarea' },
109
+ {
110
+ key: 'status',
111
+ label: '状态',
112
+ fieldType: 'select',
113
+ props: {
114
+ options: [
115
+ { label: '启用', value: 1 },
116
+ { label: '禁用', value: 0 },
117
+ ],
118
+ },
119
+ },
120
+ ])
121
+
122
+ // 页面行为 — 使用框架统一确认模态框
123
+ setPageHooks({
124
+ onDelete: async (row) => {
125
+ const confirmed = await confirmStore.confirmDelete([row.id as number])
126
+ if (!confirmed) return
127
+ store.setTableData((store.tableData as any[]).filter((r: any) => r.id !== row.id))
128
+ store.pushNotification({ type: 'success', title: '已删除' })
129
+ },
130
+
131
+ onBatchDelete: async (ids) => {
132
+ const confirmed = await confirmStore.confirmDelete(ids)
133
+ if (!confirmed) return
134
+ store.removeRows(ids)
135
+ store.clearSelection()
136
+ store.pushNotification({ type: 'success', title: \`已删除 \${ids.length} 条\` })
137
+ },
138
+
139
+ onSave: async (item) => {
140
+ // 新建 / 编辑保存逻辑
141
+ // const saved = item.id
142
+ // ? await api.update(item.id, item)
143
+ // : await api.create(item)
144
+ return { ...item }
145
+ },
146
+ })
147
+ },
148
+ })
149
+ `
150
+ }
151
+
152
+ /** form 类型 — 表单/详情页,带 LForm + LPageNavBar */
153
+ function templateForm(config) {
154
+ const { title, pagePath, icon, navGroup, navOrder, name } = config
155
+ const navOrderLine = navOrder !== undefined ? `\n navOrder: ${navOrder},` : ''
156
+ const navGroupLine = navGroup ? `\n navGroup: '${navGroup}',` : ''
157
+
158
+ return `import { definePageConfig } from 'librex'
159
+
160
+ export default definePageConfig({
161
+ title: '${title}',
162
+ path: '${pagePath}',
163
+ icon: '${icon}',${navOrderLine}${navGroupLine}
164
+
165
+ builtinBricks: ['LForm', 'LPageNavBar'],
166
+
167
+ // 详情/编辑页默认进入 reviewing 状态
168
+ defaultState: 'reviewing',
169
+
170
+ setup({ setFormFields, setTableData }) {
171
+ // 表单字段定义
172
+ setFormFields([
173
+ { key: 'id', label: 'ID', fieldType: 'input', modes: { edit: { visible: false }, create: { visible: false } } },
174
+ { key: 'name', label: '名称', fieldType: 'input', required: true },
175
+ { key: 'desc', label: '描述', fieldType: 'textarea' },
176
+ {
177
+ key: 'status',
178
+ label: '状态',
179
+ fieldType: 'select',
180
+ props: {
181
+ options: [
182
+ { label: '启用', value: 1 },
183
+ { label: '禁用', value: 0 },
184
+ ],
185
+ },
186
+ },
187
+ ])
188
+
189
+ // 示例数据(替换为实际 API 调用,如根据路由参数 id 加载详情)
190
+ setTableData([
191
+ { id: 1, name: '示例名称', desc: '示例描述', status: 1 },
192
+ ])
193
+ },
194
+ })
195
+ `
196
+ }
197
+
198
+ /** custom 类型 — 自定义 Vue 积木组件页面 */
199
+ function templateCustom(config) {
200
+ const { title, pagePath, icon, navGroup, navOrder, name } = config
201
+ const navOrderLine = navOrder !== undefined ? `\n navOrder: ${navOrder},` : ''
202
+ const navGroupLine = navGroup ? `\n navGroup: '${navGroup}',` : ''
203
+ const componentName = toPascalCase(name) + 'View'
204
+
205
+ return {
206
+ pageConfig: `import { definePageConfig } from 'librex'
207
+ import ${componentName} from './${componentName}.vue'
208
+
209
+ export default definePageConfig({
210
+ title: '${title}',
211
+ path: '${pagePath}',
212
+ icon: '${icon}',${navOrderLine}${navGroupLine}
213
+
214
+ bricks: {
215
+ ${componentName}: {
216
+ component: ${componentName},
217
+ tier: 'content',
218
+ position: 'center',
219
+ },
220
+ },
221
+ })
222
+ `,
223
+ vueComponent: `<template>
224
+ <div class="${toKebabCase(name)}-wrap">
225
+ <div class="page-header">
226
+ <h2>${title}</h2>
227
+ <p class="page-desc">页面描述,可根据实际需求修改。</p>
228
+ </div>
229
+ <div class="page-body">
230
+ <!-- 在此编写页面内容 -->
231
+ </div>
232
+ </div>
233
+ </template>
234
+
235
+ <script setup lang="ts">
236
+ import { ref } from 'vue'
237
+
238
+ // 在此编写页面逻辑
239
+ const loading = ref(false)
240
+ </script>
241
+
242
+ <style scoped>
243
+ .${toKebabCase(name)}-wrap {
244
+ padding: 24px;
245
+ }
246
+
247
+ .page-header {
248
+ margin-bottom: 24px;
249
+ }
250
+
251
+ .page-header h2 {
252
+ margin: 0 0 8px;
253
+ font-size: 20px;
254
+ font-weight: 600;
255
+ color: var(--color-text-primary);
256
+ }
257
+
258
+ .page-desc {
259
+ margin: 0;
260
+ font-size: 14px;
261
+ color: var(--color-text-tertiary);
262
+ }
263
+ </style>
264
+ `,
265
+ }
266
+ }
267
+
268
+ /** mixed 类型 — 多积木混合页(内置积木 + setup + 树/搜索) */
269
+ function templateMixed(config) {
270
+ const { title, pagePath, icon, navGroup, navOrder, name } = config
271
+ const navOrderLine = navOrder !== undefined ? `\n navOrder: ${navOrder},` : ''
272
+ const navGroupLine = navGroup ? `\n navGroup: '${navGroup}',` : ''
273
+
274
+ return `import { definePageConfig, useContext, useConfirm } from 'librex'
275
+
276
+ export default definePageConfig({
277
+ title: '${title}',
278
+ path: '${pagePath}',
279
+ icon: '${icon}',${navOrderLine}${navGroupLine}
280
+
281
+ builtinBricks: ['LDataTable', 'LSearchBar', 'LFilterPanel', 'LTreeNav'],
282
+
283
+ states: {
284
+ idle: {
285
+ LSearchBar: 'visible',
286
+ LFilterPanel: 'visible',
287
+ LTreeNav: 'visible',
288
+ },
289
+ },
290
+
291
+ setup({ setTableData, setTableColumns, setTreeData, setPageHooks }) {
292
+ const store = useContext()
293
+ const confirmStore = useConfirm()
294
+
295
+ setTableColumns([
296
+ { key: 'id', label: 'ID', sortable: true, width: 60 },
297
+ { key: 'name', label: '名称', sortable: true },
298
+ { key: 'status', label: '状态', sortable: true },
299
+ ])
300
+
301
+ setTableData([
302
+ { id: 1, name: '系统 A', status: '运行中' },
303
+ { id: 2, name: '系统 B', status: '维护中' },
304
+ { id: 3, name: '模块 C', status: '运行中' },
305
+ { id: 4, name: '服务 D', status: '已停止' },
306
+ ])
307
+
308
+ setTreeData([
309
+ { id: 100, label: '分类一', count: 4, expanded: true, children: [
310
+ { id: 101, label: '子分类 A', count: 2, expanded: false },
311
+ { id: 102, label: '子分类 B', count: 2, expanded: false },
312
+ ]},
313
+ { id: 200, label: '分类二', count: 0, expanded: false },
314
+ ])
315
+
316
+ // 统一使用框架确认模态框
317
+ setPageHooks({
318
+ onDelete: async (row) => {
319
+ const confirmed = await confirmStore.confirmDelete([row.id as number])
320
+ if (!confirmed) return
321
+ store.setTableData((store.tableData as any[]).filter((r: any) => r.id !== row.id))
322
+ store.pushNotification({ type: 'success', title: '已删除' })
323
+ },
324
+ onBatchDelete: async (ids) => {
325
+ const confirmed = await confirmStore.confirmDelete(ids)
326
+ if (!confirmed) return
327
+ store.removeRows(ids)
328
+ store.clearSelection()
329
+ store.pushNotification({ type: 'success', title: \`已删除 \${ids.length} 条\` })
330
+ },
331
+ })
332
+ },
333
+ })
334
+ `
335
+ }
336
+
337
+ const TEMPLATES = {
338
+ table: { fn: templateTable, desc: '表格列表页 — LDataTable + overlay LForm 编辑,支持增删改查 + 导出' },
339
+ form: { fn: templateForm, desc: '表单/详情页 — LForm + LPageNavBar,路径通常带 :id 参数' },
340
+ custom: { fn: templateCustom, desc: '自定义积木页 — 生成 .vue 组件 + 声明式挂载' },
341
+ mixed: { fn: templateMixed, desc: '多积木混合页 — 表格 + 搜索 + 筛选 + 树形导航' },
342
+ }
343
+
344
+ // ══════════════════════════════════════════
345
+ // 主流程
346
+ // ══════════════════════════════════════════
347
+
348
+ async function main() {
349
+ const prompter = createPrompter()
350
+
351
+ console.log(`\n ${c.magenta}┌──────────────────────────────┐${c.reset}`)
352
+ console.log(` ${c.magenta}│${c.reset} ${c.bold}LibreX ─ create page${c.reset} ${c.magenta}│${c.reset}`)
353
+ console.log(` ${c.magenta}└──────────────────────────────┘${c.reset}`)
354
+ console.log(` ${c.dim}声明式页面配置,路由编译期自动注册。${c.reset}\n`)
355
+
356
+ // ── 页面目录名 ──
357
+ let name = ''
358
+ while (!name) {
359
+ name = (await prompter.ask(` ${c.green}?${c.reset} 目录名 ${c.dim}(kebab-case):${c.reset} `)).trim()
360
+ if (!name) {
361
+ console.log(` ${c.red}名称不能为空${c.reset}`)
362
+ continue
363
+ }
364
+ // 规范化
365
+ name = toKebabCase(name)
366
+ if (fs.existsSync(path.join(PAGES_DIR, name))) {
367
+ console.log(` ${c.yellow}⚠ 目录 "${name}" 已存在${c.reset}`)
368
+ const overwrite = (await prompter.ask(` ${c.green}?${c.reset} 是否覆盖? ${c.dim}(y/N)${c.reset} `)).trim().toLowerCase()
369
+ if (overwrite !== 'y') {
370
+ console.log(` ${c.dim}已取消${c.reset}`)
371
+ prompter.close()
372
+ return
373
+ }
374
+ }
375
+ }
376
+
377
+ // ── 标题 ──
378
+ let title = (await prompter.ask(` ${c.green}?${c.reset} 标题 ${c.dim}(${name}):${c.reset} `)).trim()
379
+ if (!title) title = name
380
+
381
+ // ── 路由 ──
382
+ const defaultPath = `/${name}`
383
+ let pagePath = (await prompter.ask(` ${c.green}?${c.reset} 路由 ${c.dim}(${defaultPath}):${c.reset} `)).trim()
384
+ if (!pagePath) pagePath = defaultPath
385
+ if (!pagePath.startsWith('/')) pagePath = '/' + pagePath
386
+
387
+ // ── 类型 ──
388
+ console.log('')
389
+ console.log(` ${c.bold}页面类型:${c.reset}`)
390
+ const typeOrder = ['table', 'form', 'mixed', 'custom']
391
+ typeOrder.forEach((t, i) => {
392
+ console.log(` ${c.green}${i + 1}${c.reset}. ${c.bold}${t}${c.reset} — ${c.dim}${TEMPLATES[t].desc}${c.reset}`)
393
+ })
394
+ console.log('')
395
+ let pageType = ''
396
+ while (!TEMPLATES[pageType]) {
397
+ const answer = (await prompter.ask(` ${c.green}?${c.reset} 类型 ${c.dim}(1):${c.reset} `)).trim()
398
+ const idx = parseInt(answer, 10)
399
+ if (idx >= 1 && idx <= 4) pageType = typeOrder[idx - 1]
400
+ else if (!answer) pageType = 'table'
401
+ else if (TEMPLATES[answer]) pageType = answer
402
+ }
403
+
404
+ // ── 图标 / 分组 / 排序 ──
405
+ const iconMap = { table: 'database', form: 'file-text', custom: 'layout', mixed: 'layers' }
406
+ const defaultIcon = iconMap[pageType]
407
+ let icon = (await prompter.ask(` ${c.green}?${c.reset} 图标 ${c.dim}(${defaultIcon}):${c.reset} `)).trim()
408
+ if (!icon) icon = defaultIcon
409
+
410
+ let navGroup = (await prompter.ask(` ${c.green}?${c.reset} 分组 ${c.dim}(可选):${c.reset} `)).trim()
411
+ let navOrder = undefined
412
+ const orderAnswer = (await prompter.ask(` ${c.green}?${c.reset} 排序 ${c.dim}(可选):${c.reset} `)).trim()
413
+ if (orderAnswer && !isNaN(parseInt(orderAnswer, 10))) {
414
+ navOrder = parseInt(orderAnswer, 10)
415
+ }
416
+
417
+ prompter.close()
418
+
419
+ // ── 摘要 ──
420
+ console.log(`\n ${c.magenta}┌─ create ${c.bold}${pageType}${c.reset}${c.magenta} ─────────────────────┐${c.reset}`)
421
+ console.log(` ${c.magenta}│${c.reset} ${c.cyan}src/pages/${name}/${c.reset}`)
422
+ console.log(` ${c.magenta}│${c.reset} ${c.dim}title:${c.reset} ${title}`)
423
+ console.log(` ${c.magenta}│${c.reset} ${c.dim}path:${c.reset} ${pagePath}`)
424
+ if (navGroup) console.log(` ${c.magenta}│${c.reset} ${c.dim}group:${c.reset} ${navGroup}`)
425
+ if (navOrder !== undefined) console.log(` ${c.magenta}│${c.reset} ${c.dim}order:${c.reset} ${navOrder}`)
426
+ console.log(` ${c.magenta}└────────────────────────────────┘${c.reset}\n`)
427
+
428
+ // ── 生成 ──
429
+ const targetDir = path.join(PAGES_DIR, name)
430
+ fs.mkdirSync(targetDir, { recursive: true })
431
+
432
+ const templateFn = TEMPLATES[pageType].fn
433
+ const config = { title, pagePath, icon, navGroup, navOrder, name }
434
+
435
+ if (pageType === 'custom') {
436
+ const result = templateFn(config)
437
+ fs.writeFileSync(path.join(targetDir, 'pageConfig.ts'), result.pageConfig, 'utf-8')
438
+ fs.writeFileSync(path.join(targetDir, `${toPascalCase(name)}View.vue`), result.vueComponent, 'utf-8')
439
+ console.log(` ${c.green}✔${c.reset} ${c.cyan}${name}/pageConfig.ts${c.reset}`)
440
+ console.log(` ${c.green}✔${c.reset} ${c.cyan}${name}/${toPascalCase(name)}View.vue${c.reset}`)
441
+ } else {
442
+ const content = templateFn(config)
443
+ fs.writeFileSync(path.join(targetDir, 'pageConfig.ts'), content, 'utf-8')
444
+ console.log(` ${c.green}✔${c.reset} ${c.cyan}${name}/pageConfig.ts${c.reset}`)
445
+ }
446
+
447
+ console.log(`\n ${c.green}Done${c.reset} ${c.dim}→${c.reset} ${c.cyan}${pagePath}${c.reset}\n`)
448
+ }
449
+
450
+ main().catch((err) => {
451
+ console.error(`${c.red}✗ 生成失败:${c.reset}`, err.message)
452
+ process.exit(1)
453
+ })
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <SystemShell v-if="!isLoginPage">
3
+ <router-view v-slot="{ Component, route }">
4
+ <component :is="Component" :key="route.path" />
5
+ </router-view>
6
+ </SystemShell>
7
+ <router-view v-else v-slot="{ Component, route }">
8
+ <component :is="Component" :key="route.path" />
9
+ </router-view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue'
14
+ import { useRoute } from 'vue-router'
15
+ import SystemShell from 'librex'
16
+
17
+ const route = useRoute()
18
+ const isLoginPage = computed(() => route.path === '/login')
19
+ </script>