🪄 添加 Popover 组件,用于显示动态渲染的提示框,支持移动端与 PC 端不同的交互模式

This commit is contained in:
hi2hi 2025-02-06 06:00:11 +00:00
parent f446221f45
commit 13d66010df
5 changed files with 359 additions and 43 deletions

292
src/components/popover.vue Normal file
View File

@ -0,0 +1,292 @@
<template>
<div
ref="triggerRef"
class="popover-trigger"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@focusin="handleFocusIn"
@focusout="handleFocusOut"
@click="handleTriggerClick"
>
<slot name="trigger" />
</div>
<Teleport to="body">
<div
v-show="isShow"
ref="popoverRef"
class="popover"
:style="[popoverStyle, { zIndex: currentZIndex }]"
>
<template v-if="$slots.title || title">
<div class="popover-body">
{{ title }}
</div>
</template>
<template v-else>
<div class="popover-body">
<slot name="default" />
</div>
</template>
</div>
</Teleport>
</template>
<script setup>
/**
组件名称Popover
组件说明
该组件在移动端与 PC 端提供不同的交互模式通过 "hover" "click" 来触发显示或隐藏提示浮层
若设置 unique 属性则在显示新浮层的同时会隐藏其他已显示的浮层
使用示例
<Popover title="示例标题" trigger="click">
<template #trigger>
<button>点击触发</button>
</template>
这是 Popover 的内容
</Popover>
Props:
- visible (Boolean默认 false)
Popover 的可见状态可供外部进行手动控制
- title (String默认 '')
Popover 的标题文本如不传则展示默认内容插槽
- trigger (String默认 'hover')
触发模式可选值为 "hover" "click"
- unique (Boolean默认 true)
如果为 true则在显示当前 Popover 时会自动隐藏其他已显示的 Popover
方法说明
- handleMouseEnter()
当鼠标移入触发元素时 trigger hover会显示 Popover
- handleMouseLeave()
当鼠标移出触发元素时 trigger hover会隐藏 Popover
- handleTriggerClick(e)
当在移动端或 trigger click 点击触发元素会切换 Popover 显示状态并在移动端下自动延时隐藏
- handleFocusIn()
当触发元素获得焦点时若触发方式为 hover会显示 Popover
- handleFocusOut()
当触发元素失去焦点时若触发方式为 hover会隐藏 Popover
注意事项
- 在移动端会根据窗口宽度做适配通过 document 监听点击事件和窗口大小变化来控制显示与关闭
- visible 通过外部控制时非移动端能手动实现 Popover 的显隐
*/
import {
ref,
computed,
onMounted,
onUnmounted,
watch,
} from 'vue';
import { getNextZIndex } from '../utils/zIndexManager';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
trigger: {
type: String,
default: 'hover',
validator: (value) => ['hover', 'click'].includes(value),
},
unique: {
type: Boolean,
default: true,
},
});
// Symbol
// z-index
// const baseZIndex = 1000;
// let zIndexCounter = baseZIndex;
const popoverRef = ref(null);
const position = ref({
x: 0,
y: 0,
});
const isMobile = ref(window.innerWidth < 600);
const isShow = ref(false);
const triggerRef = ref(null);
const currentZIndex = ref(1000);
// getCurrentPopover setCurrentPopover
//
const updateMobilePosition = () => {
if (!triggerRef.value) return;
const rect = triggerRef.value.getBoundingClientRect();
position.value = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height,
};
};
//
const updateShow = (value) => {
if (value) {
currentZIndex.value = getNextZIndex();
}
isShow.value = value;
};
const handleMouseEnter = () => {
if (!isMobile.value && props.trigger === 'hover') {
updateShow(true);
}
};
const handleMouseLeave = () => {
if (!isMobile.value && props.trigger === 'hover') {
updateShow(false);
}
};
let autoCloseTimer;
const handleTriggerClick = (e) => {
if (props.trigger === 'click' || isMobile.value) {
e.stopPropagation();
updateShow(!isShow.value);
if (isShow.value && isMobile.value) {
if (autoCloseTimer) {
clearTimeout(autoCloseTimer);
}
autoCloseTimer = setTimeout(() => {
isShow.value = false;
}, 5 * 1000);
updateMobilePosition();
}
}
};
const handleFocusIn = () => {
if (!isMobile.value && props.trigger === 'hover') {
isShow.value = true;
}
};
const handleFocusOut = () => {
if (!isMobile.value && props.trigger === 'hover') {
isShow.value = false;
}
};
//
const handleDocumentClick = (e) => {
if (isShow.value && !triggerRef.value?.contains(e.target) && !popoverRef.value?.contains(e.target)) {
isShow.value = false;
}
};
const updatePosition = (e) => {
if (isMobile.value || !isShow.value) return;
position.value = {
x: e.clientX,
y: e.clientY,
};
};
const popoverStyle = computed(() => {
if (isMobile.value) {
return {
position: 'fixed',
bottom: '10vh',
left: '50%',
transform: 'translateX(-50%)',
};
}
const { x, y } = position.value;
const rect = popoverRef.value?.getBoundingClientRect();
const offset = 15; // 20px
let left = x + offset;
let top = y + offset;
if (rect) {
//
if (left + rect.width > window.innerWidth) {
left = x - rect.width - offset;
}
//
if (top + rect.height > window.innerHeight) {
top = y - rect.height - offset;
}
}
return {
position: 'fixed',
left: `${left}px`,
top: `${top}px`,
};
});
const handleResize = () => {
isMobile.value = window.innerWidth < 600;
};
// visible
watch(() => props.visible, (newVal) => {
if (!isMobile.value) {
updateShow(newVal);
}
});
onMounted(() => {
if (isMobile.value || props.trigger === 'click') {
document.addEventListener('click', handleDocumentClick);
}
if (!isMobile.value) {
document.addEventListener('mousemove', updatePosition);
}
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
if (isMobile.value || props.trigger === 'click') {
document.removeEventListener('click', handleDocumentClick);
}
if (!isMobile.value) {
document.removeEventListener('mousemove', updatePosition);
}
window.removeEventListener('resize', handleResize);
// Popover
});
</script>
<style lang="scss" scoped>
.popover-trigger {
display: inline-block;
cursor: pointer;
}
.popover {
background: rgba(#000, 0.8);
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
// z-index
max-width: 300px;
@media screen and (max-width: 600px) {
max-width: 90%;
text-align: center;
box-shadow: 0 4px 12px rgba(251, 255, 217, 0.15);
}
.popover-body {
line-height: 1.4;
font-size: 14px;
//
white-space: pre-wrap;
}
}
</style>

View File

@ -5,11 +5,13 @@ import store from './store';
import config from './config';
import DotDotBox from './components/dot-dot-box.vue';
import Popover from './components/popover.vue';
export default (app) => {
app.use(router);
app.use(store);
app.component('DotDotBox', DotDotBox);
app.component('Popover', Popover);
app.config.globalProperties.$hasSarasaTerm = !import.meta.env.VITE_DISABLE_SARASA_TERM_SC;
app.config.globalProperties.$config = config;

View File

@ -0,0 +1,13 @@
const BASE_Z_INDEX = 1000;
let zIndexCounter = BASE_Z_INDEX;
export const getNextZIndex = () => {
zIndexCounter += 1;
return zIndexCounter;
};
export const getCurrentZIndex = () => zIndexCounter;
export const resetZIndex = () => {
zIndexCounter = BASE_Z_INDEX;
};

View File

@ -68,23 +68,26 @@
</div>
<div class="server-info-content">
<div class="server-info-item-group">
<span
<template
v-for="(ttItem, ttIndex) in temperatureData.list"
:key="`${info.ID}_temperature_${ttIndex}`"
class="server-info-item"
:class="`temperature--${ttItem.type}`"
:title="ttItem?.title || ''"
>
<span
class="server-info-item-label"
:title="ttItem.label"
>
{{ ttItem.label }}
</span>
<span class="server-info-item-value">
{{ ttItem.value }}
</span>
</span>
<popover :title="ttItem?.title || (`${ttItem.label}: ${ttItem.value}`)">
<template #trigger>
<span
class="server-info-item"
:class="`temperature--${ttItem.type}`"
>
<span class="server-info-item-label">
{{ ttItem.label }}
</span>
<span class="server-info-item-value">
{{ ttItem.value }}
</span>
</span>
</template>
</popover>
</template>
</div>
</div>
</div>

View File

@ -67,38 +67,44 @@
</div>
<div class="monitor-cate-group">
<div
<template
v-for="cateItem in monitorChartData.cateList"
:key="cateItem.id"
class="monitor-cate-item"
:class="{
disabled: showCates[cateItem.id] === false,
}"
:style="{
'--cate-color': cateItem.color,
}"
:title="cateItem.title"
@click="toggleShowCate(cateItem.id)"
>
<span class="cate-legend" />
<span
class="cate-name"
>
{{ cateItem.name }}
</span>
<span
v-if="cateItem.avg !== 0"
class="cate-avg-ms"
>
{{ cateItem.avg }}ms
</span>
<span
v-else
class="cate-avg-ms"
>
-ms
</span>
</div>
<popover :title="cateItem.title">
<template #trigger>
<div
class="monitor-cate-item"
:class="{
disabled: showCates[cateItem.id] === false,
}"
:style="{
'--cate-color': cateItem.color,
}"
@click="toggleShowCate(cateItem.id)"
>
<span class="cate-legend" />
<span
class="cate-name"
>
{{ cateItem.name }}
</span>
<span
v-if="cateItem.avg !== 0"
class="cate-avg-ms"
>
{{ cateItem.avg }}ms
</span>
<span
v-else
class="cate-avg-ms"
>
-ms
</span>
</div>
</template>
</popover>
</template>
</div>
<line-chart