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
commit f81dffd072
42 changed files with 848 additions and 131 deletions

View File

@ -1,6 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -33,6 +34,9 @@ async function bootstrap(namespace: string) {
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
initTippy(app);
// 配置路由及路由守卫
app.use(router);

View File

@ -1,6 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -32,6 +33,9 @@ async function bootstrap(namespace: string) {
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
initTippy(app);
// 配置路由及路由守卫
app.use(router);

View File

@ -1,6 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -28,6 +29,9 @@ async function bootstrap(namespace: string) {
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
initTippy(app);
// 配置路由及路由守卫
app.use(router);

View File

@ -101,6 +101,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
| footerClass | modal底部区域的class | `string` | - |
| headerClass | modal顶部区域的class | `string` | - |
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
::: info appendToMain
@ -133,13 +134,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
### modalApi
### drawerApi
| 事件名 | 描述 | 类型 |
| 方法 | 描述 | 类型 |
| --- | --- | --- |
| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>drawerApi` |
| open | 打开弹窗 | `()=>void` |
| close | 关闭弹窗 | `()=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` |
| getData | 获取共享数据 | `<T>()=>T` |
| useStore | 获取可响应式状态 | - |

View File

@ -111,6 +111,7 @@ const [Modal, modalApi] = useVbenModal({
| headerClass | modal顶部区域的class | `string` | - |
| bordered | 是否显示border | `boolean` | `false` |
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
::: info appendToMain
@ -143,11 +144,11 @@ const [Modal, modalApi] = useVbenModal({
### modalApi
| 事件名 | 描述 | 类型 |
| 方法 | 描述 | 类型 |
| --- | --- | --- |
| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>modalApi` |
| open | 打开弹窗 | `()=>void` |
| close | 关闭弹窗 | `()=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>modalApi` |
| getData | 获取共享数据 | `<T>()=>T` |
| useStore | 获取可响应式状态 | - |

View File

@ -13,8 +13,7 @@ function open() {
}
function handleUpdateTitle() {
drawerApi.setState({ title: '外部动态标题' });
drawerApi.open();
drawerApi.setState({ title: '外部动态标题' }).open();
}
</script>

View File

@ -9,11 +9,12 @@ const [Drawer, drawerApi] = useVbenDrawer({
});
function open() {
drawerApi.setData({
drawerApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
});
drawerApi.open();
})
.open();
}
</script>

View File

@ -13,8 +13,7 @@ function openModal() {
}
function handleUpdateTitle() {
modalApi.setState({ title: '外部动态标题' });
modalApi.open();
modalApi.setState({ title: '外部动态标题' }).open();
}
</script>

View File

@ -9,11 +9,12 @@ const [Modal, modalApi] = useVbenModal({
});
function openModal() {
modalApi.setData({
modalApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
});
modalApi.open();
})
.open();
}
</script>

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": "标签页风格",

View File

@ -1,6 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -30,6 +31,9 @@ async function bootstrap(namespace: string) {
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
initTippy(app);
// 配置路由及路由守卫
app.use(router);

View File

@ -248,6 +248,15 @@ const routes: RouteRecordRaw[] = [
title: $t('examples.layout.col-page'),
},
},
{
name: 'TippyDemo',
path: '/examples/tippy',
component: () => import('#/views/examples/tippy/index.vue'),
meta: {
icon: 'mdi:message-settings-outline',
title: 'Tippy',
},
},
],
},
];

View File

