create-lumal-admin 0.0.1-beta
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/scaffold.d.ts +12 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +1577 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +35 -0
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
/***********************项目名称*********************/
|
|
4
|
+
function normalizePackageName(name) {
|
|
5
|
+
const normalizedName = name
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replaceAll('\\', '/')
|
|
9
|
+
.split('/')
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.at(-1)
|
|
12
|
+
?.replace(/[^a-z0-9-]/g, '-')
|
|
13
|
+
.replace(/-+/g, '-')
|
|
14
|
+
.replace(/^-|-$/g, '');
|
|
15
|
+
return normalizedName || 'lumal-admin';
|
|
16
|
+
}
|
|
17
|
+
function resolveProjectName(options) {
|
|
18
|
+
return normalizePackageName(options.name || basename(options.targetDir));
|
|
19
|
+
}
|
|
20
|
+
/***********************模板内容*********************/
|
|
21
|
+
function createPackageJson(projectName) {
|
|
22
|
+
return `${JSON.stringify({
|
|
23
|
+
name: projectName,
|
|
24
|
+
type: 'module',
|
|
25
|
+
version: '0.0.0',
|
|
26
|
+
private: true,
|
|
27
|
+
scripts: {
|
|
28
|
+
dev: 'vite --host 127.0.0.1',
|
|
29
|
+
build: 'vue-tsc --noEmit -p tsconfig.json && vite build',
|
|
30
|
+
preview: 'vite preview --host 127.0.0.1',
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
'@lumal/core': '^0.0.0',
|
|
34
|
+
'@lumal/icons': '^0.0.0',
|
|
35
|
+
'@lumal/icons-vue': '^0.0.0',
|
|
36
|
+
'element-plus': '^2.11.0',
|
|
37
|
+
'vue': '^3.5.0',
|
|
38
|
+
'vue-router': '^4.5.0',
|
|
39
|
+
},
|
|
40
|
+
devDependencies: {
|
|
41
|
+
'@vitejs/plugin-vue': '^6.0.1',
|
|
42
|
+
'typescript': '^6.0.0',
|
|
43
|
+
'vite': '^8.0.0',
|
|
44
|
+
'vue-tsc': '^3.1.1',
|
|
45
|
+
},
|
|
46
|
+
}, null, 2)}\n`;
|
|
47
|
+
}
|
|
48
|
+
function createIndexHtml() {
|
|
49
|
+
return `<!doctype html>
|
|
50
|
+
<html lang="zh-CN">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8" />
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
54
|
+
<title>Lumal Admin</title>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div id="app"></div>
|
|
58
|
+
<script type="module" src="/src/main.ts"></script>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
function createTsConfig() {
|
|
64
|
+
return `{
|
|
65
|
+
"compilerOptions": {
|
|
66
|
+
"target": "ES2022",
|
|
67
|
+
"module": "ESNext",
|
|
68
|
+
"moduleResolution": "Bundler",
|
|
69
|
+
"strict": true,
|
|
70
|
+
"jsx": "preserve",
|
|
71
|
+
"useDefineForClassFields": true,
|
|
72
|
+
"resolveJsonModule": true,
|
|
73
|
+
"types": ["vite/client"],
|
|
74
|
+
"isolatedModules": true,
|
|
75
|
+
"skipLibCheck": true
|
|
76
|
+
},
|
|
77
|
+
"include": [
|
|
78
|
+
"src/**/*.ts",
|
|
79
|
+
"src/**/*.vue",
|
|
80
|
+
"vite.config.ts"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function createViteConfig() {
|
|
86
|
+
return `import { fileURLToPath, URL } from 'node:url'
|
|
87
|
+
import vue from '@vitejs/plugin-vue'
|
|
88
|
+
import { defineConfig } from 'vite'
|
|
89
|
+
|
|
90
|
+
/***********************应用开发配置*********************/
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
plugins: [vue()],
|
|
93
|
+
build: {
|
|
94
|
+
chunkSizeWarningLimit: 500,
|
|
95
|
+
rolldownOptions: {
|
|
96
|
+
output: {
|
|
97
|
+
codeSplitting: {
|
|
98
|
+
groups: [
|
|
99
|
+
{
|
|
100
|
+
maxSize: 420 * 1024,
|
|
101
|
+
name: 'vendor-element-plus',
|
|
102
|
+
priority: 40,
|
|
103
|
+
test: /node_modules[\\\\/]element-plus[\\\\/]/,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
maxSize: 420 * 1024,
|
|
107
|
+
name: 'vendor-vue',
|
|
108
|
+
priority: 30,
|
|
109
|
+
test: /node_modules[\\\\/](?:@vue|vue|vue-router)[\\\\/]/,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
maxSize: 420 * 1024,
|
|
113
|
+
name: 'vendor-lumal',
|
|
114
|
+
priority: 20,
|
|
115
|
+
test: /[\\\\/]packages[\\\\/](?:core|icons)[\\\\/](?:dist|src)[\\\\/]/,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
entriesAware: true,
|
|
119
|
+
maxSize: 420 * 1024,
|
|
120
|
+
name: 'vendor',
|
|
121
|
+
priority: 10,
|
|
122
|
+
test: /node_modules[\\\\/]/,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
resolve: {
|
|
130
|
+
alias: {
|
|
131
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
function createMainTs() {
|
|
138
|
+
return `import type { IconDefinition } from '@lumal/icons'
|
|
139
|
+
import { createLumalAdmin } from '@lumal/core'
|
|
140
|
+
import ElementPlus from 'element-plus'
|
|
141
|
+
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
142
|
+
import App from './App.vue'
|
|
143
|
+
import { router } from './router'
|
|
144
|
+
import { permissionStore } from './services/access'
|
|
145
|
+
import '@lumal/core/theme-chalk/index.scss'
|
|
146
|
+
import '@lumal/core/style.css'
|
|
147
|
+
import '@lumal/icons-vue/style.css'
|
|
148
|
+
import 'element-plus/dist/index.css'
|
|
149
|
+
import './styles.scss'
|
|
150
|
+
|
|
151
|
+
/***********************本地图标定义*********************/
|
|
152
|
+
const localIcons: IconDefinition[] = [
|
|
153
|
+
{
|
|
154
|
+
key: 'app:dashboard',
|
|
155
|
+
label: '控制台',
|
|
156
|
+
source: 'local-svg',
|
|
157
|
+
svgText: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z"/></svg>',
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
/***********************应用启动*********************/
|
|
162
|
+
const framework = createLumalAdmin({
|
|
163
|
+
elementPlus: {
|
|
164
|
+
plugin: ElementPlus,
|
|
165
|
+
options: { locale: zhCn },
|
|
166
|
+
},
|
|
167
|
+
rootComponent: App,
|
|
168
|
+
router,
|
|
169
|
+
permissionStore,
|
|
170
|
+
icons: {
|
|
171
|
+
localSvg: localIcons,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await framework.mount('#app')
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
function createHttpRequestTs() {
|
|
179
|
+
return `import { createRequestClient } from '@lumal/core/request'
|
|
180
|
+
|
|
181
|
+
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
|
|
182
|
+
|
|
183
|
+
export const publicRequest = createRequestClient({ baseURL })
|
|
184
|
+
|
|
185
|
+
export function createAuthenticatedRequest(getToken: () => string | undefined) {
|
|
186
|
+
return createRequestClient({
|
|
187
|
+
baseURL,
|
|
188
|
+
getToken,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function createHttpAuthApiTs() {
|
|
194
|
+
return `import { publicRequest } from './request'
|
|
195
|
+
|
|
196
|
+
export interface LoginInput {
|
|
197
|
+
password: string
|
|
198
|
+
username: string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function login(input: LoginInput) {
|
|
202
|
+
return publicRequest.post('/auth/login', { body: { ...input } })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function refreshSession(refreshToken: string) {
|
|
206
|
+
return publicRequest.post('/auth/refresh', { body: { refreshToken } })
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
function createApiContractMd() {
|
|
211
|
+
return `# HTTP API 接入
|
|
212
|
+
|
|
213
|
+
项目通过 \`VITE_API_BASE_URL\` 配置接口地址。业务项目应在应用层定义响应字段、状态码、登录字段和菜单字段映射,不要把业务协议写入 \`@lumal/core\`。
|
|
214
|
+
|
|
215
|
+
建议后端至少提供:
|
|
216
|
+
|
|
217
|
+
- \`POST /auth/login\`
|
|
218
|
+
- \`POST /auth/refresh\`
|
|
219
|
+
- 当前用户和权限接口
|
|
220
|
+
- 动态菜单接口
|
|
221
|
+
|
|
222
|
+
生产环境推荐使用同域 \`/api\` 反向代理;跨域部署时填写完整 HTTPS 地址。
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
function createAppVue() {
|
|
226
|
+
return `<script setup lang="ts">
|
|
227
|
+
import type { LumalLayoutTabItem } from '@lumal/core/layout'
|
|
228
|
+
import type { LumalPreferences } from '@lumal/core/theme'
|
|
229
|
+
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
230
|
+
import {
|
|
231
|
+
findMenuItemByPath,
|
|
232
|
+
LumalLayout,
|
|
233
|
+
LumalRouterView,
|
|
234
|
+
} from '@lumal/core/layout'
|
|
235
|
+
import { computed, shallowRef, watch } from 'vue'
|
|
236
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
237
|
+
import AppHeaderActions from './components/app/AppHeaderActions.vue'
|
|
238
|
+
import AppSettingsDrawer from './components/app/AppSettingsDrawer.vue'
|
|
239
|
+
import { createAdminSidebarMenus, createAdminTabs } from './router'
|
|
240
|
+
import {
|
|
241
|
+
adminPreferenceDefaults,
|
|
242
|
+
adminPreferences,
|
|
243
|
+
adminResolvedThemeMode,
|
|
244
|
+
patchAdminPreferences,
|
|
245
|
+
} from './services/preferences'
|
|
246
|
+
import { adminTabSnapshotStorageKey, currentUser, logout } from './services/session'
|
|
247
|
+
|
|
248
|
+
/***********************页面状态*********************/
|
|
249
|
+
const title = 'Lumal Admin'
|
|
250
|
+
const settingsVisible = shallowRef(false)
|
|
251
|
+
const routeRefreshKey = shallowRef(0)
|
|
252
|
+
|
|
253
|
+
/***********************路由状态*********************/
|
|
254
|
+
const route = useRoute()
|
|
255
|
+
const router = useRouter()
|
|
256
|
+
const isPublicLayout = computed(() => route.meta.layout === 'public')
|
|
257
|
+
const allMenus = computed(() => createAdminSidebarMenus())
|
|
258
|
+
const fixedTabs = computed<LumalLayoutTabItem[]>(() => createAdminTabs())
|
|
259
|
+
const routeViewKey = computed(() => \`\${route.fullPath}:\${routeRefreshKey.value}\`)
|
|
260
|
+
const routeViewCache = computed(() => adminPreferences.value.tabbar.enable && adminPreferences.value.tabbar.cache)
|
|
261
|
+
const activePath = computed({
|
|
262
|
+
get: () => route.path,
|
|
263
|
+
set: (path: string) => {
|
|
264
|
+
if (path !== route.path) {
|
|
265
|
+
void router.push(path)
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
watch([
|
|
271
|
+
() => route.meta.title,
|
|
272
|
+
() => adminPreferences.value.app.dynamicTitle,
|
|
273
|
+
], () => {
|
|
274
|
+
const routeTitle = typeof route.meta.title === 'string' ? route.meta.title.trim() : ''
|
|
275
|
+
document.title = adminPreferences.value.app.dynamicTitle && routeTitle
|
|
276
|
+
? \`\${routeTitle} - \${title}\`
|
|
277
|
+
: title
|
|
278
|
+
}, { immediate: true })
|
|
279
|
+
|
|
280
|
+
/***********************导航事件*********************/
|
|
281
|
+
function handleMenuSelect(path: string): void {
|
|
282
|
+
const item = findMenuItemByPath(allMenus.value, path)
|
|
283
|
+
|
|
284
|
+
if (item?.externalLink && item.externalTarget !== '_self') {
|
|
285
|
+
window.open(item.externalLink, item.externalTarget ?? '_blank', 'noopener,noreferrer')
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
activePath.value = path
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleToggleSidebar(): void {
|
|
293
|
+
patchAdminPreferences({
|
|
294
|
+
sidebar: { collapsed: !adminPreferences.value.sidebar.collapsed },
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function handleTabChange(path: string): void {
|
|
299
|
+
activePath.value = path
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveAdminRouteTab(currentRoute: RouteLocationNormalizedLoaded): LumalLayoutTabItem | undefined {
|
|
303
|
+
if (currentRoute.meta.layout === 'public' || currentRoute.meta.hideInTab === true) {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const menu = findMenuItemByPath(allMenus.value, currentRoute.path)
|
|
308
|
+
const tabTitle = menu?.title ?? (typeof currentRoute.meta.title === 'string' ? currentRoute.meta.title : undefined)
|
|
309
|
+
|
|
310
|
+
if (!tabTitle) {
|
|
311
|
+
return undefined
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
closable: !['/403', '/404'].includes(currentRoute.path),
|
|
316
|
+
icon: menu?.icon ?? (typeof currentRoute.meta.icon === 'string' ? currentRoute.meta.icon : undefined),
|
|
317
|
+
path: currentRoute.path,
|
|
318
|
+
title: tabTitle,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function handleTabRefresh(path: string): Promise<void> {
|
|
323
|
+
if (path !== route.path) {
|
|
324
|
+
await router.push(path)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
routeRefreshKey.value += 1
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function handlePreferencesChange(preferences: LumalPreferences): void {
|
|
331
|
+
patchAdminPreferences(preferences)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function toggleTheme(): void {
|
|
335
|
+
patchAdminPreferences({
|
|
336
|
+
theme: {
|
|
337
|
+
mode: adminResolvedThemeMode.value === 'dark' ? 'light' : 'dark',
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleLogout(): Promise<void> {
|
|
343
|
+
await logout()
|
|
344
|
+
await router.replace('/login')
|
|
345
|
+
}
|
|
346
|
+
</script>
|
|
347
|
+
|
|
348
|
+
<template>
|
|
349
|
+
<LumalRouterView
|
|
350
|
+
v-if="isPublicLayout"
|
|
351
|
+
:view-key="routeViewKey"
|
|
352
|
+
transition
|
|
353
|
+
transition-name="fade"
|
|
354
|
+
/>
|
|
355
|
+
|
|
356
|
+
<LumalLayout
|
|
357
|
+
v-else
|
|
358
|
+
v-model:active-tab-path="activePath"
|
|
359
|
+
:title="title"
|
|
360
|
+
:menus="allMenus"
|
|
361
|
+
:preferences="adminPreferences"
|
|
362
|
+
route-driven
|
|
363
|
+
:fixed-tabs="fixedTabs"
|
|
364
|
+
:route-tab-resolver="resolveAdminRouteTab"
|
|
365
|
+
tab-fallback-path="/dashboard"
|
|
366
|
+
:tab-storage-key="adminTabSnapshotStorageKey"
|
|
367
|
+
:active-menu-path="activePath"
|
|
368
|
+
@menu-select="handleMenuSelect"
|
|
369
|
+
@toggle-sidebar="handleToggleSidebar"
|
|
370
|
+
@tab-change="handleTabChange"
|
|
371
|
+
@tab-refresh="handleTabRefresh"
|
|
372
|
+
>
|
|
373
|
+
<template #headerActions>
|
|
374
|
+
<AppHeaderActions
|
|
375
|
+
:resolved-theme-mode="adminResolvedThemeMode"
|
|
376
|
+
:user-name="currentUser?.name"
|
|
377
|
+
@logout="handleLogout"
|
|
378
|
+
@open-settings="settingsVisible = true"
|
|
379
|
+
@toggle-theme="toggleTheme"
|
|
380
|
+
/>
|
|
381
|
+
</template>
|
|
382
|
+
|
|
383
|
+
<LumalRouterView
|
|
384
|
+
:view-key="routeViewKey"
|
|
385
|
+
:progress="true"
|
|
386
|
+
:loading="true"
|
|
387
|
+
:cache="routeViewCache"
|
|
388
|
+
:cache-max="adminPreferences.tabbar.maxCount"
|
|
389
|
+
:transition="adminPreferences.transition.enable"
|
|
390
|
+
:transition-name="adminPreferences.transition.name"
|
|
391
|
+
/>
|
|
392
|
+
</LumalLayout>
|
|
393
|
+
|
|
394
|
+
<AppSettingsDrawer
|
|
395
|
+
v-model:visible="settingsVisible"
|
|
396
|
+
:defaults="adminPreferenceDefaults"
|
|
397
|
+
:preferences="adminPreferences"
|
|
398
|
+
@change="handlePreferencesChange"
|
|
399
|
+
@reset="handlePreferencesChange"
|
|
400
|
+
@update:preferences="handlePreferencesChange"
|
|
401
|
+
/>
|
|
402
|
+
</template>
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
function createRouterTs() {
|
|
406
|
+
return `import type { LumalLayoutTabItem } from '@lumal/core/layout'
|
|
407
|
+
import type { LumalStaticMenuRecord, MenuRouteRuntime, RouteRegistryRouterLike, SidebarMenuItem } from '@lumal/core/router'
|
|
408
|
+
import type { Router, RouteRecordRaw, RouterHistory } from 'vue-router'
|
|
409
|
+
import { setupPermissionGuard } from '@lumal/core/permission'
|
|
410
|
+
import { createMenuRouteRuntime } from '@lumal/core/router'
|
|
411
|
+
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
412
|
+
import { permissionStore } from '../services/access'
|
|
413
|
+
import { isAuthenticated } from '../services/session'
|
|
414
|
+
import LoginView from '../views/auth/LoginView.vue'
|
|
415
|
+
import DashboardView from '../views/dashboard/DashboardView.vue'
|
|
416
|
+
import ForbiddenView from '../views/error/ForbiddenView.vue'
|
|
417
|
+
import NotFoundView from '../views/error/NotFoundView.vue'
|
|
418
|
+
import ProjectView from '../views/project/ProjectView.vue'
|
|
419
|
+
|
|
420
|
+
/***********************菜单配置*********************/
|
|
421
|
+
export const adminRouteRecords: LumalStaticMenuRecord[] = [
|
|
422
|
+
{
|
|
423
|
+
component: DashboardView,
|
|
424
|
+
name: 'Dashboard',
|
|
425
|
+
path: '/dashboard',
|
|
426
|
+
meta: {
|
|
427
|
+
authority: ['dashboard:view'],
|
|
428
|
+
icon: 'app:dashboard',
|
|
429
|
+
order: 1,
|
|
430
|
+
title: '工作台',
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
component: ProjectView,
|
|
435
|
+
name: 'Project',
|
|
436
|
+
path: '/project',
|
|
437
|
+
meta: {
|
|
438
|
+
authority: ['project:list'],
|
|
439
|
+
icon: 'app:dashboard',
|
|
440
|
+
order: 2,
|
|
441
|
+
title: '项目管理',
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
const menuRuntimes = new WeakMap<Router, MenuRouteRuntime>()
|
|
447
|
+
|
|
448
|
+
/***********************菜单生成*********************/
|
|
449
|
+
function hasPermission(permissions: string[]): boolean {
|
|
450
|
+
return permissionStore.hasPermission(permissions, 'every')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function hasRole(roles: string[]): boolean {
|
|
454
|
+
return permissionStore.hasRole(roles, 'every')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function flattenMenuTabs(menus: SidebarMenuItem[]): LumalLayoutTabItem[] {
|
|
458
|
+
return menus.flatMap((menu) => {
|
|
459
|
+
if (menu.children.length > 0) {
|
|
460
|
+
return flattenMenuTabs(menu.children)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
icon: menu.icon,
|
|
465
|
+
path: menu.path,
|
|
466
|
+
title: menu.title,
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function createAdminSidebarMenus(): SidebarMenuItem[] {
|
|
472
|
+
return menuRuntimes.get(router)?.sidebarMenus ?? []
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 路由驱动模式下的固定页签:仅返回首页,其余页签由 LumalLayout 依据
|
|
477
|
+
* routeTabResolver 在导航时动态追加,并按 tabStorageKey 做会话级持久化。
|
|
478
|
+
*/
|
|
479
|
+
export function createAdminTabs(): LumalLayoutTabItem[] {
|
|
480
|
+
const menuTabs = flattenMenuTabs(createAdminSidebarMenus())
|
|
481
|
+
const homeTab = menuTabs.find(tab => tab.path === '/dashboard') ?? menuTabs[0]
|
|
482
|
+
|
|
483
|
+
return homeTab ? [{ ...homeTab, closable: false }] : []
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/***********************路由创建*********************/
|
|
487
|
+
function createPublicRoutes(resolveHome: () => string): RouteRecordRaw[] {
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
component: LoginView,
|
|
491
|
+
meta: { layout: 'public', requireLogin: false, title: '登录' },
|
|
492
|
+
name: 'Login',
|
|
493
|
+
path: '/login',
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
path: '/',
|
|
497
|
+
redirect: () => resolveHome(),
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
component: ForbiddenView,
|
|
501
|
+
meta: { title: '无权限' },
|
|
502
|
+
name: 'Forbidden',
|
|
503
|
+
path: '/403',
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
component: NotFoundView,
|
|
507
|
+
meta: { title: '页面不存在' },
|
|
508
|
+
name: 'NotFound',
|
|
509
|
+
path: '/:pathMatch(.*)*',
|
|
510
|
+
},
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function createAdminRouter(history: RouterHistory = createWebHashHistory()): Router {
|
|
515
|
+
let menuRuntime: MenuRouteRuntime | undefined
|
|
516
|
+
const targetRouter = createRouter({
|
|
517
|
+
history,
|
|
518
|
+
routes: createPublicRoutes(() => menuRuntime?.firstAccessiblePath || '/403'),
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
menuRuntime = createMenuRouteRuntime({
|
|
522
|
+
hasPermission,
|
|
523
|
+
hasRole,
|
|
524
|
+
router: {
|
|
525
|
+
addRoute: route => targetRouter.addRoute(route as RouteRecordRaw),
|
|
526
|
+
getRoutes: () => targetRouter.getRoutes(),
|
|
527
|
+
hasRoute: name => targetRouter.hasRoute(name),
|
|
528
|
+
removeRoute: name => targetRouter.removeRoute(name),
|
|
529
|
+
} satisfies RouteRegistryRouterLike,
|
|
530
|
+
staticMenus: adminRouteRecords,
|
|
531
|
+
})
|
|
532
|
+
menuRuntimes.set(targetRouter, menuRuntime)
|
|
533
|
+
|
|
534
|
+
setupPermissionGuard(targetRouter, permissionStore, {
|
|
535
|
+
isAuthenticated,
|
|
536
|
+
loginPath: '/login',
|
|
537
|
+
mode: 'every',
|
|
538
|
+
noAccessRedirect: '/403',
|
|
539
|
+
requireLoginByDefault: true,
|
|
540
|
+
roleMode: 'every',
|
|
541
|
+
whiteList: ['/login'],
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
return targetRouter
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export const router = createAdminRouter()
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
function createDashboardViewVue() {
|
|
551
|
+
return `<script setup lang="ts">
|
|
552
|
+
import { LumalPage } from '@lumal/core/components'
|
|
553
|
+
import { onMounted } from 'vue'
|
|
554
|
+
import RequestExamplePanel from '../../components/request/RequestExamplePanel.vue'
|
|
555
|
+
import { useMockRequestExample } from '../../composables/useMockRequestExample'
|
|
556
|
+
|
|
557
|
+
const metrics = [
|
|
558
|
+
{ label: '待处理任务', value: 12, description: '今日需要跟进的事项' },
|
|
559
|
+
{ label: '运行服务', value: 6, description: '当前在线的业务服务' },
|
|
560
|
+
{ label: '本周发布', value: 3, description: '已完成的版本发布' },
|
|
561
|
+
{ label: '系统告警', value: 0, description: '当前无未处理告警' },
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
/***********************请求示例*********************/
|
|
565
|
+
const {
|
|
566
|
+
authorizationHeader,
|
|
567
|
+
lastUrl,
|
|
568
|
+
loadProjectSummary,
|
|
569
|
+
loading: requestLoading,
|
|
570
|
+
message: requestMessage,
|
|
571
|
+
projectName,
|
|
572
|
+
projectStatus,
|
|
573
|
+
sessionExpiredCount,
|
|
574
|
+
status: requestStatus,
|
|
575
|
+
tokenInjected,
|
|
576
|
+
} = useMockRequestExample()
|
|
577
|
+
|
|
578
|
+
onMounted(() => {
|
|
579
|
+
void loadProjectSummary()
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
</script>
|
|
583
|
+
|
|
584
|
+
<template>
|
|
585
|
+
<main class="lumal-admin-home">
|
|
586
|
+
<LumalPage
|
|
587
|
+
title="工作台"
|
|
588
|
+
description="这是脚手架生成的轻量后台首页,可替换为你的业务指标和快捷入口。"
|
|
589
|
+
>
|
|
590
|
+
<section class="lumal-admin-home__metrics" aria-label="工作台指标">
|
|
591
|
+
<article v-for="metric in metrics" :key="metric.label" class="lumal-admin-home__metric">
|
|
592
|
+
<span>{{ metric.label }}</span>
|
|
593
|
+
<strong>{{ metric.value }}</strong>
|
|
594
|
+
<small>{{ metric.description }}</small>
|
|
595
|
+
</article>
|
|
596
|
+
</section>
|
|
597
|
+
</LumalPage>
|
|
598
|
+
|
|
599
|
+
<RequestExamplePanel
|
|
600
|
+
:authorization-header="authorizationHeader"
|
|
601
|
+
:last-url="lastUrl"
|
|
602
|
+
:loading="requestLoading"
|
|
603
|
+
:message="requestMessage"
|
|
604
|
+
:project-name="projectName"
|
|
605
|
+
:project-status="projectStatus"
|
|
606
|
+
:session-expired-count="sessionExpiredCount"
|
|
607
|
+
:status="requestStatus"
|
|
608
|
+
:token-injected="tokenInjected"
|
|
609
|
+
@refresh="loadProjectSummary"
|
|
610
|
+
/>
|
|
611
|
+
</main>
|
|
612
|
+
</template>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
function createProjectViewVue() {
|
|
616
|
+
return `<script setup lang="ts">
|
|
617
|
+
import { LumalPage } from '@lumal/core/components'
|
|
618
|
+
|
|
619
|
+
/***********************页面数据*********************/
|
|
620
|
+
const features = [
|
|
621
|
+
'菜单节点来自后端菜单格式',
|
|
622
|
+
'路由记录由 @lumal/core/router 生成',
|
|
623
|
+
'权限不足会通过守卫进入 403 页面',
|
|
624
|
+
]
|
|
625
|
+
</script>
|
|
626
|
+
|
|
627
|
+
<template>
|
|
628
|
+
<main class="lumal-admin-page">
|
|
629
|
+
<LumalPage
|
|
630
|
+
title="项目管理"
|
|
631
|
+
description="这是一个需要 project:list 权限的路由页面。"
|
|
632
|
+
>
|
|
633
|
+
<ul class="lumal-admin-page__list">
|
|
634
|
+
<li
|
|
635
|
+
v-for="feature in features"
|
|
636
|
+
:key="feature"
|
|
637
|
+
class="lumal-admin-page__item"
|
|
638
|
+
>
|
|
639
|
+
{{ feature }}
|
|
640
|
+
</li>
|
|
641
|
+
</ul>
|
|
642
|
+
</LumalPage>
|
|
643
|
+
</main>
|
|
644
|
+
</template>
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
function createForbiddenViewVue() {
|
|
648
|
+
return `<script setup lang="ts">
|
|
649
|
+
import { LumalPage } from '@lumal/core/components'
|
|
650
|
+
import { ElButton } from 'element-plus'
|
|
651
|
+
import { useRouter } from 'vue-router'
|
|
652
|
+
|
|
653
|
+
/***********************路由操作*********************/
|
|
654
|
+
const router = useRouter()
|
|
655
|
+
|
|
656
|
+
function handleBackDashboardClick(): void {
|
|
657
|
+
void router.push('/dashboard')
|
|
658
|
+
}
|
|
659
|
+
</script>
|
|
660
|
+
|
|
661
|
+
<template>
|
|
662
|
+
<main class="lumal-admin-page">
|
|
663
|
+
<LumalPage
|
|
664
|
+
title="无权限"
|
|
665
|
+
description="当前账号没有访问该页面所需的权限。"
|
|
666
|
+
>
|
|
667
|
+
<ElButton type="primary" @click="handleBackDashboardClick">
|
|
668
|
+
返回工作台
|
|
669
|
+
</ElButton>
|
|
670
|
+
</LumalPage>
|
|
671
|
+
</main>
|
|
672
|
+
</template>
|
|
673
|
+
`;
|
|
674
|
+
}
|
|
675
|
+
function createNotFoundViewVue() {
|
|
676
|
+
return `<script setup lang="ts">
|
|
677
|
+
import { LumalPage } from '@lumal/core/components'
|
|
678
|
+
import { ElButton } from 'element-plus'
|
|
679
|
+
import { useRouter } from 'vue-router'
|
|
680
|
+
|
|
681
|
+
const router = useRouter()
|
|
682
|
+
</script>
|
|
683
|
+
|
|
684
|
+
<template>
|
|
685
|
+
<main class="lumal-admin-page">
|
|
686
|
+
<LumalPage title="页面不存在" description="请检查地址,或返回工作台继续操作。">
|
|
687
|
+
<ElButton type="primary" @click="router.push('/dashboard')">
|
|
688
|
+
返回工作台
|
|
689
|
+
</ElButton>
|
|
690
|
+
</LumalPage>
|
|
691
|
+
</main>
|
|
692
|
+
</template>
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
function createLoginViewVue() {
|
|
696
|
+
return `<script setup lang="ts">
|
|
697
|
+
import type { FormInstance } from 'element-plus'
|
|
698
|
+
import { ElButton, ElForm, ElFormItem, ElInput } from 'element-plus'
|
|
699
|
+
import { reactive, shallowRef, useTemplateRef } from 'vue'
|
|
700
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
701
|
+
import { login } from '../../services/session'
|
|
702
|
+
|
|
703
|
+
const route = useRoute()
|
|
704
|
+
const router = useRouter()
|
|
705
|
+
const formRef = useTemplateRef<FormInstance>('formRef')
|
|
706
|
+
const loading = shallowRef(false)
|
|
707
|
+
const errorMessage = shallowRef('')
|
|
708
|
+
const form = reactive({ username: 'admin', password: 'lumal123' })
|
|
709
|
+
|
|
710
|
+
function resolveRedirect(): string {
|
|
711
|
+
const redirect = Array.isArray(route.query.redirect) ? route.query.redirect[0] : route.query.redirect
|
|
712
|
+
return redirect && redirect.startsWith('/') && !redirect.startsWith('/login') ? redirect : '/dashboard'
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function handleSubmit(): Promise<void> {
|
|
716
|
+
if (loading.value) {
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
loading.value = true
|
|
721
|
+
errorMessage.value = ''
|
|
722
|
+
|
|
723
|
+
try {
|
|
724
|
+
const valid = await formRef.value?.validate()
|
|
725
|
+
if (valid === false) {
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
await login(form)
|
|
730
|
+
await router.replace(resolveRedirect())
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
errorMessage.value = error instanceof Error ? error.message : '登录失败'
|
|
734
|
+
}
|
|
735
|
+
finally {
|
|
736
|
+
loading.value = false
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
</script>
|
|
740
|
+
|
|
741
|
+
<template>
|
|
742
|
+
<main class="lumal-admin-login">
|
|
743
|
+
<section class="lumal-admin-login__panel">
|
|
744
|
+
<div class="lumal-admin-login__brand" aria-hidden="true">L</div>
|
|
745
|
+
<h1>Lumal Admin</h1>
|
|
746
|
+
<p>轻量后台基座</p>
|
|
747
|
+
|
|
748
|
+
<ElForm ref="formRef" :model="form" label-position="top" @submit.prevent="handleSubmit">
|
|
749
|
+
<ElFormItem label="用户名" prop="username" :rules="[{ required: true, message: '请输入用户名' }]">
|
|
750
|
+
<ElInput v-model="form.username" autocomplete="username" />
|
|
751
|
+
</ElFormItem>
|
|
752
|
+
<ElFormItem label="密码" prop="password" :rules="[{ required: true, message: '请输入密码' }]">
|
|
753
|
+
<ElInput v-model="form.password" type="password" show-password autocomplete="current-password" />
|
|
754
|
+
</ElFormItem>
|
|
755
|
+
<p v-if="errorMessage" class="lumal-admin-login__error" role="alert">
|
|
756
|
+
{{ errorMessage }}
|
|
757
|
+
</p>
|
|
758
|
+
<ElButton native-type="submit" type="primary" :loading="loading">
|
|
759
|
+
登录
|
|
760
|
+
</ElButton>
|
|
761
|
+
</ElForm>
|
|
762
|
+
|
|
763
|
+
<small>演示账号:admin / lumal123</small>
|
|
764
|
+
</section>
|
|
765
|
+
</main>
|
|
766
|
+
</template>
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
function createAccessTs() {
|
|
770
|
+
return `import { createPermissionStore } from '@lumal/core/permission'
|
|
771
|
+
|
|
772
|
+
export const permissionStore = createPermissionStore()
|
|
773
|
+
|
|
774
|
+
export function syncAccess(authenticated: boolean): void {
|
|
775
|
+
if (!authenticated) {
|
|
776
|
+
permissionStore.clear()
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
permissionStore.setPermissions(['dashboard:view', 'project:list'])
|
|
781
|
+
permissionStore.setRoles(['admin'])
|
|
782
|
+
}
|
|
783
|
+
`;
|
|
784
|
+
}
|
|
785
|
+
function createSessionTs() {
|
|
786
|
+
return `import { createAuthSession } from '@lumal/core/auth'
|
|
787
|
+
import { readonly, shallowRef } from 'vue'
|
|
788
|
+
import { syncAccess } from './access'
|
|
789
|
+
|
|
790
|
+
export interface LoginPayload {
|
|
791
|
+
password: string
|
|
792
|
+
username: string
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export interface AdminUser {
|
|
796
|
+
name: string
|
|
797
|
+
username: string
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const userStorageKey = 'lumal-admin:user'
|
|
801
|
+
export const adminTabSnapshotStorageKey = 'lumal-admin:tabs'
|
|
802
|
+
const currentUserState = shallowRef<AdminUser | null>(null)
|
|
803
|
+
|
|
804
|
+
function readStoredUser(): AdminUser | null {
|
|
805
|
+
const raw = localStorage.getItem(userStorageKey)
|
|
806
|
+
if (!raw) {
|
|
807
|
+
return null
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
return JSON.parse(raw) as AdminUser
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
localStorage.removeItem(userStorageKey)
|
|
815
|
+
return null
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function clearSessionState(): void {
|
|
820
|
+
currentUserState.value = null
|
|
821
|
+
localStorage.removeItem(userStorageKey)
|
|
822
|
+
try {
|
|
823
|
+
sessionStorage.removeItem(adminTabSnapshotStorageKey)
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// sessionStorage 不可用时静默跳过。
|
|
827
|
+
}
|
|
828
|
+
syncAccess(false)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export const session = createAuthSession({
|
|
832
|
+
onSessionExpired: clearSessionState,
|
|
833
|
+
tokenKey: 'lumal-admin:token',
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
const storedUser = session.isAuthenticated() ? readStoredUser() : null
|
|
837
|
+
currentUserState.value = storedUser
|
|
838
|
+
syncAccess(Boolean(storedUser))
|
|
839
|
+
|
|
840
|
+
export const currentUser = readonly(currentUserState)
|
|
841
|
+
|
|
842
|
+
export function isAuthenticated(): boolean {
|
|
843
|
+
return session.isAuthenticated() && Boolean(currentUserState.value)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export async function login(payload: LoginPayload): Promise<AdminUser> {
|
|
847
|
+
await Promise.resolve()
|
|
848
|
+
if (payload.username !== 'admin' || payload.password !== 'lumal123') {
|
|
849
|
+
throw new Error('用户名或密码错误')
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const user = { name: '管理员', username: payload.username }
|
|
853
|
+
session.setSession({ accessToken: 'lumal-demo-token' })
|
|
854
|
+
currentUserState.value = user
|
|
855
|
+
localStorage.setItem(userStorageKey, JSON.stringify(user))
|
|
856
|
+
syncAccess(true)
|
|
857
|
+
return user
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export async function logout(): Promise<void> {
|
|
861
|
+
await session.logout()
|
|
862
|
+
}
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
865
|
+
function createPreferencesTs() {
|
|
866
|
+
return `import type { LumalPreferencesDefaults } from '@lumal/core/theme'
|
|
867
|
+
import { createPreferencesStore } from '@lumal/core/theme'
|
|
868
|
+
|
|
869
|
+
export const adminPreferenceDefaults = {
|
|
870
|
+
app: { dynamicTitle: true, layout: 'sidebar-nav' },
|
|
871
|
+
breadcrumb: { enable: true, hideOnlyOne: false, showHome: true, showIcon: true },
|
|
872
|
+
header: { globalSearch: true },
|
|
873
|
+
shortcutKeys: { globalSearch: true },
|
|
874
|
+
sidebar: { width: 220 },
|
|
875
|
+
tabbar: {
|
|
876
|
+
cache: true,
|
|
877
|
+
draggable: true,
|
|
878
|
+
enable: true,
|
|
879
|
+
maxCount: 0,
|
|
880
|
+
middleClickToClose: true,
|
|
881
|
+
persist: true,
|
|
882
|
+
showIcon: true,
|
|
883
|
+
showMaximize: true,
|
|
884
|
+
showMore: true,
|
|
885
|
+
showRefresh: true,
|
|
886
|
+
styleType: 'chrome',
|
|
887
|
+
visitHistory: true,
|
|
888
|
+
wheelable: true,
|
|
889
|
+
},
|
|
890
|
+
theme: { fontSize: 14 },
|
|
891
|
+
transition: { enable: true, name: 'fade-side' },
|
|
892
|
+
} satisfies LumalPreferencesDefaults
|
|
893
|
+
|
|
894
|
+
const preferencesStore = createPreferencesStore({
|
|
895
|
+
defaults: adminPreferenceDefaults,
|
|
896
|
+
storage: localStorage,
|
|
897
|
+
storageKey: 'lumal-admin:preferences',
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
export const adminPreferences = preferencesStore.state
|
|
901
|
+
export const adminResolvedThemeMode = preferencesStore.resolvedThemeMode
|
|
902
|
+
|
|
903
|
+
export function patchAdminPreferences(preferences: Parameters<typeof preferencesStore.patch>[0]): void {
|
|
904
|
+
preferencesStore.patch(preferences)
|
|
905
|
+
}
|
|
906
|
+
`;
|
|
907
|
+
}
|
|
908
|
+
function createHeaderActionsVue() {
|
|
909
|
+
return `<script setup lang="ts">
|
|
910
|
+
import type { ResolvedThemeMode } from '@lumal/core/theme'
|
|
911
|
+
import { ElButton } from 'element-plus'
|
|
912
|
+
|
|
913
|
+
withDefaults(defineProps<{
|
|
914
|
+
resolvedThemeMode: ResolvedThemeMode
|
|
915
|
+
userName?: string
|
|
916
|
+
}>(), {
|
|
917
|
+
userName: '管理员',
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
defineEmits<{
|
|
921
|
+
logout: []
|
|
922
|
+
openSettings: []
|
|
923
|
+
toggleTheme: []
|
|
924
|
+
}>()
|
|
925
|
+
</script>
|
|
926
|
+
|
|
927
|
+
<template>
|
|
928
|
+
<div class="lumal-admin-header-actions">
|
|
929
|
+
<ElButton text :aria-label="resolvedThemeMode === 'dark' ? '切换浅色模式' : '切换深色模式'" @click="$emit('toggleTheme')">
|
|
930
|
+
主题
|
|
931
|
+
</ElButton>
|
|
932
|
+
<ElButton text aria-label="偏好设置" @click="$emit('openSettings')">
|
|
933
|
+
设置
|
|
934
|
+
</ElButton>
|
|
935
|
+
<span>{{ userName }}</span>
|
|
936
|
+
<ElButton text aria-label="退出登录" @click="$emit('logout')">
|
|
937
|
+
退出
|
|
938
|
+
</ElButton>
|
|
939
|
+
</div>
|
|
940
|
+
</template>
|
|
941
|
+
`;
|
|
942
|
+
}
|
|
943
|
+
function createSettingsDrawerVue() {
|
|
944
|
+
return `<script setup lang="ts">
|
|
945
|
+
import type { LumalPreferences, LumalPreferencesDefaults } from '@lumal/core/theme'
|
|
946
|
+
import { LumalThemeSettingsPanel } from '@lumal/core/theme'
|
|
947
|
+
import { ElDrawer } from 'element-plus'
|
|
948
|
+
import { computed } from 'vue'
|
|
949
|
+
|
|
950
|
+
const props = defineProps<{
|
|
951
|
+
defaults: LumalPreferencesDefaults
|
|
952
|
+
preferences: LumalPreferences
|
|
953
|
+
visible: boolean
|
|
954
|
+
}>()
|
|
955
|
+
|
|
956
|
+
const emit = defineEmits<{
|
|
957
|
+
change: [preferences: LumalPreferences]
|
|
958
|
+
reset: [preferences: LumalPreferences]
|
|
959
|
+
'update:preferences': [preferences: LumalPreferences]
|
|
960
|
+
'update:visible': [visible: boolean]
|
|
961
|
+
}>()
|
|
962
|
+
|
|
963
|
+
const visible = computed({
|
|
964
|
+
get: () => props.visible,
|
|
965
|
+
set: value => emit('update:visible', value),
|
|
966
|
+
})
|
|
967
|
+
</script>
|
|
968
|
+
|
|
969
|
+
<template>
|
|
970
|
+
<ElDrawer v-model="visible" title="偏好设置" size="384px" append-to-body>
|
|
971
|
+
<LumalThemeSettingsPanel
|
|
972
|
+
:defaults="defaults"
|
|
973
|
+
:preferences="preferences"
|
|
974
|
+
@change="$emit('change', $event)"
|
|
975
|
+
@reset="$emit('reset', $event)"
|
|
976
|
+
@update:preferences="$emit('update:preferences', $event)"
|
|
977
|
+
/>
|
|
978
|
+
</ElDrawer>
|
|
979
|
+
</template>
|
|
980
|
+
`;
|
|
981
|
+
}
|
|
982
|
+
function createRequestExampleTs() {
|
|
983
|
+
return `import type { RequestContext } from '@lumal/core/request'
|
|
984
|
+
import { createRequestClient } from '@lumal/core/request'
|
|
985
|
+
import { computed, readonly, shallowRef } from 'vue'
|
|
986
|
+
|
|
987
|
+
export type RequestExampleStatus = 'error' | 'idle' | 'loading' | 'success'
|
|
988
|
+
|
|
989
|
+
interface ProjectSummary {
|
|
990
|
+
id: string
|
|
991
|
+
name: string
|
|
992
|
+
status: 'disabled' | 'enabled'
|
|
993
|
+
updatedAt: string
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
interface ApiResponse<TData> {
|
|
997
|
+
code: number
|
|
998
|
+
data: TData
|
|
999
|
+
message: string
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/***********************Mock 服务*********************/
|
|
1003
|
+
function createJsonResponse<TData>(payload: ApiResponse<TData>, init?: ResponseInit): Response {
|
|
1004
|
+
return new Response(JSON.stringify(payload), {
|
|
1005
|
+
headers: {
|
|
1006
|
+
'content-type': 'application/json',
|
|
1007
|
+
},
|
|
1008
|
+
status: 200,
|
|
1009
|
+
...init,
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function resolveRequestUrl(input: Parameters<typeof fetch>[0]): string {
|
|
1014
|
+
if (typeof input === 'string') {
|
|
1015
|
+
return input
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (input instanceof URL) {
|
|
1019
|
+
return input.toString()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return input.url
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/***********************请求示例*********************/
|
|
1026
|
+
export function useMockRequestExample() {
|
|
1027
|
+
const status = shallowRef<RequestExampleStatus>('idle')
|
|
1028
|
+
const message = shallowRef('等待发起请求')
|
|
1029
|
+
const lastUrl = shallowRef('-')
|
|
1030
|
+
const authorizationHeader = shallowRef('未请求')
|
|
1031
|
+
const sessionExpiredCount = shallowRef(0)
|
|
1032
|
+
const project = shallowRef<ProjectSummary | null>(null)
|
|
1033
|
+
|
|
1034
|
+
const mockFetch: typeof fetch = async (input, init) => {
|
|
1035
|
+
lastUrl.value = resolveRequestUrl(input)
|
|
1036
|
+
authorizationHeader.value = new Headers(init?.headers).get('Authorization') ?? '未注入'
|
|
1037
|
+
|
|
1038
|
+
return createJsonResponse({
|
|
1039
|
+
code: 0,
|
|
1040
|
+
data: {
|
|
1041
|
+
id: 'demo-001',
|
|
1042
|
+
name: 'Lumal 示例项目',
|
|
1043
|
+
status: 'enabled',
|
|
1044
|
+
updatedAt: '2026-07-09 21:00',
|
|
1045
|
+
},
|
|
1046
|
+
message: 'ok',
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const request = createRequestClient({
|
|
1051
|
+
baseURL: '/mock-api',
|
|
1052
|
+
fetch: mockFetch,
|
|
1053
|
+
getToken: () => 'mock-token-001',
|
|
1054
|
+
onResponse: <TResult>(context: RequestContext) => {
|
|
1055
|
+
const payload = context.data as ApiResponse<ProjectSummary>
|
|
1056
|
+
|
|
1057
|
+
if (payload.code !== 0) {
|
|
1058
|
+
throw new Error(payload.message || '请求失败')
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return payload.data as TResult
|
|
1062
|
+
},
|
|
1063
|
+
onSessionExpired: () => {
|
|
1064
|
+
sessionExpiredCount.value += 1
|
|
1065
|
+
},
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
const loading = computed(() => status.value === 'loading')
|
|
1069
|
+
const projectName = computed(() => project.value?.name ?? '暂无数据')
|
|
1070
|
+
const projectStatus = computed(() => project.value?.status === 'enabled' ? '启用' : '停用')
|
|
1071
|
+
const tokenInjected = computed(() => authorizationHeader.value.startsWith('Bearer '))
|
|
1072
|
+
|
|
1073
|
+
async function loadProjectSummary(): Promise<void> {
|
|
1074
|
+
status.value = 'loading'
|
|
1075
|
+
message.value = '正在请求项目摘要'
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
project.value = await request.get<ProjectSummary>('/projects/summary', {
|
|
1079
|
+
query: {
|
|
1080
|
+
scene: 'admin-demo',
|
|
1081
|
+
},
|
|
1082
|
+
})
|
|
1083
|
+
status.value = 'success'
|
|
1084
|
+
message.value = '请求成功,已完成 token 注入和响应解析'
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
status.value = 'error'
|
|
1088
|
+
message.value = error instanceof Error ? error.message : '请求失败'
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
authorizationHeader: readonly(authorizationHeader),
|
|
1094
|
+
lastUrl: readonly(lastUrl),
|
|
1095
|
+
loadProjectSummary,
|
|
1096
|
+
loading,
|
|
1097
|
+
message: readonly(message),
|
|
1098
|
+
project: readonly(project),
|
|
1099
|
+
projectName,
|
|
1100
|
+
projectStatus,
|
|
1101
|
+
sessionExpiredCount: readonly(sessionExpiredCount),
|
|
1102
|
+
status: readonly(status),
|
|
1103
|
+
tokenInjected,
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
`;
|
|
1107
|
+
}
|
|
1108
|
+
function createRequestPanelVue() {
|
|
1109
|
+
return `<script setup lang="ts">
|
|
1110
|
+
import type { RequestExampleStatus } from '../../composables/useMockRequestExample'
|
|
1111
|
+
import { ElButton, ElTag } from 'element-plus'
|
|
1112
|
+
import { computed } from 'vue'
|
|
1113
|
+
|
|
1114
|
+
interface Props {
|
|
1115
|
+
authorizationHeader: string
|
|
1116
|
+
lastUrl: string
|
|
1117
|
+
loading: boolean
|
|
1118
|
+
message: string
|
|
1119
|
+
projectName: string
|
|
1120
|
+
projectStatus: string
|
|
1121
|
+
sessionExpiredCount: number
|
|
1122
|
+
status: RequestExampleStatus
|
|
1123
|
+
tokenInjected: boolean
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
interface Emits {
|
|
1127
|
+
refresh: []
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const props = defineProps<Props>()
|
|
1131
|
+
const emit = defineEmits<Emits>()
|
|
1132
|
+
|
|
1133
|
+
/***********************展示状态*********************/
|
|
1134
|
+
const statusLabel = computed(() => {
|
|
1135
|
+
const labels: Record<RequestExampleStatus, string> = {
|
|
1136
|
+
error: '失败',
|
|
1137
|
+
idle: '待请求',
|
|
1138
|
+
loading: '请求中',
|
|
1139
|
+
success: '成功',
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return labels[props.status]
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
const statusType = computed(() => props.status === 'error' ? 'danger' : props.status === 'success' ? 'success' : 'info')
|
|
1146
|
+
</script>
|
|
1147
|
+
|
|
1148
|
+
<template>
|
|
1149
|
+
<section class="lumal-request-panel">
|
|
1150
|
+
<div class="lumal-request-panel__header">
|
|
1151
|
+
<div class="lumal-request-panel__title-group">
|
|
1152
|
+
<span class="lumal-request-panel__eyebrow">Request Client</span>
|
|
1153
|
+
<h2 class="lumal-request-panel__title">
|
|
1154
|
+
请求封装示例
|
|
1155
|
+
</h2>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<ElButton
|
|
1159
|
+
type="primary"
|
|
1160
|
+
:loading="loading"
|
|
1161
|
+
@click="emit('refresh')"
|
|
1162
|
+
>
|
|
1163
|
+
重新请求
|
|
1164
|
+
</ElButton>
|
|
1165
|
+
</div>
|
|
1166
|
+
|
|
1167
|
+
<div class="lumal-request-panel__meta">
|
|
1168
|
+
<div class="lumal-request-panel__item">
|
|
1169
|
+
<span class="lumal-request-panel__label">状态</span>
|
|
1170
|
+
<ElTag :type="statusType">
|
|
1171
|
+
{{ statusLabel }}
|
|
1172
|
+
</ElTag>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="lumal-request-panel__item">
|
|
1175
|
+
<span class="lumal-request-panel__label">项目</span>
|
|
1176
|
+
<strong class="lumal-request-panel__value">{{ projectName }}</strong>
|
|
1177
|
+
</div>
|
|
1178
|
+
<div class="lumal-request-panel__item">
|
|
1179
|
+
<span class="lumal-request-panel__label">业务状态</span>
|
|
1180
|
+
<strong class="lumal-request-panel__value">{{ projectStatus }}</strong>
|
|
1181
|
+
</div>
|
|
1182
|
+
<div class="lumal-request-panel__item">
|
|
1183
|
+
<span class="lumal-request-panel__label">Token</span>
|
|
1184
|
+
<strong class="lumal-request-panel__value">{{ tokenInjected ? '已注入' : '未注入' }}</strong>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
|
|
1188
|
+
<dl class="lumal-request-panel__details">
|
|
1189
|
+
<div class="lumal-request-panel__detail">
|
|
1190
|
+
<dt class="lumal-request-panel__detail-label">
|
|
1191
|
+
请求地址
|
|
1192
|
+
</dt>
|
|
1193
|
+
<dd class="lumal-request-panel__detail-value">
|
|
1194
|
+
{{ lastUrl }}
|
|
1195
|
+
</dd>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="lumal-request-panel__detail">
|
|
1198
|
+
<dt class="lumal-request-panel__detail-label">
|
|
1199
|
+
Authorization
|
|
1200
|
+
</dt>
|
|
1201
|
+
<dd class="lumal-request-panel__detail-value">
|
|
1202
|
+
{{ authorizationHeader }}
|
|
1203
|
+
</dd>
|
|
1204
|
+
</div>
|
|
1205
|
+
<div class="lumal-request-panel__detail">
|
|
1206
|
+
<dt class="lumal-request-panel__detail-label">
|
|
1207
|
+
会话过期次数
|
|
1208
|
+
</dt>
|
|
1209
|
+
<dd class="lumal-request-panel__detail-value">
|
|
1210
|
+
{{ sessionExpiredCount }}
|
|
1211
|
+
</dd>
|
|
1212
|
+
</div>
|
|
1213
|
+
</dl>
|
|
1214
|
+
|
|
1215
|
+
<p class="lumal-request-panel__message">
|
|
1216
|
+
{{ message }}
|
|
1217
|
+
</p>
|
|
1218
|
+
</section>
|
|
1219
|
+
</template>
|
|
1220
|
+
|
|
1221
|
+
<style scoped lang="scss">
|
|
1222
|
+
.lumal-request-panel {
|
|
1223
|
+
box-sizing: border-box;
|
|
1224
|
+
width: min(920px, 100%);
|
|
1225
|
+
padding: 24px;
|
|
1226
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
1227
|
+
border-radius: 8px;
|
|
1228
|
+
background: var(--el-bg-color);
|
|
1229
|
+
box-shadow: var(--lumal-shadow-card);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.lumal-request-panel__header {
|
|
1233
|
+
display: flex;
|
|
1234
|
+
align-items: center;
|
|
1235
|
+
justify-content: space-between;
|
|
1236
|
+
gap: 16px;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.lumal-request-panel__title-group {
|
|
1240
|
+
display: grid;
|
|
1241
|
+
gap: 6px;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.lumal-request-panel__eyebrow {
|
|
1245
|
+
color: var(--el-color-primary);
|
|
1246
|
+
font-size: 12px;
|
|
1247
|
+
font-weight: 700;
|
|
1248
|
+
letter-spacing: 0;
|
|
1249
|
+
text-transform: uppercase;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.lumal-request-panel__title {
|
|
1253
|
+
margin: 0;
|
|
1254
|
+
color: var(--el-text-color-primary);
|
|
1255
|
+
font-size: 20px;
|
|
1256
|
+
font-weight: 700;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.lumal-request-panel__meta {
|
|
1260
|
+
display: grid;
|
|
1261
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1262
|
+
gap: 12px;
|
|
1263
|
+
margin-top: 20px;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.lumal-request-panel__item {
|
|
1267
|
+
display: grid;
|
|
1268
|
+
gap: 8px;
|
|
1269
|
+
min-width: 0;
|
|
1270
|
+
padding: 14px;
|
|
1271
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
1272
|
+
border-radius: 6px;
|
|
1273
|
+
background: var(--el-fill-color-light);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.lumal-request-panel__label,
|
|
1277
|
+
.lumal-request-panel__detail-label {
|
|
1278
|
+
color: var(--el-text-color-secondary);
|
|
1279
|
+
font-size: 12px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.lumal-request-panel__value {
|
|
1283
|
+
overflow: hidden;
|
|
1284
|
+
color: var(--el-text-color-primary);
|
|
1285
|
+
font-size: 14px;
|
|
1286
|
+
text-overflow: ellipsis;
|
|
1287
|
+
white-space: nowrap;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.lumal-request-panel__details {
|
|
1291
|
+
display: grid;
|
|
1292
|
+
gap: 10px;
|
|
1293
|
+
margin: 18px 0 0;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
.lumal-request-panel__detail {
|
|
1297
|
+
display: grid;
|
|
1298
|
+
grid-template-columns: 132px minmax(0, 1fr);
|
|
1299
|
+
gap: 12px;
|
|
1300
|
+
align-items: center;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.lumal-request-panel__detail-value {
|
|
1304
|
+
min-width: 0;
|
|
1305
|
+
margin: 0;
|
|
1306
|
+
overflow-wrap: anywhere;
|
|
1307
|
+
color: var(--el-text-color-regular);
|
|
1308
|
+
font-family:
|
|
1309
|
+
"JetBrains Mono",
|
|
1310
|
+
Consolas,
|
|
1311
|
+
monospace;
|
|
1312
|
+
font-size: 13px;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.lumal-request-panel__message {
|
|
1316
|
+
margin: 18px 0 0;
|
|
1317
|
+
color: var(--el-text-color-secondary);
|
|
1318
|
+
font-size: 13px;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
@media (max-width: 760px) {
|
|
1322
|
+
.lumal-request-panel__header {
|
|
1323
|
+
align-items: flex-start;
|
|
1324
|
+
flex-direction: column;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.lumal-request-panel__meta {
|
|
1328
|
+
grid-template-columns: 1fr;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
.lumal-request-panel__detail {
|
|
1332
|
+
grid-template-columns: 1fr;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
</style>
|
|
1336
|
+
`;
|
|
1337
|
+
}
|
|
1338
|
+
function createStylesScss() {
|
|
1339
|
+
return `:root {
|
|
1340
|
+
color: var(--el-text-color-primary);
|
|
1341
|
+
background: var(--el-bg-color-page);
|
|
1342
|
+
font-family:
|
|
1343
|
+
Inter,
|
|
1344
|
+
"Segoe UI",
|
|
1345
|
+
"Microsoft YaHei",
|
|
1346
|
+
Arial,
|
|
1347
|
+
sans-serif;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
body {
|
|
1351
|
+
margin: 0;
|
|
1352
|
+
min-width: 320px;
|
|
1353
|
+
overflow-x: hidden;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.lumal-admin-home {
|
|
1357
|
+
display: grid;
|
|
1358
|
+
gap: var(--lumal-page-gutter, 20px);
|
|
1359
|
+
box-sizing: border-box;
|
|
1360
|
+
padding: var(--lumal-page-gutter, 20px);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.lumal-admin-home__metrics {
|
|
1364
|
+
display: grid;
|
|
1365
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1366
|
+
gap: 16px;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.lumal-admin-home__metric {
|
|
1370
|
+
display: grid;
|
|
1371
|
+
min-width: 0;
|
|
1372
|
+
gap: 8px;
|
|
1373
|
+
padding: 18px;
|
|
1374
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
1375
|
+
border-radius: var(--lumal-radius-base, 8px);
|
|
1376
|
+
background: var(--el-bg-color);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.lumal-admin-home__metric strong {
|
|
1380
|
+
color: var(--el-color-primary);
|
|
1381
|
+
font-size: 28px;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.lumal-admin-home__metric small {
|
|
1385
|
+
color: var(--el-text-color-secondary);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.lumal-admin-page {
|
|
1389
|
+
box-sizing: border-box;
|
|
1390
|
+
padding: var(--lumal-page-gutter, 20px);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.lumal-admin-page__list {
|
|
1394
|
+
display: grid;
|
|
1395
|
+
gap: 10px;
|
|
1396
|
+
margin: 0;
|
|
1397
|
+
padding-left: 18px;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.lumal-admin-page__item {
|
|
1401
|
+
color: var(--el-text-color-regular);
|
|
1402
|
+
font-size: 14px;
|
|
1403
|
+
line-height: 1.6;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
.lumal-admin-header-actions {
|
|
1407
|
+
display: flex;
|
|
1408
|
+
min-width: 0;
|
|
1409
|
+
align-items: center;
|
|
1410
|
+
gap: 4px;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.lumal-admin-header-actions .el-button {
|
|
1414
|
+
min-width: 44px;
|
|
1415
|
+
min-height: 44px;
|
|
1416
|
+
margin: 0;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
.lumal-admin-header-actions span {
|
|
1420
|
+
max-width: 120px;
|
|
1421
|
+
overflow: hidden;
|
|
1422
|
+
color: var(--el-text-color-regular);
|
|
1423
|
+
font-size: 13px;
|
|
1424
|
+
text-overflow: ellipsis;
|
|
1425
|
+
white-space: nowrap;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
.lumal-admin-login {
|
|
1429
|
+
display: grid;
|
|
1430
|
+
min-height: 100dvh;
|
|
1431
|
+
place-items: center;
|
|
1432
|
+
padding: 24px;
|
|
1433
|
+
background: var(--el-bg-color-page);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.lumal-admin-login__panel {
|
|
1437
|
+
box-sizing: border-box;
|
|
1438
|
+
width: min(400px, 100%);
|
|
1439
|
+
padding: 32px;
|
|
1440
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
1441
|
+
border-radius: var(--lumal-radius-large, 12px);
|
|
1442
|
+
background: var(--el-bg-color);
|
|
1443
|
+
box-shadow: var(--lumal-shadow-dialog);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.lumal-admin-login__brand {
|
|
1447
|
+
display: grid;
|
|
1448
|
+
width: 48px;
|
|
1449
|
+
height: 48px;
|
|
1450
|
+
place-items: center;
|
|
1451
|
+
border-radius: var(--lumal-radius-base, 8px);
|
|
1452
|
+
color: var(--el-color-white);
|
|
1453
|
+
background: var(--el-color-primary);
|
|
1454
|
+
font-size: 24px;
|
|
1455
|
+
font-weight: 800;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
.lumal-admin-login h1 {
|
|
1459
|
+
margin: 20px 0 6px;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.lumal-admin-login p,
|
|
1463
|
+
.lumal-admin-login small {
|
|
1464
|
+
color: var(--el-text-color-secondary);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.lumal-admin-login .el-form {
|
|
1468
|
+
margin: 24px 0 16px;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.lumal-admin-login .el-button {
|
|
1472
|
+
width: 100%;
|
|
1473
|
+
min-height: 44px;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
.lumal-admin-login__error {
|
|
1477
|
+
color: var(--el-color-danger) !important;
|
|
1478
|
+
font-size: 13px;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
@media (max-width: 900px) {
|
|
1482
|
+
.lumal-admin-home__metrics {
|
|
1483
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
@media (max-width: 640px) {
|
|
1488
|
+
.lumal-admin-home,
|
|
1489
|
+
.lumal-admin-page {
|
|
1490
|
+
padding: 12px;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.lumal-admin-home__metrics {
|
|
1494
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
.lumal-admin-header-actions span {
|
|
1498
|
+
display: none;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.lumal-admin-login {
|
|
1502
|
+
padding: 16px;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.lumal-admin-login__panel {
|
|
1506
|
+
padding: 24px 20px;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1511
|
+
*,
|
|
1512
|
+
*::before,
|
|
1513
|
+
*::after {
|
|
1514
|
+
scroll-behavior: auto !important;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
`;
|
|
1518
|
+
}
|
|
1519
|
+
function createTemplateFiles(projectName, apiMode) {
|
|
1520
|
+
const files = [
|
|
1521
|
+
{ path: 'index.html', content: createIndexHtml() },
|
|
1522
|
+
{ path: 'package.json', content: createPackageJson(projectName) },
|
|
1523
|
+
{ path: 'src/App.vue', content: createAppVue() },
|
|
1524
|
+
{ path: 'src/components/app/AppHeaderActions.vue', content: createHeaderActionsVue() },
|
|
1525
|
+
{ path: 'src/components/app/AppSettingsDrawer.vue', content: createSettingsDrawerVue() },
|
|
1526
|
+
{ path: 'src/components/request/RequestExamplePanel.vue', content: createRequestPanelVue() },
|
|
1527
|
+
{ path: 'src/composables/useMockRequestExample.ts', content: createRequestExampleTs() },
|
|
1528
|
+
{ path: 'src/main.ts', content: createMainTs() },
|
|
1529
|
+
{ path: 'src/router/index.ts', content: createRouterTs() },
|
|
1530
|
+
{ path: 'src/services/access.ts', content: createAccessTs() },
|
|
1531
|
+
{ path: 'src/services/preferences.ts', content: createPreferencesTs() },
|
|
1532
|
+
{ path: 'src/services/session.ts', content: createSessionTs() },
|
|
1533
|
+
{ path: 'src/styles.scss', content: createStylesScss() },
|
|
1534
|
+
{ path: 'src/views/auth/LoginView.vue', content: createLoginViewVue() },
|
|
1535
|
+
{ path: 'src/views/dashboard/DashboardView.vue', content: createDashboardViewVue() },
|
|
1536
|
+
{ path: 'src/views/error/ForbiddenView.vue', content: createForbiddenViewVue() },
|
|
1537
|
+
{ path: 'src/views/error/NotFoundView.vue', content: createNotFoundViewVue() },
|
|
1538
|
+
{ path: 'src/views/project/ProjectView.vue', content: createProjectViewVue() },
|
|
1539
|
+
{ path: 'tsconfig.json', content: createTsConfig() },
|
|
1540
|
+
{ path: 'vite.config.ts', content: createViteConfig() },
|
|
1541
|
+
];
|
|
1542
|
+
if (apiMode === 'http') {
|
|
1543
|
+
files.push({ path: '.env.development', content: 'VITE_API_BASE_URL=/api\n' }, { path: '.env.production.example', content: 'VITE_API_BASE_URL=/api\n' }, { path: 'API-CONTRACT.md', content: createApiContractMd() }, { path: 'src/services/auth-api.ts', content: createHttpAuthApiTs() }, { path: 'src/services/request.ts', content: createHttpRequestTs() });
|
|
1544
|
+
}
|
|
1545
|
+
return files;
|
|
1546
|
+
}
|
|
1547
|
+
/***********************目录写入*********************/
|
|
1548
|
+
async function assertEmptyTargetDir(targetDir) {
|
|
1549
|
+
await mkdir(targetDir, { recursive: true });
|
|
1550
|
+
const entries = await readdir(targetDir);
|
|
1551
|
+
if (entries.length > 0) {
|
|
1552
|
+
throw new Error('目标目录不是空目录');
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function writeTemplateFile(targetDir, file) {
|
|
1556
|
+
const filePath = join(targetDir, file.path);
|
|
1557
|
+
const directoryPath = dirname(filePath);
|
|
1558
|
+
if (directoryPath) {
|
|
1559
|
+
await mkdir(directoryPath, { recursive: true });
|
|
1560
|
+
}
|
|
1561
|
+
await writeFile(filePath, file.content, 'utf8');
|
|
1562
|
+
}
|
|
1563
|
+
/***********************公开 API*********************/
|
|
1564
|
+
export async function createLumalAdminProject(options) {
|
|
1565
|
+
const targetDir = resolve(options.targetDir);
|
|
1566
|
+
const projectName = resolveProjectName(options);
|
|
1567
|
+
const templateFiles = createTemplateFiles(projectName, options.apiMode ?? 'local');
|
|
1568
|
+
await assertEmptyTargetDir(targetDir);
|
|
1569
|
+
for (const file of templateFiles) {
|
|
1570
|
+
await writeTemplateFile(targetDir, file);
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
targetDir,
|
|
1574
|
+
createdFiles: templateFiles.map(file => file.path),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
//# sourceMappingURL=scaffold.js.map
|