feat: new interface pendant can be configured to display hidden

pull/48/MERGE
vince 2024-07-10 21:20:11 +08:00
parent db76325d68
commit a765d3bbc0
36 changed files with 256 additions and 612 deletions

View File

@ -2,7 +2,7 @@
## Description
Vben Admin Pro 数据mock服务
Vben Admin Pro 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。由于 sqlite 安装需要在本地进行编译所以这里接口是直接返回的。线上环境不再提供mock集成可自行部署服务或者对接真实数据同步 mock.js等工具有一些限制比如上传文件不行、无法模拟复杂的逻辑等所以这里使用了 真是的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
## Running the app

View File

@ -11,7 +11,7 @@
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_GLOB_APP_TITLE 变量,在 . env 内配置 -->
<!-- 由 vite 注入 VITE_GLOB_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_GLOB_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
</head>

View File

@ -10,7 +10,8 @@ import { getAllMenus } from '#/apis';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue');
const forbiddenComponent = () =>
import('#/views/_essential/fallback/forbidden.vue');
async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
@ -30,7 +31,7 @@ async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
return await getAllMenus();
},
// 可以指定没有权限跳转403页面
forbiddenComponent: forbiddenPage,
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,

View File

@ -10,7 +10,7 @@ import dayjs from 'dayjs';
const antdLocale = ref<Locale>(defaultLocale);
const modules = import.meta.glob('./langs/*.y(a)?ml');
const modules = import.meta.glob('./langs/*.json');
const localesMap = loadLocalesMap(modules);

View File

@ -0,0 +1,34 @@
{
"page": {
"demos": {
"title": "Demos",
"access": {
"title": "Access Control",
"frontend-control": "Front-end Control",
"backend-control": "Backend Control",
"page": "Page visit",
"button": "Button control",
"loading-menu": "In the loading menu",
"access-test-1": "Super visit",
"access-test-2": "Admin visit",
"access-test-3": "User visit"
},
"nested": {
"title": "Nested Menu",
"menu1": "Menu 1",
"menu2": "Menu 2",
"menu21": "Menu 2-1",
"menu3": "Menu 3",
"menu31": "Menu 3-1",
"menu32": "Menu 3-2",
"menu321": "Menu 3-2-1"
},
"outside": {
"title": "External Page",
"embedded": "embedded Page",
"external-link": "External Link"
},
"fallback": { "title": "Fallback Page" }
}
}
}

View File

@ -1,28 +0,0 @@
page:
demos:
title: Demos
access:
title: Access Control
frontend-control: Front-end Control
backend-control: Backend Control
page: Page visit
button: Button control
loading-menu: In the loading menu
access-test-1: Super visit
access-test-2: Admin visit
access-test-3: User visit
nested:
title: Nested Menu
menu1: Menu 1
menu2: Menu 2
menu21: Menu 2-1
menu3: Menu 3
menu31: Menu 3-1
menu32: Menu 3-2
menu321: Menu 3-2-1
outside:
title: External Page
embedded: embedded Page
external-link: External Link
fallback:
title: Fallback Page

View File

@ -0,0 +1,35 @@
{
"page": {
"demos": {
"title": "演示",
"access": {
"title": "访问控制",
"frontend-control": "前端控制",
"backend-control": "后端控制",
"page": "页面访问",
"button": "按钮控制",
"access-test-1": "Super 可见",
"access-test-2": "Admin 可见",
"access-test-3": "User 可见"
},
"nested": {
"title": "嵌套菜单",
"menu1": "菜单 1",
"menu2": "菜单 2",
"menu21": "菜单 2-1",
"menu3": "菜单 3",
"menu31": "菜单 3-1",
"menu32": "菜单 3-2",
"menu321": "菜单 3-2-1"
},
"outside": {
"title": "外部页面",
"embedded": "内嵌",
"external-link": "外链"
},
"fallback": {
"title": "缺省页"
}
}
}
}

View File