@ -7,6 +7,9 @@ const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onClosed() {
drawerApi.setState({ overlayBlur: 0, placement: 'right' });
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { DrawerPlacement } from '@vben/common-ui';
import type { DrawerPlacement, DrawerState } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Button, Card } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
@ -41,25 +42,25 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
});
function openBaseDrawer(placement: DrawerPlacement = 'right') {
baseDrawerApi.setState({ placement });
baseDrawerApi.open();
baseDrawerApi.setState({ placement }).open();
}
function openBlurDrawer() {
baseDrawerApi.setState({ overlayBlur: 5 }).open();
}
function openInContentDrawer(placement: DrawerPlacement = 'right') {
inContentDrawerApi.setState({ class: '', placement });
const state: Partial<DrawerState> = { class: '', placement };
if (placement === 'top') {
// 200200
inContentDrawerApi.setState({ zIndex: 199 });
} else {
inContentDrawerApi.setState({ zIndex: undefined });
state.zIndex = 199;
}
inContentDrawerApi.open();
inContentDrawerApi.setState(state).open();
}
function openMaxContentDrawer() {
// 便使Drawer
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' });
inContentDrawerApi.open();
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' }).open();
}
function openAutoHeightDrawer() {
@ -71,24 +72,25 @@ function openDynamicDrawer() {
}
function handleUpdateTitle() {
dynamicDrawerApi.setState({ title: '外部动态标题' });
dynamicDrawerApi.open();
dynamicDrawerApi.setState({ title: '外部动态标题' }).open();
}
function openSharedDrawer() {
sharedDrawerApi.setData({
sharedDrawerApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
});
sharedDrawerApi.open();
})
.open();
}
function openFormDrawer() {
formDrawerApi.setData({
formDrawerApi
.setData({
//
values: { field1: 'abc', field2: '123' },
});
formDrawerApi.open();
})
.open();
}
</script>
@ -126,6 +128,9 @@ function openFormDrawer() {
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
顶部打开
</Button>
<Button class="mb-2 ml-2" type="primary" @click="openBlurDrawer">
遮罩层模糊效果
</Button>
</Card>
<Card class="mb-4" title="在内容区域打开">

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Slider } from 'ant-design-vue';
const blur = ref(5);
const [Modal, modalApi] = useVbenModal({
overlayBlur: blur.value,
});
watch(blur, (val) => {
modalApi.setState({
overlayBlur: val,
});
});
</script>
<template>
<Modal title="遮罩层模糊">
<p>调整滑块来改变遮罩层模糊程度{{ blur }}</p>
<Slider v-model:value="blur" :max="30" :min="0" />
</Modal>
</template>

View File

