This commit is contained in:
dap 2025-03-31 15:20:14 +08:00
commit 0a19ec3122
21 changed files with 298 additions and 45 deletions

View File

@ -116,7 +116,7 @@ watch(
async (enable) => { async (enable) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
content: `${userStore.userInfo?.username}`, content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();

View File

@ -123,6 +123,10 @@ function fetchApi(): Promise<Record<string, any>> {
::: :::
## 并发和缓存
有些场景下可能需要使用多个ApiComponent它们使用了相同的远程数据源例如用在可编辑的表格中。如果直接将请求后端接口的函数传递给api属性则每一个实例都会访问一次接口这会造成资源浪费是完全没有必要的。Tanstack Query提供了并发控制、缓存、重试等诸多特性我们可以将接口请求函数用useQuery包装一下再传递给ApiComponent这样的话无论页面有多少个使用相同数据源的ApiComponent实例都只会发起一次远程请求。演示效果请参考 [Playground vue-query](https://www.vben.pro/#/demos/features/vue-query),具体代码请查看项目文件[concurrency-caching](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/views/demos/features/vue-query/concurrency-caching.vue)
## API ## API
### Props ### Props
@ -147,3 +151,10 @@ function fetchApi(): Promise<Record<string, any>> {
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | | options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | | visibleEvent | 触发重新请求数据的事件名 | `string` | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | | loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
### Methods
| 方法 | 描述 | 类型 | 版本要求 |
| --- | --- | --- | --- |
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
| updateParam | 设置接口请求参数将与params属性合并 | (newParams: Record<string, any>)=>void | >5.5.4 |

View File

@ -68,10 +68,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"sidebar": { "sidebar": {
"autoActivateChild": false, "autoActivateChild": false,
"collapsed": false, "collapsed": false,
"collapsedButton": true,
"collapsedShowTitle": false, "collapsedShowTitle": false,
"enable": true, "enable": true,
"expandOnHover": true, "expandOnHover": true,
"extraCollapse": false, "extraCollapse": false,
"fixedButton": true,
"hidden": false, "hidden": false,
"width": 224, "width": 224,
}, },

View File

@ -68,10 +68,12 @@ const defaultPreferences: Preferences = {
sidebar: { sidebar: {
autoActivateChild: false, autoActivateChild: false,
collapsed: false, collapsed: false,
collapsedButton: true,
collapsedShowTitle: false, collapsedShowTitle: false,
enable: true, enable: true,
expandOnHover: true, expandOnHover: true,
extraCollapse: false, extraCollapse: false,
fixedButton: true,
hidden: false, hidden: false,
width: 224, width: 224,
}, },

View File

@ -132,6 +132,8 @@ interface SidebarPreferences {
autoActivateChild: boolean; autoActivateChild: boolean;
/** 侧边栏是否折叠 */ /** 侧边栏是否折叠 */
collapsed: boolean; collapsed: boolean;
/** 侧边栏折叠按钮是否可见 */
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */ /** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean; collapsedShowTitle: boolean;
/** 侧边栏是否可见 */ /** 侧边栏是否可见 */
@ -140,6 +142,8 @@ interface SidebarPreferences {
expandOnHover: boolean; expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */ /** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean; extraCollapse: boolean;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */ /** 侧边栏是否隐藏 - css */
hidden: boolean; hidden: boolean;
/** 侧边栏宽度 */ /** 侧边栏宽度 */

View File

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { useScrollLock } from '@vueuse/core';
import { computed, shallowRef, useSlots, watchEffect } from 'vue'; import { computed, shallowRef, useSlots, watchEffect } from 'vue';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { useScrollLock } from '@vueuse/core';
import { SidebarCollapseButton, SidebarFixedButton } from './widgets'; import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
interface Props { interface Props {
@ -63,9 +65,14 @@ interface Props {
show?: boolean; show?: boolean;
/** /**
* 显示折叠按钮 * 显示折叠按钮
* @default false * @default true
*/ */
showCollapseButton?: boolean; showCollapseButton?: boolean;
/**
* 显示固定按钮
* @default true
*/
showFixedButton?: boolean;
/** /**
* 主题 * 主题
*/ */
@ -93,6 +100,7 @@ const props = withDefaults(defineProps<Props>(), {
paddingTop: 0, paddingTop: 0,
show: true, show: true,
showCollapseButton: true, showCollapseButton: true,
showFixedButton: true,
zIndex: 0, zIndex: 0,
}); });
@ -265,7 +273,7 @@ function handleMouseleave() {
@mouseleave="handleMouseleave" @mouseleave="handleMouseleave"
> >
<SidebarFixedButton <SidebarFixedButton
v-if="!collapse && !isSidebarMixed" v-if="!collapse && !isSidebarMixed && showFixedButton"
v-model:expand-on-hover="expandOnHover" v-model:expand-on-hover="expandOnHover"
/> />
<div v-if="slots.logo" :style="headerStyle"> <div v-if="slots.logo" :style="headerStyle">

View File

@ -106,6 +106,11 @@ interface VbenLayoutProps {
* @default false * @default false
*/ */
sidebarCollapse?: boolean; sidebarCollapse?: boolean;
/**
*
* @default true
*/
sidebarCollapsedButton?: boolean;
/** /**
* title * title
* @default true * @default true
@ -121,6 +126,11 @@ interface VbenLayoutProps {
* @default 48 * @default 48
*/ */
sidebarExtraCollapsedWidth?: number; sidebarExtraCollapsedWidth?: number;
/**
*
* @default true
*/
sidebarFixedButton?: boolean;
/** /**
* *
* @default false * @default false

View File

@ -49,8 +49,10 @@ const props = withDefaults(defineProps<Props>(), {
headerVisible: true, headerVisible: true,
isMobile: false, isMobile: false,
layout: 'sidebar-nav', layout: 'sidebar-nav',
sidebarCollapsedButton: true,
sidebarCollapseShowTitle: false, sidebarCollapseShowTitle: false,
sidebarExtraCollapsedWidth: 60, sidebarExtraCollapsedWidth: 60,
sidebarFixedButton: true,
sidebarHidden: false, sidebarHidden: false,
sidebarMixedWidth: 80, sidebarMixedWidth: 80,
sidebarTheme: 'dark', sidebarTheme: 'dark',
@ -487,6 +489,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
v-model:expand-on-hovering="sidebarExpandOnHovering" v-model:expand-on-hovering="sidebarExpandOnHovering"
v-model:extra-collapse="sidebarExtraCollapse" v-model:extra-collapse="sidebarExtraCollapse"
v-model:extra-visible="sidebarExtraVisible" v-model:extra-visible="sidebarExtraVisible"
:show-collapse-button="sidebarCollapsedButton"
:show-fixed-button="sidebarFixedButton"
:collapse-width="getSideCollapseWidth" :collapse-width="getSideCollapseWidth"
:dom-visible="!isMobile" :dom-visible="!isMobile"
:extra-width="sidebarExtraWidth" :extra-width="sidebarExtraWidth"

View File

@ -20,7 +20,7 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
showIcon: true, showIcon: true,
size: 'middle', size: 'middle',
}); });
const emit = defineEmits(['btnClick']);
const btnDefaultProps = computed(() => { const btnDefaultProps = computed(() => {
return { return {
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']), ...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
@ -90,6 +90,7 @@ async function onBtnClick(value: ValueType) {
innerValue.value = [value]; innerValue.value = [value];
modelValue.value = value; modelValue.value = value;
} }
emit('btnClick', value);
} }
</script> </script>
<template> <template>

View File

@ -11,7 +11,6 @@ import { onMounted, ref, watchEffect } from 'vue';
import { ChevronRight, IconifyIcon } from '@vben-core/icons'; import { ChevronRight, IconifyIcon } from '@vben-core/icons';
import { cn, get } from '@vben-core/shared/utils'; import { cn, get } from '@vben-core/shared/utils';
import { useVModel } from '@vueuse/core';
import { TreeItem, TreeRoot } from 'radix-vue'; import { TreeItem, TreeRoot } from 'radix-vue';
import { Checkbox } from '../checkbox'; import { Checkbox } from '../checkbox';
@ -27,7 +26,6 @@ const props = withDefaults(defineProps<TreeProps>(), {
expanded: () => [], expanded: () => [],
iconField: 'icon', iconField: 'icon',
labelField: 'label', labelField: 'label',
modelValue: () => [],
multiple: false, multiple: false,
showIcon: true, showIcon: true,
transition: true, transition: true,
@ -38,7 +36,6 @@ const props = withDefaults(defineProps<TreeProps>(), {
const emits = defineEmits<{ const emits = defineEmits<{
expand: [value: FlattenedItem<Recordable<any>>]; expand: [value: FlattenedItem<Recordable<any>>];
select: [value: FlattenedItem<Recordable<any>>]; select: [value: FlattenedItem<Recordable<any>>];
'update:modelValue': [value: Arrayable<Recordable<any>>];
}>(); }>();
interface InnerFlattenItem<T = Recordable<any>, P = number | string> { interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
@ -76,11 +73,7 @@ function flatten<T = Recordable<any>, P = number | string>(
} }
const flattenData = ref<Array<InnerFlattenItem>>([]); const flattenData = ref<Array<InnerFlattenItem>>([]);
const modelValue = useVModel(props, 'modelValue', emits, { const modelValue = defineModel<Arrayable<number | string>>();
deep: true,
defaultValue: props.defaultValue ?? [],
passive: (props.modelValue === undefined) as false,
});
const expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []); const expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []);
const treeValue = ref(); const treeValue = ref();
@ -105,9 +98,13 @@ function getItemByValue(value: number | string) {
function updateTreeValue() { function updateTreeValue() {
const val = modelValue.value; const val = modelValue.value;
if (val === undefined) {
treeValue.value = undefined;
} else {
treeValue.value = Array.isArray(val) treeValue.value = Array.isArray(val)
? val.map((v) => getItemByValue(v)) ? val.map((v) => getItemByValue(v))
: getItemByValue(val); : getItemByValue(val);
}
} }
function updateModelValue(val: Arrayable<Recordable<any>>) { function updateModelValue(val: Arrayable<Recordable<any>>) {
@ -173,13 +170,6 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
modelValue.value.push(p); modelValue.value.push(p);
} }
}); });
} else {
if (Array.isArray(modelValue.value)) {
const index = modelValue.value.indexOf(get(item.value, props.valueField));
if (index !== -1) {
modelValue.value.splice(index, 1);
}
}
} }
updateTreeValue(); updateTreeValue();
emits('select', item); emits('select', item);
@ -240,7 +230,7 @@ defineExpose({
@select=" @select="
(event) => { (event) => {
if (event.detail.originalEvent.type === 'click') { if (event.detail.originalEvent.type === 'click') {
// event.preventDefault(); event.preventDefault();
} }
onSelect(item, event.detail.isSelected); onSelect(item, event.detail.isSelected);
} }

View File

@ -27,8 +27,6 @@ export interface TreeProps {
iconField?: string; iconField?: string;
/** label字段 */ /** label字段 */
labelField?: string; labelField?: string;
/** 当前值 */
modelValue?: Arrayable<number | string>;
/** 是否多选 */ /** 是否多选 */
multiple?: boolean; multiple?: boolean;
/** 显示由iconField指定的图标 */ /** 显示由iconField指定的图标 */

View File

@ -2,8 +2,6 @@ import type { Watermark, WatermarkOptions } from 'watermark-js-plus';
import { nextTick, onUnmounted, readonly, ref } from 'vue'; import { nextTick, onUnmounted, readonly, ref } from 'vue';
import { updatePreferences } from '@vben/preferences';
const watermark = ref<Watermark>(); const watermark = ref<Watermark>();
const unmountedHooked = ref<boolean>(false); const unmountedHooked = ref<boolean>(false);
const cachedOptions = ref<Partial<WatermarkOptions>>({ const cachedOptions = ref<Partial<WatermarkOptions>>({
@ -48,7 +46,6 @@ export function useWatermark() {
...options, ...options,
}; };
watermark.value = new Watermark(cachedOptions.value); watermark.value = new Watermark(cachedOptions.value);
updatePreferences({ app: { watermark: true } });
await watermark.value?.create(); await watermark.value?.create();
} }
@ -69,7 +66,6 @@ export function useWatermark() {
watermark.value.destroy(); watermark.value.destroy();
watermark.value = undefined; watermark.value = undefined;
} }
updatePreferences({ app: { watermark: false } });
} }
// 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印 // 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印

View File

@ -192,6 +192,8 @@ const headerSlots = computed(() => {
:sidebar-collapse="preferences.sidebar.collapsed" :sidebar-collapse="preferences.sidebar.collapsed"
:sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle" :sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="sidebarVisible" :sidebar-enable="sidebarVisible"
:sidebar-collapsed-button="preferences.sidebar.collapsedButton"
:sidebar-fixed-button="preferences.sidebar.fixedButton"
: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"

View File

@ -1,7 +1,12 @@
import type { TabDefinition } from '@vben/types';
import type { IContextMenuItem } from '@vben-core/tabs-ui';
import type { RouteLocationNormalizedGeneric } from 'vue-router'; import type { RouteLocationNormalizedGeneric } from 'vue-router';
import type { TabDefinition } from '@vben/types';
import type { IContextMenuItem } from '@vben-core/tabs-ui';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben/hooks'; import { useContentMaximize, useTabs } from '@vben/hooks';
import { import {
ArrowLeftToLine, ArrowLeftToLine,
@ -19,8 +24,6 @@ import {
import { $t, useI18n } from '@vben/locales'; import { $t, useI18n } from '@vben/locales';
import { useAccessStore, useTabbarStore } from '@vben/stores'; import { useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils'; import { filterTree } from '@vben/utils';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export function useTabbar() { export function useTabbar() {
const router = useRouter(); const router = useRouter();
@ -206,7 +209,8 @@ export function useTabbar() {
text: $t('preferences.tabbar.contextMenu.closeAll'), text: $t('preferences.tabbar.contextMenu.closeAll'),
}, },
]; ];
return menus;
return menus.filter((item) => tabbarStore.getMenuList.includes(item.key));
}; };
return { return {

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import { VbenCheckButtonGroup, VbenTooltip } from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceCheckboxItem',
});
withDefaults(
defineProps<{
disabled?: boolean;
items: SelectOption[];
multiple?: boolean;
onBtnClick?: (value: string) => void;
placeholder?: string;
}>(),
{
disabled: false,
placeholder: '',
items: () => [],
onBtnClick: () => {},
multiple: false,
},
);
const inputValue = defineModel<string[]>();
const slots = useSlots();
</script>
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
>
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<template #trigger>
<CircleHelp class="ml-1 size-3 cursor-help" />
</template>
<slot name="tip"></slot>
</VbenTooltip>
</span>
<VbenCheckButtonGroup
v-model="inputValue"
class="h-8 w-[165px]"
:options="items"
:disabled="disabled"
:multiple="multiple"
@btn-click="onBtnClick"
/>
</div>
</template>

View File

@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LayoutType } from '@vben/types'; import type { LayoutType } from '@vben/types';
import { onMounted } from 'vue';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import CheckboxItem from '../checkbox-item.vue';
import NumberFieldItem from '../number-field-item.vue'; import NumberFieldItem from '../number-field-item.vue';
import SwitchItem from '../switch-item.vue'; import SwitchItem from '../switch-item.vue';
@ -18,6 +21,27 @@ const sidebarAutoActivateChild = defineModel<boolean>(
); );
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed'); const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover'); const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
const sidebarButtons = defineModel<string[]>('sidebarButtons', { default: [] });
const sidebarCollapsedButton = defineModel<boolean>('sidebarCollapsedButton');
const sidebarFixedButton = defineModel<boolean>('sidebarFixedButton');
onMounted(() => {
if (
sidebarCollapsedButton.value &&
!sidebarButtons.value.includes('collapsed')
) {
sidebarButtons.value.push('collapsed');
}
if (sidebarFixedButton.value && !sidebarButtons.value.includes('fixed')) {
sidebarButtons.value.push('fixed');
}
});
const handleCheckboxChange = () => {
sidebarCollapsedButton.value = !!sidebarButtons.value.includes('collapsed');
sidebarFixedButton.value = !!sidebarButtons.value.includes('fixed');
};
</script> </script>
<template> <template>
@ -53,6 +77,17 @@ const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
> >
{{ $t('preferences.sidebar.autoActivateChild') }} {{ $t('preferences.sidebar.autoActivateChild') }}
</SwitchItem> </SwitchItem>
<CheckboxItem
:items="[
{ label: '收缩按钮', value: 'collapsed' },
{ label: '固定按钮', value: 'fixed' },
]"
multiple
v-model="sidebarButtons"
:on-btn-click="handleCheckboxChange"
>
按钮配置
</CheckboxItem>
<NumberFieldItem <NumberFieldItem
v-model="sidebarWidth" v-model="sidebarWidth"
:disabled="!sidebarEnable || disabled" :disabled="!sidebarEnable || disabled"

View File

@ -93,8 +93,9 @@ const sidebarCollapsedShowTitle = defineModel<boolean>(
const sidebarAutoActivateChild = defineModel<boolean>( const sidebarAutoActivateChild = defineModel<boolean>(
'sidebarAutoActivateChild', 'sidebarAutoActivateChild',
); );
const SidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover'); const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
const sidebarCollapsedButton = defineModel<boolean>('sidebarCollapsedButton');
const sidebarFixedButton = defineModel<boolean>('sidebarFixedButton');
const headerEnable = defineModel<boolean>('headerEnable'); const headerEnable = defineModel<boolean>('headerEnable');
const headerMode = defineModel<LayoutHeaderModeType>('headerMode'); const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
const headerMenuAlign = const headerMenuAlign =
@ -317,8 +318,10 @@ async function handleReset() {
v-model:sidebar-collapsed="sidebarCollapsed" v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle" v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable" v-model:sidebar-enable="sidebarEnable"
v-model:sidebar-expand-on-hover="SidebarExpandOnHover" v-model:sidebar-expand-on-hover="sidebarExpandOnHover"
v-model:sidebar-width="sidebarWidth" v-model:sidebar-width="sidebarWidth"
v-model:sidebar-collapsed-button="sidebarCollapsedButton"
v-model:sidebar-fixed-button="sidebarFixedButton"
:current-layout="appLayout" :current-layout="appLayout"
:disabled="!isSideMode" :disabled="!isSideMode"
/> />

View File

@ -26,6 +26,10 @@ interface TabbarState {
* @zh_CN * @zh_CN
*/ */
excludeCachedTabs: Set<string>; excludeCachedTabs: Set<string>;
/**
* @zh_CN
*/
menuList: string[];
/** /**
* @zh_CN * @zh_CN
*/ */
@ -372,6 +376,14 @@ export const useTabbarStore = defineStore('core-tabbar', {
} }
}, },
/**
* @zh_CN
* @param list
*/
setMenuList(list: string[]) {
this.menuList = list;
},
/** /**
* @zh_CN * @zh_CN
* @param tab * @param tab
@ -388,7 +400,6 @@ export const useTabbarStore = defineStore('core-tabbar', {
await this.updateCacheTabs(); await this.updateCacheTabs();
} }
}, },
setUpdateTime() { setUpdateTime() {
this.updateTime = Date.now(); this.updateTime = Date.now();
}, },
@ -406,6 +417,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
this.tabs.splice(newIndex, 0, currentTab); this.tabs.splice(newIndex, 0, currentTab);
this.dragEndIndex = this.dragEndIndex + 1; this.dragEndIndex = this.dragEndIndex + 1;
}, },
/** /**
* @zh_CN * @zh_CN
* @param tab * @param tab
@ -439,7 +451,6 @@ export const useTabbarStore = defineStore('core-tabbar', {
// 交换位置重新排序 // 交换位置重新排序
await this.sortTabs(index, newIndex); await this.sortTabs(index, newIndex);
}, },
/** /**
* *
*/ */
@ -480,6 +491,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
getExcludeCachedTabs(): string[] { getExcludeCachedTabs(): string[] {
return [...this.excludeCachedTabs]; return [...this.excludeCachedTabs];
}, },
getMenuList(): string[] {
return this.menuList;
},
getTabs(): TabDefinition[] { getTabs(): TabDefinition[] {
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab)); const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
return [...this.affixTabs, ...normalTabs].filter(Boolean); return [...this.affixTabs, ...normalTabs].filter(Boolean);
@ -496,6 +510,17 @@ export const useTabbarStore = defineStore('core-tabbar', {
cachedTabs: new Set(), cachedTabs: new Set(),
dragEndIndex: 0, dragEndIndex: 0,
excludeCachedTabs: new Set(), excludeCachedTabs: new Set(),
menuList: [
'close',
'affix',
'maximize',
'reload',
'open-in-new-window',
'close-left',
'close-right',
'close-other',
'close-all',
],
renderRouteView: true, renderRouteView: true,
tabs: [], tabs: [],
updateTime: Date.now(), updateTime: Date.now(),

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
@ -14,13 +14,26 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useTabbarStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue'; import LoginForm from '#/views/_core/authentication/login.vue';
const { setMenuList } = useTabbarStore();
setMenuList([
'close',
'affix',
'maximize',
'reload',
'open-in-new-window',
'close-left',
'close-right',
'close-other',
'close-all',
]);
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
{ {
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
@ -113,7 +126,7 @@ watch(
async (enable) => { async (enable) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
content: `${userStore.userInfo?.username}`, content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();
@ -123,6 +136,12 @@ watch(
immediate: true, immediate: true,
}, },
); );
onBeforeMount(() => {
if (preferences.app.watermark) {
destroyWatermark();
}
});
</script> </script>
<template> <template>

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import { useQuery } from '@tanstack/vue-query';
import { useVbenForm } from '#/adapter/form';
import { getMenuList } from '#/api';
const queryKey = ['demo', 'api', 'options'];
const count = 4;
const { dataUpdatedAt, promise: fetchDataFn } = useQuery({
//
experimental_prefetchInRender: true,
//
queryFn: getMenuList,
queryKey,
// always
refetchOnMount: 'always',
//
staleTime: 1000 * 60 * 5,
});
async function fetchOptions() {
return await fetchDataFn.value;
}
const schema = [];
for (let i = 0; i < count; i++) {
schema.push({
component: 'ApiSelect',
componentProps: {
api: fetchOptions,
class: 'w-full',
filterOption: (input: string, option: Recordable<any>) => {
return option.label.toLowerCase().includes(input.toLowerCase());
},
labelField: 'name',
showSearch: true,
valueField: 'id',
},
fieldName: `field${i}`,
label: `Select ${i}`,
});
}
const [Form] = useVbenForm({
schema,
showDefaultActions: false,
});
</script>
<template>
<div>
<div class="mb-2 flex gap-2">
<div>以下{{ count }}个组件共用一个数据源</div>
<div>缓存更新时间{{ new Date(dataUpdatedAt).toLocaleString() }}</div>
</div>
<Form />
</div>
</template>

View File

@ -1,11 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue'; import { refAutoReset } from '@vueuse/core';
import { Button, Card, Empty } from 'ant-design-vue';
import ConcurrencyCaching from './concurrency-caching.vue';
import InfiniteQueries from './infinite-queries.vue'; import InfiniteQueries from './infinite-queries.vue';
import PaginatedQueries from './paginated-queries.vue'; import PaginatedQueries from './paginated-queries.vue';
import QueryRetries from './query-retries.vue'; import QueryRetries from './query-retries.vue';
const showCaching = refAutoReset(true, 1000);
</script> </script>
<template> <template>
@ -20,6 +24,17 @@ import QueryRetries from './query-retries.vue';
<Card title="错误重试"> <Card title="错误重试">
<QueryRetries /> <QueryRetries />
</Card> </Card>
<Card
title="并发和缓存"
v-spinning="!showCaching"
:body-style="{ minHeight: '330px' }"
>
<template #extra>
<Button @click="showCaching = false">重新加载</Button>
</template>
<ConcurrencyCaching v-if="showCaching" />
<Empty v-else description="正在加载..." />
</Card>
</div> </div>
</Page> </Page>
</template> </template>