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 +110 -0
- package/package.json +22 -0
- package/template/.env.development +1 -0
- package/template/_gitignore +23 -0
- package/template/env.d.ts +1 -0
- package/template/index.html +12 -0
- package/template/package.json +26 -0
- package/template/scripts/create-page.mjs +453 -0
- package/template/src/App.vue +19 -0
- package/template/src/main.ts +60 -0
- package/template/src/pages/auth/LoginPage.vue +92 -0
- package/template/src/pages/notFound/NotFound.vue +29 -0
- package/template/src/router/index.ts +64 -0
- package/template/src/stores/asyncRoute.ts +24 -0
- package/template/src/stores/user.ts +100 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/tsconfig.app.json +4 -0
- package/template/tsconfig.json +24 -0
- package/template/tsconfig.node.json +4 -0
- package/template/vite-plugin-librex.ts +293 -0
- package/template/vite.config.ts +35 -0
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>
|