|
@@ -101,11 +101,12 @@
|
|
|
<!-- 数据趋势图 -->
|
|
<!-- 数据趋势图 -->
|
|
|
<div class="content-card chart-card">
|
|
<div class="content-card chart-card">
|
|
|
<div class="card-header">
|
|
<div class="card-header">
|
|
|
- <h3>数据趋势</h3>
|
|
|
|
|
|
|
+ <h3>数据趋势(近30天)</h3>
|
|
|
<el-radio-group v-model="trendType" size="small">
|
|
<el-radio-group v-model="trendType" size="small">
|
|
|
- <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="views">播放</el-radio-button>
|
|
|
<el-radio-button label="likes">点赞</el-radio-button>
|
|
<el-radio-button label="likes">点赞</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="comments">评论</el-radio-button>
|
|
|
</el-radio-group>
|
|
</el-radio-group>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
@@ -117,7 +118,7 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onUnmounted, onActivated, watch, markRaw, nextTick } from 'vue';
|
|
import { ref, onMounted, onUnmounted, onActivated, watch, markRaw, nextTick } from 'vue';
|
|
|
-import { User, VideoPlay, ChatDotRound, TrendCharts, Refresh } from '@element-plus/icons-vue';
|
|
|
|
|
|
|
+import { User, VideoPlay, UserFilled, TrendCharts, Refresh } from '@element-plus/icons-vue';
|
|
|
import * as echarts from 'echarts';
|
|
import * as echarts from 'echarts';
|
|
|
import { accountsApi } from '@/api/accounts';
|
|
import { accountsApi } from '@/api/accounts';
|
|
|
import { dashboardApi, type TrendData } from '@/api/dashboard';
|
|
import { dashboardApi, type TrendData } from '@/api/dashboard';
|
|
@@ -131,7 +132,7 @@ const tabsStore = useTabsStore();
|
|
|
const authStore = useAuthStore();
|
|
const authStore = useAuthStore();
|
|
|
const accounts = ref<PlatformAccount[]>([]);
|
|
const accounts = ref<PlatformAccount[]>([]);
|
|
|
const tasks = ref<PublishTask[]>([]);
|
|
const tasks = ref<PublishTask[]>([]);
|
|
|
-const trendType = ref<'fans' | 'views' | 'likes'>('fans');
|
|
|
|
|
|
|
+const trendType = ref<'fansIncrease' | 'views' | 'likes' | 'comments'>('fansIncrease');
|
|
|
const chartRef = ref<HTMLElement>();
|
|
const chartRef = ref<HTMLElement>();
|
|
|
const trendData = ref<TrendData | null>(null);
|
|
const trendData = ref<TrendData | null>(null);
|
|
|
const refreshing = ref(false);
|
|
const refreshing = ref(false);
|
|
@@ -141,7 +142,7 @@ let resizeObserver: ResizeObserver | null = null;
|
|
|
const stats = ref([
|
|
const stats = ref([
|
|
|
{ label: '平台账号', value: 0, icon: markRaw(User), iconClass: 'blue' },
|
|
{ label: '平台账号', value: 0, icon: markRaw(User), iconClass: 'blue' },
|
|
|
{ label: '发布视频', value: 0, icon: markRaw(VideoPlay), iconClass: 'green' },
|
|
{ label: '发布视频', value: 0, icon: markRaw(VideoPlay), iconClass: 'green' },
|
|
|
- { label: '新增评论', value: 0, icon: markRaw(ChatDotRound), iconClass: 'orange' },
|
|
|
|
|
|
|
+ { label: '总粉丝数', value: '0', icon: markRaw(UserFilled), iconClass: 'orange' },
|
|
|
{ label: '总播放量', value: '0', icon: markRaw(TrendCharts), iconClass: 'pink' },
|
|
{ label: '总播放量', value: '0', icon: markRaw(TrendCharts), iconClass: 'pink' },
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
@@ -189,28 +190,24 @@ function formatDate(date: string) {
|
|
|
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
|
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 获取图表颜色配置
|
|
|
|
|
-function getChartColor(type: 'fans' | 'views' | 'likes') {
|
|
|
|
|
- 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)'] },
|
|
|
|
|
- };
|
|
|
|
|
- return colors[type] || colors.fans;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 获取图表数据
|
|
|
|
|
-function getChartData(type: 'fans' | 'views' | 'likes') {
|
|
|
|
|
- if (!trendData.value) return [];
|
|
|
|
|
- return trendData.value[type] || [];
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 平台颜色配置(柔和协调的配色)
|
|
|
|
|
+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') {
|
|
|
|
|
|
|
+function getChartTitle(type: 'fansIncrease' | 'views' | 'likes' | 'comments') {
|
|
|
const titles: Record<string, string> = {
|
|
const titles: Record<string, string> = {
|
|
|
- fans: '粉丝数',
|
|
|
|
|
|
|
+ fansIncrease: '涨粉数',
|
|
|
views: '播放量',
|
|
views: '播放量',
|
|
|
likes: '点赞数',
|
|
likes: '点赞数',
|
|
|
|
|
+ comments: '评论数',
|
|
|
};
|
|
};
|
|
|
return titles[type] || '';
|
|
return titles[type] || '';
|
|
|
}
|
|
}
|
|
@@ -225,47 +222,108 @@ function initChart() {
|
|
|
function updateChart() {
|
|
function updateChart() {
|
|
|
if (!chartInstance) return;
|
|
if (!chartInstance) return;
|
|
|
|
|
|
|
|
- const colorConfig = getChartColor(trendType.value);
|
|
|
|
|
- const data = getChartData(trendType.value);
|
|
|
|
|
const dates = trendData.value?.dates || [];
|
|
const dates = trendData.value?.dates || [];
|
|
|
|
|
+ const platforms = trendData.value?.platforms || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 生成每个平台的 series
|
|
|
|
|
+ const series: echarts.SeriesOption[] = platforms.map((p) => {
|
|
|
|
|
+ const data = p[trendType.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);
|
|
|
|
|
|
|
|
const option: echarts.EChartsOption = {
|
|
const option: echarts.EChartsOption = {
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
trigger: 'axis',
|
|
|
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
|
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
|
borderColor: '#e5e7eb',
|
|
borderColor: '#e5e7eb',
|
|
|
borderWidth: 1,
|
|
borderWidth: 1,
|
|
|
|
|
+ padding: [10, 14],
|
|
|
textStyle: {
|
|
textStyle: {
|
|
|
color: '#374151',
|
|
color: '#374151',
|
|
|
|
|
+ fontSize: 13,
|
|
|
},
|
|
},
|
|
|
formatter: (params: unknown) => {
|
|
formatter: (params: unknown) => {
|
|
|
- const p = params as { name: string; value: number }[];
|
|
|
|
|
- if (Array.isArray(p) && p.length > 0) {
|
|
|
|
|
- return `${p[0].name}<br/>${getChartTitle(trendType.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; font-size: 13px;">${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: {
|
|
axisPointer: {
|
|
|
- type: 'cross',
|
|
|
|
|
- crossStyle: {
|
|
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ lineStyle: {
|
|
|
color: '#9ca3af',
|
|
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: {
|
|
grid: {
|
|
|
- left: '3%',
|
|
|
|
|
- right: '4%',
|
|
|
|
|
- bottom: '3%',
|
|
|
|
|
|
|
+ left: '2%',
|
|
|
|
|
+ right: '2%',
|
|
|
|
|
+ top: '8%',
|
|
|
|
|
+ bottom: '36px',
|
|
|
containLabel: true,
|
|
containLabel: true,
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'category',
|
|
type: 'category',
|
|
|
data: dates,
|
|
data: dates,
|
|
|
|
|
+ boundaryGap: false,
|
|
|
axisLine: {
|
|
axisLine: {
|
|
|
lineStyle: { color: '#e5e7eb' },
|
|
lineStyle: { color: '#e5e7eb' },
|
|
|
},
|
|
},
|
|
|
|
|
+ axisTick: { show: false },
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
- color: '#6b7280',
|
|
|
|
|
|
|
+ color: '#9ca3af',
|
|
|
|
|
+ fontSize: 11,
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
@@ -273,34 +331,21 @@ function updateChart() {
|
|
|
axisLine: { show: false },
|
|
axisLine: { show: false },
|
|
|
axisTick: { show: false },
|
|
axisTick: { show: false },
|
|
|
splitLine: {
|
|
splitLine: {
|
|
|
- lineStyle: { color: '#f3f4f6' },
|
|
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: '#f3f4f6',
|
|
|
|
|
+ type: 'dashed',
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
- color: '#6b7280',
|
|
|
|
|
|
|
+ color: '#9ca3af',
|
|
|
|
|
+ fontSize: 11,
|
|
|
formatter: (value: number) => {
|
|
formatter: (value: number) => {
|
|
|
- if (value >= 10000) return (value / 10000).toFixed(1) + '万';
|
|
|
|
|
|
|
+ if (value >= 10000) return (value / 10000).toFixed(1) + 'w';
|
|
|
return value.toString();
|
|
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,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
chartInstance.setOption(option, true);
|
|
chartInstance.setOption(option, true);
|
|
@@ -309,12 +354,8 @@ function updateChart() {
|
|
|
// 加载数据趋势
|
|
// 加载数据趋势
|
|
|
async function loadTrendData() {
|
|
async function loadTrendData() {
|
|
|
try {
|
|
try {
|
|
|
- const userId = authStore.user?.id;
|
|
|
|
|
- if (!userId) return;
|
|
|
|
|
-
|
|
|
|
|
trendData.value = await dashboardApi.getTrend({
|
|
trendData.value = await dashboardApi.getTrend({
|
|
|
- userId,
|
|
|
|
|
- days: 14, // 获取最近14天的数据
|
|
|
|
|
|
|
+ days: 30, // 获取最近30天的数据
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 更新图表
|
|
// 更新图表
|
|
@@ -327,10 +368,9 @@ async function loadTrendData() {
|
|
|
async function loadData() {
|
|
async function loadData() {
|
|
|
try {
|
|
try {
|
|
|
// 并行获取所有数据
|
|
// 并行获取所有数据
|
|
|
- const [accountsData, worksStats, commentsStats] = await Promise.all([
|
|
|
|
|
|
|
+ const [accountsData, worksStats] = await Promise.all([
|
|
|
accountsApi.getAccounts(),
|
|
accountsApi.getAccounts(),
|
|
|
dashboardApi.getWorksStats().catch(() => null),
|
|
dashboardApi.getWorksStats().catch(() => null),
|
|
|
- dashboardApi.getCommentsStats().catch(() => null),
|
|
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
accounts.value = accountsData;
|
|
accounts.value = accountsData;
|
|
@@ -338,6 +378,12 @@ async function loadData() {
|
|
|
// 更新统计数据
|
|
// 更新统计数据
|
|
|
stats.value[0].value = accounts.value.length;
|
|
stats.value[0].value = accounts.value.length;
|
|
|
|
|
|
|
|
|
|
+ // 总粉丝数:当前用户所有平台账号的 fans_count 总和
|
|
|
|
|
+ const totalFans = accounts.value.reduce((sum, a) => sum + (a.fansCount || 0), 0);
|
|
|
|
|
+ stats.value[2].value = totalFans >= 10000
|
|
|
|
|
+ ? (totalFans / 10000).toFixed(1) + '万'
|
|
|
|
|
+ : totalFans.toString();
|
|
|
|
|
+
|
|
|
if (worksStats) {
|
|
if (worksStats) {
|
|
|
stats.value[1].value = worksStats.totalCount || 0;
|
|
stats.value[1].value = worksStats.totalCount || 0;
|
|
|
// 格式化播放量
|
|
// 格式化播放量
|
|
@@ -347,10 +393,6 @@ async function loadData() {
|
|
|
: playCount.toString();
|
|
: playCount.toString();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (commentsStats) {
|
|
|
|
|
- stats.value[2].value = commentsStats.todayCount || 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// 加载数据趋势
|
|
// 加载数据趋势
|
|
|
await loadTrendData();
|
|
await loadTrendData();
|
|
|
} catch {
|
|
} catch {
|