This commit is contained in:
dap
2025-01-07 16:24:53 +08:00
404 changed files with 4182 additions and 1468 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.0",
"version": "5.5.2",
"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.0",
"version": "5.5.2",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -29,7 +29,6 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@vben-core/form-ui": "workspace:*",
"@vben-core/popup-ui": "workspace:*",
"@vben-core/preferences": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben/constants": "workspace:*",

View File

@@ -1,12 +1,11 @@
<script lang="ts" setup>
import type { AnyPromiseFunction } from '@vben/types';
import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
import type { Component } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
import { computed, ref, unref, useAttrs, watch } from 'vue';
type OptionsItem = {
[name: string]: any;

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { PointSelectionCaptchaCardProps } from '../types';
import { computed } from 'vue';
import { $t } from '@vben/locales';
import {
Card,
@@ -11,6 +9,7 @@ import {
CardHeader,
CardTitle,
} from '@vben-core/shadcn-ui';
import { computed } from 'vue';
const props = withDefaults(defineProps<PointSelectionCaptchaCardProps>(), {
height: '220px',

View File

@@ -5,12 +5,10 @@ import type {
SliderRotateVerifyPassingData,
} from '../types';
import { reactive, unref, useTemplateRef, watch, watchEffect } from 'vue';
import { $t } from '@vben/locales';
import { cn } from '@vben-core/shared/utils';
import { useTimeoutFn } from '@vueuse/core';
import { reactive, unref, useTemplateRef, watch, watchEffect } from 'vue';
import SliderCaptchaAction from './slider-captcha-action.vue';
import SliderCaptchaBar from './slider-captcha-bar.vue';

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { Check, ChevronsRight } from '@vben/icons';
import { Slot } from '@vben-core/shadcn-ui';
import { computed, ref, useTemplateRef } from 'vue';
const props = defineProps<{
actionStyle: CSSProperties;

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { computed, type CSSProperties, ref, useTemplateRef } from 'vue';
import type { CSSProperties } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
const props = defineProps<{
barStyle: CSSProperties;

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { computed, useTemplateRef } from 'vue';
import { VbenSpineText } from '@vben-core/shadcn-ui';
import { computed, useTemplateRef } from 'vue';
const props = defineProps<{
contentStyle: CSSProperties;

View File

@@ -1,5 +1,4 @@
import type { ClassType } from '@vben/types';
import type { CSSProperties } from 'vue';
export interface CaptchaData {

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import type { ColPageProps } from './types';
import { computed, ref, useSlots } from 'vue';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@vben-core/shadcn-ui';
import Page from '../page/page.vue';
defineOptions({
name: 'ColPage',
inheritAttrs: false,
});
const props = withDefaults(defineProps<ColPageProps>(), {
leftWidth: 30,
rightWidth: 70,
resizable: true,
});
const delegatedProps = computed(() => {
const { leftWidth: _, ...delegated } = props;
return delegated;
});
const slots = useSlots();
const delegatedSlots = computed(() => {
const resultSlots: string[] = [];
for (const key of Object.keys(slots)) {
if (!['default', 'left'].includes(key)) {
resultSlots.push(key);
}
}
return resultSlots;
});
const leftPanelRef = ref<InstanceType<typeof ResizablePanel>>();
function expandLeft() {
leftPanelRef.value?.expand();
}
function collapseLeft() {
leftPanelRef.value?.collapse();
}
defineExpose({
expandLeft,
collapseLeft,
});
</script>
<template>
<Page v-bind="delegatedProps">
<!-- 继承默认的slot -->
<template
v-for="slotName in delegatedSlots"
:key="slotName"
#[slotName]="slotProps"
>
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<ResizablePanelGroup class="w-full" direction="horizontal">
<ResizablePanel
ref="leftPanelRef"
:collapsed-size="leftCollapsedWidth"
:collapsible="leftCollapsible"
:default-size="leftWidth"
:max-size="leftMaxWidth"
:min-size="leftMinWidth"
>
<template #default="slotProps">
<slot
name="left"
v-bind="{
...slotProps,
expand: expandLeft,
collapse: collapseLeft,
}"
></slot>
</template>
</ResizablePanel>
<ResizableHandle
v-if="resizable"
:style="{ backgroundColor: splitLine ? undefined : 'transparent' }"
:with-handle="splitHandle"
/>
<ResizablePanel
:collapsed-size="rightCollapsedWidth"
:collapsible="rightCollapsible"
:default-size="rightWidth"
:max-size="rightMaxWidth"
:min-size="rightMinWidth"
>
<template #default>
<slot></slot>
</template>
</ResizablePanel>
</ResizablePanelGroup>
</Page>
</template>

View File

@@ -0,0 +1,2 @@
export { default as ColPage } from './col-page.vue';
export * from './types';

View File

@@ -0,0 +1,26 @@
import type { PageProps } from '../page/types';
export interface ColPageProps extends PageProps {
/**
* 左侧宽度
* @default 30
*/
leftWidth?: number;
leftMinWidth?: number;
leftMaxWidth?: number;
leftCollapsedWidth?: number;
leftCollapsible?: boolean;
/**
* 右侧宽度
* @default 70
*/
rightWidth?: number;
rightMinWidth?: number;
rightCollapsedWidth?: number;
rightMaxWidth?: number;
rightCollapsible?: boolean;
resizable?: boolean;
splitLine?: boolean;
splitHandle?: boolean;
}

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, type CSSProperties, ref, watchEffect } from 'vue';
import type { CSSProperties } from 'vue';
import { VbenTooltip } from '@vben-core/shadcn-ui';
import { computed, ref, watchEffect } from 'vue';
interface Props {
/**

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, h, ref, type VNode, watch, watchEffect } from 'vue';
import type { VNode } from 'vue';
import { usePagination } from '@vben/hooks';
import { EmptyIcon, Grip, listIcons } from '@vben/icons';
@@ -18,8 +18,8 @@ import {
VbenIconButton,
VbenPopover,
} from '@vben-core/shadcn-ui';
import { refDebounced } from '@vueuse/core';
import { computed, h, ref, watch, watchEffect } from 'vue';
interface Props {
pageSize?: number;

View File

@@ -1,6 +1,7 @@
export * from './api-component';
export * from './captcha';
export * from './code-mirror';
export * from './col-page';
export * from './ellipsis-text';
export * from './icon-picker';
export * from './json-preview';

View File

@@ -1 +1,2 @@
export { default as Page } from './page.vue';
export * from './types';

View File

@@ -1,33 +1,17 @@
<script setup lang="ts">
import {
computed,
nextTick,
onMounted,
ref,
type StyleValue,
useTemplateRef,
} from 'vue';
import type { StyleValue } from 'vue';
import type { PageProps } from './types';
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
import { cn } from '@vben-core/shared/utils';
interface Props {
title?: string;
description?: string;
contentClass?: string;
/**
* 根据content可见高度自适应
*/
autoContentHeight?: boolean;
headerClass?: string;
footerClass?: string;
}
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
defineOptions({
name: 'Page',
});
const { autoContentHeight = false } = defineProps<Props>();
const { autoContentHeight = false } = defineProps<PageProps>();
const headerHeight = ref(0);
const footerHeight = ref(0);
@@ -100,7 +84,7 @@ onMounted(() => {
</div>
</div>
<div :class="contentClass" :style="contentStyle" class="h-full p-4">
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
<slot></slot>
</div>

View File

@@ -0,0 +1,11 @@
export interface PageProps {
title?: string;
description?: string;
contentClass?: string;
/**
* 根据content可见高度自适应
*/
autoContentHeight?: boolean;
headerClass?: string;
footerClass?: string;
}

View File

@@ -1,14 +1,13 @@
<script setup lang="ts">
import type { AboutProps, DescriptionItem } from './about';
import { h } from 'vue';
import {
VBEN_DOC_URL,
VBEN_GITHUB_URL,
VBEN_PREVIEW_URL,
} from '@vben/constants';
import { VbenRenderContent } from '@vben-core/shadcn-ui';
import { h } from 'vue';
import { Page } from '../../components';

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import type { VbenFormSchema } from '@vben-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import Title from './auth-title.vue';

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import type { VbenFormSchema } from '@vben-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import Title from './auth-title.vue';

View File

@@ -1,21 +1,23 @@
<script setup lang="ts">
import type { AuthenticationProps } from './types';
import { watch } from 'vue';
import { computed, watch } from 'vue';
import { useVbenModal } from '@vben-core/popup-ui';
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';
interface Props extends AuthenticationProps {
avatar?: string;
zIndex?: number;
}
defineOptions({
name: 'LoginExpiredModal',
});
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
avatar: '',
zIndex: 0,
});
const open = defineModel<boolean>('open');
@@ -28,6 +30,26 @@ watch(
modalApi.setState({ isOpen: val });
},
);
const getZIndex = computed(() => {
return props.zIndex || calcZIndex();
});
/**
* 获取最大的zIndex值
*/
function calcZIndex() {
let maxZ = 0;
const elements = document.querySelectorAll('*');
[...elements].forEach((element) => {
const style = window.getComputedStyle(element);
const zIndex = style.getPropertyValue('z-index');
if (zIndex && !Number.isNaN(Number.parseInt(zIndex))) {
maxZ = Math.max(maxZ, Number.parseInt(zIndex));
}
});
return maxZ + 1;
}
</script>
<template>
@@ -39,6 +61,7 @@ watch(
:footer="false"
:fullscreen-button="false"
:header="false"
:z-index="getZIndex"
class="border-none px-10 py-6 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset]"
>
<VbenAvatar :src="avatar" class="mx-auto mb-6 size-20" />

View File

@@ -3,13 +3,12 @@ import type { VbenFormSchema } from '@vben-core/form-ui';
import type { AuthenticationProps, LoginAndRegisterParams } from './types';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';
import { cloneDeep } from '@vben-core/shared/utils';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import Title from './auth-title.vue';
import ThirdPartyLogin from './third-party-login.vue';

View File

@@ -1,11 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import Title from './auth-title.vue';

View File

@@ -2,12 +2,11 @@
import type { Recordable } from '@vben/types';
import type { VbenFormSchema } from '@vben-core/form-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import Title from './auth-title.vue';

View File

@@ -38,7 +38,7 @@ defineEmits(['click']);
'border-b-0': index < 3,
'pb-4': index > 2,
}"
class="border-border group w-full cursor-pointer border-b border-r border-t p-4 transition-all hover:shadow-xl md:w-1/2 lg:w-1/3"
class="border-border group w-full cursor-pointer border-r border-t p-4 transition-all hover:shadow-xl md:w-1/2 lg:w-1/3"
>
<div class="flex items-center">
<VbenIcon

View File

@@ -38,7 +38,7 @@ defineEmits(['click']);
'pb-4': index > 2,
'border-b-0': index < 3,
}"
class="flex-col-center border-border group w-1/3 cursor-pointer border-b border-r border-t py-8 hover:shadow-xl"
class="flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl"
@click="$emit('click', item)"
>
<VbenIcon

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import type { FallbackProps } from './fallback';
import { computed, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ArrowLeft, RotateCw } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
import { computed, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
interface Props extends FallbackProps {}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.0",
"version": "5.5.2",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -25,6 +25,7 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:",
"watermark-js-plus": "catalog:"

View File

@@ -1,6 +1,7 @@
export * from './use-app-config';
export * from './use-content-maximize';
export * from './use-design-tokens';
export * from './use-hover-toggle';
export * from './use-pagination';
export * from './use-refresh';
export * from './use-tabs';

View File

@@ -0,0 +1,61 @@
import type { Arrayable, MaybeElementRef } from '@vueuse/core';
import type { Ref } from 'vue';
import { isFunction } from '@vben/utils';
import { useMouseInElement } from '@vueuse/core';
import { computed, onUnmounted, ref, watch } from 'vue';
/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true否则返回 false
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间
* @returns 返回一个数组,第一个元素是一个 ref表示鼠标是否在元素内部第二个元素是一个控制器可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef>,
delay: (() => number) | number = 500,
) {
const isOutsides: Array<Ref<boolean>> = [];
const value = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
const refs = Array.isArray(refElement) ? refElement : [refElement];
refs.forEach((refEle) => {
const listener = useMouseInElement(refEle, { handleOutside: true });
isOutsides.push(listener.isOutside);
});
const isOutsideAll = computed(() => isOutsides.every((v) => v.value));
function setValueDelay(val: boolean) {
timer.value && clearTimeout(timer.value);
timer.value = setTimeout(
() => {
value.value = val;
timer.value = undefined;
},
isFunction(delay) ? delay() : delay,
);
}
const watcher = watch(
isOutsideAll,
(val) => {
setValueDelay(!val);
},
{ immediate: true },
);
const controller = {
enable() {
watcher.resume();
},
disable() {
watcher.pause();
},
};
onUnmounted(() => {
timer.value && clearTimeout(timer.value);
});
return [value, controller] as [typeof value, typeof controller];
}

View File

@@ -1,4 +1,5 @@
import type { Ref } from 'vue';
import { computed, ref, unref } from 'vue';
/**

View File

@@ -1,6 +1,7 @@
import { type RouteLocationNormalized, useRoute, useRouter } from 'vue-router';
import type { RouteLocationNormalized } from 'vue-router';
import { useTabbarStore } from '@vben/stores';
import { useRoute, useRouter } from 'vue-router';
export function useTabs() {
const router = useRouter();

View File

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

View File

@@ -1,14 +1,13 @@
<script lang="ts" setup>
import type { VNode } from 'vue';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { type VNode } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useTabbarStore } from '@vben/stores';
import { RouterView } from 'vue-router';
import { IFrameRouterView } from '../../iframe';

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import { computed, useSlots } from 'vue';
import { useRefresh } from '@vben/hooks';
import { RotateCw } from '@vben/icons';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
import { computed, useSlots } from 'vue';
import {
GlobalSearch,
@@ -133,7 +132,10 @@ function clearPreferencesAndLogout() {
>
<slot :name="slot.name"></slot>
</template>
<div class="flex h-full min-w-0 flex-1 items-center">
<div
:class="`menu-align-${preferences.header.menuAlign}`"
class="flex h-full min-w-0 flex-1 items-center"
>
<slot name="menu"></slot>
</div>
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
@@ -166,3 +168,16 @@ function clearPreferencesAndLogout() {
</template>
</div>
</template>
<style lang="scss" scoped>
.menu-align-start {
--menu-align: start;
}
.menu-align-center {
--menu-align: center;
}
.menu-align-end {
--menu-align: end;
}
</style>

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben/types';
import { computed, useSlots, watch } from 'vue';
import type { SetupContext } from 'vue';
import { useRefresh } from '@vben/hooks';
import { $t } from '@vben/locales';
@@ -14,6 +13,7 @@ import { useLockStore } from '@vben/stores';
import { cloneDeep, mapTree } from '@vben/utils';
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { computed, useSlots, watch } from 'vue';
import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
import { LayoutContent, LayoutContentSpinner } from './content';
@@ -39,6 +39,8 @@ const {
isMixedNav,
isMobile,
isSideMixedNav,
isHeaderMixedNav,
isHeaderSidebarNav,
layout,
preferencesButtonPosition,
sidebarCollapsed,
@@ -80,16 +82,32 @@ const logoCollapsed = computed(() => {
if (isMobile.value && sidebarCollapsed.value) {
return true;
}
if (isHeaderNav.value || isMixedNav.value) {
if (isHeaderNav.value || isMixedNav.value || isHeaderSidebarNav.value) {
return false;
}
return sidebarCollapsed.value || isSideMixedNav.value;
return (
sidebarCollapsed.value || isSideMixedNav.value || isHeaderMixedNav.value
);
});
const showHeaderNav = computed(() => {
return !isMobile.value && (isHeaderNav.value || isMixedNav.value);
return (
!isMobile.value &&
(isHeaderNav.value || isMixedNav.value || isHeaderMixedNav.value)
);
});
const {
handleMenuSelect,
handleMenuOpen,
headerActive,
headerMenus,
sidebarActive,
sidebarMenus,
mixHeaderMenus,
sidebarVisible,
} = useMixedMenu();
// 侧边多列菜单
const {
extraActiveMenu,
@@ -99,16 +117,7 @@ const {
handleMixedMenuSelect,
handleSideMouseLeave,
sidebarExtraVisible,
} = useExtraMenu();
const {
handleMenuSelect,
headerActive,
headerMenus,
sidebarActive,
sidebarMenus,
sidebarVisible,
} = useMixedMenu();
} = useExtraMenu(mixHeaderMenus);
/**
* 包装菜单,翻译菜单名称
@@ -151,9 +160,9 @@ watch(
);
// 语言更新后,刷新页面
watch(() => preferences.app.locale, refresh);
watch(() => preferences.app.locale, refresh, { flush: 'post' });
const slots = useSlots();
const slots: SetupContext['slots'] = useSlots();
const headerSlots = computed(() => {
return Object.keys(slots).filter((key) => key.startsWith('header-'));
});
@@ -260,13 +269,14 @@ const headerSlots = computed(() => {
:rounded="isMenuRounded"
:theme="sidebarTheme"
mode="vertical"
@open="handleMenuOpen"
@select="handleMenuSelect"
/>
</template>
<template #mixed-menu>
<LayoutMixedMenu
:active-path="extraActiveMenu"
:menus="wrapperMenus(headerMenus, false)"
:menus="wrapperMenus(mixHeaderMenus, false)"
:rounded="isMenuRounded"
:theme="sidebarTheme"
@default-select="handleDefaultSelect"

View File

@@ -2,9 +2,8 @@
import type { MenuRecordRaw } from '@vben/types';
import type { MenuProps } from '@vben-core/menu-ui';
import { useRoute } from 'vue-router';
import { Menu } from '@vben-core/menu-ui';
import { useRoute } from 'vue-router';
import { useNavigation } from './use-navigation';

View File

@@ -14,12 +14,17 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
open: [string, string[]];
select: [string, string?];
}>();
function handleMenuSelect(key: string) {
emit('select', key, props.mode);
}
function handleMenuOpen(key: string, path: string[]) {
emit('open', key, path);
}
</script>
<template>
@@ -32,6 +37,7 @@ function handleMenuSelect(key: string) {
:mode="mode"
:rounded="rounded"
:theme="theme"
@open="handleMenuOpen"
@select="handleMenuSelect"
/>
</template>

View File

@@ -2,11 +2,10 @@
import type { MenuRecordRaw } from '@vben/types';
import type { NormalMenuProps } from '@vben-core/menu-ui';
import { onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { findMenuByPath } from '@vben/utils';
import { NormalMenu } from '@vben-core/menu-ui';
import { onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
interface Props extends NormalMenuProps {}

View File

@@ -1,24 +1,30 @@
import type { MenuRecordRaw } from '@vben/types';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { ComputedRef } from 'vue';
import { preferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { findRootMenuByPath } from '@vben/utils';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useNavigation } from './use-navigation';
function useExtraMenu() {
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
const accessStore = useAccessStore();
const { navigation } = useNavigation();
const menus = computed(() => accessStore.accessMenus);
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
/** 记录当前顶级菜单下哪个子菜单最后激活 */
const defaultSubMap = new Map<string, string>();
const extraRootMenus = ref<MenuRecordRaw[]>([]);
const route = useRoute();
const extraMenus = ref<MenuRecordRaw[]>([]);
const sidebarExtraVisible = ref<boolean>(false);
const extraActiveMenu = ref('');
const parentLevel = computed(() =>
preferences.app.layout === 'header-mixed-nav' ? 1 : 0,
);
/**
* 选择混合菜单事件
@@ -26,12 +32,18 @@ function useExtraMenu() {
*/
const handleMixedMenuSelect = async (menu: MenuRecordRaw) => {
extraMenus.value = menu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;
const hasChildren = extraMenus.value.length > 0;
sidebarExtraVisible.value = hasChildren;
if (!hasChildren) {
await navigation(menu.path);
} else if (preferences.sidebar.autoActivateChild) {
await navigation(
defaultSubMap.has(menu.path)
? (defaultSubMap.get(menu.path) as string)
: menu.path,
);
}
};
@@ -40,12 +52,12 @@ function useExtraMenu() {
* @param menu
* @param rootMenu
*/
const handleDefaultSelect = (
const handleDefaultSelect = async (
menu: MenuRecordRaw,
rootMenu?: MenuRecordRaw,
) => {
extraMenus.value = rootMenu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
extraMenus.value = rootMenu?.children ?? extraRootMenus.value ?? [];
extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;
if (preferences.sidebar.expandOnHover) {
sidebarExtraVisible.value = extraMenus.value.length > 0;
@@ -59,7 +71,6 @@ function useExtraMenu() {
if (preferences.sidebar.expandOnHover) {
return;
}
sidebarExtraVisible.value = false;
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
@@ -73,24 +84,31 @@ function useExtraMenu() {
if (!preferences.sidebar.expandOnHover) {
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
extraMenus.value = findMenu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;
sidebarExtraVisible.value = extraMenus.value.length > 0;
}
};
function calcExtraMenus(path: string) {
const currentPath = route.meta?.activePath || path;
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
currentPath,
parentLevel.value,
);
extraRootMenus.value = rootMenu?.children ?? [];
if (rootMenuPath) defaultSubMap.set(rootMenuPath, currentPath);
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
extraMenus.value = rootMenu?.children ?? [];
if (preferences.sidebar.expandOnHover) {
sidebarExtraVisible.value = extraMenus.value.length > 0;
}
}
watch(
() => route.path,
(path) => {
const currentPath = route.meta?.activePath || path;
// if (preferences.sidebar.expandOnHover) {
// return;
// }
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
currentPath,
);
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
extraMenus.value = rootMenu?.children ?? [];
() => [route.path, preferences.app.layout],
([path]) => {
calcExtraMenus(path || '');
},
{ immediate: true },
);

View File

@@ -15,11 +15,16 @@ function useMixedMenu() {
const route = useRoute();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
const rootMenuPath = ref<string>('');
const { isMixedNav } = usePreferences();
const mixedRootMenuPath = ref<string>('');
const mixExtraMenus = ref<MenuRecordRaw[]>([]);
/** 记录当前顶级菜单下哪个子菜单最后激活 */
const defaultSubMap = new Map<string, string>();
const { isMixedNav, isHeaderMixedNav } = usePreferences();
const needSplit = computed(
() => preferences.navigation.split && isMixedNav.value,
() =>
(preferences.navigation.split && isMixedNav.value) ||
isHeaderMixedNav.value,
);
const sidebarVisible = computed(() => {
@@ -53,6 +58,10 @@ function useMixedMenu() {
return needSplit.value ? splitSideMenus.value : menus.value;
});
const mixHeaderMenus = computed(() => {
return isHeaderMixedNav.value ? sidebarMenus.value : headerMenus.value;
});
/**
* 侧边菜单激活路径
*/
@@ -86,6 +95,25 @@ function useMixedMenu() {
splitSideMenus.value = rootMenu?.children ?? [];
if (splitSideMenus.value.length === 0) {
navigation(key);
} else if (rootMenu && preferences.sidebar.autoActivateChild) {
navigation(
defaultSubMap.has(rootMenu.path)
? (defaultSubMap.get(rootMenu.path) as string)
: rootMenu.path,
);
}
};
/**
* 侧边菜单展开事件
* @param key 路由路径
* @param parentsPath 父级路径
*/
const handleMenuOpen = (key: string, parentsPath: string[]) => {
if (parentsPath.length <= 1 && preferences.sidebar.autoActivateChild) {
navigation(
defaultSubMap.has(key) ? (defaultSubMap.get(key) as string) : key,
);
}
};
@@ -98,6 +126,9 @@ function useMixedMenu() {
if (!rootMenu) {
rootMenu = menus.value.find((item) => item.path === path);
}
const result = findRootMenuByPath(rootMenu?.children || [], path, 1);
mixedRootMenuPath.value = result.rootMenuPath ?? '';
mixExtraMenus.value = result.rootMenu?.children ?? [];
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? [];
}
@@ -107,6 +138,8 @@ function useMixedMenu() {
(path) => {
const currentPath = (route?.meta?.activePath as string) ?? path;
calcSideMenus(currentPath);
if (rootMenuPath.value)
defaultSubMap.set(rootMenuPath.value, currentPath);
},
{ immediate: true },
);
@@ -118,10 +151,13 @@ function useMixedMenu() {
return {
handleMenuSelect,
handleMenuOpen,
headerActive,
headerMenus,
sidebarActive,
sidebarMenus,
mixHeaderMenus,
mixExtraMenus,
sidebarVisible,
};
}

View File

@@ -1,6 +1,7 @@
import { type RouteRecordNormalized, useRouter } from 'vue-router';
import type { RouteRecordNormalized } from 'vue-router';
import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';
import { useRouter } from 'vue-router';
function useNavigation() {
const router = useRouter();

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
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 { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useTabbar } from './use-tabbar';
@@ -55,6 +54,7 @@ if (!preferences.tabbar.persist) {
:show-icon="showIcon"
:style-type="preferences.tabbar.styleType"
:tabs="currentTabs"
:wheelable="preferences.tabbar.wheelable"
@close="handleClose"
@sort-tabs="tabbarStore.sortTabs"
@unpin="unpinTab"

View File

@@ -2,9 +2,6 @@ import type { TabDefinition } from '@vben/types';
import type { IContextMenuItem } from '@vben-core/tabs-ui';
import type { RouteLocationNormalizedGeneric } from 'vue-router';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben/hooks';
import {
ArrowLeftToLine,
@@ -22,6 +19,8 @@ import {
import { $t, useI18n } from '@vben/locales';
import { useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export function useTabbar() {
const router = useRouter();

View File

@@ -1,12 +1,11 @@
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { preferences } from '@vben/preferences';
import { useTabbarStore } from '@vben/stores';
import { VbenSpinner } from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({ name: 'IFrameRouterView' });

View File

@@ -2,11 +2,10 @@
import type { BreadcrumbStyleType } from '@vben/types';
import type { IBreadcrumb } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { VbenBreadcrumbView } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
interface Props {
hideWhenOnlyOne?: boolean;

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { useVbenModal } from '@vben-core/popup-ui';
import { onMounted, onUnmounted, ref } from 'vue';
interface Props {
// 轮训时间,分钟

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben/types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import {
ArrowDown,
ArrowUp,
@@ -13,8 +11,8 @@ import {
import { $t } from '@vben/locales';
import { isWindowsOs } from '@vben/utils';
import { useVbenModal } from '@vben-core/popup-ui';
import { useMagicKeys, whenever } from '@vueuse/core';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import SearchPanel from './search-panel.vue';

View File

@@ -1,16 +1,14 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben/types';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';
import { SearchX, X } from '@vben/icons';
import { $t } from '@vben/locales';
import { mapTree, traverseTreeValues, uniqueByField } from '@vben/utils';
import { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { isHttpUrl } from '@vben-core/shared/utils';
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';
defineOptions({
name: 'SearchPanel',

View File

@@ -2,8 +2,6 @@
import type { AuthPageLayoutType } from '@vben/types';
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import { InspectionPanel, PanelLeft, PanelRight } from '@vben/icons';
import { $t } from '@vben/locales';
import {
@@ -12,6 +10,7 @@ import {
usePreferences,
} from '@vben/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
defineOptions({
name: 'AuthenticationLayoutToggle',

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { computed, reactive } from 'vue';
import { $t } from '@vben/locales';
import { useVbenForm, z } from '@vben-core/form-ui';
import { useVbenModal } from '@vben-core/popup-ui';
import { VbenAvatar, VbenButton } from '@vben-core/shadcn-ui';
import { computed, reactive } from 'vue';
interface Props {
avatar?: string;

View File

@@ -1,14 +1,12 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { LockKeyhole } from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { storeToRefs, useLockStore } from '@vben/stores';
import { useScrollLock } from '@vben-core/composables';
import { useVbenForm, z } from '@vben-core/form-ui';
import { VbenAvatar, VbenButton } from '@vben-core/shadcn-ui';
import { useDateFormat, useNow } from '@vueuse/core';
import { computed, reactive, ref } from 'vue';
interface Props {
avatar?: string;

View File

@@ -9,7 +9,6 @@ import {
VbenPopover,
VbenScrollbar,
} from '@vben-core/shadcn-ui';
import { useToggle } from '@vueuse/core';
interface Props {

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
import { useSlots } from 'vue';
defineOptions({
name: 'PreferenceSelectItem',

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { type Component, computed } from 'vue';
import type { Component } from 'vue';
import { $t } from '@vben/locales';
import { computed } from 'vue';
import { ContentCompact, ContentWide } from '../../icons';

View File

@@ -1,15 +1,22 @@
<script setup lang="ts">
import type { LayoutHeaderModeType, SelectOption } from '@vben/types';
import type {
LayoutHeaderMenuAlignType,
LayoutHeaderModeType,
SelectOption,
} from '@vben/types';
import { $t } from '@vben/locales';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
import ToggleItem from '../toggle-item.vue';
defineProps<{ disabled: boolean }>();
const headerEnable = defineModel<boolean>('headerEnable');
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
const headerMenuAlign =
defineModel<LayoutHeaderMenuAlignType>('headerMenuAlign');
const localeItems: SelectOption[] = [
{
@@ -29,6 +36,21 @@ const localeItems: SelectOption[] = [
value: 'auto-scroll',
},
];
const headerMenuAlignItems: SelectOption[] = [
{
label: $t('preferences.header.menuAlignStart'),
value: 'start',
},
{
label: $t('preferences.header.menuAlignCenter'),
value: 'center',
},
{
label: $t('preferences.header.menuAlignEnd'),
value: 'end',
},
];
</script>
<template>
@@ -42,4 +64,11 @@ const localeItems: SelectOption[] = [
>
{{ $t('preferences.mode') }}
</SelectItem>
<ToggleItem
v-model="headerMenuAlign"
:disabled="!headerEnable"
:items="headerMenuAlignItems"
>
{{ $t('preferences.header.menuAlign') }}
</ToggleItem>
</template>

View File

@@ -1,15 +1,17 @@
<script setup lang="ts">
import type { LayoutType } from '@vben/types';
import { type Component, computed } from 'vue';
import type { Component } from 'vue';
import { CircleHelp } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenTooltip } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import {
FullContent,
HeaderMixedNav,
HeaderNav,
HeaderSidebarNav,
MixedNav,
SidebarMixedNav,
SidebarNav,
@@ -33,6 +35,8 @@ const components: Record<LayoutType, Component> = {
'mixed-nav': MixedNav,
'sidebar-mixed-nav': SidebarMixedNav,
'sidebar-nav': SidebarNav,
'header-mixed-nav': HeaderMixedNav,
'header-sidebar-nav': HeaderSidebarNav,
};
const PRESET = computed((): PresetItem[] => [
@@ -51,11 +55,21 @@ const PRESET = computed((): PresetItem[] => [
tip: $t('preferences.horizontalTip'),
type: 'header-nav',
},
{
name: $t('preferences.headerSidebarNav'),
tip: $t('preferences.headerSidebarNavTip'),
type: 'header-sidebar-nav',
},
{
name: $t('preferences.mixedMenu'),
tip: $t('preferences.mixedMenuTip'),
type: 'mixed-nav',
},
{
name: $t('preferences.headerTwoColumn'),
tip: $t('preferences.headerTwoColumnTip'),
type: 'header-mixed-nav',
},
{
name: $t('preferences.fullContent'),
tip: $t('preferences.fullContentTip'),

View File

@@ -1,17 +1,23 @@
<script setup lang="ts">
import type { LayoutType } from '@vben/types';
import { $t } from '@vben/locales';
import NumberFieldItem from '../number-field-item.vue';
import SwitchItem from '../switch-item.vue';
defineProps<{ disabled: boolean }>();
defineProps<{ currentLayout?: LayoutType; disabled: boolean }>();
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarWidth = defineModel<number>('sidebarWidth');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const sidebarAutoActivateChild = defineModel<boolean>(
'sidebarAutoActivateChild',
);
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
</script>
<template>
@@ -21,12 +27,32 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
{{ $t('preferences.sidebar.collapsed') }}
</SwitchItem>
<SwitchItem
v-model="sidebarExpandOnHover"
:disabled="!sidebarEnable || disabled || !sidebarCollapsed"
:tip="$t('preferences.sidebar.expandOnHoverTip')"
>
{{ $t('preferences.sidebar.expandOnHover') }}
</SwitchItem>
<SwitchItem
v-model="sidebarCollapsedShowTitle"
:disabled="!sidebarEnable || disabled || !sidebarCollapsed"
>
{{ $t('preferences.sidebar.collapsedShowTitle') }}
</SwitchItem>
<SwitchItem
v-model="sidebarAutoActivateChild"
:disabled="
!sidebarEnable ||
!['sidebar-mixed-nav', 'mixed-nav', 'header-mixed-nav'].includes(
currentLayout as string,
) ||
disabled
"
:tip="$t('preferences.sidebar.autoActivateChildTip')"
>
{{ $t('preferences.sidebar.autoActivateChild') }}
</SwitchItem>
<NumberFieldItem
v-model="sidebarWidth"
:disabled="!sidebarEnable || disabled"

View File

@@ -18,6 +18,7 @@ const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
@@ -53,6 +54,13 @@ const styleItems = computed((): SelectOption[] => [
<SwitchItem v-model="tabbarDraggable" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.draggable') }}
</SwitchItem>
<SwitchItem
v-model="tabbarWheelable"
:disabled="!tabbarEnable"
:tip="$t('preferences.tabbar.wheelableTip')"
>
{{ $t('preferences.tabbar.wheelable') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }}
</SwitchItem>

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import {
NumberField,
@@ -12,6 +10,7 @@ import {
NumberFieldInput,
VbenTooltip,
} from '@vben-core/shadcn-ui';
import { useSlots } from 'vue';
defineOptions({
name: 'PreferenceSelectItem',

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import {
Select,
@@ -12,6 +10,7 @@ import {
SelectValue,
VbenTooltip,
} from '@vben-core/shadcn-ui';
import { useSlots } from 'vue';
defineOptions({
name: 'PreferenceSelectItem',

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import { Switch, VbenTooltip } from '@vben-core/shadcn-ui';
import { useSlots } from 'vue';
defineOptions({
name: 'PreferenceSwitchItem',
});
withDefaults(defineProps<{ disabled?: boolean }>(), {
withDefaults(defineProps<{ disabled?: boolean; tip?: string }>(), {
disabled: false,
tip: '',
});
const checked = defineModel<boolean>();
@@ -32,11 +32,17 @@ function handleClick() {
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<VbenTooltip v-if="slots.tip || tip" side="bottom">
<template #trigger>
<CircleHelp class="ml-1 size-3 cursor-help" />
</template>
<slot name="tip"></slot>
<slot name="tip">
<template v-if="tip">
<p v-for="(line, index) in tip.split('\n')" :key="index">
{{ line }}
</p>
</template>
</slot>
</VbenTooltip>
</span>
<span v-if="$slots.shortcut" class="ml-auto mr-2 text-xs opacity-60">

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import type { BuiltinThemePreset } from '@vben/preferences';
import type { BuiltinThemeType } from '@vben/types';
import { computed, ref } from 'vue';
import { UserRoundPen } from '@vben/icons';
import { $t } from '@vben/locales';
import {
BUILT_IN_THEME_PRESETS,
type BuiltinThemePreset,
} from '@vben/preferences';
import { BUILT_IN_THEME_PRESETS } from '@vben/preferences';
import { convertToHsl, TinyColor } from '@vben/utils';
import { computed, ref } from 'vue';
defineOptions({
name: 'PreferenceBuiltinTheme',

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { ThemeModeType } from '@vben/types';
import type { Component } from 'vue';
import { MoonStar, Sun, SunMoon } from '@vben/icons';

View File

@@ -0,0 +1,202 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<path
id="svg_2"
d="m-3.37838,3.7543a1.93401,4.02457 0 0 1 1.93401,-4.02457l11.3488,0l0,66.40541l-11.3488,0a1.93401,4.02457 0 0 1 -1.93401,-4.02457l0,-58.35627z"
fill="hsl(var(--primary))"
stroke="null"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="15.46086"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.67897"
rx="2"
stroke="null"
width="8.18938"
x="0.58676"
y="1.42154"
/>
<rect
id="svg_8"
fill="hsl(var(--primary))"
height="9.07027"
rx="2"
stroke="null"
width="75.91967"
x="25.38277"
y="1.42876"
/>
<rect
id="svg_9"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="27.91529"
y="3.69284"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="3.62876"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.78868"
y="3.69981"
/>
<rect
id="svg_12"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="94.6847"
y="3.62876"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="42.9287"
x="58.75427"
y="14.613"
/>
<rect
id="svg_14"
fill="currentColor"
fill-opacity="0.08"
height="20.97838"
rx="2"
stroke="null"
width="28.36894"
x="26.14342"
y="14.613"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="75.09493"
x="26.34264"
y="39.68822"
/>
<rect
id="svg_5"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.79832"
y="28.39462"
/>
<rect
id="svg_6"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="41.80156"
/>
<rect
id="svg_7"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="55.36623"
/>
<rect
id="svg_16"
fill="currentColor"
fill-opacity="0.08"
height="65.72065"
stroke="null"
width="12.49265"
x="9.85477"
y="-0.02618"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="35.14924"
y="4.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="47.25735"
y="4.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="59.23033"
y="4.07319"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,177 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<rect
id="svg_8"
fill="currentColor"
fill-opacity="0.08"
height="9.07027"
stroke="null"
width="104.07934"
x="-0.07419"
y="-0.05773"
/>
<rect
id="svg_3"
fill="#b2b2b2"
height="1.689"
rx="1.395"
stroke="null"
width="6.52486"
x="10.08168"
y="3.50832"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="2.89362"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.58249"
y="2.89362"
/>
<path
id="svg_12"
d="m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z"
fill="#ffffff"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="44.13071"
x="53.37873"
y="13.45652"
/>
<path
id="svg_14"
d="m19.4393,15.74245c0,-1.08676 0.79001,-2 1.73013,-2l23.18605,0c0.94011,0 1.73013,0.91324 1.73013,2l0,17.24865c0,1.08676 -0.79001,2 -1.73013,2l-23.18605,0c-0.94011,0 -1.73013,-0.91324 -1.73013,-2l0,-17.24865z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="78.39372"
x="19.93575"
y="39.34689"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="28.14924"
y="3.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="41.25735"
y="3.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="54.23033"
y="3.07319"
/>
<rect
id="svg_4"
fill="#ffffff"
height="5.13843"
rx="2"
stroke="null"
width="5.78397"
x="1.5327"
y="1.081"
/>
<rect
id="svg_5"
fill="hsl(var(--primary))"
height="56.81191"
stroke="null"
width="15.44642"
x="-0.06423"
y="9.03113"
/>
<path
id="svg_2"
d="m2.38669,15.38074c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_6"
d="m2.38669,28.43336c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_7"
d="m2.17616,41.27545c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
<path
id="svg_9"
d="m2.17616,54.32806c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="#fff"
opacity="undefined"
stroke="null"
/>
</g>
</svg>
</template>

View File

@@ -2,6 +2,8 @@ import HeaderNav from './header-nav.vue';
export { default as ContentCompact } from './content-compact.vue';
export { default as FullContent } from './full-content.vue';
export { default as HeaderMixedNav } from './header-mixed-nav.vue';
export { default as HeaderSidebarNav } from './header-sidebar-nav.vue';
export { default as MixedNav } from './mixed-nav.vue';
export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue';

View File

@@ -4,6 +4,7 @@ import type {
BreadcrumbStyleType,
BuiltinThemeType,
ContentCompactType,
LayoutHeaderMenuAlignType,
LayoutHeaderModeType,
LayoutType,
NavigationStyleType,
@@ -12,8 +13,6 @@ import type {
} from '@vben/types';
import type { SegmentedItem } from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
import { Copy, RotateCw } from '@vben/icons';
import { $t, loadLocaleMessages } from '@vben/locales';
import {
@@ -29,8 +28,8 @@ import {
VbenSegmented,
} from '@vben-core/shadcn-ui';
import { globalShareState } from '@vben-core/shared/global-state';
import { useClipboard } from '@vueuse/core';
import { computed, ref } from 'vue';
import {
Animation,
@@ -87,9 +86,15 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const sidebarAutoActivateChild = defineModel<boolean>(
'sidebarAutoActivateChild',
);
const SidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
const headerEnable = defineModel<boolean>('headerEnable');
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
const headerMenuAlign =
defineModel<LayoutHeaderMenuAlignType>('headerMenuAlign');
const breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');
const breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');
@@ -105,6 +110,7 @@ const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
const navigationStyleType = defineModel<NavigationStyleType>(
@@ -154,6 +160,7 @@ const {
isDark,
isFullContent,
isHeaderNav,
isHeaderSidebarNav,
isMixedNav,
isSideMixedNav,
isSideMode,
@@ -298,10 +305,13 @@ async function handleReset() {
<Block :title="$t('preferences.sidebar.title')">
<Sidebar
v-model:sidebar-auto-activate-child="sidebarAutoActivateChild"
v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable"
v-model:sidebar-expand-on-hover="SidebarExpandOnHover"
v-model:sidebar-width="sidebarWidth"
:current-layout="appLayout"
:disabled="!isSideMode"
/>
</Block>
@@ -309,6 +319,7 @@ async function handleReset() {
<Block :title="$t('preferences.header.title')">
<Header
v-model:header-enable="headerEnable"
v-model:header-menu-align="headerMenuAlign"
v-model:header-mode="headerMode"
:disabled="isFullContent"
/>
@@ -332,7 +343,8 @@ async function handleReset() {
v-model:breadcrumb-show-icon="breadcrumbShowIcon"
v-model:breadcrumb-style-type="breadcrumbStyleType"
:disabled="
!showBreadcrumbConfig || !(isSideNav || isSideMixedNav)
!showBreadcrumbConfig ||
!(isSideNav || isSideMixedNav || isHeaderSidebarNav)
"
/>
</Block>
@@ -345,6 +357,7 @@ async function handleReset() {
v-model:tabbar-show-maximize="tabbarShowMaximize"
v-model:tabbar-show-more="tabbarShowMore"
v-model:tabbar-style-type="tabbarStyleType"
v-model:tabbar-wheelable="tabbarWheelable"
/>
</Block>
<Block :title="$t('preferences.widget.title')">

View File

@@ -1,12 +1,11 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Settings } from '@vben/icons';
import { $t, loadLocaleMessages } from '@vben/locales';
import { preferences, updatePreferences } from '@vben/preferences';
import { capitalizeFirstLetter } from '@vben/utils';
import { useVbenDrawer } from '@vben-core/popup-ui';
import { VbenButton } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import PreferencesDrawer from './preferences-drawer.vue';

View File

@@ -1,9 +1,8 @@
<script setup lang="ts">
import type { AnyFunction } from '@vben/types';
import type { Component } from 'vue';
import { computed, ref } from 'vue';
import { useHoverToggle } from '@vben/hooks';
import { LockKeyhole, LogOut } from '@vben/icons';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
@@ -22,8 +21,8 @@ import {
VbenAvatar,
VbenIcon,
} from '@vben-core/shadcn-ui';
import { useMagicKeys, whenever } from '@vueuse/core';
import { computed, useTemplateRef, watch } from 'vue';
import { LockScreenModal } from '../lock-screen';
@@ -53,6 +52,10 @@ interface Props {
* 文本
*/
text?: string;
/** 触发方式 */
trigger?: 'both' | 'click' | 'hover';
/** hover触发时延迟响应的时间 */
hoverDelay?: number;
}
defineOptions({
@@ -67,10 +70,11 @@ const props = withDefaults(defineProps<Props>(), {
showShortcutKey: true,
tagText: '',
text: '',
trigger: 'click',
hoverDelay: 500,
});
const emit = defineEmits<{ logout: [] }>();
const openPopover = ref(false);
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
usePreferences();
@@ -84,6 +88,27 @@ const [LogoutModal, logoutModalApi] = useVbenModal({
},
});
const refTrigger = useTemplateRef('refTrigger');
const refContent = useTemplateRef('refContent');
const [openPopover, hoverWatcher] = useHoverToggle(
[refTrigger, refContent],
() => props.hoverDelay,
);
watch(
() => props.trigger === 'hover' || props.trigger === 'both',
(val) => {
if (val) {
hoverWatcher.enable();
} else {
hoverWatcher.disable();
}
},
{
immediate: true,
},
);
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
const enableLogoutShortcutKey = computed(() => {
@@ -155,8 +180,8 @@ if (enableShortcutKey.value) {
{{ $t('ui.widgets.logoutTip') }}
</LogoutModal>
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenu v-model:open="openPopover">
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
<div class="hover:text-accent-foreground flex-center">
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
@@ -164,64 +189,66 @@ if (enableShortcutKey.value) {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
<div ref="refContent">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

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

View File

@@ -1,12 +1,9 @@
import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import type EchartsUI from './echarts-ui.vue';
import type { Ref } from 'vue';
import { computed, nextTick, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
import {
tryOnUnmounted,
useDebounceFn,
@@ -14,6 +11,7 @@ import {
useTimeoutFn,
useWindowSize,
} from '@vueuse/core';
import { computed, nextTick, watch } from 'vue';
import echarts from './echarts';
@@ -110,6 +108,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return {
renderEcharts,
resize,
chartInstance
};
}

View File

@@ -3,15 +3,15 @@ import type { VxeGridInstance } from 'vxe-table';
import type { VxeGridProps } from './types';
import { toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import {
bindMethods,
isBoolean,
isFunction,
mergeWithArrayOverride,
StateHandler,
} from '@vben-core/shared/utils';
import { toRaw } from 'vue';
function getDefaultState(): VxeGridProps {
return {
@@ -20,6 +20,7 @@ function getDefaultState(): VxeGridProps {
gridOptions: {},
gridEvents: {},
formOptions: undefined,
showSearchForm: true,
};
}
@@ -108,6 +109,16 @@ export class VxeGridApi {
}
}
toggleSearchForm(show?: boolean) {
this.setState({
showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm,
});
// nextTick(() => {
// this.grid.recalculate();
// });
return this.state?.showSearchForm;
}
unmount() {
this.isMounted = false;
this.stateHandler.reset();

View File

@@ -1,4 +1,10 @@
export { setupVbenVxeTable } from './init';
export type { VxeTableGridOptions } from './types';
export * from './use-vxe-grid';
export { default as VbenVxeGrid } from './use-vxe-grid.vue';
export type { VxeGridDefines, VxeGridListeners, VxeGridProps } from 'vxe-table';
export type {
VxeGridDefines,
VxeGridListeners,
VxeGridProps,
VxeGridPropTypes,
} from 'vxe-table';

View File

@@ -1,10 +1,8 @@
import type { SetupVxeTable } from './types';
import { defineComponent, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
import { useVbenForm } from '@vben-core/form-ui';
import { defineComponent, watch } from 'vue';
import {
VxeButton,
VxeCheckbox,
@@ -34,7 +32,6 @@ import {
// VxeTextarea,
} from 'vxe-pc-ui';
import enUS from 'vxe-pc-ui/lib/language/en-US';
// 导入默认的语言
import zhCN from 'vxe-pc-ui/lib/language/zh-CN';
import {

View File

@@ -1,15 +1,15 @@
import type { ClassType, DeepPartial } from '@vben/types';
import type { VbenFormProps } from '@vben-core/form-ui';
import type { Ref } from 'vue';
import type {
VxeGridListeners,
VxeGridPropTypes,
VxeGridProps as VxeTableGridProps,
VxeUIExport,
} from 'vxe-table';
import type { VxeGridApi } from './api';
import type { Ref } from 'vue';
import { useVbenForm } from '@vben-core/form-ui';
export interface VxePaginationInfo {
@@ -18,6 +18,16 @@ export interface VxePaginationInfo {
total: number;
}
interface ToolbarConfigOptions extends VxeGridPropTypes.ToolbarConfig {
/** 是否显示切换搜索表单的按钮 */
search?: boolean;
}
export interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {
/** 工具栏配置 */
toolbarConfig?: ToolbarConfigOptions;
}
export interface VxeGridProps {
/**
* 标题
@@ -38,7 +48,7 @@ export interface VxeGridProps {
/**
* vxe-grid 配置
*/
gridOptions?: DeepPartial<VxeTableGridProps>;
gridOptions?: DeepPartial<VxeTableGridOptions>;
/**
* vxe-grid 事件
*/
@@ -47,6 +57,10 @@ export interface VxeGridProps {
* 表单配置
*/
formOptions?: VbenFormProps;
/**
* 显示搜索表单
*/
showSearchForm?: boolean;
}
export type ExtendedVxeGridApi = {

View File

@@ -1,12 +1,22 @@
<script lang="ts" setup>
import type { VbenFormProps } from '@vben-core/form-ui';
import type {
VxeGridDefines,
VxeGridInstance,
VxeGridListeners,
VxeGridPropTypes,
VxeGridProps as VxeTableGridProps,
VxeToolbarPropTypes,
} from 'vxe-table';
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
import { usePriorityValues } from '@vben/hooks';
import { EmptyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { usePreferences } from '@vben/preferences';
import { cloneDeep, cn, mergeWithArrayOverride } from '@vben/utils';
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
import {
computed,
nextTick,
@@ -17,14 +27,6 @@ import {
useTemplateRef,
watch,
} from 'vue';
import { usePriorityValues } from '@vben/hooks';
import { EmptyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { usePreferences } from '@vben/preferences';
import { cloneDeep, cn, mergeWithArrayOverride } from '@vben/utils';
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
import { VxeGrid, VxeUI } from 'vxe-table';
import { extendProxyOptions } from './extends';
@@ -57,6 +59,7 @@ const {
formOptions,
tableTitle,
tableTitleHelp,
showSearchForm,
} = usePriorityValues(props, state);
const { isMobile } = usePreferences();
@@ -64,6 +67,7 @@ const { isMobile } = usePreferences();
const slots = useSlots();
const [Form, formApi] = useTableForm({
compact: true,
handleSubmit: async () => {
const formValues = formApi.form.values;
formApi.setLatestSubmissionValues(toRaw(formValues));
@@ -104,22 +108,37 @@ const showToolbar = computed(() => {
const toolbarOptions = computed(() => {
const slotActions = slots[TOOLBAR_ACTIONS]?.();
const slotTools = slots[TOOLBAR_TOOLS]?.();
const searchBtn: VxeToolbarPropTypes.ToolConfig = {
code: 'search',
icon: 'vxe-icon--search',
circle: true,
status: showSearchForm.value ? 'primary' : undefined,
title: $t('common.search'),
};
// 将搜索按钮合并到用户配置的toolbarConfig.tools中
const toolbarConfig: VxeGridPropTypes.ToolbarConfig = {
tools: (gridOptions.value?.toolbarConfig?.tools ??
[]) as VxeToolbarPropTypes.ToolConfig[],
};
if (gridOptions.value?.toolbarConfig?.search && !!formOptions.value) {
toolbarConfig.tools = Array.isArray(toolbarConfig.tools)
? [...toolbarConfig.tools, searchBtn]
: [searchBtn];
}
if (!showToolbar.value) {
return {};
return { toolbarConfig };
}
// 强制使用固定的toolbar配置不允许用户自定义
// 减少配置的复杂度,以及后续维护的成本
return {
toolbarConfig: {
slots: {
...(slotActions || showTableTitle.value
? { buttons: TOOLBAR_ACTIONS }
: {}),
...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),
},
},
toolbarConfig.slots = {
...(slotActions || showTableTitle.value
? { buttons: TOOLBAR_ACTIONS }
: {}),
...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),
};
return { toolbarConfig };
});
const options = computed(() => {
@@ -128,7 +147,7 @@ const options = computed(() => {
const mergedOptions: VxeTableGridProps = cloneDeep(
mergeWithArrayOverride(
{},
toolbarOptions.value,
toRaw(toolbarOptions.value),
toRaw(gridOptions.value),
globalGridConfig,
),
@@ -175,9 +194,19 @@ const options = computed(() => {
return mergedOptions;
});
function onToolbarToolClick(event: VxeGridDefines.ToolbarToolClickEventParams) {
if (event.code === 'search') {
props.api?.toggleSearchForm?.();
}
(
gridEvents.value?.toolbarToolClick as VxeGridListeners['toolbarToolClick']
)?.(event);
}
const events = computed(() => {
return {
...gridEvents.value,
toolbarToolClick: onToolbarToolClick,
};
});
@@ -215,7 +244,12 @@ async function init() {
const autoLoad = defaultGridOptions.proxyConfig?.autoLoad;
const enableProxyConfig = options.value.proxyConfig?.enabled;
if (enableProxyConfig && autoLoad) {
props.api.reload(formApi.form?.values ?? {});
// 第一次拿到的是readonly的数据 如果需要修改 需要cloneDeep
props.api.grid.commitProxy?.(
'_init',
cloneDeep(formApi.form?.values) ?? {},
);
// props.api.reload(formApi.form?.values ?? {});
}
// form 由 vben-form代替所以不适配formConfig这里给出警告
@@ -255,6 +289,10 @@ watch(
},
);
const isCompactForm = computed(() => {
return formApi.getState()?.compact;
});
onMounted(() => {
props.api?.mount?.(gridRef.value, formApi);
init();
@@ -272,7 +310,7 @@ onUnmounted(() => {
ref="gridRef"
:class="
cn(
'p-2 pt-0',
'p-2',
{
'pt-0': showToolbar && !formOptions,
},
@@ -306,7 +344,11 @@ onUnmounted(() => {
<!-- form表单 -->
<template #form>
<div v-if="formOptions" class="relative rounded py-3 pb-4">
<div
v-if="formOptions"
v-show="showSearchForm !== false"
:class="cn('relative rounded py-3', isCompactForm ? 'pb-6' : 'pb-4')"
>
<slot name="form">
<Form>
<template

View File

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

View File

@@ -5,14 +5,14 @@ import type {
CreateAxiosDefaults,
} from 'axios';
import { bindMethods, merge } from '@vben/utils';
import type { RequestClientOptions } from './types';
import { bindMethods, merge } from '@vben/utils';
import axios from 'axios';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader';
import { type RequestClientOptions } from './types';
class RequestClient {
private readonly instance: AxiosInstance;