实现动态日活趋势图 - 支持数据驱动,自动生成曲线,支持交互点击

This commit is contained in:
AIGC Developer
2025-10-22 09:34:27 +08:00
parent 97997b0833
commit 85a625a7f1

View File

@@ -109,8 +109,8 @@
</div>
<div class="chart-container">
<div class="line-chart">
<!-- SVG 曲线图 -->
<svg width="100%" height="200" viewBox="0 0 840 200" class="chart-svg">
<!-- 动态SVG曲线图 -->
<svg width="100%" height="200" :viewBox="`0 0 ${chartWidth} ${chartHeight}`" class="chart-svg">
<!-- 网格线 -->
<defs>
<pattern id="grid" width="60" height="40" patternUnits="userSpaceOnUse">
@@ -119,61 +119,61 @@
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- 数据曲线 - 平滑连接12个月的数据点 -->
<path d="M 60,160 C 90,155 120,150 150,140 C 180,130 210,115 240,120 C 270,105 300,100 330,90 C 360,80 390,85 420,80 C 450,85 480,85 510,95 C 540,95 570,100 600,100 C 630,95 660,90 690,80 C 720,70 750,70 780,70"
<!-- 动态数据曲线 -->
<path :d="chartPath"
fill="none"
stroke="#3b82f6"
stroke-width="3"
class="chart-line-path"/>
<!-- 12个月的数据点 - 每个月一个 -->
<circle cx="60" cy="160" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="120" cy="150" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="180" cy="140" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="240" cy="120" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="300" cy="100" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="360" cy="90" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="420" cy="80" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="480" cy="85" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="540" cy="95" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="600" cy="100" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="660" cy="90" r="4" fill="#3b82f6" class="chart-dot"/>
<circle cx="720" cy="70" r="4" fill="#3b82f6" class="chart-dot"/>
<!-- 动态数据 -->
<circle
v-for="(point, index) in chartPoints"
:key="index"
:cx="point.x"
:cy="point.y"
r="4"
fill="#3b82f6"
class="chart-dot"
@click="handlePointClick(point)"
style="cursor: pointer;"/>
<!-- 高亮数据点 - 6月数据点 -->
<circle cx="360" cy="90" r="6" fill="#3b82f6" class="highlight-dot"/>
<circle cx="360" cy="90" r="12" fill="#3b82f6" opacity="0.2" class="highlight-ring"/>
<!-- 高亮数据点 -->
<template v-if="highlightedPoint">
<circle
:cx="highlightedPoint.x"
:cy="highlightedPoint.y"
r="6"
fill="#3b82f6"
class="highlight-dot"/>
<circle
:cx="highlightedPoint.x"
:cy="highlightedPoint.y"
r="12"
fill="#3b82f6"
opacity="0.2"
class="highlight-ring"/>
<!-- 工具提示 - 6月数据 -->
<g class="tooltip-group" transform="translate(360, 70)">
<rect x="-30" y="-40" width="60" height="30" rx="6" fill="#1e293b" class="tooltip-bg"/>
<text x="0" y="-25" text-anchor="middle" fill="white" font-size="12" font-weight="600" class="tooltip-value">1,000</text>
<text x="0" y="-10" text-anchor="middle" fill="white" font-size="10" opacity="0.8" class="tooltip-date">6月12号</text>
<!-- 工具提示箭头 -->
<polygon points="0,0 -5,10 5,10" fill="#1e293b" class="tooltip-arrow"/>
</g>
<!-- 动态工具提示 -->
<g class="tooltip-group" :transform="`translate(${highlightedPoint.x}, ${highlightedPoint.y - 20})`">
<rect x="-30" y="-40" width="60" height="30" rx="6" fill="#1e293b" class="tooltip-bg"/>
<text x="0" y="-25" text-anchor="middle" fill="white" font-size="12" font-weight="600" class="tooltip-value">
{{ highlightedPoint.value.toLocaleString() }}
</text>
<text x="0" y="-10" text-anchor="middle" fill="white" font-size="10" opacity="0.8" class="tooltip-date">
{{ highlightedPoint.label }}
</text>
<!-- 工具提示箭头 -->
<polygon points="0,0 -5,10 5,10" fill="#1e293b" class="tooltip-arrow"/>
</g>
</template>
</svg>
</div>
<div class="chart-x-axis">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
<span>10</span>
<span>11</span>
<span>12</span>
<span v-for="data in monthlyData" :key="data.month">{{ data.label }}</span>
</div>
<div class="chart-y-axis">
<span>1500</span>
<span>1000</span>
<span>500</span>
<span>100</span>
<span>0</span>
<span v-for="tick in [1500, 1200, 900, 600, 300, 0]" :key="tick">{{ tick }}</span>
</div>
</div>
</div>
@@ -231,7 +231,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
@@ -239,10 +239,87 @@ import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
// 模拟数据 - 实际项目中应该从API获取
const monthlyData = ref([
{ month: 1, value: 1200, label: '1月' },
{ month: 2, value: 1100, label: '2月' },
{ month: 3, value: 1000, label: '3月' },
{ month: 4, value: 900, label: '4月' },
{ month: 5, value: 800, label: '5月' },
{ month: 6, value: 1000, label: '6月' },
{ month: 7, value: 1200, label: '7月' },
{ month: 8, value: 1150, label: '8月' },
{ month: 9, value: 1300, label: '9月' },
{ month: 10, value: 1250, label: '10月' },
{ month: 11, value: 1100, label: '11月' },
{ month: 12, value: 950, label: '12月' }
])
const selectedYear = ref(2025)
const highlightedPoint = ref(null)
// 计算图表尺寸和比例
const chartWidth = 800
const chartHeight = 200
const padding = 60
// 计算数据点的SVG坐标
const chartPoints = computed(() => {
const maxValue = Math.max(...monthlyData.value.map(d => d.value))
const minValue = Math.min(...monthlyData.value.map(d => d.value))
const valueRange = maxValue - minValue
return monthlyData.value.map((data, index) => {
const x = padding + (index * (chartWidth - 2 * padding) / (monthlyData.value.length - 1))
const y = padding + ((maxValue - data.value) / valueRange) * (chartHeight - 2 * padding)
return {
x,
y,
value: data.value,
label: data.label,
month: data.month
}
})
})
// 生成SVG路径
const chartPath = computed(() => {
if (chartPoints.value.length < 2) return ''
let path = `M ${chartPoints.value[0].x},${chartPoints.value[0].y}`
for (let i = 1; i < chartPoints.value.length; i++) {
const prev = chartPoints.value[i - 1]
const curr = chartPoints.value[i]
const next = chartPoints.value[i + 1]
if (next) {
// 使用三次贝塞尔曲线创建平滑路径
const cp1x = prev.x + (curr.x - prev.x) / 3
const cp1y = prev.y
const cp2x = curr.x - (next.x - curr.x) / 3
const cp2y = curr.y
path += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${curr.x},${curr.y}`
} else {
// 最后一个点
const cp1x = prev.x + (curr.x - prev.x) / 3
const cp1y = prev.y
path += ` C ${cp1x},${cp1y} ${curr.x},${curr.y} ${curr.x},${curr.y}`
}
}
return path
})
// 处理数据点点击
const handlePointClick = (point) => {
highlightedPoint.value = point
}
// 导航功能
const goToUsers = () => {
ElMessage.info('跳转到会员管理')
// router.push('/admin/users')
}
const goToOrders = () => {
@@ -256,21 +333,37 @@ const goToOrders = () => {
const goToAPI = () => {
ElMessage.info('跳转到API管理')
// router.push('/admin/api')
}
const goToTasks = () => {
ElMessage.info('跳转到生成任务记录')
// router.push('/admin/tasks')
}
const goToSettings = () => {
ElMessage.info('跳转到系统设置')
// router.push('/admin/settings')
}
// 模拟数据加载
const loadChartData = async () => {
try {
// 这里应该调用真实的API
// const response = await fetch('/api/dashboard/monthly-active-users')
// const data = await response.json()
// monthlyData.value = data
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 可以在这里更新数据
console.log('图表数据加载完成')
} catch (error) {
console.error('加载图表数据失败:', error)
ElMessage.error('加载数据失败')
}
}
onMounted(() => {
// 初始化数据
loadChartData()
})
</script>