Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
commit
0a19ec3122
@ -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();
|
||||||
|
@ -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 |
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
/** 侧边栏宽度 */
|
/** 侧边栏宽度 */
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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指定的图标 */
|
||||||
|
@ -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 } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印
|
// 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印
|
||||||
|
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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(),
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user