This commit is contained in:
xingyu4j
2026-02-11 11:01:44 +08:00
170 changed files with 7055 additions and 2142 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

@@ -31,6 +31,7 @@ export {
FoldHorizontal,
Fullscreen,
Github,
Grid,
Grip,
GripVertical,
History,
@@ -39,6 +40,7 @@ export {
Info,
InspectionPanel,
Languages,
LayoutGrid,
LoaderCircle,
LockKeyhole,
LogOut,

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

@@ -22,6 +22,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

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

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,20 +1,38 @@
import type { ComputedRef, MaybeRef } from 'vue';
/**
* 类型级递归中增加深度计数
*/
type Increment<A extends unknown[]> = [...A, unknown];
/**
* 深层递归所有属性为可选
*/
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
type DeepPartial<
T,
D extends number = 10,
C extends unknown[] = [],
> = C['length'] extends D
? T
: T extends object
? {
[P in keyof T]?: DeepPartial<T[P], D, Increment<C>>;
}
: T;
/**
* 深层递归所有属性为只读
*/
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type DeepReadonly<
T,
D extends number = 10,
C extends unknown[] = [],
> = C['length'] extends D
? T
: T extends object
? {
readonly [P in keyof T]: DeepReadonly<T[P], D, Increment<C>>;
}
: T;
/**
* 任意类型的异步函数

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

@@ -104,7 +104,9 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"showIcon": true,
"showMaximize": true,
"showMore": true,
"showRefresh": 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

@@ -105,7 +105,9 @@ const defaultPreferences: Preferences = {
showIcon: true,
showMaximize: true,
showMore: true,
showRefresh: true,
styleType: 'chrome',
visitHistory: true,
wheelable: true,
},
theme: {

View File

@@ -38,12 +38,10 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
primaryColor: 'hsl(240 5.9% 10%)',
type: 'zinc',
},
{
color: 'hsl(181 84% 32%)',
type: 'deep-green',
},
{
color: 'hsl(211 91% 39%)',
type: 'deep-blue',
@@ -56,7 +54,6 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
color: 'hsl(0 75% 42%)',
type: 'rose',
},
{
color: 'hsl(0 0% 25%)',
darkPrimaryColor: 'hsl(0 0% 98%)',

View File

@@ -222,8 +222,12 @@ interface TabbarPreferences {
showMaximize: boolean;
/** 显示更多按钮 */
showMore: boolean;
/** 显示刷新按钮 */
showRefresh: 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

@@ -6,12 +6,30 @@ import type {
import { computed, ref, watch } from 'vue';
import { isBoolean, isFunction } from '@vben-core/shared/utils';
import { get, isBoolean, isFunction } from '@vben-core/shared/utils';
import { useFormValues } from 'vee-validate';
import { injectRenderFormProps } from './context';
/**
* 解析Nested Objects对应的字段值
* @param values 表单值
* @param fieldName 字段名
*/
function resolveValueByFieldName(
values: Record<string, any>,
fieldName: string,
) {
// vee-validate[] 表示禁用嵌套
if (fieldName.startsWith('[') && fieldName.endsWith(']')) {
const rawKey = fieldName.slice(1, -1);
return values[rawKey];
}
return get(values, fieldName);
}
export default function useDependencies(
getDependencies: () => FormItemDependencies | undefined,
) {
@@ -37,7 +55,7 @@ export default function useDependencies(
// 该字段可能会被多个字段触发
const triggerFields = getDependencies()?.triggerFields ?? [];
return triggerFields.map((dep) => {
return values.value[dep];
return resolveValueByFieldName(values.value, dep);
});
});
@@ -82,10 +100,8 @@ export default function useDependencies(
// 2. 判断show如果show为false则隐藏
if (isFunction(show)) {
isShow.value = !!(await show(formValues, formApi));
if (!isShow.value) return;
} else if (isBoolean(show)) {
isShow.value = show;
if (!isShow.value) return;
}
if (isFunction(componentProps)) {

View File

@@ -53,11 +53,8 @@ const wrapperClass = computed(() => {
provideFormRenderProps(props);
const {
isCalculated,
keepFormItemIndex,
wrapperRef: _wrapperRef,
} = useExpandable(props);
// @ts-expect-error unused
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
const shapes = computed(() => {
const resultShapes: FormShape[] = [];
@@ -174,7 +171,7 @@ const computedSchema = computed(
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="_wrapperRef" :class="wrapperClass">
<div ref="wrapperRef" :class="wrapperClass">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

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

@@ -26,8 +26,8 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {});
const { contentElement: _contentElement, overlayStyle } =
useLayoutContentStyle();
// @ts-expect-error unused
const { contentElement, overlayStyle } = useLayoutContentStyle();
const style = computed((): CSSProperties => {
const {
@@ -56,11 +56,7 @@ const style = computed((): CSSProperties => {
</script>
<template>
<main
ref="_contentElement"
:style="style"
class="relative bg-background-deep"
>
<main ref="contentElement" :style="style" class="relative bg-background-deep">
<Slot :style="overlayStyle">
<slot name="overlay"></slot>
</Slot>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { computed, useSlots, watchEffect } from 'vue';
import { computed, shallowRef, useSlots, watchEffect } from 'vue';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
@@ -114,7 +114,8 @@ const extraVisible = defineModel<boolean>('extraVisible');
const isLocked = useScrollLock(document.body);
const slots = useSlots();
// const asideRef = shallowRef<HTMLDivElement | null>();
// @ts-expect-error unused
const asideRef = shallowRef<HTMLDivElement | null>();
const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
@@ -290,6 +291,7 @@ function handleMouseleave() {
/>
<div
v-if="isSidebarMixed"
ref="asideRef"
:class="{
'border-l': extraVisible,
}"

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

@@ -463,33 +463,33 @@ $namespace: vben;
&.is-dark {
--menu-background-color: hsl(var(--menu));
// --menu-submenu-opened-background-color: hsl(var(--menu-opened-dark));
--menu-item-background-color: var(--menu-background-color);
--menu-item-color: hsl(var(--foreground) / 80%);
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--accent-foreground));
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--accent-foreground));
--menu-item-active-background-color: hsl(var(--accent));
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--foreground));
--menu-submenu-active-background-color: transparent;
--menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--accent-foreground));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--accent-foreground));
--menu-submenu-active-background-color: transparent;
}
&.is-light {
--menu-background-color: hsl(var(--menu));
// --menu-submenu-opened-background-color: hsl(var(--menu-opened));
--menu-item-color: hsl(var(--accent-foreground));
--menu-item-background-color: var(--menu-background-color);
--menu-item-color: hsl(var(--foreground));
--menu-item-hover-color: var(--menu-item-color);
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--primary));
--menu-item-active-background-color: hsl(var(--primary) / 15%);
--menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--primary));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: transparent;
--menu-submenu-background-color: var(--menu-background-color);
}
&.is-rounded {
@@ -518,25 +518,33 @@ $namespace: vben;
--menu-background-color: transparent;
&.is-dark {
--menu-background-color: hsl(var(--menu));
--menu-item-color: hsl(var(--foreground) / 80%);
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--accent-foreground));
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--accent-foreground));
--menu-item-active-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--foreground));
--menu-submenu-active-background-color: hsl(var(--accent));
--menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--accent-foreground));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--accent-foreground));
--menu-submenu-active-background-color: hsl(var(--accent));
}
&.is-light {
--menu-background-color: hsl(var(--menu));
--menu-item-color: hsl(var(--accent-foreground));
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--menu-item-color));
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--primary));
--menu-item-active-background-color: hsl(var(--primary) / 15%);
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-hover-color: hsl(var(--primary));
--menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
--menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--primary));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
}
}
}
@@ -862,16 +870,17 @@ $namespace: vben;
padding-right: 12px !important;
}
// &:not(.is-active):hover {
&:hover {
color: var(--menu-submenu-hover-color);
text-decoration: none;
cursor: pointer;
background: var(--menu-submenu-hover-background-color) !important;
&:not(.is-active):hover {
&:hover {
//color: var(--menu-submenu-hover-color);
text-decoration: none;
cursor: pointer;
background: var(--menu-submenu-hover-background-color) !important;
// svg {
// fill: var(--menu-submenu-hover-color);
// }
// svg {
// fill: var(--menu-submenu-hover-color);
// }
}
}
}
</style>

