|
@@ -0,0 +1,323 @@
|
|
|
+<template>
|
|
|
+ <div class="horizontal-bar-chart-container">
|
|
|
+ <div class="chart-wrapper">
|
|
|
+ <div class="title">
|
|
|
+ <div v-for="item in title" :key="item">
|
|
|
+ {{ item }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-container">
|
|
|
+ <div class="chart-item" v-for="item in displayData" :key="item.name" @mouseenter="showTooltip($event, item)" @mouseleave="hideTooltip" @mousemove="updateTooltipPosition($event)">
|
|
|
+ <div class="chart-item-name" :title="item.name">{{ item.name }}</div>
|
|
|
+ <div class="chart-item-bar">
|
|
|
+ <div :style="{ width: `${item.percentage ? item.percentage : item.value / count * 100 + '%'}` }"></div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-item-value">{{ item.percentage || item.value }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 鼠标跟随浮窗 -->
|
|
|
+ <Transition name="tooltip-fade" mode="out-in">
|
|
|
+ <div v-if="tooltipVisible"
|
|
|
+ class="tooltip"
|
|
|
+ :class="{ 'tooltip-left': shouldShowOnLeft }"
|
|
|
+ :style="{
|
|
|
+ left: tooltipPosition.x + 'px',
|
|
|
+ top: tooltipPosition.y + 'px',
|
|
|
+ opacity: tooltipOpacity
|
|
|
+ }"
|
|
|
+ v-html="tooltipContent">
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { computed, onMounted, watch, ref, onUnmounted } from 'vue'
|
|
|
+
|
|
|
+interface BarItem {
|
|
|
+ name: string
|
|
|
+ value: number
|
|
|
+ percentage?: string
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<{
|
|
|
+ title: string[],
|
|
|
+ data?: BarItem[],
|
|
|
+}>(), {
|
|
|
+ data: () => []
|
|
|
+})
|
|
|
+
|
|
|
+// 默认数据
|
|
|
+const defaultData: BarItem[] = []
|
|
|
+const count = ref(0);
|
|
|
+
|
|
|
+// tooltip相关状态
|
|
|
+const tooltipVisible = ref(false)
|
|
|
+const tooltipContent = ref('')
|
|
|
+const tooltipPosition = ref({ x: 0, y: 0 })
|
|
|
+const tooltipOpacity = ref(0)
|
|
|
+const isHovering = ref(false)
|
|
|
+
|
|
|
+// 计算属性:判断tooltip是否应该显示在左边
|
|
|
+const shouldShowOnLeft = computed(() => {
|
|
|
+ return tooltipPosition.value.x < (window.innerWidth / 2)
|
|
|
+})
|
|
|
+
|
|
|
+// 防抖定时器
|
|
|
+let debounceTimer: number | null = null
|
|
|
+let fadeTimer: number | null = null
|
|
|
+
|
|
|
+// 防抖函数
|
|
|
+const debounce = (func: Function, delay: number) => {
|
|
|
+ if (debounceTimer) {
|
|
|
+ clearTimeout(debounceTimer)
|
|
|
+ }
|
|
|
+ debounceTimer = window.setTimeout(func, delay)
|
|
|
+}
|
|
|
+
|
|
|
+// tooltip方法
|
|
|
+const showTooltip = (event: MouseEvent, item: BarItem) => {
|
|
|
+ tooltipContent.value = `${props.title[0]}:${item.name}<br/>${props.title[1]}:${item.percentage || item.value}`
|
|
|
+
|
|
|
+ // 设置悬停状态
|
|
|
+ isHovering.value = true
|
|
|
+
|
|
|
+ // 清除之前的淡出定时器
|
|
|
+ if (fadeTimer) {
|
|
|
+ clearTimeout(fadeTimer)
|
|
|
+ fadeTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 立即显示,但透明度为0
|
|
|
+ tooltipVisible.value = true
|
|
|
+ tooltipOpacity.value = 0
|
|
|
+
|
|
|
+ // 使用requestAnimationFrame确保DOM更新后再设置位置和透明度
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ updateTooltipPosition(event)
|
|
|
+ // 淡入效果
|
|
|
+ tooltipOpacity.value = 1
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const updateTooltipPosition = (event: MouseEvent) => {
|
|
|
+ // 获取窗口宽度
|
|
|
+ const windowWidth = window.innerWidth
|
|
|
+ const tooltipWidth = 120 // 预估的tooltip宽度
|
|
|
+ const offset = 10
|
|
|
+
|
|
|
+ // 计算tooltip应该显示的位置
|
|
|
+ let x = event.clientX + offset
|
|
|
+ let y = event.clientY - offset
|
|
|
+
|
|
|
+ // 如果tooltip会超出右边界,则显示在左边
|
|
|
+ if (x + tooltipWidth > windowWidth) {
|
|
|
+ x = event.clientX - tooltipWidth - offset
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保不超出左边界
|
|
|
+ if (x < 0) {
|
|
|
+ x = offset
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保不超出上边界
|
|
|
+ if (y < 0) {
|
|
|
+ y = offset
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在悬停,直接更新位置,不使用防抖
|
|
|
+ if (isHovering.value) {
|
|
|
+ tooltipPosition.value = { x, y }
|
|
|
+ } else {
|
|
|
+ // 只有在非悬停状态才使用防抖
|
|
|
+ debounce(() => {
|
|
|
+ tooltipPosition.value = { x, y }
|
|
|
+ }, 16) // 约60fps的更新频率
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const hideTooltip = () => {
|
|
|
+ // 设置悬停状态为false
|
|
|
+ isHovering.value = false
|
|
|
+
|
|
|
+ // 淡出效果
|
|
|
+ tooltipOpacity.value = 0
|
|
|
+
|
|
|
+ // 等待淡出动画完成后隐藏
|
|
|
+ if (fadeTimer) {
|
|
|
+ clearTimeout(fadeTimer)
|
|
|
+ }
|
|
|
+ fadeTimer = window.setTimeout(() => {
|
|
|
+ // 只有在不悬停时才隐藏
|
|
|
+ if (!isHovering.value) {
|
|
|
+ tooltipVisible.value = false
|
|
|
+ }
|
|
|
+ }, 200) // 与CSS过渡时间匹配
|
|
|
+}
|
|
|
+
|
|
|
+const displayData = computed(() => {
|
|
|
+ return props.data && props.data.length > 0 ? props.data : defaultData
|
|
|
+})
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 组件挂载后的初始化逻辑
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ // 清理定时器
|
|
|
+ if (debounceTimer) {
|
|
|
+ clearTimeout(debounceTimer)
|
|
|
+ }
|
|
|
+ if (fadeTimer) {
|
|
|
+ clearTimeout(fadeTimer)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 监听数据变化
|
|
|
+watch(displayData, (newData) => {
|
|
|
+ count.value = newData.reduce((count, item) => {
|
|
|
+ count = item.value + count;
|
|
|
+ return count;
|
|
|
+ }, 0);
|
|
|
+}, {
|
|
|
+ deep: true,
|
|
|
+ immediate: true
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.horizontal-bar-chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-wrapper {
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-weight: 400;
|
|
|
+ color: rgba(100, 100, 100, 1);
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ vertical-align: middle;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ .chart-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: left;
|
|
|
+ width: 100%;
|
|
|
+ height: 40px;
|
|
|
+ vertical-align: middle;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 0;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ &:hover {
|
|
|
+ cursor: pointer;
|
|
|
+ .chart-item-bar {
|
|
|
+ border: 1px solid rgba(92, 223, 223, 1);
|
|
|
+ div {
|
|
|
+ // background: #81b6f7;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .chart-item-name,
|
|
|
+ .chart-item-value {
|
|
|
+ width: 75px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+ text-align: left;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .chart-item-name {
|
|
|
+ width: 95px;
|
|
|
+ }
|
|
|
+ .chart-item-value {
|
|
|
+ width: 84px;
|
|
|
+ }
|
|
|
+ .chart-item-bar {
|
|
|
+ width: 100%;
|
|
|
+ height: 12px;
|
|
|
+ background: rgba(244, 244, 244, 1);
|
|
|
+ div {
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(92, 223, 223, 1);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .chart-item-value {
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+.tooltip {
|
|
|
+ position: fixed;
|
|
|
+ z-index: 9999;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ color: #000000;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.4;
|
|
|
+ pointer-events: none;
|
|
|
+ white-space: nowrap;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ transition: opacity 0.2s ease, transform 0.2s ease;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ text-align: left;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 25px;
|
|
|
+ left: -4px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-top: 4px solid transparent;
|
|
|
+ border-bottom: 4px solid transparent;
|
|
|
+ border-right: 4px solid rgba(255, 255, 255, 0.95);
|
|
|
+ filter: drop-shadow(-1px 0 1px rgba(0, 0, 0, 0.1));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 当显示在左边时的样式
|
|
|
+ &.tooltip-left {
|
|
|
+ &::before {
|
|
|
+ left: auto;
|
|
|
+ right: -4px;
|
|
|
+ border-right: none;
|
|
|
+ border-left: 4px solid rgba(255, 255, 255, 0.95);
|
|
|
+ filter: drop-shadow(1px 0 1px rgba(0, 0, 0, 0.1));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 淡入淡出动画
|
|
|
+.tooltip-fade-enter-active,
|
|
|
+.tooltip-fade-leave-active {
|
|
|
+ transition: opacity 0.2s ease, transform 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.tooltip-fade-enter-from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-4px) scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
+.tooltip-fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-4px) scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
+ </style>
|