实现动态日活趋势图 - 支持数据驱动,自动生成曲线,支持交互点击
This commit is contained in:
@@ -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"/>
|
||||
|
||||
<!-- 工具提示 - 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>
|
||||
<!-- 高亮数据点 -->
|
||||
<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"/>
|
||||
|
||||
<!-- 动态工具提示 -->
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user