@ -1,27 +0,0 @@
page:
demos:
title: 演示
access:
title: 访问控制
frontend-control: 前端控制
backend-control: 后端控制
page: 页面访问
button: 按钮控制
access-test-1: Super 可见
access-test-2: Admin 可见
access-test-3: User 可见
nested:
title: 嵌套菜单
menu1: 菜单 1
menu2: 菜单 2
menu21: 菜单 2-1
menu3: 菜单 3
menu31: 菜单 3-1
menu32: 菜单 3-2
menu321: 菜单 3-2-1
outside:
title: 外部页面
embedded: 内嵌
external-link: 外链
fallback:
title: 缺省页

View File

@ -1,8 +1,9 @@
import type { DeepPartial } from '@vben/types';
import type { Preferences } from '@vben-core/preferences';
import { defineOverridesPreferences } from '@vben-core/preferences';
/**
* @description
* 使
*/
export const overridesPreferences: DeepPartial<Preferences> = {};
export const overridesPreferences = defineOverridesPreferences({
// overrides
});

View File

@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
import { useRouter } from 'vue-router';
import { CodeAuthority, useAccess } from '@vben/access';
import { CodeAccess, useAccess } from '@vben/access';
import { Button } from 'ant-design-vue';
@ -82,20 +82,20 @@ async function changeAccount(role: string) {
<div class="card-box mt-5 p-5 font-semibold">
<div class="mb-3 text-lg">组件形式控制</div>
<CodeAuthority :value="['AC_100100']">
<CodeAccess :value="['AC_100100']">
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_100030']">
</CodeAccess>
<CodeAccess :value="['AC_100030']">
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_1000001']">
</CodeAccess>
<CodeAccess :value="['AC_1000001']">
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_100100', 'AC_100010']">
</CodeAccess>
<CodeAccess :value="['AC_100100', 'AC_100010']">
<Button class="mr-4">
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button>
</CodeAuthority>
</CodeAccess>
</div>
<div class="card-box mt-5 p-5 font-semibold">

View File

@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
import { useRouter } from 'vue-router';
import { CodeAuthority, RoleAuthority, useAccess } from '@vben/access';
import { CodeAccess, RoleAccess, useAccess } from '@vben/access';
import { Button } from 'ant-design-vue';
@ -81,18 +81,18 @@ async function changeAccount(role: string) {
</div>
<div class="card-box mt-5 p-5 font-semibold">
<div class="mb-3 text-lg">角色 - 组件形式控制</div>
<RoleAuthority :value="['super']">
<RoleAccess :value="['super']">
<Button class="mr-4"> Super 角色可见 </Button>
</RoleAuthority>
<RoleAuthority :value="['admin']">
</RoleAccess>
<RoleAccess :value="['admin']">
<Button class="mr-4"> Admin 角色可见 </Button>
</RoleAuthority>
<RoleAuthority :value="['user']">
</RoleAccess>
<RoleAccess :value="['user']">
<Button class="mr-4"> User 角色可见 </Button>
</RoleAuthority>
<RoleAuthority :value="['super', 'admin']">
</RoleAccess>
<RoleAccess :value="['super', 'admin']">
<Button class="mr-4"> Super & Admin 角色都可见 </Button>
</RoleAuthority>
</RoleAccess>
</div>
<div class="card-box mt-5 p-5 font-semibold">
@ -113,20 +113,20 @@ async function changeAccount(role: string) {
<div class="card-box mt-5 p-5 font-semibold">
<div class="mb-3 text-lg">权限码 - 组件形式控制</div>
<CodeAuthority :value="['AC_100100']">
<CodeAccess :value="['AC_100100']">
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_100030']">
</CodeAccess>
<CodeAccess :value="['AC_100030']">
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_1000001']">
</CodeAccess>
<CodeAccess :value="['AC_1000001']">
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
</CodeAuthority>
<CodeAuthority :value="['AC_100100', 'AC_100010']">
</CodeAccess>
<CodeAccess :value="['AC_100100', 'AC_100010']">
<Button class="mr-4">
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button>
</CodeAuthority>
</CodeAccess>
</div>
<div class="card-box mt-5 p-5 font-semibold">

View File

@ -7,10 +7,6 @@ import type {
LibraryPluginOptions,
} from '../typing';
import { join } from 'node:path';
import { getPackages } from '@vben/node-utils';
import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import viteVue from '@vitejs/plugin-vue';
import viteVueJsx from '@vitejs/plugin-vue-jsx';
@ -117,28 +113,10 @@ async function loadApplicationPlugins(
{
condition: i18n,
plugins: async () => {
const { packages } = await getPackages();
const include: string[] = [];
// 加载所有应用的国际化文件
for (const { dir, relativeDir } of packages) {
if (
// 排除非应用目录
!relativeDir.startsWith('apps') ||
// 排除mock目录
relativeDir.includes('backend-mock')
) {
continue;
}
include.push(`${join(dir, 'src', 'locales', 'langs')}/*.yaml`);
}
return [
viteVueI18nPlugin({
compositionOnly: true,
fullInstall: true,
include,
runtimeOnly: true,
}),
];

View File

@ -1,132 +0,0 @@
import { describe, expect, it } from 'vitest';
import { flattenObject } from './flatten-object';
describe('flattenObject', () => {
it('should flatten a nested object correctly', () => {
const nestedObject = {
language: 'en',
notifications: {
email: true,
push: {
sound: true,
vibration: false,
},
},
theme: 'light',
};
const expected = {
language: 'en',
notificationsEmail: true,
notificationsPushSound: true,
notificationsPushVibration: false,
theme: 'light',
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle empty objects', () => {
const nestedObject = {};
const expected = {};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle objects with primitive values', () => {
const nestedObject = {
active: true,
age: 30,
name: 'Alice',
};
const expected = {
active: true,
age: 30,
name: 'Alice',
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle objects with null values', () => {
const nestedObject = {
user: {
age: null,
name: null,
},
};
const expected = {
userAge: null,
userName: null,
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle nested empty objects', () => {
const nestedObject = {
a: {},
b: { c: {} },
};
const expected = {};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle arrays within objects', () => {
const nestedObject = {
hobbies: ['reading', 'gaming'],
name: 'Alice',
};
const expected = {
hobbies: ['reading', 'gaming'],
name: 'Alice',
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should flatten objects with nested arrays correctly', () => {
const nestedObject = {
person: {
hobbies: ['reading', 'gaming'],
name: 'Alice',
},
};
const expected = {
personHobbies: ['reading', 'gaming'],
personName: 'Alice',
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
it('should handle objects with undefined values', () => {
const nestedObject = {
user: {
age: undefined,
name: 'Alice',
},
};
const expected = {
userAge: undefined,
userName: 'Alice',
};
const result = flattenObject(nestedObject);
expect(result).toEqual(expected);
});
});

View File

@ -1,82 +0,0 @@
import type { Flatten } from '@vben-core/typings';
import { capitalizeFirstLetter } from '@vben-core/toolkit';
/**
*
* @param obj -
* @param parentKey -
* @param result -
* @returns
*
*
* const nestedObj = {
* user: {
* name: 'Alice',
* address: {
* city: 'Wonderland',
* zip: '12345'
* }
* },
* items: [
* { id: 1, name: 'Item 1' },
* { id: 2, name: 'Item 2' }
* ],
* active: true
* };
* const flatObj = flattenObject(nestedObj);
* console.log(flatObj);
* :
* {
* userName: 'Alice',
* userAddressCity: 'Wonderland',
* userAddressZip: '12345',
* items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ],
* active: true
* }
*/
function flattenObject<T extends Record<string, any>>(
obj: T,
parentKey: string = '',
result: Record<string, any> = {},
): Flatten<T> {
Object.keys(obj).forEach((key) => {
const newKey = parentKey
? `${parentKey}${capitalizeFirstLetter(key)}`
: key;
const value = obj[key];
if (value && typeof value === 'object' && !Array.isArray(value)) {
flattenObject(value, newKey, result);
} else {
result[newKey] = value;
}
});
return result as Flatten<T>;
}
export { flattenObject };
// 定义递归类型,用于推断扁平化后的对象类型
// 限制递归深度的辅助类型
// type FlattenDepth<
// T,
// Depth extends number,
// CurrentDepth extends number[] = [],
// > = {
// [K in keyof T as CurrentDepth['length'] extends Depth
// ? K
// : T[K] extends object
// ? `${CurrentDepth['length'] extends 0 ? UnCapitalize<K & string> : Capitalize<K & string>}${keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]> extends string ? Capitalize<keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>> : ''}`
// : `${CurrentDepth['length'] extends 0 ? UnCapitalize<K & string> : Capitalize<K & string>}`]: CurrentDepth['length'] extends Depth
// ? T[K]
// : T[K] extends object
// ? FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>[keyof FlattenDepth<
// T[K],
// Depth,
// [...CurrentDepth, 1]
// >]
// : T[K];
// };
// type Flatten<T, Depth extends number = 4> = FlattenDepth<T, Depth>;

View File

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

View File

@ -1,115 +0,0 @@
import { describe, expect, it } from 'vitest';
import { nestedObject } from './nested-object';
describe('nestedObject', () => {
it('should convert flat object to nested object with level 1', () => {
const flatObject = {
anotherKeyExample: 2,
commonAppName: 1,
someOtherKey: 3,
};
const expectedNestedObject = {
anotherKeyExample: 2,
commonAppName: 1,
someOtherKey: 3,
};
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
});
it('should convert flat object to nested object with level 2', () => {
const flatObject = {
appAnotherKeyExample: 2,
appCommonName: 1,
appSomeOtherKey: 3,
};
const expectedNestedObject = {
app: {
anotherKeyExample: 2,
commonName: 1,
someOtherKey: 3,
},
};
expect(nestedObject(flatObject, 2)).toEqual(expectedNestedObject);
});
it('should convert flat object to nested object with level 3', () => {
const flatObject = {
appAnotherKeyExampleValue: 2,
appCommonNameKey: 1,
appSomeOtherKeyItem: 3,
};
const expectedNestedObject = {
app: {
another: {
keyExampleValue: 2,
},
common: {
nameKey: 1,
},
some: {
otherKeyItem: 3,
},
},
};
expect(nestedObject(flatObject, 3)).toEqual(expectedNestedObject);
});
it('should handle empty object', () => {
const flatObject = {};
const expectedNestedObject = {};
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
});
it('should handle single key object', () => {
const flatObject = {
singleKey: 1,
};
const expectedNestedObject = {
singleKey: 1,
};
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
});
it('should handle complex keys', () => {
const flatObject = {
anotherComplexKeyWithParts: 2,
complexKeyWithMultipleParts: 1,
};
const expectedNestedObject = {
anotherComplexKeyWithParts: 2,
complexKeyWithMultipleParts: 1,
};
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
});
it('should correctly nest an object based on the specified level', () => {
const obj = {
oneFiveSix: 'Value156',
oneTwoFour: 'Value124',
oneTwoThree: 'Value123',
};
const nested = nestedObject(obj, 2);
expect(nested).toEqual({
one: {
fiveSix: 'Value156',
twoFour: 'Value124',
twoThree: 'Value123',
},
});
});
});

View File

@ -1,70 +0,0 @@
import { toLowerCaseFirstLetter } from '@vben-core/toolkit';
/**
*
*
* @template T -
* @param {Record<string, T>} obj -
* @param {number} level -
* @returns {T}
*
* @example
* 1
* const flatObject = {
* 'commonAppName': 1,
* 'anotherKeyExample': 2,
* 'someOtherKey': 3
* };
* const nestedObject = nestedObject(flatObject, 1);
* console.log(nestedObject);
* :
* {
* commonAppName: 1,
* anotherKeyExample: 2,
* someOtherKey: 3
* }
*
* @example
* 2
* const flatObject = {
* 'appCommonName': 1,
* 'appAnotherKeyExample': 2,
* 'appSomeOtherKey': 3
* };
* const nestedObject = nestedObject(flatObject, 2);
* console.log(nestedObject);
* :
* {
* app: {
* commonName: 1,
* anotherKeyExample: 2,
* someOtherKey: 3
* }
* }
*/
function nestedObject<T>(obj: Record<string, T>, level: number): T {
const result: any = {};
for (const key in obj) {
const keys = key.split(/(?=[A-Z])/);
// 将驼峰式分割为数组;
let current = result;
for (let i = 0; i < keys.length; i++) {
const lowerKey = keys[i].toLowerCase();
if (i === level - 1) {
const remainingKeys = keys.slice(i).join(''); // 保留后续部分作为键的一部分
current[toLowerCaseFirstLetter(remainingKeys)] = obj[key];
break;
} else {
current[lowerKey] = current[lowerKey] || {};
current = current[lowerKey];
}
}
}
return result as T;
}
export { nestedObject };

View File

@ -17,7 +17,6 @@ const defaultPreferences: Preferences = {
layout: 'sidebar-nav',
locale: 'zh-CN',
name: 'Vben Admin Pro',
semiDarkMenu: true,
},
breadcrumb: {
enable: true,
@ -82,6 +81,7 @@ const defaultPreferences: Preferences = {
colorWarning: 'hsl(42 84% 61%)',
mode: 'dark',
radius: '0.5',
semiDarkMenu: true,
},
transition: {
enable: true,
@ -89,6 +89,15 @@ const defaultPreferences: Preferences = {
name: 'fade-slide',
progress: true,
},
widget: {
aiAssistant: true,
fullscreen: true,
globalSearch: true,
languageToggle: true,
notification: true,
sidebarToggle: true,
themeToggle: true,
},
};
export { defaultPreferences };

View File

@ -1,3 +1,5 @@
import type { DeepPartial } from '@vben-core/typings';
import type { Preferences } from './types';
import { preferencesManager } from './preferences';
@ -5,10 +7,6 @@ import { preferencesManager } from './preferences';
// 偏好设置(带有层级关系)
const preferences: Preferences = preferencesManager.getPreferences();
// 扁平化后的偏好设置
// const flatPreferences: Flatten<Preferences> =
// preferencesManager.getFlatPreferences();
// 更新偏好设置
const updatePreferences =
preferencesManager.updatePreferences.bind(preferencesManager);
@ -20,9 +18,13 @@ const resetPreferences =
const clearPreferencesCache =
preferencesManager.clearCache.bind(preferencesManager);
function defineOverridesPreferences(preferences: DeepPartial<Preferences>) {
return preferences;
}
export {
clearPreferencesCache,
// flatPreferences,
defineOverridesPreferences,
preferences,
preferencesManager,
resetPreferences,

View File

@ -46,8 +46,6 @@ interface AppPreferences {
locale: SupportedLanguagesType;
/** 应用名 */
name: string;
/** 是否开启半深色菜单只在theme='light'时生效) */
semiDarkMenu: boolean;
}
interface BreadcrumbPreferences {
@ -164,6 +162,8 @@ interface ThemePreferences {
mode: ThemeModeType;
/** 圆角 */
radius: string;
/** 是否开启半深色菜单只在theme='light'时生效) */
semiDarkMenu: boolean;
}
interface TransitionPreferences {
@ -177,6 +177,23 @@ interface TransitionPreferences {
progress: boolean;
}
interface WidgetPreferences {
/** 是否开启vben助手部件 */
aiAssistant: boolean;
/** 是否启用全屏部件 */
fullscreen: boolean;
/** 是否启用全局搜索部件 */
globalSearch: boolean;
/** 是否启用语言切换部件 */
languageToggle: boolean;
/** 是否显示通知部件 */
notification: boolean;
/** 是否显示侧边栏显示/隐藏部件 */
sidebarToggle: boolean;
/** 是否显示主题切换部件 */
themeToggle: boolean;
}
interface Preferences {
/** 全局配置 */
app: AppPreferences;
@ -202,6 +219,8 @@ interface Preferences {
theme: ThemePreferences;
/** 动画配置 */
transition: TransitionPreferences;
/** 功能配置 */
widget: WidgetPreferences;
}
type PreferencesKeys = keyof Preferences;
@ -230,4 +249,5 @@ export type {
ThemeModeType,
ThemePreferences,
TransitionPreferences,
WidgetPreferences,
};

View File

@ -147,7 +147,6 @@
"general": "General",
"language": "Language",
"dynamic-title": "Dynamic Title",
"ai-assistant": "Ai Assistant",
"sidebar": {
"title": "Sidebar",
"width": "Width",
@ -248,6 +247,16 @@
"search": "Global Search",
"logout": "Logout",
"preferences": "Preferences"
},
"widget": {
"title": "Widget",
"global-search": "Enable Global Search",
"fullscreen": "Enable Fullscreen",
"theme-toggle": "Enable Theme Toggle",
"language-toggle": "Enable Language Toggle",
"notification": "Enable Notification",
"sidebar-toggle": "Enable Sidebar Toggle",
"ai-assistant": "Enable AI Assistant"
}
}
}

View File

@ -146,7 +146,6 @@
"general": "通用",
"language": "语言",
"dynamic-title": "动态标题",
"ai-assistant": "Ai 助手",
"sidebar": {
"title": "侧边栏",
"width": "宽度",
@ -247,6 +246,16 @@
"search": "全局搜索",
"logout": "退出登录",
"preferences": "偏好设置"
},
"widget": {
"title": "小部件",
"global-search": "启用全局搜索",
"fullscreen": "启用全屏",
"theme-toggle": "启用主题切换",
"language-toggle": "启用语言切换",
"notification": "启用通知",
"sidebar-toggle": "启用侧边栏切换",
"ai-assistant": "启用 AI 助手"
}
}
}

View File

@ -1,40 +0,0 @@
// `Prev` 类型用于表示递归深度的递减。它是一个元组,其索引代表了递归的层数,通过索引访问可以得到减少后的层数。
// 例如Prev[3] 等于 2表示递归深度从 3 减少到 2。
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
// `FlattenDepth` 类型用于将一个嵌套的对象类型“展平”,同时考虑到了递归的深度。
// 它接受三个泛型参数T要处理的类型Prefix属性名前缀默认为空字符串Depth递归深度默认为3
// 如果当前深度Depth为 0则停止递归并返回 `never`。否则,如果属性值是对象类型,则递归调用 `FlattenDepth` 并递减深度。
// 对于非对象类型的属性,将其直接映射到结果类型中,并根据前缀构造属性名。
type FlattenDepth<T, Prefix extends string = '', Depth extends number = 4> = {
[K in keyof T]: T[K] extends object
? Depth extends 0
? never
: FlattenDepth<
T[K],
`${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`,
Prev[Depth]
>
: {
[P in `${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`]: T[K];
};
}[keyof T] extends infer O
? { [P in keyof O]: O[P] }
: never;
// `UnionToIntersection` 类型用于将一个联合类型转换为交叉类型。
// 这个类型通过条件类型和类型推断的方式来实现。它先尝试将输入类型U映射为一个函数类型
// 然后通过推断这个函数类型的返回类型infer I最终得到一个交叉类型。
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
type Flatten<T> = UnionToIntersection<FlattenDepth<T>>;
type FlattenObject<T> = FlattenDepth<T>;
type FlattenObjectKeys<T> = keyof FlattenObject<T>;
export type { Flatten, FlattenObject, FlattenObjectKeys, UnionToIntersection };

View File

@ -1,5 +1,4 @@
export type * from './app';
export type * from './flatten';
export type * from './helper';
export type * from './menu-record';
export type * from './tabs';

View File

@ -87,6 +87,11 @@ interface VbenLayoutProps {
* @default 'fixed'
*/
headerMode?: LayoutHeaderModeType;
/**
* header
* @default
*/
headerToggleSidebarButton?: boolean;
/**
* header
* @default true
@ -152,21 +157,21 @@ interface VbenLayoutProps {
* @default 210
*/
sidebarWidth?: number;
/**
* footer
* @default #fff
*/
tabbarBackgroundColor?: string;
/**
* tab
* @default true
*/
tabbarEnable?: boolean;
/**
* footer
* @default #fff
*/
tabsBackgroundColor?: string;
/**
* tab
* @default 30
*/
tabsHeight?: number;
tabbarHeight?: number;
/**
* zIndex
* @default 100

View File

@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
headerHeight: 50,
headerHeightOffset: 10,
headerHidden: false,
headerMode: 'fixed',
headerToggleSidebarButton: true,
headerVisible: true,
isMobile: false,
layout: 'sidebar-nav',
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
sidebarTheme: 'dark',
sidebarWidth: 180,
tabbarEnable: true,
tabsHeight: 36,
tabbarHeight: 36,
zIndex: 200,
});
@ -122,7 +122,7 @@ const headerWrapperHeight = computed(() => {
height += getHeaderHeight.value;
}
if (props.tabbarEnable) {
height += props.tabsHeight;
height += props.tabbarHeight;
}
return height;
});
@ -364,6 +364,7 @@ const maskStyle = computed((): CSSProperties => {
const showHeaderToggleButton = computed(() => {
return (
props.headerToggleSidebarButton &&
isSideMode.value &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
@ -528,7 +529,7 @@ function handleOpenMenu() {
<LayoutTabbar
v-if="tabbarEnable"
:height="tabsHeight"
:height="tabbarHeight"
:style="tabbarStyle"
>
<slot name="tabbar"></slot>

View File

@ -13,7 +13,7 @@ interface Props {
}
defineOptions({
name: 'CodeAuthority',
name: 'CodeAccess',
});
withDefaults(defineProps<Props>(), {

View File

@ -1,5 +1,5 @@
export { default as CodeAuthority } from './code-authority.vue';
export { default as CodeAccess } from './code-access.vue';
export * from './generate-menu-and-routes';
export { default as RoleAuthority } from './role-authority.vue';
export { default as RoleAccess } from './role-access.vue';
export type * from './types';
export * from './use-access';

View File

@ -13,7 +13,7 @@ interface Props {
}
defineOptions({
name: 'RoleAuthority',
name: 'RoleAccess',
});
withDefaults(defineProps<Props>(), {

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { usePreferences } from '@vben-core/preferences';
import { preferences, usePreferences } from '@vben-core/preferences';
import { VbenFullScreen } from '@vben-core/shadcn-ui';
import { useCoreAccessStore } from '@vben-core/stores';
@ -33,14 +33,15 @@ const { globalSearchShortcutKey } = usePreferences();
</div>
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
<GlobalSearch
v-if="preferences.widget.globalSearch"
:enable-shortcut-key="globalSearchShortcutKey"
:menus="accessStore.accessMenus"
class="mr-4"
/>
<ThemeToggle class="mr-2" />
<LanguageToggle class="mr-2" />
<VbenFullScreen class="mr-2" />
<slot name="notification"></slot>
<ThemeToggle v-if="preferences.widget.themeToggle" class="mr-2" />
<LanguageToggle v-if="preferences.widget.languageToggle" class="mr-2" />
<VbenFullScreen v-if="preferences.widget.fullscreen" class="mr-2" />
<slot v-if="preferences.widget.notification" name="notification"></slot>
<slot name="user-dropdown"></slot>
</div>
</template>

View File

@ -38,7 +38,7 @@ const headerMenuTheme = computed(() => {
});
const theme = computed(() => {
const dark = isDark.value || preferences.app.semiDarkMenu;
const dark = isDark.value || preferences.theme.semiDarkMenu;
return dark ? 'dark' : 'light';
});
@ -122,6 +122,7 @@ function clearPreferencesAndLogout() {
:footer-fixed="preferences.footer.fixed"
:header-hidden="preferences.header.hidden"
:header-mode="preferences.header.mode"
:header-toggle-sidebar-button="preferences.widget.sidebarToggle"
:header-visible="preferences.header.enable"
:is-mobile="preferences.app.isMobile"
:layout="layout"
@ -131,7 +132,7 @@ function clearPreferencesAndLogout() {
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
:sidebar-hidden="preferences.sidebar.hidden"
:sidebar-semi-dark="preferences.app.semiDarkMenu"
:sidebar-semi-dark="preferences.theme.semiDarkMenu"
:sidebar-theme="theme"
:sidebar-width="preferences.sidebar.width"
:tabbar-enable="preferences.tabbar.enable"
@ -158,7 +159,7 @@ function clearPreferencesAndLogout() {
<template #floating-groups>
<CozeAssistant
v-if="preferences.app.aiAssistant"
v-if="preferences.widget.aiAssistant"
:is-mobile="preferences.app.isMobile"
/>
<VbenBackTop />

View File

@ -13,7 +13,6 @@ defineOptions({
const appLocale = defineModel<string>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appAiAssistant = defineModel<boolean>('appAiAssistant');
const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
label: item.text,
@ -28,7 +27,4 @@ const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
<SwitchItem v-model="appDynamicTitle">
{{ $t('preferences.dynamic-title') }}
</SwitchItem>
<SwitchItem v-model="appAiAssistant">
{{ $t('preferences.ai-assistant') }}
</SwitchItem>
</template>

View File

@ -10,6 +10,7 @@ export { default as Layout } from './layout/layout.vue';
export { default as Navigation } from './layout/navigation.vue';
export { default as Sidebar } from './layout/sidebar.vue';
export { default as Tabbar } from './layout/tabbar.vue';
export { default as Widget } from './layout/widget.vue';
export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
export { default as SwitchItem } from './switch-item.vue';
export { default as BuiltinTheme } from './theme/builtin.vue';

View File

@ -1,21 +0,0 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceInterfaceControl',
});
const tabsVisible = defineModel<boolean>('tabsVisible');
const logoVisible = defineModel<boolean>('logoVisible');
</script>
<template>
<SwitchItem v-model="tabsVisible">
{{ $t('preferences.tabbar.enable') }}
</SwitchItem>
<SwitchItem v-model="logoVisible">
{{ $t('preferences.logo-visible') }}
</SwitchItem>
</template>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceInterfaceControl',
});
const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
const widgetNotification = defineModel<boolean>('widgetNotification');
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
</script>
<template>
<SwitchItem v-model="widgetGlobalSearch">
{{ $t('preferences.widget.global-search') }}
</SwitchItem>
<SwitchItem v-model="widgetThemeToggle">
{{ $t('preferences.widget.theme-toggle') }}
</SwitchItem>
<SwitchItem v-model="widgetLanguageToggle">
{{ $t('preferences.widget.language-toggle') }}
</SwitchItem>
<SwitchItem v-model="widgetFullscreen">
{{ $t('preferences.widget.fullscreen') }}
</SwitchItem>
<SwitchItem v-model="widgetNotification">
{{ $t('preferences.widget.notification') }}
</SwitchItem>
<SwitchItem v-model="widgetAiAssistant">
{{ $t('preferences.widget.ai-assistant') }}
</SwitchItem>
<SwitchItem v-model="widgetSidebarToggle">
{{ $t('preferences.widget.sidebar-toggle') }}
</SwitchItem>
</template>

View File

@ -51,6 +51,7 @@ import {
Sidebar,
Tabbar,
Theme,
Widget,
} from './blocks';
import IconSetting from './icons/setting.vue';
import { useOpenPreferences } from './use-open-preferences';
@ -59,7 +60,6 @@ const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const { toast } = useToast();
const appLocale = defineModel<SupportedLanguagesType>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appAiAssistant = defineModel<boolean>('appAiAssistant');
const appLayout = defineModel<LayoutType>('appLayout');
const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
@ -129,6 +129,14 @@ const shortcutKeysGlobalPreferences = defineModel<boolean>(
'shortcutKeysGlobalPreferences',
);
const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
const widgetNotification = defineModel<boolean>('widgetNotification');
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
const {
diffPreference,
isDark,
@ -245,7 +253,6 @@ async function handleReset() {
<template #general>
<Block :title="$t('preferences.general')">
<General
v-model:app-ai-assistant="appAiAssistant"
v-model:app-dynamic-title="appDynamicTitle"
v-model:app-locale="appLocale"
/>
@ -346,6 +353,17 @@ async function handleReset() {
v-model:tabbar-show-icon="tabbarShowIcon"
/>
</Block>
<Block :title="$t('preferences.widget.title')">
<Widget
v-model:widget-ai-assistant="widgetAiAssistant"
v-model:widget-fullscreen="widgetFullscreen"
v-model:widget-global-search="widgetGlobalSearch"
v-model:widget-language-toggle="widgetLanguageToggle"
v-model:widget-notification="widgetNotification"
v-model:widget-sidebar-toggle="widgetSidebarToggle"
v-model:widget-theme-toggle="widgetThemeToggle"
/>
</Block>
<Block :title="$t('preferences.footer.title')">
<Footer
v-model:footer-enable="footerEnable"