feat: Dynamically get the menu from the back end

This commit is contained in:
vben
2024-06-30 15:03:37 +08:00
parent 1d70d71537
commit 9572d1a1c5
71 changed files with 1033 additions and 509 deletions

View File

@@ -1,6 +1,4 @@
export * from './find-menu-by-path';
export * from './flatten-object';
export * from './generator-menus';
export * from './generator-routes';
export * from './merge-route-modules';
export * from './nested-object';

View File

@@ -2,6 +2,7 @@ import type { Preferences } from './types';
const defaultPreferences: Preferences = {
app: {
accessMode: 'frontend',
aiAssistant: true,
authPageLayout: 'panel-right',
colorGrayMode: false,

View File

@@ -9,6 +9,8 @@ import type {
type BreadcrumbStyleType = 'background' | 'normal';
type accessModeType = 'allow-all' | 'backend' | 'frontend';
type NavigationStyleType = 'plain' | 'rounded';
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
@@ -16,6 +18,8 @@ type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
interface AppPreferences {
/** 权限模式 */
accessMode: accessModeType;
/** 是否开启vben助手 */
aiAssistant: boolean;
/** 登录注册页面布局 */
@@ -208,4 +212,5 @@ export type {
ThemeModeType,
ThemePreferences,
TransitionPreferences,
accessModeType,
};

View File

@@ -1,2 +1,2 @@
export * from './access';
export * from './tabs';
export * from './tabbar';

View File

@@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useTabsStore } from './tabs';
import { useTabbarStore } from './tabbar';
describe('useAccessStore', () => {
const router = createRouter({
@@ -18,7 +18,7 @@ describe('useAccessStore', () => {
});
it('adds a new tab', () => {
const store = useTabsStore();
const store = useTabbarStore();
const tab: any = {
fullPath: '/home',
meta: {},
@@ -31,7 +31,7 @@ describe('useAccessStore', () => {
});
it('adds a new tab if it does not exist', () => {
const store = useTabsStore();
const store = useTabbarStore();
const newTab: any = {
fullPath: '/new',
meta: {},
@@ -43,7 +43,7 @@ describe('useAccessStore', () => {
});
it('updates an existing tab instead of adding a new one', () => {
const store = useTabsStore();
const store = useTabbarStore();
const initialTab: any = {
fullPath: '/existing',
meta: {},
@@ -59,7 +59,7 @@ describe('useAccessStore', () => {
});
it('closes all tabs', async () => {
const store = useTabsStore();
const store = useTabbarStore();
store.tabs = [
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
] as any;
@@ -72,7 +72,7 @@ describe('useAccessStore', () => {
});
it('returns all tabs including affix tabs', () => {
const store = useTabsStore();
const store = useTabbarStore();
store.tabs = [
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
] as any;
@@ -86,7 +86,7 @@ describe('useAccessStore', () => {
});
it('closes a non-affix tab', () => {
const store = useTabsStore();
const store = useTabbarStore();
const tab: any = {
fullPath: '/closable',
meta: {},
@@ -99,7 +99,7 @@ describe('useAccessStore', () => {
});
it('does not close an affix tab', () => {
const store = useTabsStore();
const store = useTabbarStore();
const affixTab: any = {
fullPath: '/affix',
meta: { affixTab: true },
@@ -112,14 +112,14 @@ describe('useAccessStore', () => {
});
it('returns all cache tabs', () => {
const store = useTabsStore();
const store = useTabbarStore();
store.cacheTabs.add('Home');
store.cacheTabs.add('About');
expect(store.getCacheTabs).toEqual(['Home', 'About']);
});
it('returns all tabs, including affix tabs', () => {
const store = useTabsStore();
const store = useTabbarStore();
const normalTab: any = {
fullPath: '/normal',
meta: {},
@@ -139,7 +139,7 @@ describe('useAccessStore', () => {
});
it('navigates to a specific tab', async () => {
const store = useTabsStore();
const store = useTabbarStore();
const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };
await store._goToTab(tab, router);
@@ -152,7 +152,7 @@ describe('useAccessStore', () => {
});
it('closes multiple tabs by paths', async () => {
const store = useTabsStore();
const store = useTabbarStore();
store.addTab({
fullPath: '/home',
meta: {},
@@ -179,7 +179,7 @@ describe('useAccessStore', () => {
});
it('closes all tabs to the left of the specified tab', async () => {
const store = useTabsStore();
const store = useTabbarStore();
store.addTab({
fullPath: '/home',
meta: {},
@@ -207,7 +207,7 @@ describe('useAccessStore', () => {
});
it('closes all tabs except the specified tab', async () => {
const store = useTabsStore();
const store = useTabbarStore();
store.addTab({
fullPath: '/home',
meta: {},
@@ -235,7 +235,7 @@ describe('useAccessStore', () => {
});
it('closes all tabs to the right of the specified tab', async () => {
const store = useTabsStore();
const store = useTabbarStore();
const targetTab: any = {
fullPath: '/home',
meta: {},
@@ -263,7 +263,7 @@ describe('useAccessStore', () => {
});
it('closes the tab with the specified key', async () => {
const store = useTabsStore();
const store = useTabbarStore();
const keyToClose = '/about';
store.addTab({
fullPath: '/home',
@@ -293,7 +293,7 @@ describe('useAccessStore', () => {
});
it('refreshes the current tab', async () => {
const store = useTabsStore();
const store = useTabbarStore();
const currentTab: any = {
fullPath: '/dashboard',
meta: { name: 'Dashboard' },

View File

@@ -62,7 +62,7 @@ interface TabsState {
/**
* @zh_CN 访
*/
const useTabsStore = defineStore('tabs', {
const useTabbarStore = defineStore('tabbar', {
actions: {
/**
* Close tabs in bulk
@@ -395,7 +395,7 @@ const useTabsStore = defineStore('tabs', {
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useTabsStore, hot));
hot.accept(acceptHMRUpdate(useTabbarStore, hot));
}
export { useTabsStore };
export { useTabbarStore };

View File

@@ -36,7 +36,7 @@
}
},
"dependencies": {
"@ant-design/colors": "^7.0.2",
"@ant-design/colors": "^7.1.0",
"@ctrl/tinycolor": "4.1.0"
}
}

View File

@@ -36,4 +36,8 @@
.outline-box:not(.outline-box-active):hover::after {
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
}
.card-box {
@apply bg-card text-card-foreground border-border rounded-xl border shadow;
}
}

View File

@@ -1,22 +0,0 @@
import { describe, expect, it } from 'vitest';
import { generateUUID } from './hash';
describe('generateUUID', () => {
it('should return a string', () => {
const uuid = generateUUID();
expect(typeof uuid).toBe('string');
});
it('should be length 32', () => {
const uuid = generateUUID();
expect(uuid.length).toBe(36);
});
it('should have the correct format', () => {
const uuid = generateUUID();
const uuidRegex =
/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i;
expect(uuidRegex.test(uuid)).toBe(true);
});
});

View File

@@ -1,31 +0,0 @@
/**
* 生成一个UUID通用唯一标识符
*
* UUID是一种用于软件构建的标识符其目的是能够生成一个唯一的ID以便在全局范围内标识信息。
* 此函数用于生成一个符合version 4的UUID这种UUID是随机生成的。
*
* 生成的UUID的格式为xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* 其中x是任意16进制数字y是一个16进制数字取值范围为[8, b]。
*
* @returns {string} 生成的UUID。
*/
function generateUUID(): string {
let d = Date.now();
if (
typeof performance !== 'undefined' &&
typeof performance.now === 'function'
) {
d += performance.now(); // use high-precision timer if available
}
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(
/[xy]/g,
(c) => {
const r = Math.trunc((d + Math.random() * 16) % 16);
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
},
);
return uuid;
}
export { generateUUID };

View File

@@ -1,7 +1,6 @@
export * from './cn';
export * from './diff';
export * from './dom';
export * from './hash';
export * from './inference';
export * from './letter';
export * from './merge';

View File

@@ -1,18 +1,18 @@
{
"name": "@vben/hooks",
"name": "@vben/access",
"version": "5.0.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/hooks"
"directory": "packages/business/permissions"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
"build": "pnpm vite build",
"prepublishOnly": "npm run build"
},
"files": [
"dist"
@@ -32,12 +32,19 @@
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"vue": "^3.4.31"
"@vben-core/preferences": "workspace:*",
"@vben-core/stores": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben/locales": "workspace:*",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@vben/types": "workspace:*"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

View File

@@ -0,0 +1,26 @@
<!--
Access control component for fine-grained access control.
-->
<script lang="ts" setup>
interface Props {
/**
* Specified role is visible
* - When the permission mode is 'frontend', the value can be a role value.
* - When the permission mode is 'backend', the value can be a code permission value.
* @default ''
*/
value?: string[];
}
defineOptions({
name: 'Authority',
});
withDefaults(defineProps<Props>(), {
value: undefined,
});
</script>
<template>
<slot></slot>
</template>

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { generatorMenus } from './generator-menus'; // 替换为您的实际路径
import { generateMenus } from './generate-menus'; // 替换为您的实际路径
import {
type RouteRecordRaw,
type Router,
@@ -10,7 +10,7 @@ import {
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
describe('generatorMenus', () => {
describe('generateMenus', () => {
// 模拟路由数据
const mockRoutes = [
{
@@ -69,7 +69,7 @@ describe('generatorMenus', () => {
},
];
const menus = await generatorMenus(mockRoutes, mockRouter as any);
const menus = await generateMenus(mockRoutes, mockRouter as any);
expect(menus).toEqual(expectedMenus);
});
@@ -82,7 +82,7 @@ describe('generatorMenus', () => {
},
] as RouteRecordRaw[];
const menus = await generatorMenus(mockRoutesWithMeta, mockRouter as any);
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any);
expect(menus).toEqual([
{
badge: undefined,
@@ -108,7 +108,7 @@ describe('generatorMenus', () => {
},
] as RouteRecordRaw[];
const menus = await generatorMenus(mockRoutesWithParams, mockRouter as any);
const menus = await generateMenus(mockRoutesWithParams, mockRouter as any);
expect(menus).toEqual([
{
badge: undefined,
@@ -139,12 +139,12 @@ describe('generatorMenus', () => {
},
] as RouteRecordRaw[];
const menus = await generatorMenus(
const menus = await generateMenus(
mockRoutesWithRedirect,
mockRouter as any,
);
expect(menus).toEqual([
// Assuming your generatorMenus function excludes redirect routes from the menu
// Assuming your generateMenus function excludes redirect routes from the menu
{
badge: undefined,
badgeType: undefined,
@@ -191,7 +191,7 @@ describe('generatorMenus', () => {
});
it('should generate menu list with correct order', async () => {
const menus = await generatorMenus(routes, router);
const menus = await generateMenus(routes, router);
const expectedMenus = [
{
badge: undefined,
@@ -224,7 +224,7 @@ describe('generatorMenus', () => {
it('should handle empty routes', async () => {
const emptyRoutes: any[] = [];
const menus = await generatorMenus(emptyRoutes, router);
const menus = await generateMenus(emptyRoutes, router);
expect(menus).toEqual([]);
});
});

View File

@@ -1,4 +1,4 @@
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
import type { RouteRecordRaw, Router } from 'vue-router';
import { mapTree } from '@vben-core/toolkit';
@@ -7,7 +7,7 @@ import { mapTree } from '@vben-core/toolkit';
* routes
* @param routes
*/
async function generatorMenus(
async function generateMenus(
routes: RouteRecordRaw[],
router: Router,
): Promise<MenuRecordRaw[]> {
@@ -70,4 +70,4 @@ async function generatorMenus(
return menus;
}
export { generatorMenus };
export { generateMenus };

View File

@@ -0,0 +1,87 @@
import type {
ComponentRecordType,
RouteRecordStringComponent,
} from '@vben/types';
import type { RouteRecordRaw } from 'vue-router';
import type { GeneratorMenuAndRoutesOptions } from '../types';
import { $t } from '@vben/locales';
import { mapTree } from '@vben-core/toolkit';
/**
* 动态生成路由 - 后端方式
*/
async function generateRoutesByBackend(
options: GeneratorMenuAndRoutesOptions,
): Promise<RouteRecordRaw[]> {
const { fetchMenuListAsync, layoutMap, pageMap } = options;
try {
const menuRoutes = await fetchMenuListAsync?.();
if (!menuRoutes) {
return [];
}
const normalizePageMap: ComponentRecordType = {};
for (const [key, value] of Object.entries(pageMap)) {
normalizePageMap[normalizeViewPath(key)] = value;
}
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
return routes;
} catch (error) {
console.error(error);
return [];
}
}
function convertRoutes(
routes: RouteRecordStringComponent[],
layoutMap: ComponentRecordType,
pageMap: ComponentRecordType,
): RouteRecordRaw[] {
return mapTree(routes, (node) => {
const route = node as unknown as RouteRecordRaw;
const { component, name } = node;
if (!name) {
console.error('route name is required', route);
}
// layout转换
if (component && layoutMap[component]) {
route.component = layoutMap[component];
// 页面组件转换
} else if (component) {
const normalizePath = normalizeViewPath(component);
route.component =
pageMap[
normalizePath.endsWith('.vue')
? normalizePath
: `${normalizePath}.vue`
];
}
// 国际化转化
if (route.meta?.title) {
route.meta.title = $t(route.meta.title);
}
return route;
});
}
function normalizeViewPath(path: string): string {
// 去除相对路径前缀
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
// 确保路径以 '/' 开头
const viewPath = normalizedPath.startsWith('/')
? normalizedPath
: `/${normalizedPath}`;
return viewPath.replace(/^\/views/, '');
}
export { generateRoutesByBackend };

View File

@@ -2,7 +2,11 @@ import type { RouteRecordRaw } from 'vue-router';
import { describe, expect, it } from 'vitest';
import { generatorRoutes, hasAuthority, hasVisible } from './generator-routes';
import {
generateRoutesByFrontend,
hasAuthority,
hasVisible,
} from './generate-routes-frontend';
// Mock 路由数据
const mockRoutes = [
@@ -58,9 +62,11 @@ describe('hasVisible', () => {
});
});
describe('generatorRoutes', () => {
describe('generateRoutesByFrontend', () => {
it('should filter routes based on authority and visibility', async () => {
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
'user',
]);
// The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
expect(generatedRoutes).toEqual([
{
@@ -77,7 +83,9 @@ describe('generatorRoutes', () => {
});
it('should handle routes without children', async () => {
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
'user',
]);
expect(generatedRoutes).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -88,7 +96,7 @@ describe('generatorRoutes', () => {
});
it('should handle empty roles array', async () => {
const generatedRoutes = await generatorRoutes(mockRoutes, []);
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []);
expect(generatedRoutes).toEqual(
expect.arrayContaining([
// Only routes without authority should be included
@@ -115,7 +123,7 @@ describe('generatorRoutes', () => {
{ meta: {}, path: '/path2' }, // Empty meta
{ meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
];
const generatedRoutes = await generatorRoutes(
const generatedRoutes = await generateRoutesByFrontend(
routesWithMissingMeta as RouteRecordRaw[],
['admin'],
);

View File

@@ -2,26 +2,26 @@ import type { RouteRecordRaw } from 'vue-router';
import { filterTree, mapTree } from '@vben-core/toolkit';
/**
*
* -
*/
async function generatorRoutes(
async function generateRoutesByFrontend(
routes: RouteRecordRaw[],
roles: string[],
forbiddenPage?: RouteRecordRaw['component'],
forbiddenComponent?: RouteRecordRaw['component'],
): Promise<RouteRecordRaw[]> {
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
const finalRoutes = filterTree(routes, (route) => {
return hasVisible(route) && hasAuthority(route, roles);
});
if (!forbiddenPage) {
if (!forbiddenComponent) {
return finalRoutes;
}
// 如果有禁止访问的页面将禁止访问的页面替换为403页面
return mapTree(finalRoutes, (route) => {
if (menuHasVisibleWithForbidden(route)) {
route.component = forbiddenPage;
route.component = forbiddenComponent;
}
return route;
});
@@ -60,4 +60,4 @@ function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
return !!route.meta?.menuVisibleWithForbidden;
}
export { generatorRoutes, hasAuthority, hasVisible };
export { generateRoutesByFrontend, hasAuthority, hasVisible };

View File

@@ -0,0 +1,76 @@
import type { accessModeType } from '@vben-core/preferences';
import type { RouteRecordRaw } from 'vue-router';
import type { GeneratorMenuAndRoutesOptions } from '../types';
import { generateMenus } from './generate-menus';
import { generateRoutesByBackend } from './generate-routes-backend';
import { generateRoutesByFrontend } from './generate-routes-frontend';
async function generateMenusAndRoutes(
mode: accessModeType,
options: GeneratorMenuAndRoutesOptions,
) {
const { router } = options;
// 生成路由
const accessibleRoutes = await generateRoutes(mode, options);
// 动态添加到router实例内
accessibleRoutes.forEach((route) => router.addRoute(route));
// 生成菜单
const accessibleMenus = await generateMenus1(mode, accessibleRoutes, options);
return { accessibleMenus, accessibleRoutes };
}
/**
* Generate routes
* @param mode
*/
async function generateRoutes(
mode: accessModeType,
options: GeneratorMenuAndRoutesOptions,
) {
const { forbiddenComponent, roles, routes } = options;
switch (mode) {
// 允许所有路由访问,不做任何过滤处理
case 'allow-all': {
return routes;
}
case 'frontend': {
return await generateRoutesByFrontend(
routes,
roles || [],
forbiddenComponent,
);
}
case 'backend': {
return await generateRoutesByBackend(options);
}
default: {
return routes;
}
}
}
async function generateMenus1(
mode: accessModeType,
routes: RouteRecordRaw[],
options: GeneratorMenuAndRoutesOptions,
) {
const { router } = options;
switch (mode) {
case 'allow-all':
case 'frontend':
case 'backend': {
return await generateMenus(routes, router);
}
default: {
return [];
}
}
}
export { generateMenusAndRoutes };

View File

@@ -0,0 +1,4 @@
export { default as Authority } from './authority.vue';
export * from './generate-menu-and-routes';
export type * from './types';
export * from './use-access';

View File

@@ -0,0 +1,17 @@
import type {
ComponentRecordType,
RouteRecordStringComponent,
} from '@vben/types';
import type { RouteRecordRaw, Router } from 'vue-router';
interface GeneratorMenuAndRoutesOptions {
fetchMenuListAsync?: () => Promise<RouteRecordStringComponent[]>;
forbiddenComponent?: RouteRecordRaw['component'];
layoutMap?: ComponentRecordType;
pageMap?: ComponentRecordType;
roles?: string[];
router: Router;
routes: RouteRecordRaw[];
}
export type { GeneratorMenuAndRoutesOptions };

View File

@@ -0,0 +1,28 @@
import { computed } from 'vue';
import { preferences } from '@vben-core/preferences';
import { useAccessStore } from '@vben-core/stores';
function useAccess() {
const accessStore = useAccessStore();
const currentAccessMode = computed(() => {
return preferences.app.accessMode;
});
/**
* 更改账号角色
* @param roles
*/
async function changeRoles(roles: string[]): Promise<void> {
if (preferences.app.accessMode !== 'frontend') {
throw new Error(
'The current access mode is not frontend, so the role cannot be changed',
);
}
accessStore.setUserRoles(roles);
}
return { changeRoles, currentAccessMode };
}
export { useAccess };

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config';

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["@vben/types/global"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,3 @@
import { defineConfig } from '@vben/vite-config';
export default defineConfig();

View File

@@ -5,7 +5,7 @@ import type EchartsUI from './echarts-ui.vue';
import type { Ref } from 'vue';
import { computed, nextTick, watch } from 'vue';
import { usePreferences } from '@vben-core/preferences';
import { preferences, usePreferences } from '@vben-core/preferences';
import {
tryOnUnmounted,
@@ -91,9 +91,24 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
chartInstance.dispose();
initCharts();
renderEcharts(cacheOptions);
resize();
}
});
watch(
[
() => preferences.sidebar.collapsed,
() => preferences.sidebar.extraCollapse,
() => preferences.sidebar.hidden,
],
() => {
// 折叠动画200ms
setTimeout(() => {
resize();
}, 200);
},
);
tryOnUnmounted(() => {
// 销毁实例,释放资源
chartInstance?.dispose();

View File

@@ -3,14 +3,14 @@ import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { preferences, usePreferences } from '@vben-core/preferences';
import { Spinner } from '@vben-core/shadcn-ui';
import { storeToRefs, useTabsStore } from '@vben-core/stores';
import { storeToRefs, useTabbarStore } from '@vben-core/stores';
import { IFrameRouterView } from '../../iframe';
import { useContentSpinner } from './use-content-spinner';
defineOptions({ name: 'LayoutContent' });
const tabsStore = useTabsStore();
const tabsStore = useTabbarStore();
const { keepAlive } = usePreferences();
const { spinning } = useContentSpinner();

View File

@@ -19,14 +19,14 @@ import {
MdiPin,
MdiPinOff,
} from '@vben-core/iconify';
import { storeToRefs, useAccessStore, useTabsStore } from '@vben-core/stores';
import { storeToRefs, useAccessStore, useTabbarStore } from '@vben-core/stores';
import { filterTree } from '@vben-core/toolkit';
function useTabs() {
const router = useRouter();
const route = useRoute();
const accessStore = useAccessStore();
const tabsStore = useTabsStore();
const tabsStore = useTabbarStore();
const { accessMenus } = storeToRefs(accessStore);
const currentActive = computed(() => {

View File

@@ -6,12 +6,12 @@ import { useRoute } from 'vue-router';
import { preferences } from '@vben-core/preferences';
import { Spinner } from '@vben-core/shadcn-ui';
import { useTabsStore } from '@vben-core/stores';
import { useTabbarStore } from '@vben-core/stores';
defineOptions({ name: 'IFrameRouterView' });
const spinningList = ref<boolean[]>([]);
const tabsStore = useTabsStore();
const tabsStore = useTabbarStore();
const route = useRoute();
const enableTabbar = computed(() => preferences.tabbar.enable);

View File

@@ -107,7 +107,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
<template>
<div class="m-5">
<div class="bg-card border-border rounded-md border p-5 shadow">
<div class="card-box p-5">
<div>
<h3 class="text-foreground text-2xl font-semibold leading-7">
{{ title }}
@@ -135,7 +135,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
</div>
</div>
<div class="bg-card border-border mt-6 rounded-md border p-5">
<div class="card-box mt-6 p-5">
<div>
<h5 class="text-foreground text-lg">生产环境依赖</h5>
</div>
@@ -154,7 +154,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
</dl>
</div>
</div>
<div class="bg-card border-border mt-6 rounded-md border p-5">
<div class="card-box mt-6 p-5">
<div>
<h5 class="text-foreground text-lg">开发环境依赖</h5>
</div>

View File

@@ -23,9 +23,7 @@ const defaultValue = computed(() => {
</script>
<template>
<div
class="bg-card border-border w-full rounded-xl border px-4 pb-5 pt-3 shadow"
>
<div class="card-box w-full px-4 pb-5 pt-3 shadow">
<Tabs :default-value="defaultValue">
<TabsList>
<template v-for="tab in tabs" :key="tab.label">

View File

@@ -14,7 +14,7 @@ withDefaults(defineProps<Props>(), {
});
</script>
<template>
<div class="bg-card border-border rounded-xl p-4 py-6 shadow lg:flex">
<div class="card-box p-4 py-6 lg:flex">
<VbenAvatar :src="avatar" class="size-20" />
<div
v-if="$slots.title || $slots.description"

View File

@@ -1,7 +0,0 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,6 +1,16 @@
page:
demos:
title: Demos
access:
title: Access Control
frontend-control: Front-end Control
# menuVisibleWithForbidden: Menu is visible
backend-control: Backend Control
page: Page visit
button: Button control
loading-menu: In the loading menu
access-test-1: Access test page 1
access-test-2: Access test page 2
nested:
title: Nested Menu
menu1: Menu 1

View File

@@ -1,6 +1,16 @@
page:
demos:
title: 演示
access:
title: 访问控制
frontend-control: 前端控制
# menuVisibleWithForbidden: 菜单可见
backend-control: 后端控制
page: 页面访问
button: 按钮控制
access-test-1: 权限测试页1
access-test-2: 权限测试页2
nested:
title: 嵌套菜单
menu1: 菜单 1
@@ -40,6 +50,7 @@ common:
confirm: 确认
not-data: 暂无数据
refresh: 刷新
loading-menu: 加载菜单中
fallback:
page-not-found: 哎呀!未找到页面

View File

@@ -1,3 +1,4 @@
export type * from './router';
export type * from './ui';
export type * from './user';
export type * from '@vben-core/typings';

View File

@@ -0,0 +1,13 @@
import type { RouteRecordRaw } from 'vue-router';
import type { Component } from 'vue';
// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string
type RouteRecordStringComponent<T = string> = {
children?: RouteRecordStringComponent<T>[];
component: T;
} & Omit<RouteRecordRaw, 'children' | 'component'>;
type ComponentRecordType = Record<string, () => Promise<Component>>;
export type { ComponentRecordType, RouteRecordStringComponent };