实现动态日活趋势图 - 支持数据驱动,自动生成曲线,支持交互点击
This commit is contained in:
@@ -109,8 +109,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div class="line-chart">
|
<div class="line-chart">
|
||||||
<!-- SVG 曲线图 -->
|
<!-- 动态SVG曲线图 -->
|
||||||
<svg width="100%" height="200" viewBox="0 0 840 200" class="chart-svg">
|
<svg width="100%" height="200" :viewBox="`0 0 ${chartWidth} ${chartHeight}`" class="chart-svg">
|
||||||
<!-- 网格线 -->
|
<!-- 网格线 -->
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="grid" width="60" height="40" patternUnits="userSpaceOnUse">
|
<pattern id="grid" width="60" height="40" patternUnits="userSpaceOnUse">
|
||||||
@@ -119,61 +119,61 @@
|
|||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
<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"
|
fill="none"
|
||||||
stroke="#3b82f6"
|
stroke="#3b82f6"
|
||||||
stroke-width="3"
|
stroke-width="3"
|
||||||
class="chart-line-path"/>
|
class="chart-line-path"/>
|
||||||
|
|
||||||
<!-- 12个月的数据点 - 每个月一个点 -->
|
<!-- 动态数据点 -->
|
||||||
<circle cx="60" cy="160" r="4" fill="#3b82f6" class="chart-dot"/>
|
<circle
|
||||||
<circle cx="120" cy="150" r="4" fill="#3b82f6" class="chart-dot"/>
|
v-for="(point, index) in chartPoints"
|
||||||
<circle cx="180" cy="140" r="4" fill="#3b82f6" class="chart-dot"/>
|
:key="index"
|
||||||
<circle cx="240" cy="120" r="4" fill="#3b82f6" class="chart-dot"/>
|
:cx="point.x"
|
||||||
<circle cx="300" cy="100" r="4" fill="#3b82f6" class="chart-dot"/>
|
:cy="point.y"
|
||||||
<circle cx="360" cy="90" r="4" fill="#3b82f6" class="chart-dot"/>
|
r="4"
|
||||||
<circle cx="420" cy="80" r="4" fill="#3b82f6" class="chart-dot"/>
|
fill="#3b82f6"
|
||||||
<circle cx="480" cy="85" r="4" fill="#3b82f6" class="chart-dot"/>
|
class="chart-dot"
|
||||||
<circle cx="540" cy="95" r="4" fill="#3b82f6" class="chart-dot"/>
|
@click="handlePointClick(point)"
|
||||||
<circle cx="600" cy="100" r="4" fill="#3b82f6" class="chart-dot"/>
|
style="cursor: pointer;"/>
|
||||||
<circle cx="660" cy="90" r="4" fill="#3b82f6" class="chart-dot"/>
|
|
||||||
<circle cx="720" cy="70" r="4" fill="#3b82f6" class="chart-dot"/>
|
|
||||||
|
|
||||||
<!-- 高亮数据点 - 6月数据点 -->
|
<!-- 高亮数据点 -->
|
||||||
<circle cx="360" cy="90" r="6" fill="#3b82f6" class="highlight-dot"/>
|
<template v-if="highlightedPoint">
|
||||||
<circle cx="360" cy="90" r="12" fill="#3b82f6" opacity="0.2" class="highlight-ring"/>
|
<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)">
|
<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"/>
|
<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="-25" text-anchor="middle" fill="white" font-size="12" font-weight="600" class="tooltip-value">
|
||||||
<text x="0" y="-10" text-anchor="middle" fill="white" font-size="10" opacity="0.8" class="tooltip-date">6月12号</text>
|
{{ 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"/>
|
<polygon points="0,0 -5,10 5,10" fill="#1e293b" class="tooltip-arrow"/>
|
||||||
</g>
|
</g>
|
||||||
|
</template>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-x-axis">
|
<div class="chart-x-axis">
|
||||||
<span>1月</span>
|
<span v-for="data in monthlyData" :key="data.month">{{ data.label }}</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-y-axis">
|
<div class="chart-y-axis">
|
||||||
<span>1500</span>
|
<span v-for="tick in [1500, 1200, 900, 600, 300, 0]" :key="tick">{{ tick }}</span>
|
||||||
<span>1000</span>
|
|
||||||
<span>500</span>
|
|
||||||
<span>100</span>
|
|
||||||
<span>0</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
@@ -239,10 +239,87 @@ import { ElMessage } from 'element-plus'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
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 = () => {
|
const goToUsers = () => {
|
||||||
ElMessage.info('跳转到会员管理')
|
ElMessage.info('跳转到会员管理')
|
||||||
// router.push('/admin/users')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToOrders = () => {
|
const goToOrders = () => {
|
||||||
@@ -256,21 +333,37 @@ const goToOrders = () => {
|
|||||||
|
|
||||||
const goToAPI = () => {
|
const goToAPI = () => {
|
||||||
ElMessage.info('跳转到API管理')
|
ElMessage.info('跳转到API管理')
|
||||||
// router.push('/admin/api')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToTasks = () => {
|
const goToTasks = () => {
|
||||||
ElMessage.info('跳转到生成任务记录')
|
ElMessage.info('跳转到生成任务记录')
|
||||||
// router.push('/admin/tasks')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToSettings = () => {
|
const goToSettings = () => {
|
||||||
ElMessage.info('跳转到系统设置')
|
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(() => {
|
onMounted(() => {
|
||||||
// 初始化数据
|
loadChartData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user