chore: 递增版本号并标记停更版本
- 版本号: 中段递增、末段归零
- README: laravel 与 spring-boot-3 更新状态改为 ❌️
This commit is contained in:
@@ -129,10 +129,10 @@ npx skills add https://github.com/youlaitech/youlai-skills --skill vue-admin
|
||||
| [youlai-gin](https://gitee.com/youlaiorg/youlai-gin) | Go + Gorm | Go | ✅️ |
|
||||
| [youlai-django](https://gitee.com/youlaiorg/youlai-django) | Django + DRF | Python | ✅️ |
|
||||
| [youlai-fastapi](https://gitee.com/youlaiorg/youlai-fastapi) | FastAPI + SQLAlchemy | Python | ✅️ |
|
||||
| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ✅️ |
|
||||
| [youlai-think](https://gitee.com/youlaiorg/youlai-think) | ThinkPHP + ThinkORM | PHP | ✅️ |
|
||||
| [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core + EF Core | C# | ✅️ |
|
||||
| [youlai-axum](https://gitee.com/youlaiorg/youlai-axum) | Axum + SeaORM | Rust | ✅️ |
|
||||
| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ❌️ |
|
||||
|
||||
> All nine backends share the same **RESTful API** and **database schema**, frontends can switch seamlessly.
|
||||
|
||||
@@ -145,7 +145,7 @@ npx skills add https://github.com/youlaitech/youlai-skills --skill vue-admin
|
||||
| [youlai-boot-flex](https://gitee.com/youlaiorg/youlai-boot-flex) | youlai-boot | Repo | Switched to MyBatis-Flex | ✅️ |
|
||||
| [youlai-boot (db-pg)](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) | youlai-boot | Branch | PostgreSQL database branch | ✅️ |
|
||||
| [youlai-boot (multi-module)](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module) | youlai-boot | Branch | Multi-module project split | ✅️ |
|
||||
| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | Branch | Spring Boot 3 compatible branch | ✅️ |
|
||||
| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | Branch | Spring Boot 3 compatible branch | ❌️ |
|
||||
|
||||
## Development Guide
|
||||
|
||||
|
||||
@@ -131,10 +131,10 @@ npx skills add https://github.com/youlaitech/youlai-skills --skill vue-admin
|
||||
| [youlai-gin](https://gitee.com/youlaiorg/youlai-gin) | Go + Gorm | Go | ✅️ |
|
||||
| [youlai-django](https://gitee.com/youlaiorg/youlai-django) | Django + DRF | Python | ✅️ |
|
||||
| [youlai-fastapi](https://gitee.com/youlaiorg/youlai-fastapi) | FastAPI + SQLAlchemy | Python | ✅️ |
|
||||
| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ✅️ |
|
||||
| [youlai-think](https://gitee.com/youlaiorg/youlai-think) | ThinkPHP + ThinkORM | PHP | ✅️ |
|
||||
| [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core + EF Core | C# | ✅️ |
|
||||
| [youlai-axum](https://gitee.com/youlaiorg/youlai-axum) | Axum + SeaORM | Rust | ✅️ |
|
||||
| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ❌️ |
|
||||
|
||||
> 九种后端共享同一套 **RESTful API 规范** 和 **数据库结构**,前端可无缝切换。
|
||||
|
||||
@@ -147,7 +147,7 @@ npx skills add https://github.com/youlaitech/youlai-skills --skill vue-admin
|
||||
| [youlai-boot-flex](https://gitee.com/youlaiorg/youlai-boot-flex) | youlai-boot | 独立仓库 | 改用 MyBatis-Flex | ✅️ |
|
||||
| [youlai-boot (db-pg)](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) | youlai-boot | 分支 | PostgreSQL 数据库分支 | ✅️ |
|
||||
| [youlai-boot (multi-module)](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module) | youlai-boot | 分支 | 多模块工程拆分 | ✅️ |
|
||||
| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | 分支 | Spring Boot 3 兼容分支 | ✅️ |
|
||||
| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | 分支 | Spring Boot 3 兼容分支 | ❌️ |
|
||||
|
||||
## 开发指南
|
||||
|
||||
|
||||
@@ -577,7 +577,7 @@ export default defineMock([
|
||||
id: "105",
|
||||
parentId: "2",
|
||||
name: "用户查询",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -592,7 +592,7 @@ export default defineMock([
|
||||
id: "31",
|
||||
parentId: "2",
|
||||
name: "用户新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -607,7 +607,7 @@ export default defineMock([
|
||||
id: "32",
|
||||
parentId: "2",
|
||||
name: "用户编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -622,7 +622,7 @@ export default defineMock([
|
||||
id: "33",
|
||||
parentId: "2",
|
||||
name: "用户删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -637,7 +637,7 @@ export default defineMock([
|
||||
id: "88",
|
||||
parentId: "2",
|
||||
name: "重置密码",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -652,7 +652,7 @@ export default defineMock([
|
||||
id: "106",
|
||||
parentId: "2",
|
||||
name: "用户导入",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -667,7 +667,7 @@ export default defineMock([
|
||||
id: "107",
|
||||
parentId: "2",
|
||||
name: "用户导出",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -698,7 +698,7 @@ export default defineMock([
|
||||
id: "70",
|
||||
parentId: "3",
|
||||
name: "角色新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -713,7 +713,7 @@ export default defineMock([
|
||||
id: "71",
|
||||
parentId: "3",
|
||||
name: "角色编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -728,7 +728,7 @@ export default defineMock([
|
||||
id: "72",
|
||||
parentId: "3",
|
||||
name: "角色删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -759,7 +759,7 @@ export default defineMock([
|
||||
id: "73",
|
||||
parentId: "4",
|
||||
name: "菜单新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -774,7 +774,7 @@ export default defineMock([
|
||||
id: "74",
|
||||
parentId: "4",
|
||||
name: "菜单编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -789,7 +789,7 @@ export default defineMock([
|
||||
id: "75",
|
||||
parentId: "4",
|
||||
name: "菜单删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -820,7 +820,7 @@ export default defineMock([
|
||||
id: "76",
|
||||
parentId: "5",
|
||||
name: "部门新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -835,7 +835,7 @@ export default defineMock([
|
||||
id: "77",
|
||||
parentId: "5",
|
||||
name: "部门编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -850,7 +850,7 @@ export default defineMock([
|
||||
id: "78",
|
||||
parentId: "5",
|
||||
name: "部门删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -881,7 +881,7 @@ export default defineMock([
|
||||
id: "79",
|
||||
parentId: "6",
|
||||
name: "字典新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -896,7 +896,7 @@ export default defineMock([
|
||||
id: "81",
|
||||
parentId: "6",
|
||||
name: "字典编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -911,7 +911,7 @@ export default defineMock([
|
||||
id: "84",
|
||||
parentId: "6",
|
||||
name: "字典删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -942,7 +942,7 @@ export default defineMock([
|
||||
id: "136",
|
||||
parentId: "135",
|
||||
name: "字典项新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -957,7 +957,7 @@ export default defineMock([
|
||||
id: "137",
|
||||
parentId: "135",
|
||||
name: "字典项编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -972,7 +972,7 @@ export default defineMock([
|
||||
id: "138",
|
||||
parentId: "135",
|
||||
name: "字典项删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1018,7 +1018,7 @@ export default defineMock([
|
||||
id: "121",
|
||||
parentId: "120",
|
||||
name: "查询系统配置",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1033,7 +1033,7 @@ export default defineMock([
|
||||
id: "122",
|
||||
parentId: "120",
|
||||
name: "新增系统配置",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1048,7 +1048,7 @@ export default defineMock([
|
||||
id: "123",
|
||||
parentId: "120",
|
||||
name: "修改系统配置",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1063,7 +1063,7 @@ export default defineMock([
|
||||
id: "124",
|
||||
parentId: "120",
|
||||
name: "删除系统配置",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1078,7 +1078,7 @@ export default defineMock([
|
||||
id: "125",
|
||||
parentId: "120",
|
||||
name: "刷新系统配置",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1109,7 +1109,7 @@ export default defineMock([
|
||||
id: "127",
|
||||
parentId: "126",
|
||||
name: "查询",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1124,7 +1124,7 @@ export default defineMock([
|
||||
id: "128",
|
||||
parentId: "126",
|
||||
name: "新增",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1139,7 +1139,7 @@ export default defineMock([
|
||||
id: "129",
|
||||
parentId: "126",
|
||||
name: "编辑",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1154,7 +1154,7 @@ export default defineMock([
|
||||
id: "130",
|
||||
parentId: "126",
|
||||
name: "删除",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1169,7 +1169,7 @@ export default defineMock([
|
||||
id: "133",
|
||||
parentId: "126",
|
||||
name: "发布",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
@@ -1184,7 +1184,7 @@ export default defineMock([
|
||||
id: "134",
|
||||
parentId: "126",
|
||||
name: "撤回",
|
||||
type: "F",
|
||||
type: "B",
|
||||
routeName: null,
|
||||
routePath: "",
|
||||
component: null,
|
||||
|
||||
@@ -29,10 +29,10 @@ function handleClipboard() {
|
||||
// 使用 Clipboard API
|
||||
navigator.clipboard.writeText(props.text).then(
|
||||
() => {
|
||||
ElMessage.success("Copy successfully");
|
||||
ElMessage.success("复制成功");
|
||||
},
|
||||
() => {
|
||||
ElMessage.warning("Copy failed");
|
||||
ElMessage.warning("复制失败");
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -47,9 +47,9 @@ function handleClipboard() {
|
||||
const successful = document.execCommand("copy");
|
||||
|
||||
if (successful) {
|
||||
ElMessage.success("Copy successfully!");
|
||||
ElMessage.success("复制成功");
|
||||
} else {
|
||||
ElMessage.warning("Copy failed!");
|
||||
ElMessage.warning("复制失败");
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(input);
|
||||
|
||||
49
src/components/SectionTitle/index.vue
Normal file
49
src/components/SectionTitle/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="section-title">
|
||||
<span>{{ title }}</span>
|
||||
<span v-if="tip" class="section-title__tip">{{ tip }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 分区标题
|
||||
*
|
||||
* 表单中划分区块的小标题:竖线 + 标题,标题右侧可跟说明文字。
|
||||
*/
|
||||
defineOptions({ name: "SectionTitle" });
|
||||
|
||||
defineProps<{
|
||||
/** 分区标题 */
|
||||
title: string;
|
||||
/** 标题右侧说明 */
|
||||
tip?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-title {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin: 2px 0 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&::before {
|
||||
flex: none;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
content: "";
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title__tip {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,7 @@ import { setupStore } from "@/stores";
|
||||
import { setupI18n } from "@/lang";
|
||||
import { setupFormCreate } from "@/plugins/form-create";
|
||||
import * as ElementPlusIcons from "@element-plus/icons-vue";
|
||||
import { setupPermissionGuard } from "@/router/guards/permission";
|
||||
import { setupPermissionGuard } from "@/router/guards";
|
||||
import { useSse } from "@/utils/sse";
|
||||
import { useDictStoreHook } from "@/stores/dict";
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { useTenantStoreHook } from "@/stores/tenant";
|
||||
import { isTenantEnabled } from "@/utils/tenant";
|
||||
|
||||
/**
|
||||
* 路由权限守卫
|
||||
* 路由守卫
|
||||
*
|
||||
* 处理登录验证、动态路由生成、404检测等
|
||||
* 处理登录验证、动态路由生成、404 检测、页面标题与进度条
|
||||
*/
|
||||
export function setupPermissionGuard() {
|
||||
// 白名单支持前缀匹配:/f 命中所有公开表单分享页 /f/:formKey
|
||||
@@ -84,7 +84,9 @@ export function setupPermissionGuard() {
|
||||
});
|
||||
}
|
||||
|
||||
/** 初始化多租户上下文,未启用或失败时静默跳过 */
|
||||
/**
|
||||
* 初始化多租户上下文,未启用或失败时静默跳过
|
||||
*/
|
||||
async function initTenantContext(): Promise<void> {
|
||||
if (!isTenantEnabled()) return;
|
||||
|
||||
133
src/router/views.ts
Normal file
133
src/router/views.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 视图组件注册表
|
||||
*
|
||||
* 维护「菜单 component 字符串 → src/views 视图文件」的映射,路由注册与菜单表单共用。
|
||||
* 表单的「页面路径」选项树也由此派生:跳过错误页与页面内部组件,index.vue 归并到目录。
|
||||
*/
|
||||
|
||||
/** 全部视图模块,键形如 /src/views/system/user/index.vue */
|
||||
export const viewModules = import.meta.glob("/src/views/**/*.vue");
|
||||
|
||||
const VIEW_PREFIX = "/src/views/";
|
||||
|
||||
/** 视图文件路径集合,元素形如 system/user/index */
|
||||
export const viewPaths = new Set(
|
||||
Object.keys(viewModules).map((key) => key.slice(VIEW_PREFIX.length).replace(/\.vue$/, ""))
|
||||
);
|
||||
|
||||
/**
|
||||
* 规整组件路径:去首尾空白、开头斜杠与 .vue 后缀
|
||||
*/
|
||||
export function normalizeViewPath(componentPath: string): string {
|
||||
return componentPath
|
||||
.trim()
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\.vue$/i, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析组件:支持 system/user 与 system/user/index 两种写法,未命中回退 404
|
||||
*/
|
||||
export function resolveComponent(componentPath: string) {
|
||||
const path = normalizeViewPath(componentPath);
|
||||
return (
|
||||
viewModules[`${VIEW_PREFIX}${path}.vue`] ||
|
||||
viewModules[`${VIEW_PREFIX}${path}/index.vue`] ||
|
||||
viewModules[`${VIEW_PREFIX}error/404.vue`]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断组件路径是否存在对应视图文件,目录写法命中其 index.vue
|
||||
*/
|
||||
export function isViewComponent(componentPath?: string): boolean {
|
||||
if (!componentPath) return false;
|
||||
const path = normalizeViewPath(componentPath);
|
||||
return viewPaths.has(path) || viewPaths.has(`${path}/index`);
|
||||
}
|
||||
|
||||
/** 选项树节点 */
|
||||
export interface PageComponentNode {
|
||||
/** 表单取值:真实文件路径,目录无 index.vue 时不可选 */
|
||||
value?: string;
|
||||
label: string;
|
||||
children?: PageComponentNode[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function isSelectable(path: string): boolean {
|
||||
const segments = path.split("/");
|
||||
if (segments.includes("components")) return false;
|
||||
return segments[0] !== "error" && path !== "redirect";
|
||||
}
|
||||
|
||||
function buildTree(): PageComponentNode[] {
|
||||
const tree: PageComponentNode[] = [];
|
||||
|
||||
[...viewPaths].filter(isSelectable).forEach((path) => insertPath(tree, path));
|
||||
|
||||
sortTree(tree);
|
||||
markDisabled(tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视图路径插入选项树,index.vue 归并到所在目录
|
||||
*/
|
||||
function insertPath(tree: PageComponentNode[], path: string): void {
|
||||
const segments = path.split("/");
|
||||
if (segments[segments.length - 1] === "index") segments.pop();
|
||||
|
||||
let level = tree;
|
||||
segments.forEach((segment, index) => {
|
||||
const isLeaf = index === segments.length - 1;
|
||||
let node = level.find((item) => item.label === segment);
|
||||
|
||||
if (!node) {
|
||||
node = { label: segment };
|
||||
level.push(node);
|
||||
}
|
||||
|
||||
if (isLeaf) {
|
||||
// 取值始终是真实文件路径,与历史数据和列表展示一致
|
||||
node.value = path;
|
||||
return;
|
||||
}
|
||||
|
||||
node.children ??= [];
|
||||
level = node.children;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录下没有 index.vue 时不可选,只用于展开
|
||||
*/
|
||||
function markDisabled(nodes: PageComponentNode[]): void {
|
||||
nodes.forEach((node) => {
|
||||
if (!node.value) node.disabled = true;
|
||||
if (node.children) markDisabled(node.children);
|
||||
});
|
||||
}
|
||||
|
||||
function sortTree(nodes: PageComponentNode[]): void {
|
||||
nodes.sort((a, b) => a.label.localeCompare(b.label));
|
||||
nodes.forEach((node) => node.children && sortTree(node.children));
|
||||
}
|
||||
|
||||
/** 页面组件选项树 */
|
||||
export const pageComponentTree = buildTree();
|
||||
|
||||
/**
|
||||
* 节点筛选:名称与文件路径都参与匹配,输入 index 可筛出所有主页
|
||||
*/
|
||||
export function filterPageComponent(
|
||||
query: string,
|
||||
node: { label?: string; value?: string }
|
||||
): boolean {
|
||||
if (!query) return true;
|
||||
const keyword = query.toLowerCase();
|
||||
return (
|
||||
(node.label ?? "").toLowerCase().includes(keyword) ||
|
||||
(node.value ?? "").toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { constantRoutes } from "@/router";
|
||||
import { resolveComponent } from "@/router/views";
|
||||
import { store } from "@/stores";
|
||||
import router from "@/router";
|
||||
import { useUserStoreHook } from "@/stores/user";
|
||||
import { isExternal } from "@/utils";
|
||||
import { isExternal, joinRoutePath } from "@/utils";
|
||||
|
||||
import MenuAPI from "@/api/system/menu";
|
||||
import type { RouteItem } from "@/api/system/menu";
|
||||
const modules = import.meta.glob("../views/**/*.vue");
|
||||
|
||||
const Layout = () => import("../layouts/index.vue");
|
||||
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
@@ -21,7 +22,7 @@ export const usePermissionStore = defineStore("permission", () => {
|
||||
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
||||
try {
|
||||
const routeData = await MenuAPI.getRoutes();
|
||||
const menuRoutes = transformRoutes(routeData);
|
||||
const menuRoutes = buildRoutes(routeData);
|
||||
const registerRoutes = filterRoutes(menuRoutes);
|
||||
|
||||
routes.value = [...constantRoutes, ...menuRoutes];
|
||||
@@ -132,49 +133,115 @@ export const usePermissionStore = defineStore("permission", () => {
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 将后端路由数据转为 Vue Router 配置
|
||||
*/
|
||||
const transformRoutes = (routes: RouteItem[], isTopLevel: boolean = true): RouteRecordRaw[] => {
|
||||
return routes.map((route) => {
|
||||
const { children, ...args } = route;
|
||||
const componentPath = route.component;
|
||||
|
||||
// 非顶层目录壳去掉 Layout 组件,仅保留路由结构
|
||||
const resolvedComponent = isTopLevel || componentPath !== "Layout" ? componentPath : undefined;
|
||||
|
||||
const normalizedRoute = { ...args } as RouteRecordRaw;
|
||||
|
||||
if (!resolvedComponent) {
|
||||
normalizedRoute.component = undefined;
|
||||
} else {
|
||||
normalizedRoute.component =
|
||||
resolvedComponent === "Layout" ? Layout : resolveComponent(resolvedComponent);
|
||||
}
|
||||
|
||||
if (children && children.length > 0) {
|
||||
normalizedRoute.children = transformRoutes(children, false);
|
||||
}
|
||||
|
||||
return normalizedRoute;
|
||||
});
|
||||
};
|
||||
// 目录菜单在 component 上的占位值,表示该级只作路由容器
|
||||
const LAYOUT_COMPONENT = "Layout";
|
||||
|
||||
/**
|
||||
* 解析组件
|
||||
* 后端菜单树还原为 Vue Router 路由树
|
||||
*
|
||||
* 支持 xxx.vue 与 xxx/index.vue 两种写法,未命中时回退到 404
|
||||
* 顶层菜单统一由 Layout 承载:目录本身是容器,页面下沉为 path 为空的子路由;
|
||||
* 路径前缀与默认跳转在此推导,菜单数据只描述业务信息。
|
||||
*/
|
||||
function resolveComponent(componentPath: string) {
|
||||
const normalized = componentPath
|
||||
.trim()
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\.vue$/i, "");
|
||||
return (
|
||||
modules[`../views/${normalized}.vue`] ||
|
||||
modules[`../views/${normalized}/index.vue`] ||
|
||||
modules[`../views/error/404.vue`]
|
||||
);
|
||||
const buildRoutes = (menus: RouteItem[]): RouteRecordRaw[] => menus.map(buildTopLevelRoute);
|
||||
|
||||
/**
|
||||
* 顶层菜单路由:套 Layout 壳,保证页面具备侧边栏与顶栏
|
||||
*/
|
||||
function buildTopLevelRoute(menu: RouteItem): RouteRecordRaw {
|
||||
// 新标签页外链不注册路由,保留原始数据供侧边栏直接跳转
|
||||
if (isExternalLink(menu)) return toExternalRoute(menu, menu.path as string);
|
||||
|
||||
const path = joinRoutePath("", menu.path);
|
||||
const meta = { ...menu.meta };
|
||||
|
||||
// 目录:自身就是侧边栏分组,标题与图标保留在壳层
|
||||
if (isContainer(menu)) {
|
||||
return {
|
||||
path,
|
||||
name: path,
|
||||
component: Layout,
|
||||
meta,
|
||||
redirect: menu.redirect || firstVisiblePath(menu.children, path),
|
||||
children: buildChildRoutes(menu.children, path),
|
||||
};
|
||||
}
|
||||
|
||||
// 页面:壳层只承载显示状态,标题与图标下沉到页面子路由,避免面包屑与标签页多出一层
|
||||
return {
|
||||
path,
|
||||
name: path,
|
||||
component: Layout,
|
||||
meta: { hidden: meta.hidden },
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: menu.name,
|
||||
component: resolveComponent(menu.component as string),
|
||||
meta,
|
||||
children: buildChildRoutes(menu.children, path),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 子级菜单路由:目录保留为路径分组,页面挂载实际组件
|
||||
*/
|
||||
function buildChildRoutes(menus: RouteItem[] | undefined, basePath: string): RouteRecordRaw[] {
|
||||
return (menus ?? []).map((menu) => {
|
||||
const fullPath = joinRoutePath(basePath, menu.path);
|
||||
|
||||
if (isExternalLink(menu)) return toExternalRoute(menu, fullPath);
|
||||
|
||||
if (isContainer(menu)) {
|
||||
const children = buildChildRoutes(menu.children, fullPath);
|
||||
return {
|
||||
path: menu.path ?? "",
|
||||
meta: { ...menu.meta },
|
||||
children,
|
||||
...(children.length
|
||||
? { redirect: menu.redirect || firstVisiblePath(menu.children, fullPath) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: menu.path ?? "",
|
||||
name: menu.name,
|
||||
component: resolveComponent(menu.component as string),
|
||||
meta: { ...menu.meta },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新标签页外链路由:不参与路由注册,仅保留侧边栏跳转所需信息
|
||||
*/
|
||||
function toExternalRoute(menu: RouteItem, path: string): RouteRecordRaw {
|
||||
const { children: _children, ...rest } = menu;
|
||||
return { ...rest, path } as RouteRecordRaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录菜单:没有页面组件,只作路由容器
|
||||
*/
|
||||
function isContainer(menu: RouteItem): boolean {
|
||||
return !menu.component || menu.component === LAYOUT_COMPONENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新标签页外链:外链地址直接写在路由路径上且不带组件
|
||||
*/
|
||||
function isExternalLink(menu: RouteItem): boolean {
|
||||
return !menu.component && isExternal(menu.path ?? "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认跳转:第一个可见子菜单的完整路径
|
||||
*/
|
||||
function firstVisiblePath(menus: RouteItem[] | undefined, basePath: string): string | undefined {
|
||||
const first = (menus ?? []).find((menu) => !menu.meta?.hidden && !isExternal(menu.path ?? ""));
|
||||
return first ? joinRoutePath(basePath, first.path) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,5 +11,8 @@ export { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "
|
||||
// 文件下载
|
||||
export { downloadFile } from "./download";
|
||||
|
||||
// 路由路径
|
||||
export { joinRoutePath } from "./route";
|
||||
|
||||
// 本地存储
|
||||
export { Storage } from "./storage";
|
||||
|
||||
19
src/utils/route.ts
Normal file
19
src/utils/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 路由路径工具
|
||||
*/
|
||||
|
||||
import { isExternal } from "./validate";
|
||||
|
||||
/**
|
||||
* 拼接路由路径
|
||||
*
|
||||
* 外链与绝对路径原样返回,相对路径挂到父级之后;顶级相对路径自动补前导斜杠。
|
||||
*
|
||||
* @param basePath 父级完整路径
|
||||
* @param path 当前段路径
|
||||
*/
|
||||
export function joinRoutePath(basePath: string, path?: string): string {
|
||||
if (!path) return basePath || "/";
|
||||
if (isExternal(path) || path.startsWith("/")) return path;
|
||||
return `${basePath.replace(/\/+$/, "")}/${path}`;
|
||||
}
|
||||
@@ -549,12 +549,12 @@ onMounted(() => {
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.form-name__text {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user