View File

@@ -210,6 +210,7 @@ onBeforeUnmount(() => {
opened ? '' : 'hidden',
'overflow-auto',
'max-h-[calc(var(--reka-hover-card-content-available-height)-20px)]',
mode === 'horizontal' ? 'is-horizontal' : '',
]"
:content-props="contentProps"
:open="true"

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

@@ -54,7 +54,8 @@ const components = globalShareState.getComponents();
const id = useId();
provide('DISMISSABLE_DRAWER_ID', id);
// const wrapperRef = ref<HTMLElement>();
// @ts-expect-error unused
const wrapperRef = ref<HTMLElement>();
const { $t } = useSimpleLocale();
const { isMobile } = useIsMobile();
@@ -281,6 +282,7 @@ const getForceMount = computed(() => {
</VisuallyHidden>
</template>
<div
ref="wrapperRef"
:class="
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
'pointer-events-none': showLoading || submitting,

View File

@@ -50,10 +50,12 @@ const props = withDefaults(defineProps<Props>(), {
const components = globalShareState.getComponents();
const contentRef = ref();
// const wrapperRef = ref<HTMLElement>();
// @ts-expect-error unused
const wrapperRef = ref<HTMLElement>();
const dialogRef = ref();
const headerRef = ref();
// const footerRef = ref();
// @ts-expect-error unused
const footerRef = ref();
const id = useId();
@@ -306,6 +308,7 @@ function handleClosed() {
</VisuallyHidden>
</DialogHeader>
<div
ref="wrapperRef"
:class="
cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
'pointer-events-none': showLoading || submitting,
@@ -325,6 +328,7 @@ function handleClosed() {
</VbenIconButton>
<DialogFooter
ref="footerRef"
v-if="showFooter"
:class="
cn(

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

@@ -42,14 +42,17 @@ export default defineComponent({
return props.content;
}
}
return h(props.content as never, {
...attrs,
props: {
...props,
return h(
props.content as never,
{
...attrs,
props: {
...props,
...attrs,
},
},
slots,
});
);
};
},
});

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

@@ -3,7 +3,7 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { Pin, X } from '@vben-core/icons';
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
@@ -28,8 +28,10 @@ const emit = defineEmits<{
}>();
const active = defineModel<string>('active');
// const contentRef = ref();
// const tabRef = ref();
// @ts-expect-error unused
const contentRef = ref();
// @ts-expect-error unused
const tabRef = ref();
const style = computed(() => {
const { gap } = props;
@@ -73,6 +75,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<template>
<div
ref="contentRef"
:class="contentClass"
:style="style"
class="tabs-chrome !flex h-full w-max overflow-y-hidden pr-6"
@@ -81,6 +84,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<div
v-for="(tab, i) in tabsView"
:key="tab.key"
ref="tabRef"
:class="[
{
'is-active': tab.key === active,
@@ -154,7 +158,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-1 flex size-4 items-center overflow-hidden"
class="mr-1 flex size-4 items-center overflow-hidden group-hover:animate-[shrink_0.3s_ease-in-out]"
/>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">

View File

@@ -132,7 +132,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden"
class="mr-2 flex size-4 items-center overflow-hidden group-hover:animate-[shrink_0.3s_ease-in-out]"
fallback
/>

View File

@@ -1,2 +1,3 @@
export { default as TabsToolMore } from './tool-more.vue';
export { default as TabsToolRefresh } from './tool-refresh.vue';
export { default as TabsToolScreen } from './tool-screen.vue';

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { DropdownMenuProps } from '@vben-core/shadcn-ui';
import { ChevronDown } from '@vben-core/icons';
import { LayoutGrid } from '@vben-core/icons';
import { VbenDropdownMenu } from '@vben-core/shadcn-ui';
defineProps<DropdownMenuProps>();
@@ -12,7 +12,7 @@ defineProps<DropdownMenuProps>();
<div
class="flex-center h-full cursor-pointer border-l border-border px-2 text-lg font-semibold text-muted-foreground hover:bg-muted hover:text-foreground"
>
<ChevronDown class="size-4" />
<LayoutGrid class="size-4" />
</div>
</VbenDropdownMenu>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { RotateCw } from '@vben-core/icons';
const emit = defineEmits(['refresh']);
const handleRefresh = () => {
emit('refresh');
};
</script>
<template>
<div
class="flex-center h-full cursor-pointer border-l border-border px-2 text-lg font-semibold text-muted-foreground hover:bg-muted hover:text-foreground"
@click="handleRefresh"
>
<RotateCw class="size-4" />
</div>
</template>

View File

@@ -2,7 +2,7 @@
import type { TabsEmits, TabsProps } from './types';
import { useForwardPropsEmits } from '@vben-core/composables';
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
import { ChevronsLeft, ChevronsRight } from '@vben-core/icons';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { Tabs, TabsChrome } from './components';
@@ -29,7 +29,8 @@ const forward = useForwardPropsEmits(props, emit);
const {
handleScrollAt,
handleWheel,
scrollbarRef: _scrollbarRef,
// @ts-expect-error unused
scrollbarRef,
scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
@@ -59,7 +60,7 @@ useTabsDrag(props, emit);
class="border-r px-2"
@click="scrollDirection('left')"
>
<ChevronLeft class="size-4 h-full" />
<ChevronsLeft class="size-4 h-full" />
</span>
<div
@@ -69,7 +70,7 @@ useTabsDrag(props, emit);
class="size-full flex-1 overflow-hidden"
>
<VbenScrollbar
ref="_scrollbarRef"
ref="scrollbarRef"
:shadow-bottom="false"
:shadow-top="false"
class="h-full"
@@ -100,7 +101,7 @@ useTabsDrag(props, emit);
class="cursor-pointer border-l px-2 text-muted-foreground hover:bg-muted"
@click="scrollDirection('right')"
>
<ChevronRight class="size-4 h-full" />
<ChevronsRight class="size-4 h-full" />
</span>
</div>
</template>

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

@@ -5,6 +5,8 @@ import type { VbenFormSchema } from '@vben-core/form-ui';
import { computed, reactive } from 'vue';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
@@ -50,7 +52,7 @@ defineExpose({
<div @keydown.enter.prevent="handleSubmit">
<Form />
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
更新基本信息
{{ $t('profile.updateBasicProfile') }}
</VbenButton>
</div>
</template>

View File

@@ -5,6 +5,8 @@ import type { VbenFormSchema } from '@vben-core/form-ui';
import { computed, reactive } from 'vue';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
@@ -23,6 +25,7 @@ const emit = defineEmits<{
const [Form, formApi] = useVbenForm(
reactive({
commonConfig: {
labelWidth: 130,
// 所有表单项
componentProps: {
class: 'w-full',
@@ -50,7 +53,7 @@ defineExpose({
<div>
<Form />
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
更新密码
{{ $t('profile.updatePassword') }}
</VbenButton>
</div>
</template>

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

@@ -411,7 +411,7 @@ const headerSlots = computed(() => {
<template v-if="preferencesButtonPosition.fixed">
<Preferences
class="z-100 fixed bottom-20 right-0"
class="z-100 fixed right-0 top-1/2 -translate-y-1/2 transform"
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
</template>

View File

@@ -6,7 +6,12 @@ import { useContentMaximize, useTabs } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { useTabbarStore } from '@vben/stores';
import { TabsToolMore, TabsToolScreen, TabsView } from '@vben-core/tabs-ui';
import {
TabsToolMore,
TabsToolRefresh,
TabsToolScreen,
TabsView,
} from '@vben-core/tabs-ui';
import { useTabbar } from './use-tabbar';
@@ -19,7 +24,7 @@ defineProps<{ showIcon?: boolean; theme?: string }>();
const route = useRoute();
const tabbarStore = useTabbarStore();
const { contentIsMaximize, toggleMaximize } = useContentMaximize();
const { unpinTab } = useTabs();
const { refreshTab, unpinTab } = useTabs();
const {
createContextMenus,
@@ -65,6 +70,10 @@ if (!preferences.tabbar.persist) {
/>
<div class="flex-center h-full">
<TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" />
<TabsToolRefresh
v-if="preferences.tabbar.showRefresh"
@refresh="refreshTab"
/>
<TabsToolScreen
v-if="preferences.tabbar.showMaximize"
:screen="contentIsMaximize"

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

@@ -58,7 +58,6 @@ function typeView(name: BuiltinThemeType) {
case 'green': {
return $t('preferences.theme.builtin.green');
}
case 'neutral': {
return $t('preferences.theme.builtin.neutral');
}

View File

@@ -120,6 +120,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');
@@ -400,6 +401,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

@@ -119,7 +119,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
});
};
const updateDate = (
const updateData = (
option: EChartsOption,
notMerge = false, // false = 合并保留动画true = 完全替换
lazyUpdate = false, // true 时不立即重绘,适合短时间内多次调用
@@ -184,7 +184,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return {
renderEcharts,
resize,
updateDate,
updateData,
getChartInstance: () => chartInstance,
};
}

View File

@@ -165,6 +165,7 @@ const toolbarOptions = computed(() => {
}
if (!showToolbar.value) {
toolbarConfig.enabled = false;
return { toolbarConfig };
}
@@ -378,9 +379,11 @@ onUnmounted(() => {
<!-- 左侧操作区域或者title -->
<template v-if="showToolbar" #toolbar-actions="slotProps">
<slot v-if="showTableTitle" name="table-title">
<div class="mr-1 pl-1 text-[1rem]">
<div
class="flex items-center justify-center gap-1 text-[1rem] font-bold"
>
{{ tableTitle }}
<VbenHelpTooltip v-if="tableTitleHelp" trigger-class="pb-1">
<VbenHelpTooltip v-if="tableTitleHelp">
{{ tableTitleHelp }}
</VbenHelpTooltip>
</div>

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

@@ -17,6 +17,7 @@ const SvgQQChatIcon = createIconifyIcon('svg:qqchat');
const SvgWeChatIcon = createIconifyIcon('svg:wechat');
const SvgDingDingIcon = createIconifyIcon('svg:dingding');
const SvgTDesignIcon = createIconifyIcon('svg:tdesign-logo');
const SvgAntdvNextLogoIcon = createIconifyIcon('svg:antdv-next-logo');
/** AI */
const SvgGptIcon = createIconifyIcon('svg:gpt');
@@ -48,6 +49,7 @@ export {
SvgAlipayQrIcon,
SvgAlipayWapIcon,
SvgAntdvLogoIcon,
SvgAntdvNextLogoIcon,
SvgAvatar1Icon,
SvgAvatar2Icon,
SvgAvatar3Icon,

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

@@ -0,0 +1,4 @@
{
"updatePassword": "Update Password",
"updateBasicProfile": "Update Basic Profile"
}

View File

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

View File

@@ -0,0 +1,4 @@
{
"updatePassword": "更新密码",
"updateBasicProfile": "更新基本信息"
}

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;
});
}
// add by 芋艿:合并静态路由和动态路由
return [...options.routes, ...routes];