|
|
@@ -35,7 +35,7 @@
|
|
|
<div class="card-header">
|
|
|
<h3>数据趋势</h3>
|
|
|
<el-radio-group v-model="trendMetric" size="small" @change="updateTrendChart">
|
|
|
- <el-radio-button label="fans">粉丝</el-radio-button>
|
|
|
+ <el-radio-button label="fansIncrease">涨粉</el-radio-button>
|
|
|
<el-radio-button label="views">播放</el-radio-button>
|
|
|
<el-radio-button label="likes">点赞</el-radio-button>
|
|
|
<el-radio-button label="comments">评论</el-radio-button>
|
|
|
@@ -82,15 +82,20 @@ import { useAuthStore } from '@/stores/auth';
|
|
|
import dayjs from 'dayjs';
|
|
|
import request from '@/api/request';
|
|
|
|
|
|
-// 趋势数据类型
|
|
|
-interface TrendData {
|
|
|
- dates: string[];
|
|
|
- fans: number[];
|
|
|
+// 单个平台的趋势数据
|
|
|
+interface PlatformTrendItem {
|
|
|
+ platform: string;
|
|
|
+ platformName: string;
|
|
|
+ fansIncrease: number[];
|
|
|
views: number[];
|
|
|
likes: number[];
|
|
|
comments: number[];
|
|
|
- shares: number[];
|
|
|
- collects: number[];
|
|
|
+}
|
|
|
+
|
|
|
+// 趋势数据类型
|
|
|
+interface TrendData {
|
|
|
+ dates: string[];
|
|
|
+ platforms: PlatformTrendItem[];
|
|
|
}
|
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
@@ -117,56 +122,41 @@ const dateRange = ref<[Date, Date]>([
|
|
|
dayjs().toDate(),
|
|
|
]);
|
|
|
|
|
|
-const trendMetric = ref<'fans' | 'views' | 'likes' | 'comments'>('fans');
|
|
|
+const trendMetric = ref<'fansIncrease' | 'views' | 'likes' | 'comments'>('fansIncrease');
|
|
|
const trendData = ref<TrendData | null>(null);
|
|
|
const platformComparison = ref<PlatformComparison[]>([]);
|
|
|
|
|
|
// 从趋势数据计算统计摘要
|
|
|
-// 注意:数据库存储的是累积值,所以区间增量 = 最后一天 - 第一天
|
|
|
+// 口径变更:user_day_statistics 存储的是每日单独值,区间汇总需要直接 SUM
|
|
|
const summaryItems = computed(() => {
|
|
|
const data = trendData.value;
|
|
|
- if (!data || data.dates.length === 0) {
|
|
|
+ if (!data || data.platforms.length === 0) {
|
|
|
return [
|
|
|
- { label: '总粉丝', value: 0 },
|
|
|
- { label: '粉丝增量', value: 0 },
|
|
|
- { label: '播放增量', value: 0 },
|
|
|
- { label: '点赞增量', value: 0 },
|
|
|
- { label: '评论增量', value: 0 },
|
|
|
- { label: '收藏增量', value: 0 },
|
|
|
+ { label: '涨粉总计', value: 0 },
|
|
|
+ { label: '播放总计', value: 0 },
|
|
|
+ { label: '点赞总计', value: 0 },
|
|
|
+ { label: '评论总计', value: 0 },
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- const len = data.fans.length;
|
|
|
-
|
|
|
- // 取最后一天的粉丝数作为当前粉丝数
|
|
|
- const latestFans = data.fans[len - 1] || 0;
|
|
|
- // 取第一天的值
|
|
|
- const firstFans = data.fans[0] || 0;
|
|
|
- const firstViews = data.views[0] || 0;
|
|
|
- const firstLikes = data.likes[0] || 0;
|
|
|
- const firstComments = data.comments[0] || 0;
|
|
|
- const firstCollects = data.collects[0] || 0;
|
|
|
+ // 计算所有平台的汇总
|
|
|
+ let totalFansIncrease = 0;
|
|
|
+ let totalViews = 0;
|
|
|
+ let totalLikes = 0;
|
|
|
+ let totalComments = 0;
|
|
|
|
|
|
- // 取最后一天的值
|
|
|
- const lastViews = data.views[len - 1] || 0;
|
|
|
- const lastLikes = data.likes[len - 1] || 0;
|
|
|
- const lastComments = data.comments[len - 1] || 0;
|
|
|
- const lastCollects = data.collects[len - 1] || 0;
|
|
|
-
|
|
|
- // 计算区间增量(累积值:最后一天 - 第一天)
|
|
|
- const fansIncrease = latestFans - firstFans;
|
|
|
- const viewsIncrease = lastViews - firstViews;
|
|
|
- const likesIncrease = lastLikes - firstLikes;
|
|
|
- const commentsIncrease = lastComments - firstComments;
|
|
|
- const collectsIncrease = lastCollects - firstCollects;
|
|
|
+ for (const platform of data.platforms) {
|
|
|
+ totalFansIncrease += platform.fansIncrease.reduce((sum, v) => sum + v, 0);
|
|
|
+ totalViews += platform.views.reduce((sum, v) => sum + v, 0);
|
|
|
+ totalLikes += platform.likes.reduce((sum, v) => sum + v, 0);
|
|
|
+ totalComments += platform.comments.reduce((sum, v) => sum + v, 0);
|
|
|
+ }
|
|
|
|
|
|
return [
|
|
|
- { label: '总粉丝', value: latestFans },
|
|
|
- { label: '粉丝增量', value: fansIncrease },
|
|
|
- { label: '播放增量', value: viewsIncrease },
|
|
|
- { label: '点赞增量', value: likesIncrease },
|
|
|
- { label: '评论增量', value: commentsIncrease },
|
|
|
- { label: '收藏增量', value: collectsIncrease },
|
|
|
+ { label: '涨粉总计', value: totalFansIncrease },
|
|
|
+ { label: '播放总计', value: totalViews },
|
|
|
+ { label: '点赞总计', value: totalLikes },
|
|
|
+ { label: '评论总计', value: totalComments },
|
|
|
];
|
|
|
});
|
|
|
|
|
|
@@ -262,21 +252,21 @@ async function loadData() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 获取图表颜色配置
|
|
|
-function getChartColor(type: 'fans' | 'views' | 'likes' | 'comments') {
|
|
|
- const colors: Record<string, { main: string; gradient: string[] }> = {
|
|
|
- fans: { main: '#4facfe', gradient: ['#4facfe', 'rgba(79, 172, 254, 0)'] },
|
|
|
- views: { main: '#11998e', gradient: ['#11998e', 'rgba(17, 153, 142, 0)'] },
|
|
|
- likes: { main: '#f5576c', gradient: ['#f5576c', 'rgba(245, 87, 108, 0)'] },
|
|
|
- comments: { main: '#fa709a', gradient: ['#fa709a', 'rgba(250, 112, 154, 0)'] },
|
|
|
- };
|
|
|
- return colors[type] || colors.fans;
|
|
|
-}
|
|
|
+// 平台颜色配置(柔和协调的配色)
|
|
|
+const platformColors: Record<string, string> = {
|
|
|
+ xiaohongshu: '#E91E63',
|
|
|
+ douyin: '#374151',
|
|
|
+ kuaishou: '#F59E0B',
|
|
|
+ weixin: '#10B981',
|
|
|
+ weixin_video: '#10B981',
|
|
|
+ shipinhao: '#10B981',
|
|
|
+ baijiahao: '#3B82F6',
|
|
|
+};
|
|
|
|
|
|
// 获取图表标题
|
|
|
-function getChartTitle(type: 'fans' | 'views' | 'likes' | 'comments') {
|
|
|
+function getChartTitle(type: 'fansIncrease' | 'views' | 'likes' | 'comments') {
|
|
|
const titles: Record<string, string> = {
|
|
|
- fans: '粉丝数',
|
|
|
+ fansIncrease: '涨粉数',
|
|
|
views: '播放量',
|
|
|
likes: '点赞数',
|
|
|
comments: '评论数',
|
|
|
@@ -291,67 +281,102 @@ function updateTrendChart() {
|
|
|
trendChart = echarts.init(trendChartRef.value);
|
|
|
}
|
|
|
|
|
|
- // 使用 Python API 返回的数据
|
|
|
const dates = trendData.value?.dates || [];
|
|
|
- const data = trendData.value?.[trendMetric.value] || [];
|
|
|
- const colorConfig = getChartColor(trendMetric.value);
|
|
|
+ const platforms = trendData.value?.platforms || [];
|
|
|
+
|
|
|
+ // 生成每个平台的 series
|
|
|
+ const series: echarts.SeriesOption[] = platforms.map((p) => {
|
|
|
+ const data = p[trendMetric.value] || [];
|
|
|
+ const color = platformColors[p.platform] || '#6B7280';
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: p.platformName,
|
|
|
+ data: data,
|
|
|
+ type: 'line',
|
|
|
+ smooth: 0.3,
|
|
|
+ showSymbol: false,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 4,
|
|
|
+ lineStyle: { width: 2.5, color: color },
|
|
|
+ itemStyle: { color: color, borderWidth: 2, borderColor: '#fff' },
|
|
|
+ emphasis: {
|
|
|
+ focus: 'series',
|
|
|
+ showSymbol: true,
|
|
|
+ symbolSize: 6,
|
|
|
+ lineStyle: { width: 3 },
|
|
|
+ itemStyle: { borderWidth: 2, borderColor: '#fff' },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const legendData = platforms.map(p => p.platformName);
|
|
|
|
|
|
trendChart.setOption({
|
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
|
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
|
borderColor: '#e5e7eb',
|
|
|
borderWidth: 1,
|
|
|
- textStyle: { color: '#374151' },
|
|
|
+ padding: [10, 14],
|
|
|
+ textStyle: { color: '#374151', fontSize: 13 },
|
|
|
formatter: (params: unknown) => {
|
|
|
- const p = params as { name: string; value: number }[];
|
|
|
- if (Array.isArray(p) && p.length > 0) {
|
|
|
- return `${p[0].name}<br/>${getChartTitle(trendMetric.value)}: <b>${p[0].value.toLocaleString()}</b>`;
|
|
|
+ const p = params as { seriesName: string; name: string; value: number; color: string }[];
|
|
|
+ if (!Array.isArray(p) || p.length === 0) return '';
|
|
|
+ let html = `<div style="font-weight: 600; margin-bottom: 8px;">${p[0].name}</div>`;
|
|
|
+ for (const item of p) {
|
|
|
+ html += `<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">
|
|
|
+ <span style="display: inline-block; width: 8px; height: 8px; background: ${item.color}; border-radius: 2px;"></span>
|
|
|
+ <span style="color: #6b7280;">${item.seriesName}</span>
|
|
|
+ <span style="font-weight: 600; margin-left: auto;">${item.value.toLocaleString()}</span>
|
|
|
+ </div>`;
|
|
|
}
|
|
|
- return '';
|
|
|
+ return html;
|
|
|
},
|
|
|
+ axisPointer: {
|
|
|
+ type: 'line',
|
|
|
+ lineStyle: { color: '#9ca3af', type: 'dashed', width: 1 },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: legendData,
|
|
|
+ bottom: 4,
|
|
|
+ type: 'scroll',
|
|
|
+ itemWidth: 14,
|
|
|
+ itemHeight: 10,
|
|
|
+ itemGap: 20,
|
|
|
+ textStyle: { color: '#6b7280', fontSize: 12 },
|
|
|
+ icon: 'roundRect',
|
|
|
},
|
|
|
grid: {
|
|
|
- left: '3%',
|
|
|
- right: '4%',
|
|
|
- bottom: '3%',
|
|
|
+ left: '2%',
|
|
|
+ right: '2%',
|
|
|
+ top: '8%',
|
|
|
+ bottom: '36px',
|
|
|
containLabel: true,
|
|
|
},
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
data: dates,
|
|
|
+ boundaryGap: false,
|
|
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
|
- axisLabel: { color: '#6b7280' },
|
|
|
+ axisTick: { show: false },
|
|
|
+ axisLabel: { color: '#9ca3af', fontSize: 11 },
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
axisLine: { show: false },
|
|
|
axisTick: { show: false },
|
|
|
- splitLine: { lineStyle: { color: '#f3f4f6' } },
|
|
|
+ splitLine: { lineStyle: { color: '#f3f4f6', type: 'dashed' } },
|
|
|
axisLabel: {
|
|
|
- color: '#6b7280',
|
|
|
+ color: '#9ca3af',
|
|
|
+ fontSize: 11,
|
|
|
formatter: (value: number) => {
|
|
|
- if (value >= 10000) return (value / 10000).toFixed(1) + '万';
|
|
|
+ if (value >= 10000) return (value / 10000).toFixed(1) + 'w';
|
|
|
return value.toString();
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
- series: [{
|
|
|
- data: data,
|
|
|
- type: 'line',
|
|
|
- smooth: true,
|
|
|
- symbol: 'circle',
|
|
|
- symbolSize: 6,
|
|
|
- lineStyle: { width: 3 },
|
|
|
- areaStyle: {
|
|
|
- opacity: 0.1,
|
|
|
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: colorConfig.gradient[0] },
|
|
|
- { offset: 1, color: colorConfig.gradient[1] },
|
|
|
- ]),
|
|
|
- },
|
|
|
- }],
|
|
|
- color: [colorConfig.main],
|
|
|
+ series: series,
|
|
|
}, true);
|
|
|
}
|
|
|
|