This commit is contained in:
oneao committed 2025-09-05 17:29:10 +08:00
1 parent 48dac36685
commit d17b49cdd9
18 files changed
+532 -340

No files matched your search

+1
View File
@@ -10,6 +10,7 @@
},
"dependencies": {
"@ao/ao-design": "workspace:*",
"@floating-ui/vue": "^1.1.9",
"@iconify/vue": "5.0.0",
"naive-ui": "^2.42.0",
"vue": "^3.5.18"
@@ -4,5 +4,5 @@
@use './input/index.scss' as *;
@use './textarea/index.scss' as *;
@use './group/index.scss' as *;
@use "./popup/tooltip.scss" as *;
@use "./popup/index.scss" as *;
@@ -0,0 +1,10 @@
@use '../_styles/_variables.scss' as *;
$popup-prefix: #{$prefix}-popup;
.#{$popup-prefix} {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
}
@@ -1,158 +1,15 @@
// Tooltip.tsx
import type { PropType } from 'vue'
import { cloneVNode, defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
interface Position {
top: number
left: number
}
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
export default defineComponent({
name: 'Tooltip',
props: {
content: { type: String, required: true },
placement: { type: String as PropType<'top' | 'bottom' | 'left' | 'right'>, default: 'top' },
trigger: { type: String as PropType<'hover' | 'click'>, default: 'hover' },
offset: { type: Number, default: 6 },
},
name: 'Popup',
setup(props, { slots }) {
const visible = ref(false)
const targetEl = ref<HTMLElement | null>(null)
const tooltipEl = ref<HTMLDivElement | null>(null)
const prefixCls = getPrefixCls('popup')
const createTooltip = () => {
const el = document.createElement('div')
el.className = `tooltip tooltip-${props.placement}`
const contentEl = document.createElement('div')
contentEl.className = 'tooltip-content'
contentEl.textContent = props.content
const arrowEl = document.createElement('div')
arrowEl.className = 'tooltip-arrow'
el.appendChild(contentEl)
el.appendChild(arrowEl)
// 初始状态
el.style.position = 'absolute'
el.style.top = '0'
el.style.left = '0'
el.style.opacity = '0'
el.style.visibility = 'hidden'
el.style.pointerEvents = 'none'
document.body.appendChild(el)
tooltipEl.value = el
}
const getTargetDom = (): HTMLElement | null => {
if (!targetEl.value)
return null
let dom = targetEl.value
if ('$el' in dom)
dom = (dom as any).$el
return dom instanceof HTMLElement ? dom : null
}
const updatePosition = () => {
const tooltip = tooltipEl.value
const target = getTargetDom()
if (!tooltip || !target)
return
const rect = target.getBoundingClientRect()
const tooltipRect = tooltip.getBoundingClientRect()
const scrollX = window.scrollX || window.pageXOffset
const scrollY = window.scrollY || window.pageYOffset
let top = 0
let left = 0
const offset = props.offset
switch (props.placement) {
case 'top':
top = rect.top + scrollY - tooltipRect.height - offset
left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2
break
case 'bottom':
top = rect.bottom + scrollY + offset
left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2
break
case 'left':
top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2
left = rect.left + scrollX - tooltipRect.width - offset
break
case 'right':
top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2
left = rect.right + scrollX + offset
break
}
tooltip.style.top = `${top}px`
tooltip.style.left = `${left}px`
}
const show = () => {
const tooltip = tooltipEl.value
if (!tooltip || !getTargetDom())
return
tooltip.style.visibility = 'visible'
tooltip.style.pointerEvents = 'auto'
nextTick(() => {
updatePosition()
tooltip.style.opacity = '1'
tooltip.style.transform = 'scale(1)'
})
visible.value = true
}
const hide = () => {
const tooltip = tooltipEl.value
if (!tooltip)
return
tooltip.style.opacity = '0'
tooltip.style.transform = 'scale(0.95)'
tooltip.style.pointerEvents = 'none'
tooltip.style.visibility = 'hidden'
visible.value = false
}
const toggle = () => (visible.value ? hide() : show())
onMounted(() => {
createTooltip()
const onUpdate = () => {
if (visible.value)
updatePosition()
}
window.addEventListener('scroll', onUpdate)
window.addEventListener('resize', onUpdate)
onBeforeUnmount(() => {
window.removeEventListener('scroll', onUpdate)
window.removeEventListener('resize', onUpdate)
if (tooltipEl.value && tooltipEl.value.parentNode) {
tooltipEl.value.parentNode.removeChild(tooltipEl.value)
}
})
})
return () => {
const child = slots.default?.()[0]
if (!child)
return null
return cloneVNode(child, {
ref: targetEl,
onMouseenter: props.trigger === 'hover' ? show : undefined,
onMouseleave: props.trigger === 'hover' ? hide : undefined,
onClick: props.trigger === 'click' ? toggle : undefined,
})
}
return () => (
<div class={prefixCls}>
{/* {slots?.default()} */}
</div>
)
},
})
@@ -1,58 +0,0 @@
@use '../_styles/_variables.scss' as *;
.tooltip {
z-index: 9999;
padding: 6px 10px;
background: #fff;
border-radius: 4px;
color: #333;
font-size: 12px;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition:
opacity 0.2s ease,
transform 0.2s ease;
pointer-events: none;
opacity: 0;
transform: scale(0.95);
visibility: hidden;
&.visible {
opacity: 1;
pointer-events: auto;
}
.tooltip-arrow {
position: absolute;
width: 8px;
height: 8px;
background: #fff;
transform: rotate(45deg);
}
&.tooltip-top .tooltip-arrow,
&.tooltip-bottom .tooltip-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
&.tooltip-left .tooltip-arrow,
&.tooltip-right .tooltip-arrow {
top: 50%;
transform: translateY(-50%) rotate(45deg);
}
&.tooltip-top .tooltip-arrow {
bottom: -4px;
}
&.tooltip-bottom .tooltip-arrow {
top: -4px;
}
&.tooltip-left .tooltip-arrow {
right: -4px;
}
&.tooltip-right .tooltip-arrow {
left: -4px;
}
}
@@ -38,7 +38,6 @@ export default defineComponent({
icon={props.icon}
size={props.size}
spin
rotate={props.rotate}
/>
{props.tip && <span class={`${prefixCls}-tip`}>{props.tip}</span>}
</div>
+50
View File
@@ -11,6 +11,9 @@ importers:
'@ao/ao-design':
specifier: workspace:*
version: link:packages/ao-design
'@floating-ui/vue':
specifier: ^1.1.9
version: 1.1.9(vue@3.5.19(typescript@5.8.3))
'@iconify/vue':
specifier: 5.0.0
version: 5.0.0(vue@3.5.19(typescript@5.8.3))
@@ -510,6 +513,18 @@ packages:
resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.4':
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/vue@1.1.9':
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -2292,6 +2307,17 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-eslint-parser@10.2.0:
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2768,6 +2794,26 @@ snapshots:
'@eslint/core': 0.15.2
levn: 0.4.1
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.4':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
'@floating-ui/utils@0.2.10': {}
'@floating-ui/vue@1.1.9(vue@3.5.19(typescript@5.8.3))':
dependencies:
'@floating-ui/dom': 1.7.4
'@floating-ui/utils': 0.2.10
vue-demi: 0.14.10(vue@3.5.19(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -4751,6 +4797,10 @@ snapshots:
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.19(typescript@5.8.3)):
dependencies:
vue: 3.5.19(typescript@5.8.3)
vue-eslint-parser@10.2.0(eslint@9.34.0):
dependencies:
debug: 4.4.1
+57 -20
View File
@@ -1,28 +1,65 @@
<script setup lang="ts">
function setDark() {
document.documentElement.setAttribute('theme-mode', 'dark')
document.body.style.backgroundColor = '#242424'
}
<script setup>
import { arrow, offset, useFloating } from '@floating-ui/vue'
import { ref } from 'vue'
function setLight() {
document.documentElement.setAttribute('theme-mode', 'light')
document.body.style.backgroundColor = '#ffffff'
}
const reference = ref(null) // 按钮
const floating = ref(null) // tooltip
const floatingArrow = ref(null)
// 使用 useFloating 计算浮动位置
const { floatingStyles, middlewareData } = useFloating(reference, floating, {
middleware: [offset(8), arrow({ element: floatingArrow })],
placement: 'bottom',
})
</script>
<template>
<div style="height: 100vh;width: 100%;display: flex;align-items: center;flex-direction: column;justify-content: center;gap:16px">
<div style="display: flex; gap: 8px;">
<AButton status="primary" size="medium" @click="setDark">
黑色主题
</AButton>
<div style="padding: 100px;">
<APopup />
<button
ref="reference" style="width: 200px;"
>
Button
</button>
<AButton status="primary" size="medium" @click="setLight">
白色主题
</AButton>
<div ref="floating" class="floating" :style="floatingStyles">
Tooltip
<div
ref="floatingArrow"
class="arrow"
:style="{
position: 'absolute',
left:
middlewareData.arrow?.x != null
? `${middlewareData.arrow.x}px`
: '',
top:
middlewareData.arrow?.y != null
? `${middlewareData.arrow.y}px`
: '',
}"
/>
</div>
<APopup content="123456" trigger="hover">
<AButton>测试</AButton>
</APopup>
</div>
</template>
<style scoped>
/* tooltip 样式 */
.floating {
padding: 6px 12px;
background-color: #fff;
color: #333;
border-radius: 2px;
font-size: 12px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); /* 添加阴影 */
}
/* 箭头 */
.arrow {
position: absolute;
width: 8px;
height: 8px;
background-color: #fff;
transform: rotate(45deg);
}
</style>
+98 -108
View File
@@ -1,115 +1,105 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>按钮示例</title>
<!-- 这里使用 Font Awesome CDN,你可以换成自己的图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<style>
.btn {
display: inline-flex; /* 内部水平排列 */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
gap: 4px; /* 图标和文字间距 */
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f4f5f5;
cursor: pointer;
font-size: 14px;
color: #333;
font-family: sans-serif;
}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popup with Arrow</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 50px;
}
.btn:hover {
background-color: #eaeaea;
}
/* 触发按钮 */
.btn {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
}
.btn:active {
background-color: #dcdcdc;
}
/* Popup 基础样式 */
.popup {
position: absolute;
padding: 10px 15px;
background-color: #333;
color: #fff;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: none;
z-index: 100;
max-width: 200px;
text-align: center;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px; /* 控制图标大小 */
}
/* 箭头公共样式 */
.popup::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
}
.btn-text {
display: inline-block;
}
</style>
</head>
<body>
<!-- 图标在左 -->
<button class="btn">
<i class="fas fa-check btn-icon"></i>
<span class="btn-text">按钮文字</span>
</button>
/* 箭头方向 */
.popup.top::after {
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0 8px;
border-color: #333 transparent transparent transparent;
}
<!-- 图标在右 -->
<button class="btn">
<span class="btn-text">按钮文字</span>
<i class="fas fa-arrow-right btn-icon"></i>
</button>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>按钮示例</title>
<!-- 这里使用 Font Awesome CDN,你可以换成自己的图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<style>
.btn {
display: inline-flex; /* 内部水平排列 */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
gap: 4px; /* 图标和文字间距 */
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f4f5f5;
cursor: pointer;
font-size: 14px;
color: #333;
}
.btn:hover {
background-color: #eaeaea;
}
.btn:active {
background-color: #dcdcdc;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px; /* 控制图标大小 */
}
.btn-text {
display: inline-block;
}
</style>
</head>
<body>
<!-- 图标在左 -->
<button class="btn">
<i class="fas fa-check btn-icon"></i>
<span class="btn-text">按钮文字</span>
</button>
<!-- 图标在右 -->
<button class="btn">
<span class="btn-text">按钮文字</span>
<i class="fas fa-arrow-right btn-icon"></i>
</button>
</body>
.popup.bottom::after {
top: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 0 8px 8px 8px;
border-color: transparent transparent #333 transparent;
}
.popup.left::after {
top: 50%;
right: -8px;
transform: translateY(-50%);
border-width: 8px 0 8px 8px;
border-color: transparent transparent transparent #333;
}
.popup.right::after {
top: 50%;
left: -8px;
transform: translateY(-50%);
border-width: 8px 8px 8px 0;
border-color: transparent #333 transparent transparent;
}
</style>
</head>
<body>
<button class="btn" id="btn">Hover me</button>
<div class="popup top" id="popup">这是一个带箭头的 Popup!</div>
<script>
const btn = document.getElementById('btn');
const popup = document.getElementById('popup');
// 鼠标悬停显示
btn.addEventListener('mouseenter', () => {
const rect = btn.getBoundingClientRect();
popup.style.display = 'block';
// 设置 Popup 位置(示例:顶部)
popup.style.left = rect.left + rect.width / 2 - popup.offsetWidth / 2 + 'px';
popup.style.top = rect.top - popup.offsetHeight - 10 + 'px';
});
btn.addEventListener('mouseleave', () => {
popup.style.display = 'none';
});
</script>
</body>
</html>