feat: new interface pendant can be configured to display hidden
parent
db76325d68
commit
a765d3bbc0
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Vben Admin Pro 数据mock服务
|
Vben Admin Pro 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。由于 sqlite 安装需要在本地进行编译,所以这里接口是直接返回的。线上环境不再提供mock集成,可自行部署服务或者对接真实数据,同步 mock.js等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了 真是的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
|
||||||
|
|
||||||
## Running the app
|
## Running the app
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
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>
|
<title><%= VITE_GLOB_APP_TITLE %></title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -10,7 +10,8 @@ import { getAllMenus } from '#/apis';
|
||||||
import { BasicLayout, IFrameView } from '#/layouts';
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
import { $t } from '#/locales';
|
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) {
|
async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
@ -30,7 +31,7 @@ async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
|
||||||
return await getAllMenus();
|
return await getAllMenus();
|
||||||
},
|
},
|
||||||
// 可以指定没有权限跳转403页面
|
// 可以指定没有权限跳转403页面
|
||||||
forbiddenComponent: forbiddenPage,
|
forbiddenComponent,
|
||||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||||
layoutMap,
|
layoutMap,
|
||||||
pageMap,
|
pageMap,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import dayjs from 'dayjs';
|
||||||
|
|
||||||
const antdLocale = ref<Locale>(defaultLocale);
|
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);
|
const localesMap = loadLocalesMap(modules);
|
||||||
|
|
||||||
|
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
|
@ -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": "缺省页"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: 缺省页
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { DeepPartial } from '@vben/types';
|
import { defineOverridesPreferences } from '@vben-core/preferences';
|
||||||
import type { Preferences } from '@vben-core/preferences';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 项目配置文件
|
* @description 项目配置文件
|
||||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||||
*/
|
*/
|
||||||
export const overridesPreferences: DeepPartial<Preferences> = {};
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
|
||||||
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { CodeAuthority, useAccess } from '@vben/access';
|
import { CodeAccess, useAccess } from '@vben/access';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
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="card-box mt-5 p-5 font-semibold">
|
||||||
<div class="mb-3 text-lg">组件形式控制</div>
|
<div class="mb-3 text-lg">组件形式控制</div>
|
||||||
<CodeAuthority :value="['AC_100100']">
|
<CodeAccess :value="['AC_100100']">
|
||||||
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_100030']">
|
<CodeAccess :value="['AC_100030']">
|
||||||
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_1000001']">
|
<CodeAccess :value="['AC_1000001']">
|
||||||
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_100100', 'AC_100010']">
|
<CodeAccess :value="['AC_100100', 'AC_100010']">
|
||||||
<Button class="mr-4">
|
<Button class="mr-4">
|
||||||
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
||||||
</Button>
|
</Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5 font-semibold">
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
|
||||||
|
|
||||||
import { useRouter } from 'vue-router';
|
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';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
@ -81,18 +81,18 @@ async function changeAccount(role: string) {
|
||||||
</div>
|
</div>
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5 font-semibold">
|
||||||
<div class="mb-3 text-lg">角色 - 组件形式控制</div>
|
<div class="mb-3 text-lg">角色 - 组件形式控制</div>
|
||||||
<RoleAuthority :value="['super']">
|
<RoleAccess :value="['super']">
|
||||||
<Button class="mr-4"> Super 角色可见 </Button>
|
<Button class="mr-4"> Super 角色可见 </Button>
|
||||||
</RoleAuthority>
|
</RoleAccess>
|
||||||
<RoleAuthority :value="['admin']">
|
<RoleAccess :value="['admin']">
|
||||||
<Button class="mr-4"> Admin 角色可见 </Button>
|
<Button class="mr-4"> Admin 角色可见 </Button>
|
||||||
</RoleAuthority>
|
</RoleAccess>
|
||||||
<RoleAuthority :value="['user']">
|
<RoleAccess :value="['user']">
|
||||||
<Button class="mr-4"> User 角色可见 </Button>
|
<Button class="mr-4"> User 角色可见 </Button>
|
||||||
</RoleAuthority>
|
</RoleAccess>
|
||||||
<RoleAuthority :value="['super', 'admin']">
|
<RoleAccess :value="['super', 'admin']">
|
||||||
<Button class="mr-4"> Super & Admin 角色都可见 </Button>
|
<Button class="mr-4"> Super & Admin 角色都可见 </Button>
|
||||||
</RoleAuthority>
|
</RoleAccess>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<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="card-box mt-5 p-5 font-semibold">
|
||||||
<div class="mb-3 text-lg">权限码 - 组件形式控制</div>
|
<div class="mb-3 text-lg">权限码 - 组件形式控制</div>
|
||||||
<CodeAuthority :value="['AC_100100']">
|
<CodeAccess :value="['AC_100100']">
|
||||||
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_100030']">
|
<CodeAccess :value="['AC_100030']">
|
||||||
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_1000001']">
|
<CodeAccess :value="['AC_1000001']">
|
||||||
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
<CodeAuthority :value="['AC_100100', 'AC_100010']">
|
<CodeAccess :value="['AC_100100', 'AC_100010']">
|
||||||
<Button class="mr-4">
|
<Button class="mr-4">
|
||||||
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
||||||
</Button>
|
</Button>
|
||||||
</CodeAuthority>
|
</CodeAccess>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5 font-semibold">
|
||||||
|
|
|
@ -7,10 +7,6 @@ import type {
|
||||||
LibraryPluginOptions,
|
LibraryPluginOptions,
|
||||||
} from '../typing';
|
} from '../typing';
|
||||||
|
|
||||||
import { join } from 'node:path';
|
|
||||||
|
|
||||||
import { getPackages } from '@vben/node-utils';
|
|
||||||
|
|
||||||
import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
||||||
import viteVue from '@vitejs/plugin-vue';
|
import viteVue from '@vitejs/plugin-vue';
|
||||||
import viteVueJsx from '@vitejs/plugin-vue-jsx';
|
import viteVueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
@ -117,28 +113,10 @@ async function loadApplicationPlugins(
|
||||||
{
|
{
|
||||||
condition: i18n,
|
condition: i18n,
|
||||||
plugins: async () => {
|
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 [
|
return [
|
||||||
viteVueI18nPlugin({
|
viteVueI18nPlugin({
|
||||||
compositionOnly: true,
|
compositionOnly: true,
|
||||||
fullInstall: true,
|
fullInstall: true,
|
||||||
include,
|
|
||||||
runtimeOnly: true,
|
runtimeOnly: true,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>;
|
|
|
@ -1,4 +1,2 @@
|
||||||
export * from './find-menu-by-path';
|
export * from './find-menu-by-path';
|
||||||
export * from './flatten-object';
|
|
||||||
export * from './merge-route-modules';
|
export * from './merge-route-modules';
|
||||||
export * from './nested-object';
|
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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 };
|
|
|
@ -17,7 +17,6 @@ const defaultPreferences: Preferences = {
|
||||||
layout: 'sidebar-nav',
|
layout: 'sidebar-nav',
|
||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
name: 'Vben Admin Pro',
|
name: 'Vben Admin Pro',
|
||||||
semiDarkMenu: true,
|
|
||||||
},
|
},
|
||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
@ -82,6 +81,7 @@ const defaultPreferences: Preferences = {
|
||||||
colorWarning: 'hsl(42 84% 61%)',
|
colorWarning: 'hsl(42 84% 61%)',
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
radius: '0.5',
|
radius: '0.5',
|
||||||
|
semiDarkMenu: true,
|
||||||
},
|
},
|
||||||
transition: {
|
transition: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
@ -89,6 +89,15 @@ const defaultPreferences: Preferences = {
|
||||||
name: 'fade-slide',
|
name: 'fade-slide',
|
||||||
progress: true,
|
progress: true,
|
||||||
},
|
},
|
||||||
|
widget: {
|
||||||
|
aiAssistant: true,
|
||||||
|
fullscreen: true,
|
||||||
|
globalSearch: true,
|
||||||
|
languageToggle: true,
|
||||||
|
notification: true,
|
||||||
|
sidebarToggle: true,
|
||||||
|
themeToggle: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { defaultPreferences };
|
export { defaultPreferences };
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { DeepPartial } from '@vben-core/typings';
|
||||||
|
|
||||||
import type { Preferences } from './types';
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
import { preferencesManager } from './preferences';
|
import { preferencesManager } from './preferences';
|
||||||
|
@ -5,10 +7,6 @@ import { preferencesManager } from './preferences';
|
||||||
// 偏好设置(带有层级关系)
|
// 偏好设置(带有层级关系)
|
||||||
const preferences: Preferences = preferencesManager.getPreferences();
|
const preferences: Preferences = preferencesManager.getPreferences();
|
||||||
|
|
||||||
// 扁平化后的偏好设置
|
|
||||||
// const flatPreferences: Flatten<Preferences> =
|
|
||||||
// preferencesManager.getFlatPreferences();
|
|
||||||
|
|
||||||
// 更新偏好设置
|
// 更新偏好设置
|
||||||
const updatePreferences =
|
const updatePreferences =
|
||||||
preferencesManager.updatePreferences.bind(preferencesManager);
|
preferencesManager.updatePreferences.bind(preferencesManager);
|
||||||
|
@ -20,9 +18,13 @@ const resetPreferences =
|
||||||
const clearPreferencesCache =
|
const clearPreferencesCache =
|
||||||
preferencesManager.clearCache.bind(preferencesManager);
|
preferencesManager.clearCache.bind(preferencesManager);
|
||||||
|
|
||||||
|
function defineOverridesPreferences(preferences: DeepPartial<Preferences>) {
|
||||||
|
return preferences;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
clearPreferencesCache,
|
clearPreferencesCache,
|
||||||
// flatPreferences,
|
defineOverridesPreferences,
|
||||||
preferences,
|
preferences,
|
||||||
preferencesManager,
|
preferencesManager,
|
||||||
resetPreferences,
|
resetPreferences,
|
||||||
|
|
|
@ -46,8 +46,6 @@ interface AppPreferences {
|
||||||
locale: SupportedLanguagesType;
|
locale: SupportedLanguagesType;
|
||||||
/** 应用名 */
|
/** 应用名 */
|
||||||
name: string;
|
name: string;
|
||||||
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
|
||||||
semiDarkMenu: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreadcrumbPreferences {
|
interface BreadcrumbPreferences {
|
||||||
|
@ -164,6 +162,8 @@ interface ThemePreferences {
|
||||||
mode: ThemeModeType;
|
mode: ThemeModeType;
|
||||||
/** 圆角 */
|
/** 圆角 */
|
||||||
radius: string;
|
radius: string;
|
||||||
|
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
||||||
|
semiDarkMenu: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransitionPreferences {
|
interface TransitionPreferences {
|
||||||
|
@ -177,6 +177,23 @@ interface TransitionPreferences {
|
||||||
progress: boolean;
|
progress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WidgetPreferences {
|
||||||
|
/** 是否开启vben助手部件 */
|
||||||
|
aiAssistant: boolean;
|
||||||
|
/** 是否启用全屏部件 */
|
||||||
|
fullscreen: boolean;
|
||||||
|
/** 是否启用全局搜索部件 */
|
||||||
|
globalSearch: boolean;
|
||||||
|
/** 是否启用语言切换部件 */
|
||||||
|
languageToggle: boolean;
|
||||||
|
/** 是否显示通知部件 */
|
||||||
|
notification: boolean;
|
||||||
|
/** 是否显示侧边栏显示/隐藏部件 */
|
||||||
|
sidebarToggle: boolean;
|
||||||
|
/** 是否显示主题切换部件 */
|
||||||
|
themeToggle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Preferences {
|
interface Preferences {
|
||||||
/** 全局配置 */
|
/** 全局配置 */
|
||||||
app: AppPreferences;
|
app: AppPreferences;
|
||||||
|
@ -202,6 +219,8 @@ interface Preferences {
|
||||||
theme: ThemePreferences;
|
theme: ThemePreferences;
|
||||||
/** 动画配置 */
|
/** 动画配置 */
|
||||||
transition: TransitionPreferences;
|
transition: TransitionPreferences;
|
||||||
|
/** 功能配置 */
|
||||||
|
widget: WidgetPreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferencesKeys = keyof Preferences;
|
type PreferencesKeys = keyof Preferences;
|
||||||
|
@ -230,4 +249,5 @@ export type {
|
||||||
ThemeModeType,
|
ThemeModeType,
|
||||||
ThemePreferences,
|
ThemePreferences,
|
||||||
TransitionPreferences,
|
TransitionPreferences,
|
||||||
|
WidgetPreferences,
|
||||||
};
|
};
|
||||||
|
|
|
@ -147,7 +147,6 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"dynamic-title": "Dynamic Title",
|
"dynamic-title": "Dynamic Title",
|
||||||
"ai-assistant": "Ai Assistant",
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "Sidebar",
|
"title": "Sidebar",
|
||||||
"width": "Width",
|
"width": "Width",
|
||||||
|
@ -248,6 +247,16 @@
|
||||||
"search": "Global Search",
|
"search": "Global Search",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"preferences": "Preferences"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,6 @@
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"dynamic-title": "动态标题",
|
"dynamic-title": "动态标题",
|
||||||
"ai-assistant": "Ai 助手",
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "侧边栏",
|
"title": "侧边栏",
|
||||||
"width": "宽度",
|
"width": "宽度",
|
||||||
|
@ -247,6 +246,16 @@
|
||||||
"search": "全局搜索",
|
"search": "全局搜索",
|
||||||
"logout": "退出登录",
|
"logout": "退出登录",
|
||||||
"preferences": "偏好设置"
|
"preferences": "偏好设置"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"title": "小部件",
|
||||||
|
"global-search": "启用全局搜索",
|
||||||
|
"fullscreen": "启用全屏",
|
||||||
|
"theme-toggle": "启用主题切换",
|
||||||
|
"language-toggle": "启用语言切换",
|
||||||
|
"notification": "启用通知",
|
||||||
|
"sidebar-toggle": "启用侧边栏切换",
|
||||||
|
"ai-assistant": "启用 AI 助手"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
|
|
@ -1,5 +1,4 @@
|
||||||
export type * from './app';
|
export type * from './app';
|
||||||
export type * from './flatten';
|
|
||||||
export type * from './helper';
|
export type * from './helper';
|
||||||
export type * from './menu-record';
|
export type * from './menu-record';
|
||||||
export type * from './tabs';
|
export type * from './tabs';
|
||||||
|
|
|
@ -87,6 +87,11 @@ interface VbenLayoutProps {
|
||||||
* @default 'fixed'
|
* @default 'fixed'
|
||||||
*/
|
*/
|
||||||
headerMode?: LayoutHeaderModeType;
|
headerMode?: LayoutHeaderModeType;
|
||||||
|
/**
|
||||||
|
* 是否显示header切换侧边栏按钮
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
headerToggleSidebarButton?: boolean;
|
||||||
/**
|
/**
|
||||||
* header是否显示
|
* header是否显示
|
||||||
* @default true
|
* @default true
|
||||||
|
@ -152,21 +157,21 @@ interface VbenLayoutProps {
|
||||||
* @default 210
|
* @default 210
|
||||||
*/
|
*/
|
||||||
sidebarWidth?: number;
|
sidebarWidth?: number;
|
||||||
|
/**
|
||||||
|
* footer背景颜色
|
||||||
|
* @default #fff
|
||||||
|
*/
|
||||||
|
tabbarBackgroundColor?: string;
|
||||||
/**
|
/**
|
||||||
* tab是否可见
|
* tab是否可见
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
tabbarEnable?: boolean;
|
tabbarEnable?: boolean;
|
||||||
/**
|
|
||||||
* footer背景颜色
|
|
||||||
* @default #fff
|
|
||||||
*/
|
|
||||||
tabsBackgroundColor?: string;
|
|
||||||
/**
|
/**
|
||||||
* tab高度
|
* tab高度
|
||||||
* @default 30
|
* @default 30
|
||||||
*/
|
*/
|
||||||
tabsHeight?: number;
|
tabbarHeight?: number;
|
||||||
/**
|
/**
|
||||||
* zIndex
|
* zIndex
|
||||||
* @default 100
|
* @default 100
|
||||||
|
|
|
@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
headerHeight: 50,
|
headerHeight: 50,
|
||||||
headerHeightOffset: 10,
|
headerHeightOffset: 10,
|
||||||
headerHidden: false,
|
headerHidden: false,
|
||||||
|
|
||||||
headerMode: 'fixed',
|
headerMode: 'fixed',
|
||||||
|
headerToggleSidebarButton: true,
|
||||||
headerVisible: true,
|
headerVisible: true,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
layout: 'sidebar-nav',
|
layout: 'sidebar-nav',
|
||||||
|
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
sidebarTheme: 'dark',
|
sidebarTheme: 'dark',
|
||||||
sidebarWidth: 180,
|
sidebarWidth: 180,
|
||||||
tabbarEnable: true,
|
tabbarEnable: true,
|
||||||
tabsHeight: 36,
|
tabbarHeight: 36,
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ const headerWrapperHeight = computed(() => {
|
||||||
height += getHeaderHeight.value;
|
height += getHeaderHeight.value;
|
||||||
}
|
}
|
||||||
if (props.tabbarEnable) {
|
if (props.tabbarEnable) {
|
||||||
height += props.tabsHeight;
|
height += props.tabbarHeight;
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
});
|
});
|
||||||
|
@ -364,6 +364,7 @@ const maskStyle = computed((): CSSProperties => {
|
||||||
|
|
||||||
const showHeaderToggleButton = computed(() => {
|
const showHeaderToggleButton = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
props.headerToggleSidebarButton &&
|
||||||
isSideMode.value &&
|
isSideMode.value &&
|
||||||
!isSidebarMixedNav.value &&
|
!isSidebarMixedNav.value &&
|
||||||
!isMixedNav.value &&
|
!isMixedNav.value &&
|
||||||
|
@ -528,7 +529,7 @@ function handleOpenMenu() {
|
||||||
|
|
||||||
<LayoutTabbar
|
<LayoutTabbar
|
||||||
v-if="tabbarEnable"
|
v-if="tabbarEnable"
|
||||||
:height="tabsHeight"
|
:height="tabbarHeight"
|
||||||
:style="tabbarStyle"
|
:style="tabbarStyle"
|
||||||
>
|
>
|
||||||
<slot name="tabbar"></slot>
|
<slot name="tabbar"></slot>
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'CodeAuthority',
|
name: 'CodeAccess',
|
||||||
});
|
});
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
|
@ -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 * 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 type * from './types';
|
||||||
export * from './use-access';
|
export * from './use-access';
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'RoleAuthority',
|
name: 'RoleAccess',
|
||||||
});
|
});
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<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 { VbenFullScreen } from '@vben-core/shadcn-ui';
|
||||||
import { useCoreAccessStore } from '@vben-core/stores';
|
import { useCoreAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
|
@ -33,14 +33,15 @@ const { globalSearchShortcutKey } = usePreferences();
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
|
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
|
||||||
<GlobalSearch
|
<GlobalSearch
|
||||||
|
v-if="preferences.widget.globalSearch"
|
||||||
:enable-shortcut-key="globalSearchShortcutKey"
|
:enable-shortcut-key="globalSearchShortcutKey"
|
||||||
:menus="accessStore.accessMenus"
|
:menus="accessStore.accessMenus"
|
||||||
class="mr-4"
|
class="mr-4"
|
||||||
/>
|
/>
|
||||||
<ThemeToggle class="mr-2" />
|
<ThemeToggle v-if="preferences.widget.themeToggle" class="mr-2" />
|
||||||
<LanguageToggle class="mr-2" />
|
<LanguageToggle v-if="preferences.widget.languageToggle" class="mr-2" />
|
||||||
<VbenFullScreen class="mr-2" />
|
<VbenFullScreen v-if="preferences.widget.fullscreen" class="mr-2" />
|
||||||
<slot name="notification"></slot>
|
<slot v-if="preferences.widget.notification" name="notification"></slot>
|
||||||
<slot name="user-dropdown"></slot>
|
<slot name="user-dropdown"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -38,7 +38,7 @@ const headerMenuTheme = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = computed(() => {
|
const theme = computed(() => {
|
||||||
const dark = isDark.value || preferences.app.semiDarkMenu;
|
const dark = isDark.value || preferences.theme.semiDarkMenu;
|
||||||
return dark ? 'dark' : 'light';
|
return dark ? 'dark' : 'light';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,6 +122,7 @@ function clearPreferencesAndLogout() {
|
||||||
:footer-fixed="preferences.footer.fixed"
|
:footer-fixed="preferences.footer.fixed"
|
||||||
:header-hidden="preferences.header.hidden"
|
:header-hidden="preferences.header.hidden"
|
||||||
:header-mode="preferences.header.mode"
|
:header-mode="preferences.header.mode"
|
||||||
|
:header-toggle-sidebar-button="preferences.widget.sidebarToggle"
|
||||||
:header-visible="preferences.header.enable"
|
:header-visible="preferences.header.enable"
|
||||||
:is-mobile="preferences.app.isMobile"
|
:is-mobile="preferences.app.isMobile"
|
||||||
:layout="layout"
|
:layout="layout"
|
||||||
|
@ -131,7 +132,7 @@ function clearPreferencesAndLogout() {
|
||||||
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
|
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
|
||||||
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
|
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
|
||||||
:sidebar-hidden="preferences.sidebar.hidden"
|
:sidebar-hidden="preferences.sidebar.hidden"
|
||||||
:sidebar-semi-dark="preferences.app.semiDarkMenu"
|
:sidebar-semi-dark="preferences.theme.semiDarkMenu"
|
||||||
:sidebar-theme="theme"
|
:sidebar-theme="theme"
|
||||||
:sidebar-width="preferences.sidebar.width"
|
:sidebar-width="preferences.sidebar.width"
|
||||||
:tabbar-enable="preferences.tabbar.enable"
|
:tabbar-enable="preferences.tabbar.enable"
|
||||||
|
@ -158,7 +159,7 @@ function clearPreferencesAndLogout() {
|
||||||
|
|
||||||
<template #floating-groups>
|
<template #floating-groups>
|
||||||
<CozeAssistant
|
<CozeAssistant
|
||||||
v-if="preferences.app.aiAssistant"
|
v-if="preferences.widget.aiAssistant"
|
||||||
:is-mobile="preferences.app.isMobile"
|
:is-mobile="preferences.app.isMobile"
|
||||||
/>
|
/>
|
||||||
<VbenBackTop />
|
<VbenBackTop />
|
||||||
|
|
|
@ -13,7 +13,6 @@ defineOptions({
|
||||||
|
|
||||||
const appLocale = defineModel<string>('appLocale');
|
const appLocale = defineModel<string>('appLocale');
|
||||||
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
|
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
|
||||||
const appAiAssistant = defineModel<boolean>('appAiAssistant');
|
|
||||||
|
|
||||||
const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
|
const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
|
||||||
label: item.text,
|
label: item.text,
|
||||||
|
@ -28,7 +27,4 @@ const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
|
||||||
<SwitchItem v-model="appDynamicTitle">
|
<SwitchItem v-model="appDynamicTitle">
|
||||||
{{ $t('preferences.dynamic-title') }}
|
{{ $t('preferences.dynamic-title') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<SwitchItem v-model="appAiAssistant">
|
|
||||||
{{ $t('preferences.ai-assistant') }}
|
|
||||||
</SwitchItem>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -10,6 +10,7 @@ export { default as Layout } from './layout/layout.vue';
|
||||||
export { default as Navigation } from './layout/navigation.vue';
|
export { default as Navigation } from './layout/navigation.vue';
|
||||||
export { default as Sidebar } from './layout/sidebar.vue';
|
export { default as Sidebar } from './layout/sidebar.vue';
|
||||||
export { default as Tabbar } from './layout/tabbar.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 GlobalShortcutKeys } from './shortcut-keys/global.vue';
|
||||||
export { default as SwitchItem } from './switch-item.vue';
|
export { default as SwitchItem } from './switch-item.vue';
|
||||||
export { default as BuiltinTheme } from './theme/builtin.vue';
|
export { default as BuiltinTheme } from './theme/builtin.vue';
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -51,6 +51,7 @@ import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
Tabbar,
|
Tabbar,
|
||||||
Theme,
|
Theme,
|
||||||
|
Widget,
|
||||||
} from './blocks';
|
} from './blocks';
|
||||||
import IconSetting from './icons/setting.vue';
|
import IconSetting from './icons/setting.vue';
|
||||||
import { useOpenPreferences } from './use-open-preferences';
|
import { useOpenPreferences } from './use-open-preferences';
|
||||||
|
@ -59,7 +60,6 @@ const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const appLocale = defineModel<SupportedLanguagesType>('appLocale');
|
const appLocale = defineModel<SupportedLanguagesType>('appLocale');
|
||||||
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
|
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
|
||||||
const appAiAssistant = defineModel<boolean>('appAiAssistant');
|
|
||||||
const appLayout = defineModel<LayoutType>('appLayout');
|
const appLayout = defineModel<LayoutType>('appLayout');
|
||||||
const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
|
const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
|
||||||
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
|
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
|
||||||
|
@ -129,6 +129,14 @@ const shortcutKeysGlobalPreferences = defineModel<boolean>(
|
||||||
'shortcutKeysGlobalPreferences',
|
'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 {
|
const {
|
||||||
diffPreference,
|
diffPreference,
|
||||||
isDark,
|
isDark,
|
||||||
|
@ -245,7 +253,6 @@ async function handleReset() {
|
||||||
<template #general>
|
<template #general>
|
||||||
<Block :title="$t('preferences.general')">
|
<Block :title="$t('preferences.general')">
|
||||||
<General
|
<General
|
||||||
v-model:app-ai-assistant="appAiAssistant"
|
|
||||||
v-model:app-dynamic-title="appDynamicTitle"
|
v-model:app-dynamic-title="appDynamicTitle"
|
||||||
v-model:app-locale="appLocale"
|
v-model:app-locale="appLocale"
|
||||||
/>
|
/>
|
||||||
|
@ -346,6 +353,17 @@ async function handleReset() {
|
||||||
v-model:tabbar-show-icon="tabbarShowIcon"
|
v-model:tabbar-show-icon="tabbarShowIcon"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</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')">
|
<Block :title="$t('preferences.footer.title')">
|
||||||
<Footer
|
<Footer
|
||||||
v-model:footer-enable="footerEnable"
|
v-model:footer-enable="footerEnable"
|
||||||
|
|
Loading…
Reference in New Issue