@ -1,15 +1,17 @@
<script lang="ts" setup>
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Card } from 'ant-design-vue';
import { Button, Card, Flex } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
import AutoHeightDemo from './auto-height-demo.vue';
import BaseDemo from './base-demo.vue';
import BlurDemo from './blur-demo.vue';
import DragDemo from './drag-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormModalDemo from './form-modal-demo.vue';
import InContentModalDemo from './in-content-demo.vue';
import NestedDemo from './nested-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
const [BaseModal, baseModalApi] = useVbenModal({
@ -42,6 +44,14 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [NestedModal, nestedModalApi] = useVbenModal({
connectedComponent: NestedDemo,
});
const [BlurModal, blurModalApi] = useVbenModal({
connectedComponent: BlurDemo,
});
function openBaseModal() {
baseModalApi.open();
}
@ -63,24 +73,33 @@ function openDynamicModal() {
}
function openSharedModal() {
sharedModalApi.setData({
sharedModalApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
});
sharedModalApi.open();
})
.open();
}
function openNestedModal() {
nestedModalApi.open();
}
function openBlurModal() {
blurModalApi.open();
}
function handleUpdateTitle() {
dynamicModalApi.setState({ title: '外部动态标题' });
dynamicModalApi.open();
dynamicModalApi.setState({ title: '外部动态标题' }).open();
}
function openFormModal() {
formModalApi.setData({
formModalApi
.setData({
//
values: { field1: 'abc' },
});
formModalApi.open();
})
.open();
}
</script>
@ -100,44 +119,80 @@ function openFormModal() {
<DynamicModal />
<SharedDataModal />
<FormModal />
<Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的弹窗示例</p>
<NestedModal />
<BlurModal />
<Flex wrap="wrap" class="w-full" gap="10">
<Card class="w-[300px]" title="基本使用">
<p>一个基础的弹窗示例</p>
<template #actions>
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
</template>
</Card>
<Card class="mb-4" title="指定容器">
<p class="mb-3">在内容区域打开弹窗的示例</p>
<Card class="w-[300px]" title="指定容器">
<p>在内容区域打开弹窗的示例</p>
<template #actions>
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
</template>
</Card>
<Card class="mb-4" title="内容高度自适应">
<p class="mb-3">可根据内容并自动调整高度</p>
<Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>
</Card>
<Card class="mb-4" title="可拖拽示例">
<p class="mb-3">配置 draggable 可开启拖拽功能</p>
<Button type="primary" @click="openDragModal">打开弹窗</Button>
</Card>
<Card class="mb-4" title="动态配置示例">
<p class="mb-3">通过 setState 动态调整弹窗数据</p>
<Button type="primary" @click="openDynamicModal">打开弹窗</Button>
<Button class="ml-2" type="primary" @click="handleUpdateTitle">
从外部修改标题并打开
<Card class="w-[300px]" title="内容高度自适应">
<p>可根据内容并自动调整高度</p>
<template #actions>
<Button type="primary" @click="openAutoHeightModal">
打开弹窗
</Button>
</template>
</Card>
<Card class="mb-4" title="内外数据共享示例">
<p class="mb-3">通过共享 sharedData 来进行数据交互</p>
<Card class="w-[300px]" title="可拖拽示例">
<p>配置 draggable 可开启拖拽功能</p>
<template #actions>
<Button type="primary" @click="openDragModal"> 打开弹窗 </Button>
</template>
</Card>
<Card class="w-[300px]" title="动态配置示例">
<p>通过 setState 动态调整弹窗数据</p>
<template #extra>
<Button type="link" @click="openDynamicModal">打开弹窗</Button>
</template>
<template #actions>
<Button type="primary" @click="handleUpdateTitle">
外部修改标题并打开
</Button>
</template>
</Card>
<Card class="w-[300px]" title="内外数据共享示例">
<p>通过共享 sharedData 来进行数据交互</p>
<template #actions>
<Button type="primary" @click="openSharedModal">
打开弹窗并传递数据
</Button>
</template>
</Card>
<Card class="mb-4" title="表单弹窗示例">
<p class="mb-3">弹窗与表单结合</p>
<Button type="primary" @click="openFormModal"> 打开弹窗 </Button>
<Card class="w-[300px]" title="表单弹窗示例">
<p>弹窗与表单结合</p>
<template #actions>
<Button type="primary" @click="openFormModal"> 打开表单弹窗 </Button>
</template>
</Card>
<Card class="w-[300px]" title="嵌套弹窗示例">
<p>在已经打开的弹窗中再次打开弹窗</p>
<template #actions>
<Button type="primary" @click="openNestedModal">打开嵌套弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" title="遮罩模糊示例">
<p>遮罩层应用类似毛玻璃的模糊效果</p>
<template #actions>
<Button type="primary" @click="openBlurModal">打开弹窗</Button>
</template>
</Card>
</Flex>
</Page>
</template>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import DragDemo from './drag-demo.vue';
const [Modal] = useVbenModal({
destroyOnClose: true,
});
const [BaseModal, baseModalApi] = useVbenModal({
connectedComponent: DragDemo,
});
function openNestedModal() {
baseModalApi.open();
}
</script>
<template>
<Modal title="嵌套弹窗示例">
<Button @click="openNestedModal" type="primary">打开子弹窗</Button>
<BaseModal />
</Modal>
</template>

View File

@ -0,0 +1,303 @@
<script lang="ts" setup>
import type { TippyProps } from '@vben/common-ui';
import { reactive } from 'vue';
import { Page, Tippy } from '@vben/common-ui';
import { Button, Card, Flex } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const tippyProps = reactive<TippyProps>({
animation: 'shift-away',
arrow: true,
content: '这是一个提示',
delay: [200, 200],
duration: 200,
followCursor: false,
hideOnClick: false,
inertia: true,
maxWidth: 'none',
placement: 'top',
theme: 'dark',
trigger: 'mouseenter focusin',
});
function parseBoolean(value: string) {
switch (value) {
case 'false': {
return false;
}
case 'true': {
return true;
}
default: {
return value;
}
}
}
const [Form] = useVbenForm({
handleValuesChange(values) {
Object.assign(tippyProps, {
...values,
delay: [values.delay1, values.delay2],
followCursor: parseBoolean(values.followCursor),
hideOnClick: parseBoolean(values.hideOnClick),
trigger: values.trigger.join(' '),
});
},
schema: [
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
class: 'w-full',
options: [
{ label: '自动', value: 'auto' },
{ label: '暗色', value: 'dark' },
{ label: '亮色', value: 'light' },
],
optionType: 'button',
},
defaultValue: tippyProps.theme,
fieldName: 'theme',
label: '主题',
},
{
component: 'Select',
componentProps: {
class: 'w-full',
options: [
{ label: '向上滑入', value: 'shift-away' },
{ label: '向下滑入', value: 'shift-toward' },
{ label: '缩放', value: 'scale' },
{ label: '透视', value: 'perspective' },
{ label: '淡入', value: 'fade' },
],
},
defaultValue: tippyProps.animation,
fieldName: 'animation',
label: '动画类型',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '是', value: true },
{ label: '否', value: false },
],
optionType: 'button',
},
defaultValue: tippyProps.inertia,
fieldName: 'inertia',
label: '动画惯性',
},
{
component: 'Select',
componentProps: {
class: 'w-full',
options: [
{ label: '顶部', value: 'top' },
{ label: '顶左', value: 'top-start' },
{ label: '顶右', value: 'top-end' },
{ label: '底部', value: 'bottom' },
{ label: '底左', value: 'bottom-start' },
{ label: '底右', value: 'bottom-end' },
{ label: '左侧', value: 'left' },
{ label: '左上', value: 'left-start' },
{ label: '左下', value: 'left-end' },
{ label: '右侧', value: 'right' },
{ label: '右上', value: 'right-start' },
{ label: '右下', value: 'right-end' },
],
},
defaultValue: tippyProps.placement,
fieldName: 'placement',
label: '位置',
},
{
component: 'InputNumber',
componentProps: {
addonAfter: '毫秒',
},
defaultValue: tippyProps.duration,
fieldName: 'duration',
label: '动画时长',
},
{
component: 'InputNumber',
componentProps: {
addonAfter: '毫秒',
},
defaultValue: 100,
fieldName: 'delay1',
label: '显示延时',
},
{
component: 'InputNumber',
componentProps: {
addonAfter: '毫秒',
},
defaultValue: 100,
fieldName: 'delay2',
label: '隐藏延时',
},
{
component: 'Input',
defaultValue: tippyProps.content,
fieldName: 'content',
label: '内容',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '是', value: true },
{ label: '否', value: false },
],
optionType: 'button',
},
defaultValue: tippyProps.arrow,
fieldName: 'arrow',
label: '指示箭头',
},
{
component: 'Select',
componentProps: {
class: 'w-full',
options: [
{ label: '不跟随', value: 'false' },
{ label: '完全跟随', value: 'true' },
{ label: '仅横向', value: 'horizontal' },
{ label: '仅纵向', value: 'vertical' },
{ label: '仅初始', value: 'initial' },
],
},
defaultValue: tippyProps.followCursor?.toString(),
fieldName: 'followCursor',
label: '跟随指针',
},
{
component: 'Select',
componentProps: {
class: 'w-full',
mode: 'multiple',
options: [
{ label: '鼠标移入', value: 'mouseenter' },
{ label: '被点击', value: 'click' },
{ label: '获得焦点', value: 'focusin' },
{ label: '无触发,仅手动', value: 'manual' },
],
},
defaultValue: tippyProps.trigger?.split(' '),
fieldName: 'trigger',
label: '触发方式',
},
{
component: 'Select',
componentProps: {
class: 'w-full',
options: [
{ label: '否', value: 'false' },
{ label: '是', value: 'true' },
{ label: '仅内部', value: 'toggle' },
],
},
defaultValue: tippyProps.hideOnClick?.toString(),
dependencies: {
componentProps(_, formAction) {
return {
disabled: !formAction.values.trigger.includes('click'),
};
},
triggerFields: ['trigger'],
},
fieldName: 'hideOnClick',
help: '只有在触发方式为`click`时才有效',
label: '点击后隐藏',
},
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: 'none、200px',
},
defaultValue: tippyProps.maxWidth,
fieldName: 'maxWidth',
label: '最大宽度',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function goDoc() {
window.open('https://atomiks.github.io/tippyjs/v6/all-props/');
}
</script>
<template>
<Page title="Tippy">
<template #description>
<div class="flex items-center">
<p>
Tippy
是一个轻量级的提示工具库它可以用来创建各种交互式提示如工具提示引导提示等
</p>
<Button type="link" size="small" @click="goDoc">查看文档</Button>
</div>
</template>
<Card title="指令形式使用">
<p class="mb-4">
指令形式使用比较简洁直接在需要展示tooltip的组件上用v-tippy传递配置适用于固定内容的工具提示
</p>
<Flex warp="warp" gap="20">
<Button v-tippy="'这是一个提示,使用了默认的配置'">默认配置</Button>
<Button
v-tippy="{ theme: 'light', content: '这是一个提示总是light主题' }"
>
指定主题
</Button>
<Button
v-tippy="{
theme: 'light',
content: '这个提示将在点燃组件100毫秒后激活',
delay: 100,
}"
>
指定延时
</Button>
<Button
v-tippy="{
content: '本提示的动画为`scale`',
animation: 'scale',
}"
>
指定动画
</Button>
</Flex>
</Card>
<Card title="组件形式使用" class="mt-4">
<div class="flex w-full justify-center">
<Tippy v-bind="tippyProps">
<Button>鼠标移到这个组件上来体验效果</Button>
</Tippy>
</div>
<Form class="mt-4" />
<template #actions>
<p
class="text-secondary-foreground hover:text-secondary-foreground cursor-default"
>
更多配置请
<Button type="link" size="small" @click="goDoc">查看文档</Button>
这里只列出了一些常用的配置
</p>
</template>
</Card>
</Page>
</template>

View File

@ -156,6 +156,7 @@ catalog:
tailwindcss: ^3.4.17
tailwindcss-animate: ^1.0.7
theme-colors: ^0.1.0
tippy.js: ^6.2.5
turbo: ^2.3.3
typescript: ^5.7.3
unbuild: ^3.2.0
@ -175,6 +176,7 @@ catalog:
vue-eslint-parser: ^9.4.3
vue-i18n: ^11.0.1
vue-router: ^4.5.0
vue-tippy: ^6.6.0
vue-tsc: 2.1.10
vxe-pc-ui: ^4.3.4
vxe-table: 4.10.0