mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-01-24 09:23:07 +08:00
feat: Echarts 替换 chart.js
This commit is contained in:
@@ -36,9 +36,11 @@
|
||||
"@zumer/snapdom": "^2.0.1",
|
||||
"ai": "^6.0.41",
|
||||
"better-sqlite3": "^12.4.6",
|
||||
"echarts": "^6.0.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"stream-json": "^1.9.1",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
@@ -57,7 +59,6 @@
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"axios": "^1.13.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.19",
|
||||
"electron": "^35.0.0",
|
||||
@@ -72,7 +73,6 @@
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.3.5",
|
||||
"vue": "^3.5.25",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
67
pnpm-lock.yaml
generated
67
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
||||
better-sqlite3:
|
||||
specifier: ^12.4.6
|
||||
version: 12.5.0
|
||||
echarts:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
electron-updater:
|
||||
specifier: ^6.6.2
|
||||
version: 6.7.3
|
||||
@@ -47,6 +50,9 @@ importers:
|
||||
stream-json:
|
||||
specifier: ^1.9.1
|
||||
version: 1.9.1
|
||||
vue-echarts:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1(echarts@6.0.0)(vue@3.5.26(typescript@5.9.3))
|
||||
vue-i18n:
|
||||
specifier: ^11.2.8
|
||||
version: 11.2.8(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -96,9 +102,6 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
chart.js:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
@@ -141,9 +144,6 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.25
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
vue-chartjs:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3(chart.js@4.5.1)(vue@3.5.26(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.6.3
|
||||
version: 4.6.4(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -677,9 +677,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
'@malept/cross-spawn-promise@2.0.0':
|
||||
resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
@@ -1790,10 +1787,6 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chart.js@4.5.1:
|
||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -2028,6 +2021,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
echarts@6.0.0:
|
||||
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
|
||||
|
||||
ejs@3.1.10:
|
||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3735,6 +3731,9 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -3975,12 +3974,6 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vue-chartjs@5.3.3:
|
||||
resolution: {integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==}
|
||||
peerDependencies:
|
||||
chart.js: ^4.1.1
|
||||
vue: ^3.0.0-0 || ^2.7.0
|
||||
|
||||
vue-component-type-helpers@3.2.2:
|
||||
resolution: {integrity: sha512-x8C2nx5XlUNM0WirgfTkHjJGO/ABBxlANZDtHw2HclHtQnn+RFPTnbjMJn8jHZW4TlUam0asHcA14lf1C6Jb+A==}
|
||||
|
||||
@@ -3995,6 +3988,12 @@ packages:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-echarts@8.0.1:
|
||||
resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==}
|
||||
peerDependencies:
|
||||
echarts: ^6.0.0
|
||||
vue: ^3.3.0
|
||||
|
||||
vue-eslint-parser@9.4.3:
|
||||
resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
@@ -4117,6 +4116,9 @@ packages:
|
||||
zod@4.3.5:
|
||||
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||
|
||||
zrender@6.0.0:
|
||||
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
7zip-bin@5.2.0: {}
|
||||
@@ -4686,8 +4688,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@malept/cross-spawn-promise@2.0.0':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -6042,10 +6042,6 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chart.js@4.5.1:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -6262,6 +6258,11 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
echarts@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.0.0
|
||||
|
||||
ejs@3.1.10:
|
||||
dependencies:
|
||||
jake: 10.9.4
|
||||
@@ -8134,6 +8135,8 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
@@ -8323,17 +8326,17 @@ snapshots:
|
||||
lightningcss: 1.30.2
|
||||
yaml: 2.8.2
|
||||
|
||||
vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
chart.js: 4.5.1
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
vue-component-type-helpers@3.2.2: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
vue-echarts@8.0.1(echarts@6.0.0)(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
echarts: 6.0.0
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -8451,3 +8454,7 @@ snapshots:
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@4.3.5: {}
|
||||
|
||||
zrender@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { HourlyActivity, WeekdayActivity, MonthlyActivity } from '@/types/analysis'
|
||||
import { BarChart } from '@/components/charts'
|
||||
import type { BarChartData } from '@/components/charts'
|
||||
import { EChartBar } from '@/components/charts'
|
||||
import type { EChartBarData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
@@ -20,7 +20,7 @@ const props = defineProps<{
|
||||
|
||||
// --- 24小时分布逻辑 ---
|
||||
// 24小时分布图数据
|
||||
const hourlyChartData = computed<BarChartData>(() => {
|
||||
const hourlyChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.hourlyActivity.map((h) => `${h.hour}:00`),
|
||||
values: props.hourlyActivity.map((h) => h.messageCount),
|
||||
@@ -60,7 +60,7 @@ const eveningRatio = computed(() => {
|
||||
|
||||
// --- 星期分布逻辑 ---
|
||||
// 星期分布图数据
|
||||
const weekdayChartData = computed<BarChartData>(() => {
|
||||
const weekdayChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.weekdayActivity.map((w) => props.weekdayNames[w.weekday - 1]),
|
||||
values: props.weekdayActivity.map((w) => w.messageCount),
|
||||
@@ -69,7 +69,7 @@ const weekdayChartData = computed<BarChartData>(() => {
|
||||
|
||||
// --- 月份分布逻辑 ---
|
||||
// 月份分布图数据
|
||||
const monthlyChartData = computed<BarChartData>(() => {
|
||||
const monthlyChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.monthlyActivity.map((m) => {
|
||||
// 中文用 X月,英文用 Jan, Feb 等
|
||||
@@ -89,11 +89,7 @@ const monthlyChartData = computed<BarChartData>(() => {
|
||||
<!-- 24小时分布 -->
|
||||
<SectionCard :title="t('hourlyTitle')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<BarChart
|
||||
:data="hourlyChartData"
|
||||
:height="256"
|
||||
:x-label-filter="(_, index) => (index % 3 === 0 ? `${index}:00` : '')"
|
||||
/>
|
||||
<EChartBar :data="hourlyChartData" :height="256" />
|
||||
|
||||
<div class="mt-6 grid grid-cols-4 gap-2">
|
||||
<div class="text-center">
|
||||
@@ -135,7 +131,7 @@ const monthlyChartData = computed<BarChartData>(() => {
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<BarChart :data="weekdayChartData" :height="256" />
|
||||
<EChartBar :data="weekdayChartData" :height="256" />
|
||||
|
||||
<div class="mt-6 grid grid-cols-2 gap-4">
|
||||
<div class="text-center">
|
||||
@@ -173,7 +169,7 @@ const monthlyChartData = computed<BarChartData>(() => {
|
||||
<div v-if="isLoadingMonthly" class="flex h-64 items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<BarChart v-else :data="monthlyChartData" :height="256" />
|
||||
<EChartBar v-else :data="monthlyChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
import { LineChart } from '@/components/charts'
|
||||
import type { LineChartData } from '@/components/charts'
|
||||
import { EChartLine } from '@/components/charts'
|
||||
import type { EChartLineData } from '@/components/charts'
|
||||
import type { DailyActivity } from '@/types/analysis'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
dailyActivity: DailyActivity[]
|
||||
dailyChartData: LineChartData
|
||||
dailyChartData: EChartLineData
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionCard v-if="dailyActivity.length > 0" :title="t('title')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<LineChart :data="dailyChartData" :height="288" />
|
||||
<EChartLine :data="dailyChartData" :height="288" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</template>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
export interface BarChartData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
colors?: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: BarChartData
|
||||
height?: number
|
||||
showLegend?: boolean
|
||||
borderRadius?: number
|
||||
colorMode?: 'static' | 'gradient' // static: 使用提供的colors, gradient: 根据值自动渐变
|
||||
xLabelFilter?: (label: string, index: number) => string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 256,
|
||||
showLegend: false,
|
||||
borderRadius: 4,
|
||||
colorMode: 'gradient',
|
||||
})
|
||||
|
||||
// 根据数据值计算渐变颜色
|
||||
const calculateColors = computed(() => {
|
||||
if (props.colorMode === 'static' && props.data.colors) {
|
||||
return props.data.colors
|
||||
}
|
||||
|
||||
const maxValue = Math.max(...props.data.values)
|
||||
return props.data.values.map((value) => {
|
||||
const intensity = value / maxValue
|
||||
if (intensity > 0.8) return '#6366f1'
|
||||
if (intensity > 0.6) return '#818cf8'
|
||||
if (intensity > 0.4) return '#a5b4fc'
|
||||
if (intensity > 0.2) return '#c7d2fe'
|
||||
return '#e0e7ff'
|
||||
})
|
||||
})
|
||||
|
||||
const chartData = computed(() => {
|
||||
return {
|
||||
labels: props.data.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('count'),
|
||||
data: props.data.values,
|
||||
backgroundColor: calculateColors.value,
|
||||
borderRadius: props.borderRadius,
|
||||
borderSkipped: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
const options: any = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.showLegend,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 添加自定义 x 轴标签过滤器
|
||||
if (props.xLabelFilter) {
|
||||
options.scales.x.ticks.callback = function (this: any, _: unknown, index: number) {
|
||||
const label = this.getLabelForValue(index)
|
||||
return props.xLabelFilter!(label, index)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ height: `${height}px` }">
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"count": "数量"
|
||||
},
|
||||
"en-US": {
|
||||
"count": "Count"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Doughnut } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend)
|
||||
|
||||
export interface DoughnutChartData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
colors?: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: DoughnutChartData
|
||||
cutout?: number | string
|
||||
height?: number
|
||||
showLegend?: boolean
|
||||
legendPosition?: 'top' | 'bottom' | 'left' | 'right'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
cutout: '60%',
|
||||
height: 256,
|
||||
showLegend: true,
|
||||
legendPosition: 'bottom',
|
||||
})
|
||||
|
||||
// 默认颜色方案
|
||||
const defaultColors = [
|
||||
'#6366f1', // indigo
|
||||
'#8b5cf6', // violet
|
||||
'#ec4899', // pink
|
||||
'#f43f5e', // rose
|
||||
'#f97316', // orange
|
||||
'#eab308', // yellow
|
||||
'#22c55e', // green
|
||||
'#94a3b8', // gray
|
||||
]
|
||||
|
||||
const chartData = computed(() => {
|
||||
return {
|
||||
labels: props.data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: props.data.values,
|
||||
backgroundColor: props.data.colors || defaultColors.slice(0, props.data.values.length),
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.showLegend,
|
||||
position: props.legendPosition,
|
||||
labels: {
|
||||
padding: 16,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
},
|
||||
},
|
||||
cutout: props.cutout,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ height: `${height}px` }">
|
||||
<Doughnut :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
158
src/components/charts/EChart.vue
Normal file
158
src/components/charts/EChart.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ECharts 基础封装组件
|
||||
* 提供自动响应式、主题适配、加载状态等通用功能
|
||||
*/
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import {
|
||||
PieChart,
|
||||
BarChart,
|
||||
LineChart,
|
||||
HeatmapChart,
|
||||
} from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
VisualMapComponent,
|
||||
} from 'echarts/components'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([
|
||||
CanvasRenderer,
|
||||
PieChart,
|
||||
BarChart,
|
||||
LineChart,
|
||||
HeatmapChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
VisualMapComponent,
|
||||
])
|
||||
|
||||
interface Props {
|
||||
option: EChartsOption
|
||||
height?: number | string
|
||||
loading?: boolean
|
||||
theme?: 'light' | 'dark' | 'auto'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300,
|
||||
loading: false,
|
||||
theme: 'auto',
|
||||
})
|
||||
|
||||
const chartRef = ref<HTMLDivElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 计算高度样式
|
||||
const heightStyle = computed(() => {
|
||||
if (typeof props.height === 'number') {
|
||||
return `${props.height}px`
|
||||
}
|
||||
return props.height
|
||||
})
|
||||
|
||||
// 检测暗色模式
|
||||
const isDark = computed(() => {
|
||||
if (props.theme === 'auto') {
|
||||
return document.documentElement.classList.contains('dark')
|
||||
}
|
||||
return props.theme === 'dark'
|
||||
})
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
if (!chartRef.value) return
|
||||
|
||||
// 销毁旧实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
chartInstance = echarts.init(chartRef.value, isDark.value ? 'dark' : undefined)
|
||||
chartInstance.setOption(props.option)
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateChart() {
|
||||
if (!chartInstance) {
|
||||
initChart()
|
||||
return
|
||||
}
|
||||
chartInstance.setOption(props.option, { notMerge: true })
|
||||
}
|
||||
|
||||
// 调整大小
|
||||
function handleResize() {
|
||||
chartInstance?.resize()
|
||||
}
|
||||
|
||||
// 监听 option 变化
|
||||
watch(() => props.option, updateChart, { deep: true })
|
||||
|
||||
// 监听主题变化
|
||||
watch(isDark, () => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
// 监听加载状态
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading) => {
|
||||
if (loading) {
|
||||
chartInstance?.showLoading('default', {
|
||||
text: '',
|
||||
spinnerRadius: 12,
|
||||
lineWidth: 2,
|
||||
})
|
||||
} else {
|
||||
chartInstance?.hideLoading()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听暗色模式变化
|
||||
let observer: MutationObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 监听 HTML 元素的 class 变化(用于检测暗色模式切换)
|
||||
observer = new MutationObserver(() => {
|
||||
if (props.theme === 'auto') {
|
||||
initChart()
|
||||
}
|
||||
})
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
observer?.disconnect()
|
||||
chartInstance?.dispose()
|
||||
chartInstance = null
|
||||
})
|
||||
|
||||
// 暴露方法供父组件调用
|
||||
defineExpose({
|
||||
getInstance: () => chartInstance,
|
||||
resize: handleResize,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height: heightStyle, width: '100%' }" />
|
||||
</template>
|
||||
|
||||
142
src/components/charts/EChartBar.vue
Normal file
142
src/components/charts/EChartBar.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ECharts 柱状图组件
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import EChart from './EChart.vue'
|
||||
|
||||
export interface EChartBarData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: EChartBarData
|
||||
height?: number
|
||||
/** 是否为横向柱状图 */
|
||||
horizontal?: boolean
|
||||
/** 是否显示渐变色 */
|
||||
gradient?: boolean
|
||||
/** 柱子圆角 */
|
||||
borderRadius?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 200,
|
||||
horizontal: false,
|
||||
gradient: true,
|
||||
borderRadius: 4,
|
||||
})
|
||||
|
||||
// 渐变色
|
||||
const gradientColor = {
|
||||
type: 'linear' as const,
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#ec4899' }, // pink-500
|
||||
{ offset: 1, color: '#f472b6' }, // pink-400
|
||||
],
|
||||
}
|
||||
|
||||
const option = computed<EChartsOption>(() => {
|
||||
const isHorizontal = props.horizontal
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: isHorizontal ? 60 : 40,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: isHorizontal ? 20 : 30,
|
||||
containLabel: false,
|
||||
},
|
||||
xAxis: isHorizontal
|
||||
? {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#e5e7eb',
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: 'category',
|
||||
data: props.data.labels,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
color: '#6b7280',
|
||||
},
|
||||
},
|
||||
yAxis: isHorizontal
|
||||
? {
|
||||
type: 'category',
|
||||
data: props.data.labels,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
color: '#6b7280',
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#e5e7eb',
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: props.data.values,
|
||||
itemStyle: {
|
||||
color: props.gradient ? gradientColor : '#ec4899',
|
||||
borderRadius: props.borderRadius,
|
||||
},
|
||||
barMaxWidth: 40,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: props.gradient
|
||||
? {
|
||||
...gradientColor,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#db2777' }, // pink-600
|
||||
{ offset: 1, color: '#ec4899' }, // pink-500
|
||||
],
|
||||
}
|
||||
: '#db2777',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EChart :option="option" :height="height" />
|
||||
</template>
|
||||
|
||||
116
src/components/charts/EChartLine.vue
Normal file
116
src/components/charts/EChartLine.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ECharts 折线图组件
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import EChart from './EChart.vue'
|
||||
|
||||
export interface EChartLineData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: EChartLineData
|
||||
height?: number
|
||||
/** 是否显示面积 */
|
||||
showArea?: boolean
|
||||
/** 是否平滑曲线 */
|
||||
smooth?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 288,
|
||||
showArea: true,
|
||||
smooth: true,
|
||||
})
|
||||
|
||||
const option = computed<EChartsOption>(() => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: 50,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 30,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.data.labels,
|
||||
boundaryGap: false,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
color: '#6b7280',
|
||||
// 自动间隔显示标签
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#e5e7eb',
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
data: props.data.values,
|
||||
smooth: props.smooth,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#ec4899', // pink-500
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#ec4899',
|
||||
},
|
||||
areaStyle: props.showArea
|
||||
? {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(236, 72, 153, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(236, 72, 153, 0.05)' },
|
||||
],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
color: '#ec4899',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EChart :option="option" :height="height" />
|
||||
</template>
|
||||
|
||||
109
src/components/charts/EChartPie.vue
Normal file
109
src/components/charts/EChartPie.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ECharts 饼图/环形图组件
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import EChart from './EChart.vue'
|
||||
|
||||
export interface EChartPieData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: EChartPieData
|
||||
height?: number
|
||||
/** 是否为环形图 */
|
||||
doughnut?: boolean
|
||||
/** 内圈半径(环形图时生效) */
|
||||
innerRadius?: string
|
||||
/** 是否显示图例 */
|
||||
showLegend?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 280,
|
||||
doughnut: true,
|
||||
innerRadius: '50%',
|
||||
showLegend: true,
|
||||
})
|
||||
|
||||
// 颜色方案
|
||||
const colors = [
|
||||
'#6366f1', // indigo
|
||||
'#8b5cf6', // violet
|
||||
'#ec4899', // pink
|
||||
'#f43f5e', // rose
|
||||
'#f97316', // orange
|
||||
'#eab308', // yellow
|
||||
'#22c55e', // green
|
||||
'#14b8a6', // teal
|
||||
'#06b6d4', // cyan
|
||||
'#3b82f6', // blue
|
||||
]
|
||||
|
||||
const option = computed<EChartsOption>(() => {
|
||||
const seriesData = props.data.labels.map((label, index) => ({
|
||||
name: label,
|
||||
value: props.data.values[index],
|
||||
}))
|
||||
|
||||
return {
|
||||
color: colors,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
legend: props.showLegend
|
||||
? {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: props.doughnut ? [props.innerRadius, '70%'] : '70%',
|
||||
center: props.showLegend ? ['35%', '50%'] : ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EChart :option="option" :height="height" />
|
||||
</template>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
export interface HorizontalBarChartData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
colors?: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: HorizontalBarChartData
|
||||
height?: number
|
||||
showLegend?: boolean
|
||||
borderRadius?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 320,
|
||||
showLegend: false,
|
||||
borderRadius: 8,
|
||||
})
|
||||
|
||||
// 默认渐变色方案
|
||||
const defaultColors = [
|
||||
'#6366f1', // indigo
|
||||
'#8b5cf6', // violet
|
||||
'#a855f7', // purple
|
||||
'#d946ef', // fuchsia
|
||||
'#ec4899', // pink
|
||||
'#f43f5e', // rose
|
||||
'#f97316', // orange
|
||||
'#eab308', // yellow
|
||||
'#22c55e', // green
|
||||
'#14b8a6', // teal
|
||||
]
|
||||
|
||||
const chartData = computed(() => {
|
||||
return {
|
||||
labels: props.data.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('count'),
|
||||
data: props.data.values,
|
||||
backgroundColor: props.data.colors || defaultColors.slice(0, props.data.values.length),
|
||||
borderRadius: props.borderRadius,
|
||||
borderSkipped: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y' as const,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.showLegend,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ height: `${height}px` }">
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"count": "数量"
|
||||
},
|
||||
"en-US": {
|
||||
"count": "Count"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,128 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
export interface LineChartData {
|
||||
labels: string[]
|
||||
values: number[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: LineChartData
|
||||
height?: number
|
||||
fill?: boolean
|
||||
lineColor?: string
|
||||
fillColor?: string
|
||||
tension?: number
|
||||
showLegend?: boolean
|
||||
xAxisRotation?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 288,
|
||||
fill: true,
|
||||
lineColor: '#6366f1',
|
||||
fillColor: 'rgba(99, 102, 241, 0.1)',
|
||||
tension: 0.4,
|
||||
showLegend: false,
|
||||
xAxisRotation: 45,
|
||||
})
|
||||
|
||||
const chartData = computed(() => {
|
||||
return {
|
||||
labels: props.data.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('count'),
|
||||
data: props.data.values,
|
||||
fill: props.fill,
|
||||
borderColor: props.lineColor,
|
||||
backgroundColor: props.fillColor,
|
||||
tension: props.tension,
|
||||
pointBackgroundColor: props.lineColor,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.showLegend,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: props.xAxisRotation,
|
||||
minRotation: props.xAxisRotation,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index' as const,
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ height: `${height}px` }">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"count": "数量"
|
||||
},
|
||||
"en-US": {
|
||||
"count": "Count"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,17 +1,22 @@
|
||||
// 图表组件统一导出
|
||||
export { default as DoughnutChart } from './DoughnutChart.vue'
|
||||
export { default as HorizontalBarChart } from './HorizontalBarChart.vue'
|
||||
export { default as LineChart } from './LineChart.vue'
|
||||
export { default as BarChart } from './BarChart.vue'
|
||||
|
||||
// ECharts 组件
|
||||
export { default as EChart } from './EChart.vue'
|
||||
export { default as EChartPie } from './EChartPie.vue'
|
||||
export { default as EChartBar } from './EChartBar.vue'
|
||||
export { default as EChartLine } from './EChartLine.vue'
|
||||
|
||||
// 其他组件
|
||||
export { default as RankList } from './RankList.vue'
|
||||
export { default as RankListPro } from './RankListPro.vue'
|
||||
export { default as ListPro } from './ListPro.vue'
|
||||
export { default as ProgressBar } from './ProgressBar.vue'
|
||||
export { default as MemberNicknameHistory } from './MemberNicknameHistory.vue'
|
||||
|
||||
// 导出类型定义
|
||||
export type { DoughnutChartData } from './DoughnutChart.vue'
|
||||
export type { HorizontalBarChartData } from './HorizontalBarChart.vue'
|
||||
export type { LineChartData } from './LineChart.vue'
|
||||
export type { BarChartData } from './BarChart.vue'
|
||||
// ECharts 类型
|
||||
export type { EChartPieData } from './EChartPie.vue'
|
||||
export type { EChartBarData } from './EChartBar.vue'
|
||||
export type { EChartLineData } from './EChartLine.vue'
|
||||
|
||||
// 其他类型
|
||||
export type { RankItem } from './RankList.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed } from 'vue'
|
||||
import type { DailyActivity } from '@/types/analysis'
|
||||
import type { LineChartData } from '@/components/charts'
|
||||
import type { EChartLineData } from '@/components/charts'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export function useDailyTrend(dailyActivity: DailyActivity[]) {
|
||||
@@ -12,7 +12,7 @@ export function useDailyTrend(dailyActivity: DailyActivity[]) {
|
||||
})
|
||||
|
||||
// 每日趋势图数据(动态聚合)
|
||||
const dailyChartData = computed<LineChartData>(() => {
|
||||
const dailyChartData = computed<EChartLineData>(() => {
|
||||
const rawData = dailyActivity
|
||||
const maxPoints = 50 // 最大展示点数
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
WeekdayActivity,
|
||||
MonthlyActivity,
|
||||
} from '@/types/analysis'
|
||||
import { DoughnutChart } from '@/components/charts'
|
||||
import type { DoughnutChartData } from '@/components/charts'
|
||||
import { EChartPie } from '@/components/charts'
|
||||
import type { EChartPieData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
import { useOverviewStatistics } from '@/composables/analysis/useOverviewStatistics'
|
||||
import { useDailyTrend } from '@/composables/analysis/useDailyTrend'
|
||||
@@ -62,7 +62,7 @@ const {
|
||||
const { dailyChartData } = useDailyTrend(props.dailyActivity)
|
||||
|
||||
// 消息类型图表数据
|
||||
const typeChartData = computed<DoughnutChartData>(() => {
|
||||
const typeChartData = computed<EChartPieData>(() => {
|
||||
return {
|
||||
labels: props.messageTypes.map((t) => getMessageTypeName(t.type)),
|
||||
values: props.messageTypes.map((t) => t.count),
|
||||
@@ -70,7 +70,7 @@ const typeChartData = computed<DoughnutChartData>(() => {
|
||||
})
|
||||
|
||||
// 成员水群分布图表数据
|
||||
const memberChartData = computed<DoughnutChartData>(() => {
|
||||
const memberChartData = computed<EChartPieData>(() => {
|
||||
const sortedMembers = [...props.memberActivity].sort((a, b) => b.messageCount - a.messageCount)
|
||||
const top10 = sortedMembers.slice(0, 10)
|
||||
const othersCount = sortedMembers.slice(10).reduce((sum, m) => sum + m.messageCount, 0)
|
||||
@@ -161,14 +161,14 @@ watch(
|
||||
<!-- 消息类型分布 -->
|
||||
<SectionCard :title="t('messageTypeDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<DoughnutChart :data="typeChartData" :height="256" />
|
||||
<EChartPie :data="typeChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 成员水群分布 -->
|
||||
<SectionCard :title="t('memberDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<DoughnutChart :data="memberChartData" :height="256" />
|
||||
<EChartPie :data="memberChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { RepeatAnalysis } from '@/types/analysis'
|
||||
import { RankListPro, BarChart, ListPro } from '@/components/charts'
|
||||
import type { RankItem, BarChartData } from '@/components/charts'
|
||||
import { RankListPro, EChartBar, ListPro } from '@/components/charts'
|
||||
import type { RankItem, EChartBarData } from '@/components/charts'
|
||||
import { SectionCard, EmptyState, LoadingState } from '@/components/UI'
|
||||
import { getRankBadgeClass } from '@/utils'
|
||||
|
||||
@@ -65,7 +65,7 @@ const breakerRankData = computed<RankItem[]>(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const chainLengthChartData = computed<BarChartData>(() => {
|
||||
const chainLengthChartData = computed<EChartBarData>(() => {
|
||||
if (!analysis.value) return { labels: [], values: [] }
|
||||
const distribution = analysis.value.chainLengthDistribution
|
||||
return {
|
||||
@@ -115,7 +115,7 @@ watch(
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">每次复读有多少人参与</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<BarChart v-if="chainLengthChartData.labels.length > 0" :data="chainLengthChartData" :height="200" />
|
||||
<EChartBar v-if="chainLengthChartData.labels.length > 0" :data="chainLengthChartData" :height="200" />
|
||||
<EmptyState v-else padding="md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import { getMessageTypeName } from '@/types/base'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity, WeekdayActivity, MonthlyActivity } from '@/types/analysis'
|
||||
import { DoughnutChart } from '@/components/charts'
|
||||
import type { DoughnutChartData } from '@/components/charts'
|
||||
import { EChartPie } from '@/components/charts'
|
||||
import type { EChartPieData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
import { useOverviewStatistics } from '@/composables/analysis/useOverviewStatistics'
|
||||
import { useDailyTrend } from '@/composables/analysis/useDailyTrend'
|
||||
@@ -54,7 +54,7 @@ const {
|
||||
const { dailyChartData } = useDailyTrend(props.dailyActivity)
|
||||
|
||||
// 消息类型图表数据
|
||||
const typeChartData = computed<DoughnutChartData>(() => {
|
||||
const typeChartData = computed<EChartPieData>(() => {
|
||||
return {
|
||||
labels: props.messageTypes.map((t) => getMessageTypeName(t.type)),
|
||||
values: props.messageTypes.map((t) => t.count),
|
||||
@@ -89,7 +89,7 @@ const memberComparisonData = computed(() => {
|
||||
})
|
||||
|
||||
// 双方对比图表数据
|
||||
const comparisonChartData = computed<DoughnutChartData>(() => {
|
||||
const comparisonChartData = computed<EChartPieData>(() => {
|
||||
if (!memberComparisonData.value) {
|
||||
return { labels: [], values: [] }
|
||||
}
|
||||
@@ -252,14 +252,14 @@ watch(
|
||||
<!-- 消息类型分布 -->
|
||||
<SectionCard :title="t('messageTypeDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<DoughnutChart :data="typeChartData" :height="256" />
|
||||
<EChartPie :data="typeChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 双方消息占比饼图 -->
|
||||
<SectionCard v-if="memberComparisonData" :title="t('memberComparison')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<DoughnutChart :data="comparisonChartData" :height="256" />
|
||||
<EChartPie :data="comparisonChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import type { MessageType } from '@/types/base'
|
||||
import { getMessageTypeName } from '@/types/base'
|
||||
import type { HourlyActivity, WeekdayActivity, MonthlyActivity } from '@/types/analysis'
|
||||
import { DoughnutChart, BarChart } from '@/components/charts'
|
||||
import type { DoughnutChartData, BarChartData } from '@/components/charts'
|
||||
import { EChartPie, EChartBar } from '@/components/charts'
|
||||
import type { EChartPieData, EChartBarData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -57,7 +57,7 @@ const monthNames = computed(() => [
|
||||
])
|
||||
|
||||
// 消息类型饼图数据
|
||||
const typeChartData = computed<DoughnutChartData>(() => {
|
||||
const typeChartData = computed<EChartPieData>(() => {
|
||||
// 按数量排序
|
||||
const sorted = [...messageTypes.value].sort((a, b) => b.count - a.count)
|
||||
return {
|
||||
@@ -67,7 +67,7 @@ const typeChartData = computed<DoughnutChartData>(() => {
|
||||
})
|
||||
|
||||
// 小时分布图表数据
|
||||
const hourlyChartData = computed<BarChartData>(() => {
|
||||
const hourlyChartData = computed<EChartBarData>(() => {
|
||||
// 补全 24 小时数据
|
||||
const hourMap = new Map(hourlyActivity.value.map((h) => [h.hour, h.messageCount]))
|
||||
const labels: string[] = []
|
||||
@@ -82,7 +82,7 @@ const hourlyChartData = computed<BarChartData>(() => {
|
||||
})
|
||||
|
||||
// 星期分布图表数据
|
||||
const weekdayChartData = computed<BarChartData>(() => {
|
||||
const weekdayChartData = computed<EChartBarData>(() => {
|
||||
// 补全 7 天数据(weekday: 1=周一, 2=周二, ..., 7=周日)
|
||||
const dayMap = new Map(weekdayActivity.value.map((w) => [w.weekday, w.messageCount]))
|
||||
const values: number[] = []
|
||||
@@ -99,7 +99,7 @@ const weekdayChartData = computed<BarChartData>(() => {
|
||||
})
|
||||
|
||||
// 月份分布图表数据
|
||||
const monthlyChartData = computed<BarChartData>(() => {
|
||||
const monthlyChartData = computed<EChartBarData>(() => {
|
||||
// 补全 12 个月数据
|
||||
const monthMap = new Map(monthlyActivity.value.map((m) => [m.month, m.messageCount]))
|
||||
const values: number[] = []
|
||||
@@ -115,7 +115,7 @@ const monthlyChartData = computed<BarChartData>(() => {
|
||||
})
|
||||
|
||||
// 年份分布图表数据
|
||||
const yearlyChartData = computed<BarChartData>(() => {
|
||||
const yearlyChartData = computed<EChartBarData>(() => {
|
||||
// 按年份排序
|
||||
const sorted = [...yearlyActivity.value].sort((a, b) => a.year - b.year)
|
||||
return {
|
||||
@@ -216,7 +216,7 @@ watch(
|
||||
<!-- 消息类型分布 -->
|
||||
<SectionCard :title="t('typeDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<DoughnutChart v-if="typeChartData.values.length > 0" :data="typeChartData" :height="280" />
|
||||
<EChartPie v-if="typeChartData.values.length > 0" :data="typeChartData" :height="280" />
|
||||
<div v-else class="flex h-48 items-center justify-center text-gray-400">
|
||||
{{ t('noData') }}
|
||||
</div>
|
||||
@@ -228,14 +228,14 @@ watch(
|
||||
<!-- 小时分布 -->
|
||||
<SectionCard :title="t('hourlyDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<BarChart :data="hourlyChartData" :height="200" />
|
||||
<EChartBar :data="hourlyChartData" :height="200" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 星期分布 -->
|
||||
<SectionCard :title="t('weekdayDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<BarChart :data="weekdayChartData" :height="200" />
|
||||
<EChartBar :data="weekdayChartData" :height="200" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
@@ -245,14 +245,14 @@ watch(
|
||||
<!-- 月份分布 -->
|
||||
<SectionCard :title="t('monthlyDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<BarChart :data="monthlyChartData" :height="200" />
|
||||
<EChartBar :data="monthlyChartData" :height="200" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 年份分布 -->
|
||||
<SectionCard :title="t('yearlyDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<BarChart
|
||||
<EChartBar
|
||||
v-if="yearlyChartData.values.length > 0"
|
||||
:data="yearlyChartData"
|
||||
:height="200"
|
||||
|
||||
Reference in New Issue
Block a user