36 changed files with 256 additions and 612 deletions
@ -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 type { Preferences } from '@vben-core/preferences'; |
|||
import { defineOverridesPreferences } from '@vben-core/preferences'; |
|||
|
|||
/** |
|||
* @description 项目配置文件 |
|||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 |
|||
*/ |
|||
export const overridesPreferences: DeepPartial<Preferences> = {}; |
|||
export const overridesPreferences = defineOverridesPreferences({ |
|||
// overrides
|
|||
}); |
|||
|
|||
@ -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,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 }; |
|||
@ -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,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'; |
|||
|
|||
@ -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> |
|||
Loading…
Reference in new issue