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

This commit is contained in:
dap
2025-01-12 10:07:39 +08:00
42 changed files with 848 additions and 131 deletions

View File

@@ -80,6 +80,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"enable": true,
"height": 38,
"keepAlive": true,
"middleClickToClose": false,
"persist": true,
"showIcon": true,
"showMaximize": true,

View File

@@ -80,6 +80,7 @@ const defaultPreferences: Preferences = {
enable: true,
height: 38,
keepAlive: true,
middleClickToClose: false,
persist: true,
showIcon: true,
showMaximize: true,

View File

@@ -168,6 +168,8 @@ interface TabbarPreferences {
height: number;
/** 开启标签页缓存功能 */
keepAlive: boolean;
/** 是否点击中键时关闭标签 */
middleClickToClose: boolean;
/** 是否持久化标签 */
persist: boolean;
/** 是否开启多标签页图标 */

View File

@@ -4,6 +4,12 @@ import { Store } from '@vben-core/shared/store';
import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class DrawerApi {
// 共享数据
public sharedData: Record<'payload', any> = {
payload: {},
};
public store: Store<DrawerState>;
private api: Pick<
DrawerApiOptions,
| 'onBeforeClose'
@@ -13,16 +19,10 @@ export class DrawerApi {
| 'onOpenChange'
| 'onOpened'
>;
// private prevState!: DrawerState;
private state!: DrawerState;
// 共享数据
public sharedData: Record<'payload', any> = {
payload: {},
};
public store: Store<DrawerState>;
constructor(options: DrawerApiOptions = {}) {
const {
connectedComponent: _,
@@ -149,6 +149,7 @@ export class DrawerApi {
setData<T>(payload: T) {
this.sharedData.payload = payload;
return this;
}
setState(
@@ -161,5 +162,6 @@ export class DrawerApi {
} else {
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
}
return this;
}
}

View File

@@ -1,6 +1,7 @@
import type { ClassType } from '@vben-core/typings';
import type { Component, Ref } from 'vue';
import type { ClassType } from '@vben-core/typings';
import type { DrawerApi } from './drawer-api';
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
@@ -84,12 +85,16 @@ export interface DrawerProps {
* 是否自动聚焦
*/
openAutoFocus?: boolean;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/**
* 抽屉位置
* @default right
*/
placement?: DrawerPlacement;
/**
* 是否显示取消按钮
* @default true
@@ -123,11 +128,11 @@ export interface DrawerState extends DrawerProps {
sharedData?: Record<string, any>;
}
export type ExtendedDrawerApi = {
export type ExtendedDrawerApi = DrawerApi & {
useStore: <T = NoInfer<DrawerState>>(
selector?: (state: NoInfer<DrawerState>) => T,
) => Readonly<Ref<T>>;
} & DrawerApi;
};
export interface DrawerApiOptions extends DrawerState {
/**

View File

@@ -68,6 +68,7 @@ const {
loading: showLoading,
modal,
openAutoFocus,
overlayBlur,
placement,
showCancelButton,
showConfirmButton,
@@ -140,6 +141,7 @@ const getAppendTo = computed(() => {
:open="state?.isOpen"
:side="placement"
:z-index="zIndex"
:overlay-blur="overlayBlur"
@close-auto-focus="handleFocusOutside"
@closed="() => drawerApi?.onClosed()"
@escape-key-down="escapeKeyDown"

View File

@@ -4,6 +4,12 @@ import { Store } from '@vben-core/shared/store';
import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class ModalApi {
// 共享数据
public sharedData: Record<'payload', any> = {
payload: {},
};
public store: Store<ModalState>;
private api: Pick<
ModalApiOptions,
| 'onBeforeClose'
@@ -13,16 +19,10 @@ export class ModalApi {
| 'onOpenChange'
| 'onOpened'
>;
// private prevState!: ModalState;
private state!: ModalState;
// 共享数据
public sharedData: Record<'payload', any> = {
payload: {},
};
public store: Store<ModalState>;
constructor(options: ModalApiOptions = {}) {
const {
connectedComponent: _,
@@ -159,6 +159,7 @@ export class ModalApi {
setData<T>(payload: T) {
this.sharedData.payload = payload;
return this;
}
setState(
@@ -171,5 +172,6 @@ export class ModalApi {
} else {
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
}
return this;
}
}

View File

@@ -99,6 +99,10 @@ export interface ModalProps {
* 是否自动聚焦
*/
openAutoFocus?: boolean;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/**
* 是否显示取消按钮
* @default true
@@ -132,11 +136,11 @@ export interface ModalState extends ModalProps {
sharedData?: Record<string, any>;
}
export type ExtendedModalApi = {
export type ExtendedModalApi = ModalApi & {
useStore: <T = NoInfer<ModalState>>(
selector?: (state: NoInfer<ModalState>) => T,
) => Readonly<Ref<T>>;
} & ModalApi;
};
export interface ModalApiOptions extends ModalState {
/**

View File

@@ -77,6 +77,7 @@ const {
loading: showLoading,
modal,
openAutoFocus,
overlayBlur,
showCancelButton,
showConfirmButton,
title,
@@ -196,6 +197,7 @@ const getAppendTo = computed(() => {
:open="state?.isOpen"
:show-close="closable"
:z-index="zIndex"
:overlay-blur="overlayBlur"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"

View File

@@ -1,8 +1,12 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type { DialogContentEmits, DialogContentProps } from 'radix-vue';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import {
DialogClose,
@@ -10,26 +14,26 @@ import {
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import { computed, ref } from 'vue';
import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
{
DialogContentProps & {
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
showClose?: boolean;
zIndex?: number;
} & DialogContentProps
}
>(),
{ appendTo: 'body', showClose: true, zIndex: 1000 },
);
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
DialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
@@ -79,7 +83,12 @@ defineExpose({
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
:style="{ zIndex, position }"
:style="{
zIndex,
position,
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@click="() => emits('close')"
/>
</Transition>

View File

@@ -3,10 +3,12 @@ import type { DialogContentEmits, DialogContentProps } from 'radix-vue';
import type { SheetVariants } from './sheet';
import { cn } from '@vben-core/shared/utils';
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
import { sheetVariants } from './sheet';
import SheetOverlay from './SheetOverlay.vue';
@@ -15,6 +17,7 @@ interface SheetContentProps extends DialogContentProps {
class?: any;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
side?: SheetVariants['side'];
zIndex?: number;
}
@@ -29,7 +32,7 @@ const props = withDefaults(defineProps<SheetContentProps>(), {
});
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
DialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
@@ -73,12 +76,23 @@ function onAnimationEnd(event: AnimationEvent) {
<template>
<DialogPortal :to="appendTo">
<Transition name="fade">
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
<SheetOverlay
v-if="open && modal"
:style="{
zIndex,
position,
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
/>
</Transition>
<DialogContent
ref="contentRef"
:class="cn(sheetVariants({ side }), props.class)"
:style="{ zIndex, position }"
:style="{
zIndex,
position,
}"
@animationend="onAnimationEnd"
v-bind="{ ...forwarded, ...$attrs }"
>

View File

@@ -56,6 +56,20 @@ const tabsView = computed(() => {
} as TabConfig;
});
});
function onMouseDown(e: MouseEvent, tab: TabConfig) {
if (
e.button === 1 &&
tab.closable &&
!tab.affixTab &&
tabsView.value.length > 1 &&
props.middleClickToClose
) {
e.preventDefault();
e.stopPropagation();
emit('close', tab.key);
}
}
</script>
<template>
@@ -82,6 +96,7 @@ const tabsView = computed(() => {
class="tabs-chrome__item draggable translate-all group relative -mr-3 flex h-full select-none items-center"
data-tab-item="true"
@click="active = tab.key"
@mousedown="onMouseDown($event, tab)"
>
<VbenContextMenu
:handler-data="tab"

View File

@@ -62,6 +62,20 @@ const tabsView = computed(() => {
} as TabConfig;
});
});
function onMouseDown(e: MouseEvent, tab: TabConfig) {
if (
e.button === 1 &&
tab.closable &&
!tab.affixTab &&
tabsView.value.length > 1 &&
props.middleClickToClose
) {
e.preventDefault();
e.stopPropagation();
emit('close', tab.key);
}
}
</script>
<template>
@@ -85,6 +99,7 @@ const tabsView = computed(() => {
class="tab-item [&:not(.is-active)]:hover:bg-accent translate-all group relative flex cursor-pointer select-none"
data-tab-item="true"
@click="active = tab.key"
@mousedown="onMouseDown($event, tab)"
>
<VbenContextMenu
:handler-data="tab"

View File

@@ -33,6 +33,11 @@ export interface TabsProps {
* 仅限 tabs-chrome
*/
maxWidth?: number;
/**
* @zh_CN 点击中键时关闭Tab
*/
middleClickToClose?: boolean;
/**
* @zh_CN tab最小宽度
* 仅限 tabs-chrome
@@ -43,11 +48,11 @@ export interface TabsProps {
* @zh_CN 是否显示图标
*/
showIcon?: boolean;
/**
* @zh_CN 标签页风格
*/
styleType?: TabsStyleType;
/**
* @zh_CN 选项卡数据
*/

View File

@@ -29,6 +29,7 @@
"@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:*",
@@ -41,11 +42,13 @@
"@vueuse/integrations": "catalog:",
"codemirror": "6.0.1",
"qrcode": "catalog:",
"tippy.js": "catalog:",
"vditor": "3.10.7",
"vue": "catalog:",
"vue-codemirror6": "1.3.4",
"vue-json-pretty": "^2.4.0",
"vue-router": "catalog:"
"vue-router": "catalog:",
"vue-tippy": "catalog:"
},
"devDependencies": {
"@types/qrcode": "catalog:"

View File

@@ -8,6 +8,7 @@ export * from './json-preview';
export * from './markdown';
export * from './page';
export * from './resize';
export * from './tippy';
export * from '@vben-core/form-ui';
export * from '@vben-core/popup-ui';

View File

@@ -0,0 +1,100 @@
import type { ComputedRef, Directive } from 'vue';
import { useTippy } from 'vue-tippy';
export default function useTippyDirective(isDark: ComputedRef<boolean>) {
const directive: Directive = {
mounted(el, binding, vnode) {
const opts =
typeof binding.value === 'string'
? { content: binding.value }
: binding.value || {};
const modifiers = Object.keys(binding.modifiers || {});
const placement = modifiers.find((modifier) => modifier !== 'arrow');
const withArrow = modifiers.includes('arrow');
if (placement) {
opts.placement = opts.placement || placement;
}
if (withArrow) {
opts.arrow = opts.arrow === undefined ? true : opts.arrow;
}
if (vnode.props && vnode.props.onTippyShow) {
opts.onShow = function (...args: any[]) {
return vnode.props?.onTippyShow(...args);
};
}
if (vnode.props && vnode.props.onTippyShown) {
opts.onShown = function (...args: any[]) {
return vnode.props?.onTippyShown(...args);
};
}
if (vnode.props && vnode.props.onTippyHidden) {
opts.onHidden = function (...args: any[]) {
return vnode.props?.onTippyHidden(...args);
};
}
if (vnode.props && vnode.props.onTippyHide) {
opts.onHide = function (...args: any[]) {
return vnode.props?.onTippyHide(...args);
};
}
if (vnode.props && vnode.props.onTippyMount) {
opts.onMount = function (...args: any[]) {
return vnode.props?.onTippyMount(...args);
};
}
if (el.getAttribute('title') && !opts.content) {
opts.content = el.getAttribute('title');
el.removeAttribute('title');
}
if (el.getAttribute('content') && !opts.content) {
opts.content = el.getAttribute('content');
}
useTippy(el, opts);
},
unmounted(el) {
if (el.$tippy) {
el.$tippy.destroy();
} else if (el._tippy) {
el._tippy.destroy();
}
},
updated(el, binding) {
const opts =
typeof binding.value === 'string'
? { content: binding.value, theme: isDark.value ? '' : 'light' }
: Object.assign(
{ theme: isDark.value ? '' : 'light' },
binding.value,
);
if (el.getAttribute('title') && !opts.content) {
opts.content = el.getAttribute('title');
el.removeAttribute('title');
}
if (el.getAttribute('content') && !opts.content) {
opts.content = el.getAttribute('content');
}
if (el.$tippy) {
el.$tippy.setProps(opts || {});
} else if (el._tippy) {
el._tippy.setProps(opts || {});
}
},
};
return directive;
}

View File

@@ -0,0 +1,67 @@
import type { DefaultProps, Props } from 'tippy.js';
import type { App, SetupContext } from 'vue';
import { h, watchEffect } from 'vue';
import { setDefaultProps, Tippy as TippyComponent } from 'vue-tippy';
import { usePreferences } from '@vben-core/preferences';
import useTippyDirective from './directive';
import 'tippy.js/dist/tippy.css';
import 'tippy.js/dist/backdrop.css';
import 'tippy.js/themes/light.css';
import 'tippy.js/animations/scale.css';
import 'tippy.js/animations/shift-toward.css';
import 'tippy.js/animations/shift-away.css';
import 'tippy.js/animations/perspective.css';
const { isDark } = usePreferences();
export type TippyProps = Partial<
Props & {
animation?:
| 'fade'
| 'perspective'
| 'scale'
| 'shift-away'
| 'shift-toward'
| boolean;
theme?: 'auto' | 'dark' | 'light';
}
>;
export function initTippy(app: App<Element>, options?: DefaultProps) {
setDefaultProps({
allowHTML: true,
delay: [500, 200],
theme: isDark.value ? '' : 'light',
...options,
});
if (!options || !Reflect.has(options, 'theme') || options.theme === 'auto') {
watchEffect(() => {
setDefaultProps({ theme: isDark.value ? '' : 'light' });
});
}
app.directive('tippy', useTippyDirective(isDark));
}
export const Tippy = (props: any, { attrs, slots }: SetupContext) => {
let theme: string = (attrs.theme as string) ?? 'auto';
if (theme === 'auto') {
theme = isDark.value ? '' : 'light';
}
if (theme === 'dark') {
theme = '';
}
return h(
TippyComponent,
{
...props,
...attrs,
theme,
},
slots,
);
};

View File

@@ -1,10 +1,12 @@
<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 +57,7 @@ if (!preferences.tabbar.persist) {
:style-type="preferences.tabbar.styleType"
:tabs="currentTabs"
:wheelable="preferences.tabbar.wheelable"
:middle-click-to-close="preferences.tabbar.middleClickToClose"
@close="handleClose"
@sort-tabs="tabbarStore.sortTabs"
@unpin="unpinTab"

View File

@@ -22,6 +22,9 @@ const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
const tabbarMiddleClickToClose = defineModel<boolean>(
'tabbarMiddleClickToClose',
);
const styleItems = computed((): SelectOption[] => [
{
@@ -61,6 +64,9 @@ const styleItems = computed((): SelectOption[] => [
>
{{ $t('preferences.tabbar.wheelable') }}
</SwitchItem>
<SwitchItem v-model="tabbarMiddleClickToClose" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.middleClickClose') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }}
</SwitchItem>

View File

@@ -11,8 +11,11 @@ import type {
PreferencesButtonPositionType,
ThemeModeType,
} 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 {
@@ -21,6 +24,7 @@ import {
resetPreferences,
usePreferences,
} from '@vben/preferences';
import { useVbenDrawer } from '@vben-core/popup-ui';
import {
VbenButton,
@@ -28,8 +32,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,
@@ -112,6 +116,9 @@ const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
const tabbarMiddleClickToClose = defineModel<boolean>(
'tabbarMiddleClickToClose',
);
const navigationStyleType = defineModel<NavigationStyleType>(
'navigationStyleType',
@@ -358,6 +365,7 @@ async function handleReset() {
v-model:tabbar-show-more="tabbarShowMore"
v-model:tabbar-style-type="tabbarStyleType"
v-model:tabbar-wheelable="tabbarWheelable"
v-model:tabbar-middle-click-to-close="tabbarMiddleClickToClose"
/>
</Block>
<Block :title="$t('preferences.widget.title')">

View File

@@ -2,6 +2,8 @@ import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import type { Nullable } from '@vben/types';
import type EchartsUI from './echarts-ui.vue';
import { computed, nextTick, watch } from 'vue';
@@ -50,7 +52,10 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return chartInstance;
};
const renderEcharts = (options: EChartsOption, clear = true) => {
const renderEcharts = (
options: EChartsOption,
clear = true,
): Promise<Nullable<echarts.ECharts>> => {
cacheOptions = options;
const currentOptions = {
...options,
@@ -58,9 +63,8 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
};
return new Promise((resolve) => {
if (chartRef.value?.offsetHeight === 0) {
useTimeoutFn(() => {
renderEcharts(currentOptions);
resolve(null);
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
@@ -72,7 +76,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
}
clear && chartInstance?.clear();
chartInstance?.setOption(currentOptions);
resolve(null);
resolve(chartInstance);
}, 30);
});
});
@@ -111,7 +115,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return {
renderEcharts,
resize,
chartInstance,
getChartInstance: () => chartInstance,
};
}

View File

@@ -19,6 +19,8 @@
"twoColumnTip": "Vertical Two Column Menu Mode",
"headerSidebarNav": "Header Vertical",
"headerSidebarNavTip": "Header Full Width, Sidebar Navigation Mode",
"headerTwoColumn": "Header Two Column",
"headerTwoColumnTip": "Header Navigation & Sidebar Two Column co-exists",
"mixedMenu": "Mixed Menu",
"mixedMenuTip": "Vertical & Horizontal Menu Co-exists",
"fullContent": "Full Content",
@@ -62,6 +64,7 @@
"persist": "Persist Tabs",
"draggable": "Enable Draggable Sort",
"wheelable": "Support Mouse Wheel",
"middleClickClose": "Close Tab when Mouse Middle Button Click",
"wheelableTip": "When enabled, the Tabbar area responds to vertical scrolling events of the scroll wheel.",
"styleType": {
"title": "Tabs Style",

View File

@@ -64,6 +64,7 @@
"persist": "持久化标签页",
"draggable": "启动拖拽排序",
"wheelable": "启用纵向滚轮响应",
"middleClickClose": "点击鼠标中键关闭标签页",
"wheelableTip": "开启后,标签栏区域可以响应滚轮的纵向滚动事件。\n关闭时只能响应系统的横向滚动事件需要按下Shift再滚动滚轮",
"styleType": {
"title": "标签页风格",