create-lumal-admin 0.0.3-beta → 0.0.5-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/CHANGELOG.md +19 -0
- package/README.md +67 -10
- package/dist/cli.d.ts +12 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -28
- package/dist/cli.js.map +1 -1
- package/dist/create.d.ts +2 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +85 -2
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/presets.d.ts +15 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +30 -0
- package/dist/presets.js.map +1 -0
- package/dist/scaffold.d.ts +2 -11
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +2 -1602
- package/dist/scaffold.js.map +1 -1
- package/dist/templates.d.ts +5 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +62 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +4 -3
- package/templates/base/.env.example +4 -0
- package/templates/base/API-CONTRACT.md +37 -0
- package/templates/base/README.md +42 -0
- package/templates/base/_gitignore +6 -0
- package/templates/base/eslint.config.mjs +8 -0
- package/templates/base/index.html +12 -0
- package/templates/base/package.json +38 -0
- package/templates/base/src/App.vue +214 -0
- package/templates/base/src/components/app/AppHeaderActions.vue +34 -0
- package/templates/base/src/components/app/AppSettingsDrawer.vue +37 -0
- package/templates/base/src/main.ts +41 -0
- package/templates/base/src/router/index.ts +86 -0
- package/templates/base/src/router/views.ts +16 -0
- package/templates/base/src/services/access.ts +2 -0
- package/templates/base/src/services/contracts.ts +29 -0
- package/templates/base/src/services/preferences.ts +41 -0
- package/templates/base/src/services/session-runtime.ts +137 -0
- package/templates/base/src/services/session.ts +14 -0
- package/templates/base/src/styles.scss +178 -0
- package/templates/base/src/views/auth/LoginView.vue +80 -0
- package/templates/base/src/views/dashboard/DashboardView.vue +27 -0
- package/templates/base/src/views/error/ForbiddenView.vue +25 -0
- package/templates/base/src/views/error/NotFoundView.vue +18 -0
- package/templates/base/tests/session.test.ts.template +34 -0
- package/templates/base/tsconfig.json +22 -0
- package/templates/base/vite.config.ts +54 -0
- package/templates/base/vitest.config.ts +3 -0
- package/templates/features/examples/src/modules/examples.ts +10 -0
- package/templates/features/examples/src/views/examples/CrudExampleView.vue +30 -0
- package/templates/features/examples/src/views/examples/FormExampleView.vue +25 -0
- package/templates/features/system/src/business/system/entities.ts +93 -0
- package/templates/features/system/src/modules/system.ts +28 -0
- package/templates/features/system/src/views/system/SystemEntityView.vue +85 -0
- package/templates/modes/http/src/services/data-source.ts +120 -0
- package/templates/modes/http/src/services/http-adapter.ts +124 -0
- package/templates/modes/http/tests/http.test.ts.template +165 -0
- package/templates/modes/local/src/services/data-source.ts +176 -0
- package/templates/modes/local/tests/local.test.ts.template +92 -0
- package/templates/vben-compatible/MIGRATION.md +28 -0
- package/templates/vben-compatible/src/components/migration/VbenFormExample.vue +34 -0
- package/templates/vben-compatible/src/components/migration/VbenUsers.vue +135 -0
- package/templates/vben-compatible/src/modules/vben.ts +17 -0
- package/templates/vben-compatible/src/views/migration/VbenFormView.vue +10 -0
- package/templates/vben-compatible/src/views/migration/VbenUsersView.vue +11 -0
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { resolveOptions } from './presets.js';
|
|
2
|
+
import type { TemplateFile } from './types.js';
|
|
3
|
+
export declare function getVersion(): Promise<string>;
|
|
4
|
+
export declare function composeTemplates(options: ReturnType<typeof resolveOptions>): Promise<TemplateFile[]>;
|
|
5
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAO9C,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAGlD;AAoBD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAwC1G"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const templateRoot = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
5
|
+
export async function getVersion() {
|
|
6
|
+
const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
return manifest.version;
|
|
8
|
+
}
|
|
9
|
+
/***********************公共模板组合*********************/
|
|
10
|
+
async function readLayer(name, directory = '', result = []) {
|
|
11
|
+
const entries = await readdir(join(templateRoot, name, directory), { withFileTypes: true });
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
await readLayer(name, path, result);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
result.push({
|
|
19
|
+
path: path.replace(/\.template$/, '').replace(/^_gitignore$/, '.gitignore'),
|
|
20
|
+
content: await readFile(join(templateRoot, name, path), 'utf8'),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export async function composeTemplates(options) {
|
|
27
|
+
const layers = ['base', `modes/${options.apiMode}`, ...options.features.map(feature => `features/${feature}`)];
|
|
28
|
+
if (options.preset === 'vben-compatible')
|
|
29
|
+
layers.push('vben-compatible');
|
|
30
|
+
const files = new Map();
|
|
31
|
+
for (const layer of layers) {
|
|
32
|
+
for (const file of await readLayer(layer))
|
|
33
|
+
files.set(file.path, file.content);
|
|
34
|
+
}
|
|
35
|
+
const version = await getVersion();
|
|
36
|
+
const manifest = JSON.parse(files.get('package.json'));
|
|
37
|
+
manifest.name = options.name;
|
|
38
|
+
for (const name of ['@lumal/core', '@lumal/icons', '@lumal/icons-vue'])
|
|
39
|
+
manifest.dependencies[name] = version;
|
|
40
|
+
if (options.preset === 'vben-compatible')
|
|
41
|
+
manifest.dependencies['@lumal/vben-compat'] = version;
|
|
42
|
+
manifest.dependencies = Object.fromEntries(Object.entries(manifest.dependencies).sort(([left], [right]) => left.localeCompare(right)));
|
|
43
|
+
files.set('package.json', `${JSON.stringify(manifest, null, 2)}\n`);
|
|
44
|
+
files.set('.env.development', 'VITE_API_BASE_URL=\nVITE_API_ADAPTER=standard\n');
|
|
45
|
+
files.set('.env.production.example', 'VITE_API_BASE_URL=https://api.example.com\nVITE_API_ADAPTER=standard\n# Local 演示生产构建默认禁止登录;仅演示站点显式启用。\nVITE_ENABLE_LOCAL_DEMO=false\n');
|
|
46
|
+
// 只注入小型配置;完整 SFC、服务和工程文件均保存在模板目录中。
|
|
47
|
+
files.set('src/app.config.ts', `export const appConfig: {
|
|
48
|
+
apiMode: 'http' | 'local'
|
|
49
|
+
features: string[]
|
|
50
|
+
preset: 'minimal' | 'standard' | 'vben-compatible'
|
|
51
|
+
routerMode: 'hash' | 'history'
|
|
52
|
+
title: string
|
|
53
|
+
} = {
|
|
54
|
+
apiMode: '${options.apiMode}',
|
|
55
|
+
features: [${options.features.map(feature => `'${feature}'`).join(', ')}],
|
|
56
|
+
preset: '${options.preset}',
|
|
57
|
+
routerMode: '${options.routerMode}',
|
|
58
|
+
title: '${options.name}',
|
|
59
|
+
}\n`);
|
|
60
|
+
return [...files].map(([path, content]) => ({ path, content })).sort((a, b) => a.path.localeCompare(b.path));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type AdminPreset = 'minimal' | 'standard' | 'vben-compatible';
|
|
2
|
+
export type ApiMode = 'http' | 'local';
|
|
3
|
+
export type RouterMode = 'hash' | 'history';
|
|
4
|
+
export type AdminFeature = 'examples' | 'system';
|
|
5
|
+
export interface CreateLumalAdminProjectOptions {
|
|
6
|
+
apiMode?: ApiMode;
|
|
7
|
+
features?: AdminFeature[];
|
|
8
|
+
name: string;
|
|
9
|
+
preset?: AdminPreset;
|
|
10
|
+
routerMode?: RouterMode;
|
|
11
|
+
targetDir: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateLumalAdminProjectResult {
|
|
14
|
+
targetDir: string;
|
|
15
|
+
createdFiles: string[];
|
|
16
|
+
preset: AdminPreset;
|
|
17
|
+
apiMode: ApiMode;
|
|
18
|
+
routerMode: RouterMode;
|
|
19
|
+
features: AdminFeature[];
|
|
20
|
+
}
|
|
21
|
+
export interface TemplateFile {
|
|
22
|
+
path: string;
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,iBAAiB,CAAA;AACpE,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;AACtC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,CAAA;AAC3C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEhD,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,YAAY,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lumal-admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"description": "Create a
|
|
4
|
+
"version": "0.0.5-beta",
|
|
5
|
+
"description": "Create a modular Lumal admin application with minimal, standard and Vben 5 presets.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"lumal",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"CHANGELOG.md",
|
|
28
28
|
"README.md",
|
|
29
|
-
"dist"
|
|
29
|
+
"dist",
|
|
30
|
+
"templates"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"build": "tsc -p tsconfig.json && node ../../scripts/clean-package-dist.mjs dist && node ../../scripts/sync-create-lumal-admin-version.mjs",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 应用 API 契约
|
|
2
|
+
|
|
3
|
+
Local 与 HTTP 实现 `src/services/contracts.ts` 的同一服务接口。Core 不包含业务协议。
|
|
4
|
+
|
|
5
|
+
| 方法 | 路径 | 标准 JSON 响应 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| POST | `/auth/login`,body `{ username, password }` | `{ accessToken, refreshToken?, expiresAt? }` |
|
|
8
|
+
| POST | `/auth/refresh`,body `{ refreshToken }` | 同上 |
|
|
9
|
+
| GET | `/session/me` | `{ id, username, name }`,均为字符串 |
|
|
10
|
+
| GET | `/session/access` | `{ roles: string[], permissions: string[] }` |
|
|
11
|
+
| GET | `/menus` | `LumalMenuRecord[]` |
|
|
12
|
+
| POST | `/auth/logout` | 204 或 JSON |
|
|
13
|
+
| GET | `/system/:resource?page=1&pageSize=10&keyword=` | `{ items: object[], total: number }` |
|
|
14
|
+
| POST/PUT/DELETE | `/system/:resource[/:id]` | JSON 或 204 |
|
|
15
|
+
|
|
16
|
+
菜单组件使用 `src/router/views.ts` 的本地白名单,禁止任意远程 import。标准模块资源为
|
|
17
|
+
`users`、`roles`、`menus`、`organizations`、`dictionaries/types`、`dictionaries/items`、`config`。
|
|
18
|
+
标准 CRUD 接口按记录管理;配置记录使用 `id/key/value/description`。
|
|
19
|
+
后端仍需检查每项权限,不能依赖前端隐藏按钮。
|
|
20
|
+
|
|
21
|
+
HTTP 模式必须配置 `VITE_API_BASE_URL`,未配置立即报错,绝不回退本地登录。
|
|
22
|
+
默认直接消费标准 JSON。包装、字段及权限映射集中在应用 `http-adapter.ts`。
|
|
23
|
+
需要 Cookie/CSRF、租户或 SSO 时在应用 transport 接入,不将密钥写到 `VITE_*`。
|
|
24
|
+
默认凭据存于 localStorage,请按安全要求选择内存/HttpOnly Cookie 策略。
|
|
25
|
+
|
|
26
|
+
## 接入仓库 Lumal Mock API
|
|
27
|
+
|
|
28
|
+
启动独立的 `lumal-mock-api` 后,设置 `VITE_API_BASE_URL=http://127.0.0.1:5320/api`
|
|
29
|
+
和 `VITE_API_ADAPTER=lumal-mock`。这是显式选择的演示后端,不是生产默认值。
|
|
30
|
+
该 adapter 映射 `statusCode/result/resultMsg`、snake_case Token、`records/totalNum`;
|
|
31
|
+
只选择本预设存在的菜单,其余示例页面不生成。
|
|
32
|
+
Mock 的系统配置通过显式 adapter 转换为记录型配置;更新时保留原值类型。
|
|
33
|
+
|
|
34
|
+
401 GET 自动单飞刷新并最多重放一次;写请求不自动重放,避免重复提交。
|
|
35
|
+
初始化等待用户、权限和菜单全部就绪后注册路由。退出、刷新失败和重新登录清理
|
|
36
|
+
权限、菜单、动态路由、请求缓存、标签和页面缓存;旧会话请求会被取消。
|
|
37
|
+
真实接口的 403、校验或网络错误保持可见,不转换成成功响应。
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Lumal Admin
|
|
2
|
+
|
|
3
|
+
## 开发
|
|
4
|
+
|
|
5
|
+
Node.js 22.12+,pnpm 10.33.0。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev
|
|
10
|
+
pnpm test
|
|
11
|
+
pnpm lint
|
|
12
|
+
pnpm typecheck
|
|
13
|
+
pnpm build
|
|
14
|
+
pnpm preview
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
预设、数据源、路由模式和可选模块见 `src/app.config.ts`。
|
|
18
|
+
业务组件均显式导入,不依赖组件自动导入或 Monorepo 私有 alias。
|
|
19
|
+
`@` 指向 `src`,TS 与 Vite 使用相同映射。SCSS 编译使用 sass-embedded。
|
|
20
|
+
|
|
21
|
+
## 接口与认证
|
|
22
|
+
|
|
23
|
+
HTTP 接入前配置 `.env.development`,完整接口见 [API 契约](./API-CONTRACT.md)。
|
|
24
|
+
Local 模式是明确的开发演示,账号 `admin/guest`,密码 `lumal123`;
|
|
25
|
+
生产默认禁用,不应将演示模式当作生产认证。
|
|
26
|
+
标准业务模块使用应用服务和原生 CRUD,初始数据为空,可自行新增。
|
|
27
|
+
|
|
28
|
+
## 部署
|
|
29
|
+
|
|
30
|
+
以 `.env.production.example` 为参考创建生产配置,不提交密钥。
|
|
31
|
+
Vite 环境变量在构建时注入,预览前修改变量必须重新构建。
|
|
32
|
+
推荐同域 `/api` 反向代理和 HTTPS。History 路由需要服务器将非文件路径回退到
|
|
33
|
+
`index.html`,Hash 不需要;子目录部署需配置 Vite `base`。
|
|
34
|
+
不要将生产 API 请求回退到 HTML。上传文件限制和后端权限属于服务端职责。
|
|
35
|
+
localStorage Token 示例不是生产安全策略的唯一选择。
|
|
36
|
+
|
|
37
|
+
## 模块
|
|
38
|
+
|
|
39
|
+
minimal 没有管理页面与兼容层;standard 的 system/examples 是可选模板层。
|
|
40
|
+
系统页面的 CRUD 和 Schema 不进入 Core 的业务实现。图表、驾驶舱、富文本、
|
|
41
|
+
国际化与 VXE 高级编辑均不默认安装。新增页面时在 `src/modules` 声明白名单懒加载器,
|
|
42
|
+
并让后端菜单使用相应 component key。服务端菜单改变后需重新加载会话。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Lumal Admin</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "template-base",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"packageManager": "pnpm@10.33.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite --host 127.0.0.1",
|
|
12
|
+
"build": "pnpm typecheck && vite build",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"preview": "vite preview --host 127.0.0.1",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@iconify/vue": "^5.0.0",
|
|
20
|
+
"@lumal/core": "^0.0.0",
|
|
21
|
+
"@lumal/icons": "^0.0.0",
|
|
22
|
+
"@lumal/icons-vue": "^0.0.0",
|
|
23
|
+
"element-plus": "^2.11.0",
|
|
24
|
+
"vue": "^3.5.0",
|
|
25
|
+
"vue-router": "^4.5.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@antfu/eslint-config": "^8.2.0",
|
|
29
|
+
"@types/node": "^24.6.1",
|
|
30
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
31
|
+
"eslint": "^9.37.0",
|
|
32
|
+
"sass-embedded": "^1.95.1",
|
|
33
|
+
"typescript": "^6.0.0",
|
|
34
|
+
"vite": "^8.0.0",
|
|
35
|
+
"vitest": "^4.1.5",
|
|
36
|
+
"vue-tsc": "^3.1.1"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { LumalLayoutTabItem } from '@lumal/core/layout'
|
|
3
|
+
import type { LumalPreferences } from '@lumal/core/theme'
|
|
4
|
+
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
5
|
+
import {
|
|
6
|
+
findMenuItemByPath,
|
|
7
|
+
LumalLayout,
|
|
8
|
+
LumalRouterView,
|
|
9
|
+
} from '@lumal/core/layout'
|
|
10
|
+
import { ElAlert, ElConfigProvider, ElMessage } from 'element-plus'
|
|
11
|
+
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
12
|
+
import { computed, defineAsyncComponent, onErrorCaptured, reactive, shallowRef, watch } from 'vue'
|
|
13
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
14
|
+
import { appConfig } from './app.config'
|
|
15
|
+
import AppHeaderActions from './components/app/AppHeaderActions.vue'
|
|
16
|
+
import { createAdminSidebarMenus, createAdminTabs } from './router'
|
|
17
|
+
import {
|
|
18
|
+
adminPreferenceDefaults,
|
|
19
|
+
adminPreferences,
|
|
20
|
+
adminResolvedThemeMode,
|
|
21
|
+
patchAdminPreferences,
|
|
22
|
+
} from './services/preferences'
|
|
23
|
+
import { adminTabSnapshotStorageKey, currentUser, logout } from './services/session'
|
|
24
|
+
|
|
25
|
+
/***********************页面状态*********************/
|
|
26
|
+
const AppSettingsDrawer = defineAsyncComponent(() => import('./components/app/AppSettingsDrawer.vue'))
|
|
27
|
+
const title = appConfig.title
|
|
28
|
+
const settingsVisible = shallowRef(false)
|
|
29
|
+
const pageError = shallowRef('')
|
|
30
|
+
const routeRefreshKeys = reactive<Record<string, number>>({})
|
|
31
|
+
onErrorCaptured((error) => {
|
|
32
|
+
pageError.value = error.message
|
|
33
|
+
return false
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/***********************路由状态*********************/
|
|
37
|
+
const route = useRoute()
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
const isPublicLayout = computed(() => route.meta.layout === 'public')
|
|
40
|
+
const allMenus = computed(() => createAdminSidebarMenus())
|
|
41
|
+
const fixedTabs = computed<LumalLayoutTabItem[]>(() => createAdminTabs())
|
|
42
|
+
// 标签和视图使用相同的 path/version 身份,刷新只替换当前标签的缓存。
|
|
43
|
+
function resolveRouteViewKey(path: string): string {
|
|
44
|
+
return `${path}:${routeRefreshKeys[path] ?? 0}`
|
|
45
|
+
}
|
|
46
|
+
const routeViewKey = computed(() => resolveRouteViewKey(route.path))
|
|
47
|
+
const routeViewCache = computed(() => adminPreferences.value.tabbar.enable && adminPreferences.value.tabbar.cache)
|
|
48
|
+
const activePath = computed({
|
|
49
|
+
get: () => route.path,
|
|
50
|
+
set: (path: string) => {
|
|
51
|
+
if (path !== route.path) {
|
|
52
|
+
void router.push(path)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
watch([
|
|
58
|
+
() => route.meta.title,
|
|
59
|
+
() => adminPreferences.value.app.dynamicTitle,
|
|
60
|
+
], () => {
|
|
61
|
+
const routeTitle = typeof route.meta.title === 'string' ? route.meta.title.trim() : ''
|
|
62
|
+
document.title = adminPreferences.value.app.dynamicTitle && routeTitle
|
|
63
|
+
? `${routeTitle} - ${title}`
|
|
64
|
+
: title
|
|
65
|
+
}, { immediate: true })
|
|
66
|
+
|
|
67
|
+
/***********************导航事件*********************/
|
|
68
|
+
function handleMenuSelect(path: string): void {
|
|
69
|
+
const item = findMenuItemByPath(allMenus.value, path)
|
|
70
|
+
|
|
71
|
+
if (item?.externalLink && item.externalTarget !== '_self') {
|
|
72
|
+
window.open(item.externalLink, item.externalTarget ?? '_blank', 'noopener,noreferrer')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
activePath.value = path
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleToggleSidebar(): void {
|
|
80
|
+
patchAdminPreferences({
|
|
81
|
+
sidebar: { collapsed: !adminPreferences.value.sidebar.collapsed },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleTabChange(path: string): void {
|
|
86
|
+
activePath.value = path
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveAdminRouteTab(currentRoute: RouteLocationNormalizedLoaded): LumalLayoutTabItem | undefined {
|
|
90
|
+
if (currentRoute.meta.layout === 'public' || currentRoute.meta.hideInTab === true) {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const menu = findMenuItemByPath(allMenus.value, currentRoute.path)
|
|
95
|
+
const tabTitle = menu?.title ?? (typeof currentRoute.meta.title === 'string' ? currentRoute.meta.title : undefined)
|
|
96
|
+
|
|
97
|
+
if (!tabTitle) {
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
closable: !['/403', '/404'].includes(currentRoute.path),
|
|
103
|
+
icon: menu?.icon ?? (typeof currentRoute.meta.icon === 'string' ? currentRoute.meta.icon : undefined),
|
|
104
|
+
path: currentRoute.path,
|
|
105
|
+
title: tabTitle,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleTabRefresh(path: string): Promise<void> {
|
|
110
|
+
if (path !== route.path) {
|
|
111
|
+
await router.push(path)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
routeRefreshKeys[path] = (routeRefreshKeys[path] ?? 0) + 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleTabsChange(tabs: LumalLayoutTabItem[]): void {
|
|
118
|
+
const paths = new Set(tabs.map(tab => tab.path))
|
|
119
|
+
for (const path of Object.keys(routeRefreshKeys)) {
|
|
120
|
+
if (!paths.has(path))
|
|
121
|
+
delete routeRefreshKeys[path]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handlePreferencesChange(preferences: LumalPreferences): void {
|
|
126
|
+
patchAdminPreferences(preferences)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toggleTheme(): void {
|
|
130
|
+
patchAdminPreferences({
|
|
131
|
+
theme: {
|
|
132
|
+
mode: adminResolvedThemeMode.value === 'dark' ? 'light' : 'dark',
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function handleLogout(): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
await logout()
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
ElMessage.warning(error instanceof Error ? error.message : '远端退出失败,本地会话已清理')
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
pageError.value = ''
|
|
146
|
+
await router.replace('/login')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<ElConfigProvider :locale="zhCn">
|
|
153
|
+
<LumalRouterView
|
|
154
|
+
v-if="isPublicLayout"
|
|
155
|
+
:view-key="routeViewKey"
|
|
156
|
+
transition
|
|
157
|
+
transition-name="fade"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<LumalLayout
|
|
161
|
+
v-else
|
|
162
|
+
:key="currentUser?.username"
|
|
163
|
+
v-model:active-tab-path="activePath"
|
|
164
|
+
:title="title"
|
|
165
|
+
:menus="allMenus"
|
|
166
|
+
:preferences="adminPreferences"
|
|
167
|
+
route-driven
|
|
168
|
+
:fixed-tabs="fixedTabs"
|
|
169
|
+
:route-tab-resolver="resolveAdminRouteTab"
|
|
170
|
+
tab-fallback-path="/dashboard"
|
|
171
|
+
:tab-storage-key="currentUser ? adminTabSnapshotStorageKey : ''"
|
|
172
|
+
:active-menu-path="activePath"
|
|
173
|
+
@menu-select="handleMenuSelect"
|
|
174
|
+
@toggle-sidebar="handleToggleSidebar"
|
|
175
|
+
@tab-change="handleTabChange"
|
|
176
|
+
@tab-refresh="handleTabRefresh"
|
|
177
|
+
@tabs-change="handleTabsChange"
|
|
178
|
+
>
|
|
179
|
+
<template #headerActions>
|
|
180
|
+
<AppHeaderActions
|
|
181
|
+
:resolved-theme-mode="adminResolvedThemeMode"
|
|
182
|
+
:user-name="currentUser?.name"
|
|
183
|
+
@logout="handleLogout"
|
|
184
|
+
@open-settings="settingsVisible = true"
|
|
185
|
+
@toggle-theme="toggleTheme"
|
|
186
|
+
/>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<template #default="{ tabs }">
|
|
190
|
+
<ElAlert v-if="pageError" :title="pageError" type="error" role="alert" closable @close="pageError = ''" />
|
|
191
|
+
<LumalRouterView
|
|
192
|
+
:view-key="routeViewKey"
|
|
193
|
+
:cache-keys="tabs.map((tab: LumalLayoutTabItem) => resolveRouteViewKey(tab.path))"
|
|
194
|
+
:progress="adminPreferences.transition.progress"
|
|
195
|
+
:loading="adminPreferences.transition.loading"
|
|
196
|
+
:cache="routeViewCache"
|
|
197
|
+
:cache-max="adminPreferences.tabbar.maxCount"
|
|
198
|
+
:transition="adminPreferences.transition.enable"
|
|
199
|
+
:transition-name="adminPreferences.transition.name"
|
|
200
|
+
/>
|
|
201
|
+
</template>
|
|
202
|
+
</LumalLayout>
|
|
203
|
+
|
|
204
|
+
<AppSettingsDrawer
|
|
205
|
+
v-if="settingsVisible"
|
|
206
|
+
v-model:visible="settingsVisible"
|
|
207
|
+
:defaults="adminPreferenceDefaults"
|
|
208
|
+
:preferences="adminPreferences"
|
|
209
|
+
@change="handlePreferencesChange"
|
|
210
|
+
@reset="handlePreferencesChange"
|
|
211
|
+
@update:preferences="handlePreferencesChange"
|
|
212
|
+
/>
|
|
213
|
+
</ElConfigProvider>
|
|
214
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ResolvedThemeMode } from '@lumal/core/theme'
|
|
3
|
+
import { LumalIcon } from '@lumal/icons-vue'
|
|
4
|
+
import { ElButton } from 'element-plus'
|
|
5
|
+
|
|
6
|
+
/***********************头部操作契约*********************/
|
|
7
|
+
withDefaults(defineProps<{
|
|
8
|
+
resolvedThemeMode: ResolvedThemeMode
|
|
9
|
+
userName?: string
|
|
10
|
+
}>(), {
|
|
11
|
+
userName: '管理员',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
defineEmits<{
|
|
15
|
+
logout: []
|
|
16
|
+
openSettings: []
|
|
17
|
+
toggleTheme: []
|
|
18
|
+
}>()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="lumal-admin-header-actions">
|
|
23
|
+
<ElButton text :title="resolvedThemeMode === 'dark' ? '切换浅色模式' : '切换深色模式'" :aria-label="resolvedThemeMode === 'dark' ? '切换浅色模式' : '切换深色模式'" @click="$emit('toggleTheme')">
|
|
24
|
+
<LumalIcon :name="resolvedThemeMode === 'dark' ? 'lumal:sun' : 'lumal:moon'" />
|
|
25
|
+
</ElButton>
|
|
26
|
+
<ElButton text title="偏好设置" aria-label="偏好设置" @click="$emit('openSettings')">
|
|
27
|
+
<LumalIcon name="lumal:settings" />
|
|
28
|
+
</ElButton>
|
|
29
|
+
<span class="lumal-admin-header-actions__user">{{ userName }}</span>
|
|
30
|
+
<ElButton text aria-label="退出登录" @click="$emit('logout')">
|
|
31
|
+
退出
|
|
32
|
+
</ElButton>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { LumalPreferences, LumalPreferencesDefaults } from '@lumal/core/theme'
|
|
3
|
+
import { LumalThemeSettingsPanel } from '@lumal/core/theme'
|
|
4
|
+
import { ElDrawer } from 'element-plus'
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
|
|
7
|
+
/***********************偏好设置抽屉*********************/
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
defaults: LumalPreferencesDefaults
|
|
10
|
+
preferences: LumalPreferences
|
|
11
|
+
visible: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
'change': [preferences: LumalPreferences]
|
|
16
|
+
'reset': [preferences: LumalPreferences]
|
|
17
|
+
'update:preferences': [preferences: LumalPreferences]
|
|
18
|
+
'update:visible': [visible: boolean]
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const visible = computed({
|
|
22
|
+
get: () => props.visible,
|
|
23
|
+
set: value => emit('update:visible', value),
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<ElDrawer v-model="visible" title="偏好设置" size="384px" append-to-body>
|
|
29
|
+
<LumalThemeSettingsPanel
|
|
30
|
+
:defaults="defaults"
|
|
31
|
+
:preferences="preferences"
|
|
32
|
+
@change="$emit('change', $event)"
|
|
33
|
+
@reset="$emit('reset', $event)"
|
|
34
|
+
@update:preferences="$emit('update:preferences', $event)"
|
|
35
|
+
/>
|
|
36
|
+
</ElDrawer>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IconDefinition } from '@lumal/icons'
|
|
2
|
+
import { registerAuthorityDirectives } from '@lumal/core/directives'
|
|
3
|
+
import { createLumalAdmin } from '@lumal/core/minimal'
|
|
4
|
+
import App from './App.vue'
|
|
5
|
+
import { router } from './router'
|
|
6
|
+
import { permissionStore } from './services/access'
|
|
7
|
+
import '@lumal/core/theme-chalk/index.scss'
|
|
8
|
+
import '@lumal/core/style.css'
|
|
9
|
+
import '@lumal/icons-vue/style.css'
|
|
10
|
+
import 'element-plus/theme-chalk/el-alert.css'
|
|
11
|
+
import 'element-plus/theme-chalk/el-date-picker.css'
|
|
12
|
+
import 'element-plus/theme-chalk/dark/css-vars.css'
|
|
13
|
+
import './styles.scss'
|
|
14
|
+
|
|
15
|
+
/***********************本地图标定义*********************/
|
|
16
|
+
const localIcons: IconDefinition[] = [
|
|
17
|
+
{
|
|
18
|
+
key: 'app:dashboard',
|
|
19
|
+
label: '控制台',
|
|
20
|
+
source: 'local-svg',
|
|
21
|
+
svgText: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z"/></svg>',
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/***********************应用启动*********************/
|
|
26
|
+
const framework = createLumalAdmin({
|
|
27
|
+
rootComponent: App,
|
|
28
|
+
router,
|
|
29
|
+
permissionStore,
|
|
30
|
+
icons: {
|
|
31
|
+
localSvg: localIcons,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
registerAuthorityDirectives(framework.app, permissionStore)
|
|
36
|
+
void framework.mount('#app').catch((error: unknown) => {
|
|
37
|
+
console.error('应用启动失败', error)
|
|
38
|
+
const element = document.getElementById('app')
|
|
39
|
+
if (element)
|
|
40
|
+
element.textContent = '应用启动失败,请检查配置与控制台。'
|
|
41
|
+
})
|