|
@@ -107,6 +107,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
|
|
+ <!-- 收益与推荐量暂未接入,先隐藏
|
|
|
<el-table-column prop="income" label="收益" width="100" align="center">
|
|
<el-table-column prop="income" label="收益" width="100" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<span>{{ row.income ?? '未支持' }}</span>
|
|
<span>{{ row.income ?? '未支持' }}</span>
|
|
@@ -117,6 +118,7 @@
|
|
|
<span>{{ row.recommendCount ?? '未支持' }}</span>
|
|
<span>{{ row.recommendCount ?? '未支持' }}</span>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
|
|
+ -->
|
|
|
<el-table-column prop="viewsCount" label="阅读(播放)量" width="130" align="center">
|
|
<el-table-column prop="viewsCount" label="阅读(播放)量" width="130" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<span>{{ row.viewsCount ?? 0 }}</span>
|
|
<span>{{ row.viewsCount ?? 0 }}</span>
|
|
@@ -162,7 +164,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 账号详情抽屉 -->
|
|
<!-- 账号详情抽屉 -->
|
|
|
- <el-drawer v-model="drawerVisible" :title="drawerTitle" size="50%">
|
|
|
|
|
|
|
+ <el-drawer v-model="drawerVisible" :title="drawerTitle" size="70%">
|
|
|
<div v-if="selectedAccount" class="account-detail">
|
|
<div v-if="selectedAccount" class="account-detail">
|
|
|
<!-- 账号基本信息 -->
|
|
<!-- 账号基本信息 -->
|
|
|
<div class="detail-header">
|
|
<div class="detail-header">
|
|
@@ -177,32 +179,142 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 数据统计 -->
|
|
|
|
|
- <div class="detail-stats">
|
|
|
|
|
- <div class="stat-item">
|
|
|
|
|
- <div class="stat-value">{{ formatNumber(selectedAccount.fansCount || 0) }}</div>
|
|
|
|
|
- <div class="stat-label">粉丝数</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-item">
|
|
|
|
|
- <div class="stat-value">{{ formatNumber(selectedAccount.viewsCount || 0) }}</div>
|
|
|
|
|
- <div class="stat-label">播放量</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-item">
|
|
|
|
|
- <div class="stat-value">{{ formatNumber(selectedAccount.likesCount || 0) }}</div>
|
|
|
|
|
- <div class="stat-label">点赞数</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-item">
|
|
|
|
|
- <div class="stat-value">{{ formatNumber(selectedAccount.commentsCount || 0) }}</div>
|
|
|
|
|
- <div class="stat-label">评论数</div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 顶部日期筛选 -->
|
|
|
|
|
+ <div class="detail-filter-bar">
|
|
|
|
|
+ <span class="filter-label">开始时间</span>
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="detailStartDate"
|
|
|
|
|
+ type="date"
|
|
|
|
|
+ placeholder="选择日期"
|
|
|
|
|
+ format="YYYY-MM-DD"
|
|
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
|
|
+ style="width: 140px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="filter-label">结束时间</span>
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="detailEndDate"
|
|
|
|
|
+ type="date"
|
|
|
|
|
+ placeholder="选择日期"
|
|
|
|
|
+ format="YYYY-MM-DD"
|
|
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
|
|
+ style="width: 140px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="quick-btns">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-for="btn in quickDateBtns"
|
|
|
|
|
+ :key="btn.value"
|
|
|
|
|
+ :type="detailActiveQuickBtn === btn.value ? 'primary' : 'default'"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @click="handleDetailQuickDate(btn.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ btn.label }}
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-button type="primary" @click="loadAccountDetailData">查询</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 趋势图 -->
|
|
|
|
|
- <div class="detail-chart">
|
|
|
|
|
- <h4>数据趋势</h4>
|
|
|
|
|
- <div ref="accountChartRef" style="height: 300px" v-loading="chartLoading"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详情 Tab -->
|
|
|
|
|
+ <el-tabs v-model="detailActiveTab">
|
|
|
|
|
+ <el-tab-pane label="数据" name="data">
|
|
|
|
|
+ <!-- 汇总统计 -->
|
|
|
|
|
+ <div class="detail-summary-cards">
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">{{ detailSummary.viewsCount || 0 }}</div>
|
|
|
|
|
+ <div class="stat-label">播放(阅读)量</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">{{ detailSummary.commentsCount || 0 }}</div>
|
|
|
|
|
+ <div class="stat-label">评论量</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">{{ detailSummary.likesCount || 0 }}</div>
|
|
|
|
|
+ <div class="stat-label">点赞量</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">{{ detailSummary.fansIncrease || 0 }}</div>
|
|
|
|
|
+ <div class="stat-label">涨粉量</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 每日数据表格 -->
|
|
|
|
|
+ <el-table :data="detailDailyData" v-loading="detailLoading" stripe>
|
|
|
|
|
+ <el-table-column prop="date" label="时间" width="120" align="center" />
|
|
|
|
|
+ <el-table-column prop="income" label="收益" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.income ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="recommendationCount" label="推荐量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.recommendationCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="viewsCount" label="播放(阅读)量" width="130" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.viewsCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="commentsCount" label="评论量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.commentsCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="likesCount" label="点赞量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.likesCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="fansIncrease" label="涨粉量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.fansIncrease ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <el-tab-pane label="作品" name="works">
|
|
|
|
|
+ <el-table :data="detailWorks" v-loading="detailLoading" stripe>
|
|
|
|
|
+ <el-table-column label="标题" min-width="260">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <div class="work-title-cell">
|
|
|
|
|
+ <div class="work-title-text">{{ row.title }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="viewsCount" label="播放(阅读)量" width="130" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.viewsCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="commentsCount" label="评论量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.commentsCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="likesCount" label="点赞量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.likesCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="sharesCount" label="分享量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.sharesCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="collectsCount" label="收藏量" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.collectsCount ?? 0 }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="publishTime" label="发布时间" width="160" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span class="publish-time">{{ formatTime(row.publishTime) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ </el-tabs>
|
|
|
</div>
|
|
</div>
|
|
|
</el-drawer>
|
|
</el-drawer>
|
|
|
</div>
|
|
</div>
|
|
@@ -210,18 +322,22 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
|
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
|
|
|
|
+import { useRoute } from 'vue-router';
|
|
|
import * as echarts from 'echarts';
|
|
import * as echarts from 'echarts';
|
|
|
-import { Search, User, Coin, Pointer, View, ChatDotRound, Star, TrendCharts } from '@element-plus/icons-vue';
|
|
|
|
|
|
|
+import { Search, User, View, ChatDotRound, Star, TrendCharts } from '@element-plus/icons-vue';
|
|
|
import { PLATFORMS } from '@media-manager/shared';
|
|
import { PLATFORMS } from '@media-manager/shared';
|
|
|
import type { PlatformType } from '@media-manager/shared';
|
|
import type { PlatformType } from '@media-manager/shared';
|
|
|
-import { useAuthStore } from '@/stores/auth';
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
import { ElMessage } from 'element-plus';
|
|
|
import dayjs from 'dayjs';
|
|
import dayjs from 'dayjs';
|
|
|
import request from '@/api/request';
|
|
import request from '@/api/request';
|
|
|
-
|
|
|
|
|
-const PYTHON_API_URL = 'http://localhost:5005';
|
|
|
|
|
-
|
|
|
|
|
-const authStore = useAuthStore();
|
|
|
|
|
|
|
+import iconDefaultUrl from '@/assets/platforms/default.svg?url';
|
|
|
|
|
+import douyinIconUrl from '@/assets/platforms/douyin.svg?url';
|
|
|
|
|
+import xhsIconUrl from '@/assets/platforms/xiaohongshu.svg?url';
|
|
|
|
|
+import bilibiliIconUrl from '@/assets/platforms/bilibili.svg?url';
|
|
|
|
|
+import kuaishouIconUrl from '@/assets/platforms/kuaishou.svg?url';
|
|
|
|
|
+import weixinVideoIconUrl from '@/assets/platforms/weixin_video.svg?url';
|
|
|
|
|
+import baijiahaoIconUrl from '@/assets/platforms/baijiahao.svg?url';
|
|
|
|
|
+const route = useRoute();
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
const chartLoading = ref(false);
|
|
const chartLoading = ref(false);
|
|
|
|
|
|
|
@@ -259,13 +375,16 @@ const availablePlatforms = computed(() => {
|
|
|
}));
|
|
}));
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 平台图标映射
|
|
|
|
|
|
|
+// 平台图标映射(与平台数据页保持一致,使用本地 SVG,避免外链被拦截)
|
|
|
|
|
+const iconDefault = iconDefaultUrl;
|
|
|
const platformIcons: Record<string, string> = {
|
|
const platformIcons: Record<string, string> = {
|
|
|
- douyin: 'https://lf1-cdn-tos.bytescm.com/obj/static/ies/douyin_web/public/favicon.ico',
|
|
|
|
|
- xiaohongshu: 'https://fe-video-qc.xhscdn.com/fe-platform/ed8fe603ce7e10bff8eb0d7c0a7bdf70cedf7f92.ico',
|
|
|
|
|
- bilibili: 'https://www.bilibili.com/favicon.ico',
|
|
|
|
|
- kuaishou: 'https://www.kuaishou.com/favicon.ico',
|
|
|
|
|
- weixin: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico',
|
|
|
|
|
|
|
+ douyin: douyinIconUrl,
|
|
|
|
|
+ xiaohongshu: xhsIconUrl,
|
|
|
|
|
+ bilibili: bilibiliIconUrl,
|
|
|
|
|
+ kuaishou: kuaishouIconUrl,
|
|
|
|
|
+ weixin: weixinVideoIconUrl,
|
|
|
|
|
+ weixin_video: weixinVideoIconUrl,
|
|
|
|
|
+ baijiahao: baijiahaoIconUrl,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 汇总统计
|
|
// 汇总统计
|
|
@@ -282,8 +401,10 @@ const summaryData = ref({
|
|
|
// 统计卡片数据
|
|
// 统计卡片数据
|
|
|
const summaryStats = computed(() => [
|
|
const summaryStats = computed(() => [
|
|
|
{ label: '账号总数', value: summaryData.value.totalAccounts, icon: User },
|
|
{ label: '账号总数', value: summaryData.value.totalAccounts, icon: User },
|
|
|
- { label: '收益', value: summaryData.value.income, icon: Coin },
|
|
|
|
|
- { label: '推荐量', value: summaryData.value.recommendCount, icon: Pointer },
|
|
|
|
|
|
|
+ // 后端暂未支持收益统计,先隐藏
|
|
|
|
|
+ // { label: '收益', value: summaryData.value.income, icon: Coin },
|
|
|
|
|
+ // 后端暂未支持推荐量统计,先隐藏
|
|
|
|
|
+ // { label: '推荐量', value: summaryData.value.recommendCount, icon: Pointer },
|
|
|
{ label: '播放(阅读)量', value: summaryData.value.viewsCount, icon: View },
|
|
{ label: '播放(阅读)量', value: summaryData.value.viewsCount, icon: View },
|
|
|
{ label: '评论量', value: summaryData.value.commentsCount, icon: ChatDotRound },
|
|
{ label: '评论量', value: summaryData.value.commentsCount, icon: ChatDotRound },
|
|
|
{ label: '点赞量', value: summaryData.value.likesCount, icon: Star },
|
|
{ label: '点赞量', value: summaryData.value.likesCount, icon: Star },
|
|
@@ -345,12 +466,52 @@ const drawerTitle = computed(() => {
|
|
|
return `${selectedAccount.value.nickname} - 数据详情`;
|
|
return `${selectedAccount.value.nickname} - 数据详情`;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// 详情 Tab 与数据
|
|
|
|
|
+const detailActiveTab = ref<'data' | 'works'>('data');
|
|
|
|
|
+const detailLoading = ref(false);
|
|
|
|
|
+const detailStartDate = ref(startDate.value);
|
|
|
|
|
+const detailEndDate = ref(endDate.value);
|
|
|
|
|
+const detailActiveQuickBtn = ref(activeQuickBtn.value);
|
|
|
|
|
+
|
|
|
|
|
+const detailSummary = ref({
|
|
|
|
|
+ income: 0,
|
|
|
|
|
+ recommendationCount: 0,
|
|
|
|
|
+ viewsCount: 0,
|
|
|
|
|
+ commentsCount: 0,
|
|
|
|
|
+ likesCount: 0,
|
|
|
|
|
+ fansIncrease: 0,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const detailDailyData = ref<Array<{
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ income: number;
|
|
|
|
|
+ recommendationCount: number;
|
|
|
|
|
+ viewsCount: number;
|
|
|
|
|
+ commentsCount: number;
|
|
|
|
|
+ likesCount: number;
|
|
|
|
|
+ fansIncrease: number;
|
|
|
|
|
+}>>([]);
|
|
|
|
|
+
|
|
|
|
|
+const detailWorks = ref<Array<{
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ coverUrl: string;
|
|
|
|
|
+ platform: string;
|
|
|
|
|
+ publishTime: string | null;
|
|
|
|
|
+ recommendCount: number;
|
|
|
|
|
+ viewsCount: number;
|
|
|
|
|
+ commentsCount: number;
|
|
|
|
|
+ sharesCount: number;
|
|
|
|
|
+ collectsCount: number;
|
|
|
|
|
+ likesCount: number;
|
|
|
|
|
+}>>([]);
|
|
|
|
|
+
|
|
|
function getPlatformName(platform: PlatformType) {
|
|
function getPlatformName(platform: PlatformType) {
|
|
|
return PLATFORMS[platform]?.name || platform;
|
|
return PLATFORMS[platform]?.name || platform;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getPlatformIcon(platform: PlatformType) {
|
|
function getPlatformIcon(platform: PlatformType) {
|
|
|
- return platformIcons[platform] || '';
|
|
|
|
|
|
|
+ return platformIcons[platform] || iconDefault;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function formatNumber(num: number) {
|
|
function formatNumber(num: number) {
|
|
@@ -360,7 +521,10 @@ function formatNumber(num: number) {
|
|
|
|
|
|
|
|
function formatTime(time: string) {
|
|
function formatTime(time: string) {
|
|
|
if (!time) return '-';
|
|
if (!time) return '-';
|
|
|
- return dayjs(time).format('MM-DD HH:mm');
|
|
|
|
|
|
|
+ const d = dayjs(time);
|
|
|
|
|
+ if (!d.isValid()) return time;
|
|
|
|
|
+ const nowYear = dayjs().year();
|
|
|
|
|
+ return d.year() === nowYear ? d.format('MM-DD HH:mm') : d.format('YYYY-MM-DD HH:mm');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 快捷日期选择
|
|
// 快捷日期选择
|
|
@@ -390,6 +554,9 @@ function handleQuickDate(type: string) {
|
|
|
endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 选择快捷日期后,直接刷新数据列表,无需再点击“查询”
|
|
|
|
|
+ loadData();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 查询
|
|
// 查询
|
|
@@ -401,8 +568,11 @@ function handleQuery() {
|
|
|
async function loadGroups() {
|
|
async function loadGroups() {
|
|
|
try {
|
|
try {
|
|
|
const res = await request.get('/api/accounts/groups');
|
|
const res = await request.get('/api/accounts/groups');
|
|
|
- if (res.data.success) {
|
|
|
|
|
- accountGroups.value = res.data.data || [];
|
|
|
|
|
|
|
+ if ((res as any).success && (res as any).data) {
|
|
|
|
|
+ accountGroups.value = (res as any).data || [];
|
|
|
|
|
+ } else if ((res as any).data?.success) {
|
|
|
|
|
+ // 兼容旧返回格式 { data: { success, data } }
|
|
|
|
|
+ accountGroups.value = (res as any).data.data || [];
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载分组失败:', error);
|
|
console.error('加载分组失败:', error);
|
|
@@ -411,34 +581,43 @@ async function loadGroups() {
|
|
|
|
|
|
|
|
// 加载数据
|
|
// 加载数据
|
|
|
async function loadData() {
|
|
async function loadData() {
|
|
|
- const userId = authStore.user?.id;
|
|
|
|
|
- if (!userId) return;
|
|
|
|
|
-
|
|
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const queryParams = new URLSearchParams({
|
|
|
|
|
- user_id: userId.toString(),
|
|
|
|
|
- start_date: startDate.value,
|
|
|
|
|
- end_date: endDate.value,
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const params: Record<string, any> = {
|
|
|
|
|
+ startDate: startDate.value,
|
|
|
|
|
+ endDate: endDate.value,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
if (selectedPlatform.value) {
|
|
if (selectedPlatform.value) {
|
|
|
- queryParams.append('platform', selectedPlatform.value);
|
|
|
|
|
|
|
+ params.platform = selectedPlatform.value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/accounts?${queryParams}`);
|
|
|
|
|
- const result = await response.json();
|
|
|
|
|
-
|
|
|
|
|
- if (result.success && result.data) {
|
|
|
|
|
- accounts.value = result.data.accounts || [];
|
|
|
|
|
-
|
|
|
|
|
- if (result.data.summary) {
|
|
|
|
|
- summaryData.value = result.data.summary;
|
|
|
|
|
|
|
+ if (selectedGroup.value) {
|
|
|
|
|
+ params.groupId = selectedGroup.value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await request.get('/api/work-day-statistics/accounts', {
|
|
|
|
|
+ params,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ accounts.value = (data.accounts || []) as AccountData[];
|
|
|
|
|
+
|
|
|
|
|
+ if (data.summary) {
|
|
|
|
|
+ summaryData.value = {
|
|
|
|
|
+ totalAccounts: data.summary.totalAccounts || 0,
|
|
|
|
|
+ income: 0,
|
|
|
|
|
+ recommendCount: null,
|
|
|
|
|
+ viewsCount: data.summary.viewsCount || 0,
|
|
|
|
|
+ commentsCount: data.summary.commentsCount || 0,
|
|
|
|
|
+ likesCount: data.summary.likesCount || 0,
|
|
|
|
|
+ fansIncrease: data.summary.fansIncrease || 0,
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载账号数据失败:', error);
|
|
console.error('加载账号数据失败:', error);
|
|
|
|
|
+ ElMessage.error('加载账号数据失败,请稍后重试');
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
}
|
|
}
|
|
@@ -448,8 +627,17 @@ async function loadData() {
|
|
|
async function handleDetail(row: AccountData) {
|
|
async function handleDetail(row: AccountData) {
|
|
|
selectedAccount.value = row;
|
|
selectedAccount.value = row;
|
|
|
drawerVisible.value = true;
|
|
drawerVisible.value = true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化详情的时间范围与快捷按钮,与主筛选保持一致
|
|
|
|
|
+ detailStartDate.value = startDate.value;
|
|
|
|
|
+ detailEndDate.value = endDate.value;
|
|
|
|
|
+ detailActiveQuickBtn.value = activeQuickBtn.value;
|
|
|
|
|
+ detailActiveTab.value = 'data';
|
|
|
|
|
+
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
|
|
+ // 加载账号详情(数据 + 作品)
|
|
|
|
|
+ loadAccountDetailData();
|
|
|
|
|
+ // 同时加载趋势图
|
|
|
loadAccountTrend(row.id);
|
|
loadAccountTrend(row.id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -460,21 +648,23 @@ async function loadAccountTrend(accountId: number) {
|
|
|
chartLoading.value = true;
|
|
chartLoading.value = true;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const userId = authStore.user?.id;
|
|
|
|
|
- if (!userId) return;
|
|
|
|
|
-
|
|
|
|
|
- const queryParams = new URLSearchParams({
|
|
|
|
|
- user_id: userId.toString(),
|
|
|
|
|
- account_id: accountId.toString(),
|
|
|
|
|
- start_date: startDate.value,
|
|
|
|
|
- end_date: endDate.value,
|
|
|
|
|
|
|
+ const trend = await request.get('/api/analytics/trend', {
|
|
|
|
|
+ params: {
|
|
|
|
|
+ startDate: startDate.value,
|
|
|
|
|
+ endDate: endDate.value,
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
- const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/account_trend?${queryParams}`);
|
|
|
|
|
- const result = await response.json();
|
|
|
|
|
-
|
|
|
|
|
- if (result.success && result.data) {
|
|
|
|
|
- updateAccountChart(result.data);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (trend) {
|
|
|
|
|
+ const dates = (trend.views || []).map((p: { date: string; value: number }) =>
|
|
|
|
|
+ (p.date || '').slice(5)
|
|
|
|
|
+ );
|
|
|
|
|
+ const fans = (trend.fans || []).map((p: { date: string; value: number }) => p.value || 0);
|
|
|
|
|
+ const views = (trend.views || []).map((p: { date: string; value: number }) => p.value || 0);
|
|
|
|
|
+ const likes = (trend.likes || []).map((p: { date: string; value: number }) => p.value || 0);
|
|
|
|
|
+
|
|
|
|
|
+ updateAccountChart({ dates, fans, views, likes });
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载账号趋势失败:', error);
|
|
console.error('加载账号趋势失败:', error);
|
|
@@ -483,6 +673,72 @@ async function loadAccountTrend(accountId: number) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 账号详情:快捷日期选择
|
|
|
|
|
+function handleDetailQuickDate(type: string) {
|
|
|
|
|
+ detailActiveQuickBtn.value = type;
|
|
|
|
|
+ const today = dayjs();
|
|
|
|
|
+
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case 'yesterday':
|
|
|
|
|
+ detailStartDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ detailEndDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'beforeYesterday':
|
|
|
|
|
+ detailStartDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ detailEndDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'last3days':
|
|
|
|
|
+ detailStartDate.value = today.subtract(3, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ detailEndDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'last7days':
|
|
|
|
|
+ detailStartDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ detailEndDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'lastMonth':
|
|
|
|
|
+ detailStartDate.value = today.subtract(30, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ detailEndDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadAccountDetailData();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 加载账号详情(汇总 + 每日 + 作品)
|
|
|
|
|
+async function loadAccountDetailData() {
|
|
|
|
|
+ if (!selectedAccount.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ detailLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await request.get('/api/work-day-statistics/account-detail', {
|
|
|
|
|
+ params: {
|
|
|
|
|
+ accountId: selectedAccount.value.id,
|
|
|
|
|
+ startDate: detailStartDate.value,
|
|
|
|
|
+ endDate: detailEndDate.value,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ detailSummary.value = {
|
|
|
|
|
+ income: data.summary?.income ?? 0,
|
|
|
|
|
+ recommendationCount: data.summary?.recommendationCount ?? 0,
|
|
|
|
|
+ viewsCount: data.summary?.viewsCount ?? 0,
|
|
|
|
|
+ commentsCount: data.summary?.commentsCount ?? 0,
|
|
|
|
|
+ likesCount: data.summary?.likesCount ?? 0,
|
|
|
|
|
+ fansIncrease: data.summary?.fansIncrease ?? 0,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ detailDailyData.value = Array.isArray(data.dailyData) ? data.dailyData : [];
|
|
|
|
|
+ detailWorks.value = Array.isArray(data.works) ? data.works : [];
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载账号详情失败:', error);
|
|
|
|
|
+ ElMessage.error('加载账号详情失败,请稍后重试');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ detailLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 更新账号趋势图
|
|
// 更新账号趋势图
|
|
|
function updateAccountChart(trendData: { dates: string[]; fans: number[]; views: number[]; likes: number[] }) {
|
|
function updateAccountChart(trendData: { dates: string[]; fans: number[]; views: number[]; likes: number[] }) {
|
|
|
if (!accountChartRef.value) return;
|
|
if (!accountChartRef.value) return;
|
|
@@ -529,10 +785,31 @@ watch(drawerVisible, (visible) => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
- // 默认选择昨天
|
|
|
|
|
- handleQuickDate('yesterday');
|
|
|
|
|
|
|
+ // 如果从平台详情页跳转过来,优先使用路由上的时间范围
|
|
|
|
|
+ if (route.query.startDate) {
|
|
|
|
|
+ startDate.value = String(route.query.startDate);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route.query.endDate) {
|
|
|
|
|
+ endDate.value = String(route.query.endDate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 默认选择昨天(如果路由未指定时间)
|
|
|
|
|
+ if (!route.query.startDate && !route.query.endDate) {
|
|
|
|
|
+ handleQuickDate('yesterday');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
loadGroups();
|
|
loadGroups();
|
|
|
- loadData();
|
|
|
|
|
|
|
+ // 加载列表后,如有 accountId 参数,则自动打开对应账号详情
|
|
|
|
|
+ loadData().then(() => {
|
|
|
|
|
+ const accountIdParam = route.query.accountId;
|
|
|
|
|
+ if (accountIdParam) {
|
|
|
|
|
+ const targetId = Number(accountIdParam);
|
|
|
|
|
+ const target = accounts.value.find(a => a.id === targetId);
|
|
|
|
|
+ if (target) {
|
|
|
|
|
+ handleDetail(target);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -710,25 +987,43 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .detail-stats {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .detail-filter-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin: 16px 0 12px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .filter-label {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: $text-regular;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .quick-btns {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .detail-summary-cards {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
gap: 16px;
|
|
gap: 16px;
|
|
|
- margin-bottom: 24px;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
.stat-item {
|
|
.stat-item {
|
|
|
background: #f8fafc;
|
|
background: #f8fafc;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
padding: 16px;
|
|
padding: 16px;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.stat-value {
|
|
.stat-value {
|
|
|
- font-size: 24px;
|
|
|
|
|
|
|
+ font-size: 20px;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
color: $primary-color;
|
|
color: $primary-color;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.stat-label {
|
|
.stat-label {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
color: $text-secondary;
|
|
color: $text-secondary;
|
|
@@ -736,11 +1031,10 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .detail-chart {
|
|
|
|
|
- h4 {
|
|
|
|
|
- margin: 0 0 16px 0;
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .work-title-cell {
|
|
|
|
|
+ .work-title-text {
|
|
|
|
|
+ font-weight: 500;
|
|
|
color: $text-primary;
|
|
color: $text-primary;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|