Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into antdv-next

This commit is contained in:
dap
2026-02-09 16:26:26 +08:00
128 changed files with 4183 additions and 68 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -16,13 +16,13 @@
html {
@apply text-foreground bg-background font-sans;
scroll-behavior: smooth;
font-size: var(--font-size-base, 16px);
font-variation-settings: normal;
font-synthesis-weight: none;
line-height: 1.15;
text-size-adjust: 100%;
scroll-behavior: smooth;
text-rendering: optimizelegibility;
text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
/* -webkit-font-smoothing: antialiased;

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -19,6 +19,8 @@ export const VBEN_LOGO_URL =
*/
export const VBEN_PREVIEW_URL = 'https://www.vben.pro';
export const VBEN_ANTDV_NEXT_PREVIEW_URL = 'https://antdv-next.vben.pro';
export const VBEN_ELE_PREVIEW_URL = 'https://ele.vben.pro';
export const VBEN_NAIVE_PREVIEW_URL = 'https://naive.vben.pro';

View File

@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { createStack, Stack } from '../stack';
describe('stack', () => {
let stack: Stack<number>;
beforeEach(() => {
stack = new Stack<number>();
});
it('push & size should work', () => {
stack.push(1, 2);
expect(stack.size).toBe(2);
});
it('peek should return top element without removing it', () => {
stack.push(1, 2);
expect(stack.peek()).toBe(2);
expect(stack.size).toBe(2);
});
it('pop should remove and return top element', () => {
stack.push(1, 2);
expect(stack.pop()).toBe(2);
expect(stack.size).toBe(1);
expect(stack.peek()).toBe(1);
});
it('pop on empty stack should return undefined', () => {
expect(stack.pop()).toBeUndefined();
expect(stack.peek()).toBeUndefined();
});
it('clear should remove all elements', () => {
stack.push(1, 2);
stack.clear();
expect(stack.size).toBe(0);
expect(stack.peek()).toBeUndefined();
});
it('toArray should return a shallow copy', () => {
stack.push(1, 2);
const arr = stack.toArray();
arr.push(3);
expect(stack.size).toBe(2);
expect(stack.toArray()).toEqual([1, 2]);
});
it('dedup should remove existing item before push', () => {
stack.push(1, 2, 1);
expect(stack.toArray()).toEqual([2, 1]);
expect(stack.size).toBe(2);
});
it('dedup = false should allow duplicate items', () => {
const s = new Stack<number>(false);
s.push(1, 1, 1);
expect(s.toArray()).toEqual([1, 1, 1]);
expect(s.size).toBe(3);
});
it('remove should delete all matching items', () => {
stack.push(1, 2, 1);
stack.remove(1);
expect(stack.toArray()).toEqual([2]);
expect(stack.size).toBe(1);
});
it('maxSize should limit stack capacity', () => {
const s = new Stack<number>(true, 3);
s.push(1, 2, 3, 4);
expect(s.toArray()).toEqual([2, 3, 4]);
expect(s.size).toBe(3);
});
it('dedup + maxSize should work together', () => {
const s = new Stack<number>(true, 3);
s.push(1, 2, 3, 2); // 去重并重新入栈
expect(s.toArray()).toEqual([1, 3, 2]);
expect(s.size).toBe(3);
});
it('createStack should create a stack instance', () => {
const s = createStack<number>(true, 2);
s.push(1, 2, 3);
expect(s.toArray()).toEqual([2, 3]);
});
});

View File

@@ -8,6 +8,7 @@ export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './resources';
export * from './stack';
export * from './state-handler';
export * from './to';
export * from './tree';

View File

@@ -0,0 +1,103 @@
/**
* @zh_CN 栈数据结构
*/
export class Stack<T> {
/**
* @zh_CN 栈内元素数量
*/
get size() {
return this.items.length;
}
/**
* @zh_CN 是否去重
*/
private readonly dedup: boolean;
/**
* @zh_CN 栈内元素
*/
private items: T[] = [];
/**
* @zh_CN 栈的最大容量
*/
private readonly maxSize?: number;
constructor(dedup = true, maxSize?: number) {
this.maxSize = maxSize;
this.dedup = dedup;
}
/**
* @zh_CN 清空栈内元素
*/
clear() {
this.items.length = 0;
}
/**
* @zh_CN 查看栈顶元素
* @returns 栈顶元素
*/
peek(): T | undefined {
return this.items[this.items.length - 1];
}
/**
* @zh_CN 出栈
* @returns 栈顶元素
*/
pop(): T | undefined {
return this.items.pop();
}
/**
* @zh_CN 入栈
* @param items 要入栈的元素
*/
push(...items: T[]) {
items.forEach((item) => {
// 去重
if (this.dedup) {
const index = this.items.indexOf(item);
if (index !== -1) {
this.items.splice(index, 1);
}
}
this.items.push(item);
if (this.maxSize && this.items.length > this.maxSize) {
this.items.splice(0, this.items.length - this.maxSize);
}
});
}
/**
* @zh_CN 移除栈内元素
* @param itemList 要移除的元素列表
*/
remove(...itemList: T[]) {
this.items = this.items.filter((i) => !itemList.includes(i));
}
/**
* @zh_CN 保留栈内元素
* @param itemList 要保留的元素列表
*/
retain(itemList: T[]) {
this.items = this.items.filter((i) => itemList.includes(i));
}
/**
* @zh_CN 转换为数组
* @returns 栈内元素数组
*/
toArray(): T[] {
return [...this.items];
}
}
/**
* @zh_CN 创建一个栈实例
* @param dedup 是否去重
* @param maxSize 栈的最大容量
* @returns 栈实例
*/
export const createStack = <T>(dedup = true, maxSize?: number) =>
new Stack<T>(dedup, maxSize);

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -105,6 +105,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"showMaximize": true,
"showMore": true,
"styleType": "chrome",
"visitHistory": true,
"wheelable": true,
},
"theme": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -106,6 +106,7 @@ const defaultPreferences: Preferences = {
showMaximize: true,
showMore: true,
styleType: 'chrome',
visitHistory: true,
wheelable: true,
},
theme: {

View File

@@ -224,6 +224,8 @@ interface TabbarPreferences {
showMore: boolean;
/** 标签页风格 */
styleType: TabsStyleType;
/** 是否开启访问历史记录 */
visitHistory: boolean;
/** 是否开启鼠标滚轮响应 */
wheelable: boolean;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/popup-ui",
"version": "5.2.1",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.9",
"version": "5.6.0",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

View File

@@ -33,5 +33,16 @@ const modelValue = useVModel(props, 'modelValue', emits, {
<style lang="scss" scoped>
input {
--ring: var(--primary);
&::-ms-reveal,
&::-ms-clear {
display: none;
}
&::-webkit-credentials-auto-fill-button,
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
display: none;
}
}
</style>

View File

@@ -65,16 +65,24 @@ const modelValue = defineModel<Arrayable<number | string>>();
const expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []);
const treeValue = ref();
let lastTreeData: any = null;
onMounted(() => {
watchEffect(() => {
flattenData.value = flatten(props.treeData, props.childrenField);
updateTreeValue();
if (
props.defaultExpandedLevel !== undefined &&
props.defaultExpandedLevel > 0
)
expandToLevel(props.defaultExpandedLevel);
// 只在 treeData 变化时执行展开
const currentTreeData = JSON.stringify(props.treeData);
if (lastTreeData !== currentTreeData) {
lastTreeData = currentTreeData;
if (
props.defaultExpandedLevel !== undefined &&
props.defaultExpandedLevel > 0
) {
expandToLevel(props.defaultExpandedLevel);
}
}
});
});
@@ -87,9 +95,11 @@ function getItemByValue(value: number | string) {
function updateTreeValue() {
const val = modelValue.value;
if (val === undefined) {
treeValue.value = undefined;
} else {
if (Array.isArray(val)) {
treeValue.value = props.multiple ? [] : undefined;
} else if (Array.isArray(val)) {
if (val.length === 0) {
treeValue.value = [];
} else {
const filteredValues = val.filter((v) => {
const item = getItemByValue(v);
return item && !get(item, props.disabledField);
@@ -99,14 +109,14 @@ function updateTreeValue() {
if (filteredValues.length !== val.length) {
modelValue.value = filteredValues;
}
}
} else {
const item = getItemByValue(val);
if (item && !get(item, props.disabledField)) {
treeValue.value = item;
} else {
const item = getItemByValue(val);
if (item && !get(item, props.disabledField)) {
treeValue.value = item;
} else {
treeValue.value = undefined;
modelValue.value = undefined;
}
treeValue.value = props.multiple ? [] : undefined;
modelValue.value = props.multiple ? [] : undefined;
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -18,6 +18,7 @@ defineProps<{ disabled?: boolean }>();
const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarVisitHistory = defineModel<boolean>('tabbarVisitHistory');
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
@@ -56,6 +57,13 @@ const styleItems = computed((): SelectOption[] => [
<SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.persist') }}
</SwitchItem>
<SwitchItem
v-model="tabbarVisitHistory"
:disabled="!tabbarEnable"
:tip="$t('preferences.tabbar.visitHistoryTip')"
>
{{ $t('preferences.tabbar.visitHistory') }}
</SwitchItem>
<NumberFieldItem
v-model="tabbarMaxCount"
:disabled="!tabbarEnable"

View File

@@ -122,6 +122,7 @@ const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarVisitHistory = defineModel<boolean>('tabbarVisitHistory');
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
@@ -405,6 +406,7 @@ async function handleReset() {
v-model:tabbar-draggable="tabbarDraggable"
v-model:tabbar-enable="tabbarEnable"
v-model:tabbar-persist="tabbarPersist"
v-model:tabbar-visit-history="tabbarVisitHistory"
v-model:tabbar-show-icon="tabbarShowIcon"
v-model:tabbar-show-maximize="tabbarShowMaximize"
v-model:tabbar-show-more="tabbarShowMore"

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/plugins",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -167,6 +167,7 @@ const toolbarOptions = computed(() => {
}
if (!showToolbar.value) {
toolbarConfig.enabled = false;
return { toolbarConfig };
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="100" height="100" viewBox="0 0 100 100"><defs><clipPath id="master_svg0_15_16"><rect x="0" y="0" width="100" height="100" rx="0"/></clipPath><linearGradient x1="0.696908175945282" y1="-0.12974359095096588" x2="0.4750439931090237" y2="1.2243471980169296" id="master_svg1_15_26"><stop offset="0%" stop-color="#FA816E" stop-opacity="1"/><stop offset="41.472604870796204%" stop-color="#F74A5C" stop-opacity="1"/><stop offset="100%" stop-color="#F51D2C" stop-opacity="1"/></linearGradient><linearGradient x1="0.552905797958374" y1="0.30240023136138916" x2="-0.011689962096274294" y2="0.67748094524159" id="master_svg2_15_23"><stop offset="0%" stop-color="#29CDFF" stop-opacity="1"/><stop offset="36.24463677406311%" stop-color="#148EFF" stop-opacity="1"/><stop offset="100%" stop-color="#0A60FF" stop-opacity="1"/></linearGradient><linearGradient x1="0.5" y1="0" x2="0.40112216980036397" y2="1.1742942637447706" id="master_svg3_15_20"><stop offset="0%" stop-color="#4285EB" stop-opacity="1"/><stop offset="100%" stop-color="#2EC7FF" stop-opacity="1"/></linearGradient></defs><g><g clip-path="url(#master_svg0_15_16)"><g><g transform="matrix(0,1,-1,0,128.38507080078125,1.3009490966796875)"><path d="M64.69695375205077,90.04532994873047C66.12390785205078,91.47199394873047,68.43745515205079,91.47199394873047,69.86440945205078,90.04532994873047L79.03075785205078,80.88083794873047C80.62410185205079,79.28782094873047,80.62410185205079,76.71482494873047,79.03094385205078,75.12199194873047L69.78469225205077,65.91039584873047C68.35435965205077,64.48542385873047,66.04007265205078,64.48747908873047,64.61227515205078,65.91498784873046C63.18532103205078,67.34165384873047,63.18532103205078,69.65473504873047,64.61227515205078,71.08139894873047L70.85724495205078,77.32510194873046C71.25382805205078,77.72160194873047,71.25382805205078,78.32337194873047,70.85724495205078,78.71987294873047L64.69695375205077,84.87891394873047C63.26999962205078,86.30557994873047,63.26999962205078,88.61865994873047,64.69695375205077,90.04532994873047Z" fill="url(#master_svg1_15_26)" fill-opacity="1"/></g><g><path d="M45.071075,20.462244300000002C50.317059,17.4434862,57.016956,19.24900955,60.035713,24.4949932L87.180679,71.667404C88.863739,74.592205,87.871445,78.327076,84.958672,80.030849C84.949287,80.036343,84.939903,80.041798,84.930489,80.047234C82.030266,81.722801,78.320847,80.73002600000001,76.64527100000001,77.829803L52.21682,35.546923C52.036407,35.234653,51.776993,34.975366,51.464634,34.795109C50.481709,34.227886,49.22506,34.56488,48.657841,35.547808L24.197922,77.933762C24.188855,77.949471,24.179718,77.965141,24.170509,77.98077C22.496816600000002,80.820942,18.837607900000002,81.76656,15.9974368,80.09286900000001C13.12257836,78.398735,12.14775169,74.705757,13.81193882,71.81346500000001L41.038097,24.4953909C42.002892,22.8186166,43.394342,21.4271069,45.071075,20.462244300000002Z" fill-rule="evenodd" fill="url(#master_svg2_15_23)" fill-opacity="1"/></g><g><path d="M56.65453717255859,29.786590576171875C60.05306537255859,29.786590576171875,62.594127372558596,31.106560476171875,64.2777233725586,33.74650027617187L86.61734737255858,72.63262957617187C88.29955337255859,75.56081357617188,87.3005633725586,79.29788257617187,84.3816263725586,80.99607857617187C84.37327937255859,81.00094257617187,84.36491437255859,81.00577557617189,84.3565443725586,81.01059357617189C81.44811637255859,82.68416957617188,77.73366737255859,81.68312457617188,76.0600893725586,78.77469657617188L55.2176894725586,42.553740576171876L51.6441945725586,36.51258137617187C51.45962977255859,36.200564876171875,51.19580437255859,35.942928776171875,50.87949277255859,35.765822876171875C49.889291872558594,35.21139527617188,48.63712227255859,35.564660076171876,48.082697242558595,36.55486197617188L48.056419372558594,36.601793776171874C50.389970572558596,32.05832457617188,53.256010072558595,29.786590576171875,56.65453717255859,29.786590576171875Z" fill-rule="evenodd" fill="url(#master_svg3_15_20)" fill-opacity="1"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -68,6 +68,8 @@
"showMore": "Show More Button",
"showMaximize": "Show Maximize Button",
"persist": "Persist Tabs",
"visitHistory": "Visit History",
"visitHistoryTip": "When enabled, the tab bar records tab visit history. \nClosing the current tab will automatically select the last opened tab.",
"maxCount": "Max Count of Tabs",
"maxCountTip": "When the number of tabs exceeds the maximum,\nthe oldest tab will be closed.\n Set to 0 to disable count checking.",
"draggable": "Enable Draggable Sort",

View File

@@ -68,6 +68,8 @@
"showMore": "显示更多按钮",
"showMaximize": "显示最大化按钮",
"persist": "持久化标签页",
"visitHistory": "访问历史记录",
"visitHistoryTip": "开启后,标签栏会记录标签访问历史\n关闭当前标签会自动选中上一个打开的标签",
"maxCount": "最大标签数",
"maxCountTip": "每次打开新的标签时如果超过最大标签数,\n会自动关闭一个最先打开的标签\n设置为 0 则不限制",
"draggable": "启动拖拽排序",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -11,7 +11,9 @@ import { toRaw } from 'vue';
import { preferences } from '@vben-core/preferences';
import {
createStack,
openRouteInNewWindow,
Stack,
startProgress,
stopProgress,
} from '@vben-core/shared/utils';
@@ -47,8 +49,17 @@ interface TabbarState {
* @zh_CN 更新时间用于一些更新场景使用watch深度监听的话会损耗性能
*/
updateTime?: number;
/**
* @zh_CN 上一个标签页打开的标签
*/
visitHistory: Stack<string>;
}
/**
* @zh_CN 访问历史记录最大数量
*/
const MAX_VISIT_HISTORY = 50;
/**
* @zh_CN 访问权限相关
*/
@@ -62,6 +73,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
this.tabs = this.tabs.filter(
(item) => !keySet.has(getTabKeyFromTab(item)),
);
if (isVisitHistory()) {
this.visitHistory.remove(...keys);
}
await this.updateCacheTabs();
},
@@ -166,6 +180,10 @@ export const useTabbarStore = defineStore('core-tabbar', {
this.tabs.splice(tabIndex, 1, mergedTab);
}
this.updateCacheTabs();
// 添加访问历史记录
if (isVisitHistory()) {
this.visitHistory.push(tab.key as string);
}
return tab;
},
/**
@@ -174,6 +192,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
async closeAllTabs(router: Router) {
const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
// 设置访问历史记录
if (isVisitHistory()) {
this.visitHistory.retain(
this.tabs.map((item) => getTabKeyFromTab(item)),
);
}
await this._goToDefaultTab(router);
this.updateCacheTabs();
},
@@ -249,12 +273,44 @@ export const useTabbarStore = defineStore('core-tabbar', {
*/
async closeTab(tab: TabDefinition, router: Router) {
const { currentRoute } = router;
const currentTabKey = getTabKey(currentRoute.value);
// 关闭不是激活选项卡
if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {
if (currentTabKey !== getTabKeyFromTab(tab)) {
this._close(tab);
this.updateCacheTabs();
// 移除访问历史记录
if (isVisitHistory()) {
this.visitHistory.remove(getTabKeyFromTab(tab));
}
return;
}
if (this.getTabs.length <= 1) {
console.error('Failed to close the tab; only one tab remains open.');
return;
}
// 从访问历史记录中移除当前关闭的tab
if (isVisitHistory()) {
this.visitHistory.remove(currentTabKey);
this._close(tab);
let previousTab: TabDefinition | undefined;
let previousTabKey: string | undefined;
while (true) {
previousTabKey = this.visitHistory.pop();
if (!previousTabKey) {
break;
}
previousTab = this.getTabByKey(previousTabKey);
if (previousTab) {
break;
}
}
await (previousTab
? this._goToTab(previousTab, router)
: this._goToDefaultTab(router));
return;
}
// 未开启访问历史记录直接跳转下一个或上一个tab
const index = this.getTabs.findIndex(
(item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
);
@@ -270,8 +326,6 @@ export const useTabbarStore = defineStore('core-tabbar', {
} else if (before) {
this._close(tab);
await this._goToTab(before, router);
} else {
console.error('Failed to close the tab; only one tab remains open.');
}
},
@@ -527,11 +581,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
persist: [
// tabs不需要保存在localStorage
{
pick: ['tabs'],
pick: ['tabs', 'visitHistory'],
storage: sessionStorage,
},
],
state: (): TabbarState => ({
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
cachedTabs: new Set(),
dragEndIndex: 0,
excludeCachedTabs: new Set(),
@@ -628,6 +683,13 @@ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
}
}
/**
* @zh_CN 是否开启访问历史记录
*/
function isVisitHistory() {
return preferences.tabbar.visitHistory;
}
/**
* 从tab获取tab页的key
* 如果tab没有key,那么就从route获取key

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/styles",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -18,6 +18,9 @@
"./antd": {
"default": "./src/antd/index.css"
},
"./antdv-next": {
"default": "./src/antdv-next/index.css"
},
"./ele": {
"default": "./src/ele/index.css"
},

View File

@@ -0,0 +1,77 @@
/* antdv-next 组件库的一些样式重置 */
.ant-app {
width: 100%;
height: 100%;
overscroll-behavior: none;
color: inherit;
}
.ant-btn {
.anticon {
display: inline-flex;
}
/* * 修复按钮添加图标时的位置问题 */
> .ant-btn-icon {
svg {
display: inline-block;
}
svg + span {
margin-inline-start: 6px;
}
}
}
.ant-tag {
> svg {
display: inline-block;
}
> svg + span {
margin-inline-start: 4px;
}
}
.ant-message-notice-content,
.ant-notification-notice {
@apply dark:border-border/60 dark:border;
}
.form-valid-error {
/** select 选择器的样式 */
.ant-select:not(.valid-success) {
border-color: hsl(var(--destructive)) !important;
}
.ant-select-focused {
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%) !important;
}
/** 数字输入框样式 */
.ant-input-number-focused {
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);
}
/** 密码输入框样式 */
.ant-input-affix-wrapper:hover {
border-color: hsl(var(--destructive));
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);
}
.ant-input:not(.valid-success) {
border-color: hsl(var(--destructive)) !important;
}
}
/** 区间选择器下面来回切换时的样式 */
.ant-app .form-valid-error .ant-picker-active-bar {
background-color: hsl(var(--destructive));
}
/** 时间选择器的样式 */
.ant-app .form-valid-error .ant-picker-focused {
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/types",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/utils",
"version": "5.5.9",
"version": "5.6.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -8,13 +8,26 @@ import type {
import { mapTree } from '@vben-core/shared/utils';
/**
* 判断路由是否在菜单中显示但访问时展示 403让用户知悉功能并申请权限
*/
function menuHasVisibleWithForbidden(route: RouteRecordRaw): boolean {
return !!route.meta?.menuVisibleWithForbidden;
}
/**
* 动态生成路由 - 后端方式
* 对 meta.menuVisibleWithForbidden 为 true 的项直接替换为 403 组件,让用户知悉功能并申请权限。
*/
async function generateRoutesByBackend(
options: GenerateMenuAndRoutesOptions,
): Promise<RouteRecordRaw[]> {
const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options;
const {
fetchMenuListAsync,
layoutMap = {},
pageMap = {},
forbiddenComponent,
} = options;
try {
const menuRoutes = await fetchMenuListAsync?.();
@@ -28,7 +41,16 @@ async function generateRoutesByBackend(
normalizePageMap[normalizeViewPath(key)] = value;
}
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
let routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
if (forbiddenComponent) {
routes = mapTree(routes, (route) => {
if (menuHasVisibleWithForbidden(route)) {
route.component = forbiddenComponent;
}
return route;
});
}
return routes;
} catch (error) {