committed by
GitHub
12 changed files with 462 additions and 1 deletions
@ -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; |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
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/themes/light.css'; |
||||
|
import 'tippy.js/animations/scale.css'; |
||||
|
import 'tippy.js/animations/scale-subtle.css'; |
||||
|
import 'tippy.js/animations/scale-extreme.css'; |
||||
|
import 'tippy.js/animations/shift-away.css'; |
||||
|
import 'tippy.js/animations/perspective.css'; |
||||
|
|
||||
|
const { isDark } = usePreferences(); |
||||
|
export type TippyProps = Props & { |
||||
|
animation?: |
||||
|
| 'fade' |
||||
|
| 'perspective' |
||||
|
| 'scale' |
||||
|
| 'scale-extreme' |
||||
|
| 'scale-subtle' |
||||
|
| 'shift-away' |
||||
|
| 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, |
||||
|
); |
||||
|
}; |
||||
@ -0,0 +1,226 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { computed, reactive } from 'vue'; |
||||
|
|
||||
|
import { Page, Tippy } from '@vben/common-ui'; |
||||
|
|
||||
|
import { Button, Card, Flex } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useVbenForm } from '#/adapter/form'; |
||||
|
|
||||
|
const props = reactive({ |
||||
|
animation: 'shift-away', |
||||
|
arrow: true, |
||||
|
content: '这是一个提示', |
||||
|
delay: 200, |
||||
|
duration: 200, |
||||
|
followCursor: '', |
||||
|
hideOnClick: '', |
||||
|
maxWidth: 'none', |
||||
|
placement: 'top', |
||||
|
theme: 'dark', |
||||
|
}); |
||||
|
|
||||
|
const tippyProps = computed(() => { |
||||
|
return { |
||||
|
...props, |
||||
|
followCursor: ['', 'true'].includes(props.followCursor) |
||||
|
? !!props.followCursor |
||||
|
: props.followCursor, |
||||
|
hideOnClick: ['', 'true'].includes(props.hideOnClick) |
||||
|
? !!props.hideOnClick |
||||
|
: props.hideOnClick, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const [Form] = useVbenForm({ |
||||
|
handleValuesChange(values) { |
||||
|
Object.assign(props, { ...values }); |
||||
|
}, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
options: [ |
||||
|
{ label: 'shift-away', value: 'shift-away' }, |
||||
|
{ label: 'scale', value: 'scale' }, |
||||
|
{ label: 'scale-extreme', value: 'scale-extreme' }, |
||||
|
{ label: 'scale-subtle', value: 'scale-subtle' }, |
||||
|
{ label: 'perspective', value: 'perspective' }, |
||||
|
{ label: 'fade', value: 'fade' }, |
||||
|
], |
||||
|
}, |
||||
|
defaultValue: props.animation, |
||||
|
fieldName: 'animation', |
||||
|
label: '动画', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'InputNumber', |
||||
|
defaultValue: props.duration, |
||||
|
fieldName: 'duration', |
||||
|
label: '动画时长', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
defaultValue: props.content, |
||||
|
fieldName: 'content', |
||||
|
label: '内容', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Switch', |
||||
|
defaultValue: props.arrow, |
||||
|
fieldName: 'arrow', |
||||
|
label: '箭头', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
options: [ |
||||
|
{ label: '不跟随', value: '' }, |
||||
|
{ label: '完全跟随', value: 'true' }, |
||||
|
{ label: '仅横向', value: 'horizontal' }, |
||||
|
{ label: '仅纵向', value: 'vertical' }, |
||||
|
{ label: '仅初始', value: 'initial' }, |
||||
|
], |
||||
|
}, |
||||
|
defaultValue: props.followCursor, |
||||
|
fieldName: 'followCursor', |
||||
|
label: '跟随指针', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
options: [ |
||||
|
{ label: '否', value: '' }, |
||||
|
{ label: '是', value: 'true' }, |
||||
|
{ label: '仅内部点击', value: 'toggle' }, |
||||
|
], |
||||
|
}, |
||||
|
defaultValue: props.hideOnClick, |
||||
|
fieldName: 'hideOnClick', |
||||
|
label: '点击后隐藏', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'InputNumber', |
||||
|
defaultValue: 100, |
||||
|
fieldName: 'delay', |
||||
|
label: '延时', |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
component: 'RadioGroup', |
||||
|
componentProps: { |
||||
|
options: [ |
||||
|
{ label: 'auto', value: 'auto' }, |
||||
|
{ label: 'dark', value: 'dark' }, |
||||
|
{ label: 'light', value: 'light' }, |
||||
|
], |
||||
|
}, |
||||
|
defaultValue: props.theme, |
||||
|
fieldName: 'theme', |
||||
|
label: '主题', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
placeholder: 'none、200px', |
||||
|
}, |
||||
|
defaultValue: props.maxWidth, |
||||
|
fieldName: 'maxWidth', |
||||
|
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: 'top', |
||||
|
fieldName: 'placement', |
||||
|
label: '位置', |
||||
|
}, |
||||
|
], |
||||
|
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> |
||||
Loading…
Reference in new issue