create-lumal-admin 0.0.1-beta → 0.0.2-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 -21
- package/README.md +35 -35
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/create.d.ts +3 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +44 -0
- package/dist/create.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scaffold.js +1407 -1407
- package/package.json +2 -2
package/dist/scaffold.js
CHANGED
|
@@ -30,9 +30,9 @@ function createPackageJson(projectName) {
|
|
|
30
30
|
preview: 'vite preview --host 127.0.0.1',
|
|
31
31
|
},
|
|
32
32
|
dependencies: {
|
|
33
|
-
'@lumal/core':
|
|
34
|
-
'@lumal/icons':
|
|
35
|
-
'@lumal/icons-vue':
|
|
33
|
+
'@lumal/core': "0.0.2-beta",
|
|
34
|
+
'@lumal/icons': "0.0.2-beta",
|
|
35
|
+
'@lumal/icons-vue': "0.0.2-beta",
|
|
36
36
|
'element-plus': '^2.11.0',
|
|
37
37
|
'vue': '^3.5.0',
|
|
38
38
|
'vue-router': '^4.5.0',
|
|
@@ -46,1474 +46,1474 @@ function createPackageJson(projectName) {
|
|
|
46
46
|
}, null, 2)}\n`;
|
|
47
47
|
}
|
|
48
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>
|
|
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
61
|
`;
|
|
62
62
|
}
|
|
63
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
|
-
}
|
|
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
83
|
`;
|
|
84
84
|
}
|
|
85
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
|
-
})
|
|
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
135
|
`;
|
|
136
136
|
}
|
|
137
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'
|
|
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
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')
|
|
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
176
|
`;
|
|
177
177
|
}
|
|
178
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
|
-
}
|
|
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
191
|
`;
|
|
192
192
|
}
|
|
193
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
|
-
}
|
|
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
208
|
`;
|
|
209
209
|
}
|
|
210
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 地址。
|
|
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
223
|
`;
|
|
224
224
|
}
|
|
225
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>
|
|
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
403
|
`;
|
|
404
404
|
}
|
|
405
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()
|
|
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
548
|
`;
|
|
549
549
|
}
|
|
550
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>
|
|
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
613
|
`;
|
|
614
614
|
}
|
|
615
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>
|
|
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
645
|
`;
|
|
646
646
|
}
|
|
647
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>
|
|
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
673
|
`;
|
|
674
674
|
}
|
|
675
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>
|
|
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
693
|
`;
|
|
694
694
|
}
|
|
695
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>
|
|
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
767
|
`;
|
|
768
768
|
}
|
|
769
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
|
-
}
|
|
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
783
|
`;
|
|
784
784
|
}
|
|
785
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
|
-
}
|
|
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
863
|
`;
|
|
864
864
|
}
|
|
865
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
|
-
}
|
|
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
906
|
`;
|
|
907
907
|
}
|
|
908
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>
|
|
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
941
|
`;
|
|
942
942
|
}
|
|
943
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>
|
|
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
980
|
`;
|
|
981
981
|
}
|
|
982
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
|
-
}
|
|
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
1106
|
`;
|
|
1107
1107
|
}
|
|
1108
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>
|
|
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
1336
|
`;
|
|
1337
1337
|
}
|
|
1338
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
|
-
}
|
|
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
1517
|
`;
|
|
1518
1518
|
}
|
|
1519
1519
|
function createTemplateFiles(projectName, apiMode) {
|