diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 963d1c7f7..30ab99888 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -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'; @@ -29,6 +30,9 @@ async function bootstrap(namespace: string) { // 安装权限指令 registerAccessDirective(app); + // 初始化 tippy + initTippy(app); + // 配置路由及路由守卫 app.use(router); diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index ad1dce0f9..9cdb0863b 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -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); diff --git a/apps/web-naive/src/bootstrap.ts b/apps/web-naive/src/bootstrap.ts index 40416d826..d4172b28c 100644 --- a/apps/web-naive/src/bootstrap.ts +++ b/apps/web-naive/src/bootstrap.ts @@ -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); diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json index 83ca5b29d..dc8ee773c 100644 --- a/packages/effects/common-ui/package.json +++ b/packages/effects/common-ui/package.json @@ -22,6 +22,7 @@ "dependencies": { "@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:*", @@ -32,8 +33,10 @@ "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", "qrcode": "catalog:", + "tippy.js": "catalog:", "vue": "catalog:", - "vue-router": "catalog:" + "vue-router": "catalog:", + "vue-tippy": "catalog:" }, "devDependencies": { "@types/qrcode": "catalog:" diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts index 6bba55262..e1874de7f 100644 --- a/packages/effects/common-ui/src/components/index.ts +++ b/packages/effects/common-ui/src/components/index.ts @@ -5,6 +5,7 @@ export * from './ellipsis-text'; export * from './icon-picker'; export * from './page'; export * from './resize'; +export * from './tippy'; export * from '@vben-core/form-ui'; export * from '@vben-core/popup-ui'; diff --git a/packages/effects/common-ui/src/components/tippy/directive.ts b/packages/effects/common-ui/src/components/tippy/directive.ts new file mode 100644 index 000000000..ed277d6a6 --- /dev/null +++ b/packages/effects/common-ui/src/components/tippy/directive.ts @@ -0,0 +1,100 @@ +import type { ComputedRef, Directive } from 'vue'; + +import { useTippy } from 'vue-tippy'; + +export default function useTippyDirective(isDark: ComputedRef) { + 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; +} diff --git a/packages/effects/common-ui/src/components/tippy/index.ts b/packages/effects/common-ui/src/components/tippy/index.ts new file mode 100644 index 000000000..344a7bdaf --- /dev/null +++ b/packages/effects/common-ui/src/components/tippy/index.ts @@ -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, 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, + ); +}; diff --git a/playground/src/bootstrap.ts b/playground/src/bootstrap.ts index 09b8cfa8b..d82c8e5e6 100644 --- a/playground/src/bootstrap.ts +++ b/playground/src/bootstrap.ts @@ -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); diff --git a/playground/src/router/routes/modules/examples.ts b/playground/src/router/routes/modules/examples.ts index b02bc4da9..796897870 100644 --- a/playground/src/router/routes/modules/examples.ts +++ b/playground/src/router/routes/modules/examples.ts @@ -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: 'material-symbols:chat-bubble', + title: 'Tippy', + }, + }, ], }, ]; diff --git a/playground/src/views/examples/tippy/index.vue b/playground/src/views/examples/tippy/index.vue new file mode 100644 index 000000000..d73c4559d --- /dev/null +++ b/playground/src/views/examples/tippy/index.vue @@ -0,0 +1,226 @@ + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d33595845..1f59633b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,9 @@ catalogs: theme-colors: specifier: ^0.1.0 version: 0.1.0 + tippy.js: + specifier: ^6.2.5 + version: 6.3.7 turbo: specifier: ^2.3.3 version: 2.3.3 @@ -474,6 +477,9 @@ catalogs: vue-router: specifier: ^4.5.0 version: 4.5.0 + vue-tippy: + specifier: ^6.6.0 + version: 6.6.0 vue-tsc: specifier: 2.1.10 version: 2.1.10 @@ -1488,6 +1494,9 @@ importers: '@vben-core/popup-ui': specifier: workspace:* version: link:../../@core/ui-kit/popup-ui + '@vben-core/preferences': + specifier: workspace:* + version: link:../../@core/preferences '@vben-core/shadcn-ui': specifier: workspace:* version: link:../../@core/ui-kit/shadcn-ui @@ -1518,12 +1527,18 @@ importers: qrcode: specifier: 'catalog:' version: 1.5.4 + tippy.js: + specifier: 'catalog:' + version: 6.3.7 vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.3) vue-router: specifier: 'catalog:' version: 4.5.0(vue@3.5.13(typescript@5.7.3)) + vue-tippy: + specifier: 'catalog:' + version: 6.6.0(vue@3.5.13(typescript@5.7.3)) devDependencies: '@types/qrcode': specifier: 'catalog:' @@ -3802,6 +3817,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@redocly/ajv@8.11.2': resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} @@ -8872,6 +8890,7 @@ packages: rollup-plugin-visualizer@5.13.1: resolution: {integrity: sha512-vMg8i6BprL8aFm9DKvL2c8AwS8324EgymYQo9o6E26wgVvwMhsJxS37aNL6ZsU7X9iAcMYwdME7gItLfG5fwJg==} engines: {node: '>=18'} + deprecated: Contains unintended breaking changes hasBin: true peerDependencies: rolldown: 1.x @@ -9518,6 +9537,9 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -10097,6 +10119,11 @@ packages: peerDependencies: vue: ^3.5.13 + vue-tippy@6.6.0: + resolution: {integrity: sha512-ISRIUQDlcEP05K1nCbvlVcd8yuWS6S3dI91qD0A2slgtwwWjih8Fn9Aymq4SNaHQsdiP5+MLRPZVDxFjKMPgKA==} + peerDependencies: + vue: ^3.5.13 + vue-tsc@2.1.10: resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} hasBin: true @@ -12722,6 +12749,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + '@redocly/ajv@8.11.2': dependencies: fast-deep-equal: 3.1.3 @@ -19034,6 +19063,10 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -19728,6 +19761,11 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.7.3) + vue-tippy@6.6.0(vue@3.5.13(typescript@5.7.3)): + dependencies: + tippy.js: 6.3.7 + vue: 3.5.13(typescript@5.7.3) + vue-tsc@2.1.10(typescript@5.7.3): dependencies: '@volar/typescript': 2.4.11 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a054ec64..3830d757a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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.67 vxe-table: 4.10.0