| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216 |
- <template>
- <div class="account-analytics">
- <!-- 顶部筛选栏 -->
- <div class="filter-bar">
- <div class="filter-left">
- <span class="filter-label">开始时间</span>
- <el-date-picker
- v-model="startDate"
- type="date"
- placeholder="选择日期"
- format="YYYY-MM-DD"
- value-format="YYYY-MM-DD"
- style="width: 140px"
- :disabled-date="(date: Date) => endDate ? date > new Date(endDate) : false"
- @change="handleQuery"
- />
- <span class="filter-label">结束时间</span>
- <el-date-picker
- v-model="endDate"
- type="date"
- placeholder="选择日期"
- format="YYYY-MM-DD"
- value-format="YYYY-MM-DD"
- style="width: 140px"
- :disabled-date="(date: Date) => startDate ? date < new Date(startDate) : false"
- @change="handleQuery"
- />
- <div class="quick-btns">
- <el-button
- v-for="btn in quickDateBtns"
- :key="btn.value"
- :type="activeQuickBtn === btn.value ? 'primary' : 'default'"
- size="small"
- @click="handleQuickDate(btn.value)"
- >
- {{ btn.label }}
- </el-button>
- </div>
- <el-select v-model="selectedGroup" placeholder="全部分组" clearable style="width: 120px" @change="handleQuery">
- <el-option label="全部分组" value="" />
- <el-option
- v-for="group in accountGroups"
- :key="group.id"
- :label="group.name"
- :value="group.id"
- />
- </el-select>
- <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px" @change="handleQuery">
- <el-option label="全部平台" value="" />
- <el-option
- v-for="platform in availablePlatforms"
- :key="platform.value"
- :label="platform.label"
- :value="platform.value"
- />
- </el-select>
- </div>
- <div class="filter-right">
- <el-button @click="handleExport">导出数据</el-button>
- </div>
- </div>
-
- <!-- 统计卡片 -->
- <div class="stats-row">
- <div class="stat-card" v-for="(item, index) in summaryStats" :key="index">
- <div class="stat-icon">
- <el-icon :size="18"><component :is="item.icon" /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-label">{{ item.label }}</div>
- <div class="stat-value">{{ item.value }}</div>
- </div>
- </div>
- </div>
-
- <!-- 搜索框 -->
- <div class="search-bar">
- <el-input
- v-model="searchKeyword"
- placeholder="请输入要搜索的账号"
- clearable
- style="width: 300px"
- >
- <template #prefix>
- <el-icon><Search /></el-icon>
- </template>
- </el-input>
- </div>
-
- <!-- 数据表格 -->
- <div class="data-table">
- <el-table :data="filteredAccounts" v-loading="loading" stripe>
- <el-table-column label="账号" min-width="150">
- <template #default="{ row }">
- <div class="account-cell">
- <el-avatar :size="36" :src="row.avatarUrl">
- {{ row.nickname?.[0] || row.username?.[0] }}
- </el-avatar>
- <span class="account-name">{{ row.nickname || row.username }}</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="平台" width="120" align="center">
- <template #default="{ row }">
- <div class="platform-cell">
- <img :src="getPlatformIcon(row.platform)" class="platform-icon" :alt="row.platform" />
- <span>{{ getPlatformName(row.platform) }}</span>
- </div>
- </template>
- </el-table-column>
- <!-- 收益与推荐量暂未接入,先隐藏
- <el-table-column prop="income" label="收益" width="100" align="center">
- <template #default="{ row }">
- <span>{{ row.income ?? '未支持' }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="recommendCount" label="推荐量" width="100" align="center">
- <template #default="{ row }">
- <span>{{ row.recommendCount ?? '未支持' }}</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 :class="{ 'increase': row.fansIncrease > 0, 'decrease': row.fansIncrease < 0 }">
- {{ row.fansIncrease ?? 0 }}
- </span>
- </template>
- </el-table-column>
- <el-table-column prop="updateTime" label="更新时间" width="120" align="center">
- <template #default="{ row }">
- <span class="update-time">{{ formatTime(row.updateTime) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="status" label="状态" width="80" align="center">
- <template #default="{ row }">
- <span :class="['status-tag', row.status === 'active' ? 'active' : 'inactive']">
- {{ row.status === 'active' ? '正常' : '异常' }}
- </span>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="80" align="center" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleDetail(row)">
- 详情
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
-
- <!-- 账号详情抽屉 -->
- <el-drawer v-model="drawerVisible" :title="drawerTitle" size="70%">
- <div v-if="selectedAccount" class="account-detail">
- <!-- 账号基本信息 -->
- <div class="detail-header">
- <el-avatar :size="64" :src="selectedAccount.avatarUrl">
- {{ selectedAccount.nickname?.[0] }}
- </el-avatar>
- <div class="header-info">
- <h3>{{ selectedAccount.nickname }}</h3>
- <div class="platform-info">
- <img :src="getPlatformIcon(selectedAccount.platform)" class="platform-icon" />
- <span>{{ getPlatformName(selectedAccount.platform) }}</span>
- </div>
- </div>
- </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"
- :disabled-date="(date: Date) => detailEndDate ? date > new Date(detailEndDate) : false"
- @change="loadAccountDetailData"
- />
- <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"
- :disabled-date="(date: Date) => detailStartDate ? date < new Date(detailStartDate) : false"
- @change="loadAccountDetailData"
- />
- <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 class="detail-export-wrap">
- <el-button type="primary" plain size="small" :disabled="!detailDailyData.length" @click="exportDetailDailyData">
- 导出数据
- </el-button>
- </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="pagedDetailWorks" 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>
- <div
- class="pagination-wrapper"
- v-if="detailWorksTotal > detailWorksPageSize"
- >
- <el-pagination
- v-model:current-page="detailWorksPage"
- :page-size="detailWorksPageSize"
- :total="detailWorksTotal"
- layout="total, prev, pager, next"
- small
- @current-change="handleDetailWorksPageChange"
- />
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </el-drawer>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted, watch, nextTick } from 'vue';
- import { useRoute } from 'vue-router';
- import * as echarts from 'echarts';
- import { Search, User, View, ChatDotRound, Star, TrendCharts } from '@element-plus/icons-vue';
- import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
- import type { PlatformType } from '@media-manager/shared';
- import { ElMessage } from 'element-plus';
- import * as XLSX from 'xlsx';
- import dayjs from 'dayjs';
- import request from '@/api/request';
- import { useTaskQueueStore } from '@/stores/taskQueue';
- 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 taskStore = useTaskQueueStore();
- const loading = ref(false);
- const chartLoading = ref(false);
- // 日期筛选
- const startDate = ref(dayjs().format('YYYY-MM-DD'));
- const endDate = ref(dayjs().format('YYYY-MM-DD'));
- const activeQuickBtn = ref('yesterday');
- // 快捷日期按钮
- const quickDateBtns = [
- { label: '昨天', value: 'yesterday' },
- { label: '前天', value: 'beforeYesterday' },
- { label: '近三天', value: 'last3days' },
- { label: '近七天', value: 'last7days' },
- { label: '近一个月', value: 'lastMonth' },
- ];
- // 分组和平台筛选
- const selectedGroup = ref<number | ''>('');
- const selectedPlatform = ref<PlatformType | ''>('');
- const searchKeyword = ref('');
- // 分组列表
- interface AccountGroup {
- id: number;
- name: string;
- }
- const accountGroups = ref<AccountGroup[]>([]);
- // 可用平台(统一配置:小红书、抖音、视频号、百家号)
- const availablePlatforms = computed(() => {
- return AVAILABLE_PLATFORM_TYPES.map(key => ({
- value: key,
- label: PLATFORMS[key].name,
- }));
- });
- // 平台图标映射(与平台数据页保持一致,使用本地 SVG,避免外链被拦截)
- const iconDefault = iconDefaultUrl;
- const platformIcons: Record<string, string> = {
- douyin: douyinIconUrl,
- xiaohongshu: xhsIconUrl,
- bilibili: bilibiliIconUrl,
- kuaishou: kuaishouIconUrl,
- weixin: weixinVideoIconUrl,
- weixin_video: weixinVideoIconUrl,
- baijiahao: baijiahaoIconUrl,
- };
- // 汇总统计
- const summaryData = ref({
- totalAccounts: 0,
- income: 0,
- recommendCount: 0,
- viewsCount: 0,
- commentsCount: 0,
- likesCount: 0,
- fansIncrease: 0,
- });
- // 统计卡片数据
- const summaryStats = computed(() => [
- { 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.viewsCount, icon: View },
- { label: '评论量', value: summaryData.value.commentsCount, icon: ChatDotRound },
- { label: '点赞量', value: summaryData.value.likesCount, icon: Star },
- { label: '涨粉量', value: summaryData.value.fansIncrease, icon: TrendCharts },
- ]);
- // 账号数据
- interface AccountData {
- id: number;
- nickname: string;
- username: string;
- avatarUrl: string;
- platform: PlatformType;
- groupId?: number;
- fansCount: number;
- income: number | null;
- recommendCount: number | null;
- viewsCount: number;
- commentsCount: number;
- likesCount: number;
- fansIncrease: number;
- updateTime: string;
- status: string;
- }
- const accounts = ref<AccountData[]>([]);
- // 过滤后的账号列表
- const filteredAccounts = computed(() => {
- let result = accounts.value;
-
- if (selectedGroup.value) {
- result = result.filter(a => a.groupId === selectedGroup.value);
- }
-
- if (selectedPlatform.value) {
- result = result.filter(a => a.platform === selectedPlatform.value);
- }
-
- if (searchKeyword.value) {
- const keyword = searchKeyword.value.toLowerCase();
- result = result.filter(a =>
- a.nickname?.toLowerCase().includes(keyword) ||
- a.username?.toLowerCase().includes(keyword)
- );
- }
-
- return result;
- });
- // 抽屉相关
- const drawerVisible = ref(false);
- const selectedAccount = ref<AccountData | null>(null);
- const accountChartRef = ref<HTMLElement>();
- let accountChart: echarts.ECharts | null = null;
- const drawerTitle = computed(() => {
- if (!selectedAccount.value) return '账号详情';
- 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;
- }>>([]);
- // 账号详情 - 作品列表分页
- const detailWorksPage = ref(1);
- const detailWorksPageSize = ref(15);
- const pagedDetailWorks = computed(() => {
- const start = (detailWorksPage.value - 1) * detailWorksPageSize.value;
- const end = start + detailWorksPageSize.value;
- return detailWorks.value.slice(start, end);
- });
- const detailWorksTotal = computed(() => detailWorks.value.length);
- function handleDetailWorksPageChange(page: number) {
- detailWorksPage.value = page;
- }
- function getPlatformName(platform: PlatformType) {
- return PLATFORMS[platform]?.name || platform;
- }
- function getPlatformIcon(platform: PlatformType) {
- return platformIcons[platform] || iconDefault;
- }
- function formatNumber(num: number) {
- if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
- return num.toString();
- }
- function formatTime(time: string) {
- if (!time) return '-';
- 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');
- }
- // 快捷日期选择
- function handleQuickDate(type: string) {
- activeQuickBtn.value = type;
- const today = dayjs();
-
- switch (type) {
- case 'yesterday':
- startDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
- endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
- break;
- case 'beforeYesterday':
- startDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
- endDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
- break;
- case 'last3days':
- // 含今天,共 3 天:今天/昨天/前天
- startDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
- endDate.value = today.format('YYYY-MM-DD');
- break;
- case 'last7days':
- // 含今天,共 7 天
- startDate.value = today.subtract(6, 'day').format('YYYY-MM-DD');
- endDate.value = today.format('YYYY-MM-DD');
- break;
- case 'lastMonth':
- // 含今天,共 30 天
- startDate.value = today.subtract(29, 'day').format('YYYY-MM-DD');
- endDate.value = today.format('YYYY-MM-DD');
- break;
- }
- // 选择快捷日期后,直接刷新数据列表,无需再点击“查询”
- loadData();
- }
- // 查询
- function handleQuery() {
- loadData();
- }
- // 加载分组列表
- async function loadGroups() {
- try {
- const res = await request.get('/api/accounts/groups');
- // 新接口:request 已经解包,res 就是分组数组
- if (Array.isArray(res)) {
- accountGroups.value = res as AccountGroup[];
- } else if ((res as any)?.data?.data) {
- // 兼容旧返回格式 { data: { success, data } }
- accountGroups.value = (res as any).data.data || [];
- }
- } catch (error) {
- console.error('加载分组失败:', error);
- }
- }
- // 加载数据
- async function loadData() {
- loading.value = true;
-
- try {
- const params: Record<string, any> = {
- startDate: startDate.value,
- endDate: endDate.value,
- };
- if (selectedPlatform.value) {
- params.platform = selectedPlatform.value;
- }
- 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) {
- console.error('加载账号数据失败:', error);
- ElMessage.error('加载账号数据失败,请稍后重试');
- } finally {
- loading.value = false;
- }
- }
- // 查看详情
- async function handleDetail(row: AccountData) {
- selectedAccount.value = row;
- drawerVisible.value = true;
- // 初始化详情的时间范围与快捷按钮,与主筛选保持一致
- detailStartDate.value = startDate.value;
- detailEndDate.value = endDate.value;
- detailActiveQuickBtn.value = activeQuickBtn.value;
- detailActiveTab.value = 'data';
- await nextTick();
- // 加载账号详情(数据 + 作品)
- loadAccountDetailData();
- // 同时加载趋势图
- loadAccountTrend(row.id);
- }
- // 加载账号趋势数据
- async function loadAccountTrend(accountId: number) {
- if (!accountChartRef.value) return;
-
- chartLoading.value = true;
-
- try {
- const trend = await request.get('/api/analytics/trend', {
- params: {
- startDate: startDate.value,
- endDate: endDate.value,
- accountId,
- },
- });
- 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) {
- console.error('加载账号趋势失败:', error);
- } finally {
- chartLoading.value = false;
- }
- }
- // 账号详情:快捷日期选择
- 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':
- // 含今天,共 3 天:今天/昨天/前天
- detailStartDate.value = today.subtract(2, 'day').format('YYYY-MM-DD');
- detailEndDate.value = today.format('YYYY-MM-DD');
- break;
- case 'last7days':
- // 含今天,共 7 天
- detailStartDate.value = today.subtract(6, 'day').format('YYYY-MM-DD');
- detailEndDate.value = today.format('YYYY-MM-DD');
- break;
- case 'lastMonth':
- // 含今天,共 30 天
- detailStartDate.value = today.subtract(29, 'day').format('YYYY-MM-DD');
- detailEndDate.value = today.format('YYYY-MM-DD');
- break;
- }
- loadAccountDetailData();
- }
- // 导出当前时间范围内的每日数据为 xlsx(表头与列表一致)
- function exportDetailDailyData() {
- if (!detailDailyData.value.length) return;
- const headers = ['时间', '播放(阅读)量', '评论量', '点赞量', '涨粉量'];
- const rows = detailDailyData.value.map((row) => [
- row.date ?? '',
- row.viewsCount ?? 0,
- row.commentsCount ?? 0,
- row.likesCount ?? 0,
- row.fansIncrease ?? 0,
- ]);
- const data = [headers, ...rows];
- const ws = XLSX.utils.aoa_to_sheet(data);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, '数据详情');
- const arrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
- const blob = new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- const name = (selectedAccount.value?.nickname || '账号').replace(/[/\\?*:"|]/g, '_');
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${name}_数据详情_${detailStartDate.value}_${detailEndDate.value}.xlsx`;
- a.click();
- URL.revokeObjectURL(url);
- ElMessage.success('导出成功');
- }
- // 加载账号详情(汇总 + 每日 + 作品)
- 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,
- };
- const rawDaily = Array.isArray(data.dailyData) ? data.dailyData : [];
- detailDailyData.value = [...rawDaily].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
- detailWorks.value = Array.isArray(data.works) ? data.works : [];
- // 重新加载详情数据时,重置作品分页
- detailWorksPage.value = 1;
- }
- } catch (error) {
- console.error('加载账号详情失败:', error);
- ElMessage.error('加载账号详情失败,请稍后重试');
- } finally {
- detailLoading.value = false;
- }
- }
- // 更新账号趋势图
- function updateAccountChart(trendData: { dates: string[]; fans: number[]; views: number[]; likes: number[] }) {
- if (!accountChartRef.value) return;
-
- if (!accountChart) {
- accountChart = echarts.init(accountChartRef.value);
- }
-
- accountChart.setOption({
- tooltip: { trigger: 'axis' },
- legend: { data: ['粉丝', '播放', '点赞'], bottom: 0 },
- grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
- xAxis: {
- type: 'category',
- data: trendData.dates,
- axisLabel: { color: '#6b7280' },
- },
- yAxis: {
- type: 'value',
- axisLabel: {
- color: '#6b7280',
- formatter: (value: number) => value >= 10000 ? (value / 10000).toFixed(1) + '万' : value.toString(),
- },
- },
- series: [
- { name: '粉丝', type: 'line', data: trendData.fans, smooth: true },
- { name: '播放', type: 'line', data: trendData.views, smooth: true },
- { name: '点赞', type: 'line', data: trendData.likes, smooth: true },
- ],
- });
- }
- // 导出数据(客户端生成 xlsx,无需后端支持)
- async function handleExport() {
- if (!filteredAccounts.value.length) {
- ElMessage.warning('暂无数据可导出');
- return;
- }
- try {
- const headers = ['账号', '平台', '播放(阅读)量', '评论量', '点赞量', '涨粉量', '更新时间', '状态'];
- const rows = filteredAccounts.value.map((row) => [
- row.nickname || row.username || '',
- getPlatformName(row.platform),
- row.viewsCount ?? 0,
- row.commentsCount ?? 0,
- row.likesCount ?? 0,
- row.fansIncrease ?? 0,
- formatTime(row.updateTime),
- row.status === 'active' ? '正常' : '异常',
- ]);
- const data = [headers, ...rows];
- const ws = XLSX.utils.aoa_to_sheet(data);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, '账号数据');
- const arrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
- const blob = new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `账号数据_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`;
- a.click();
- URL.revokeObjectURL(url);
- ElMessage.success('导出成功');
- } catch (error) {
- console.error('导出失败:', error);
- ElMessage.error('导出失败,请稍后重试');
- }
- }
- // 监听抽屉关闭
- watch(drawerVisible, (visible) => {
- if (!visible && accountChart) {
- accountChart.dispose();
- accountChart = null;
- }
- });
- // Bug #6070: 监听账号数据变更事件,新增账号后自动刷新数据
- watch(() => taskStore.accountRefreshTrigger, () => {
- console.log('[Analytics/Account] Account data changed, reloading...');
- loadData();
- });
- onMounted(() => {
- // 如果从平台详情页跳转过来,优先使用路由上的时间范围
- 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();
- // 加载列表后,如有 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>
- <style lang="scss" scoped>
- @use '@/styles/variables.scss' as *;
- .account-analytics {
- .filter-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
- padding: 16px 20px;
- background: #fff;
- border-radius: $radius-lg;
- box-shadow: $shadow-sm;
-
- .filter-left {
- display: flex;
- align-items: center;
- gap: 12px;
-
- .filter-label {
- font-size: 14px;
- color: $text-regular;
- }
-
- .quick-btns {
- display: flex;
- gap: 8px;
- }
- }
- }
-
- .stats-row {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 0;
- margin-bottom: 20px;
- background: #fff;
- border-radius: $radius-lg;
- box-shadow: $shadow-sm;
- overflow: hidden;
-
- .stat-card {
- padding: 20px 16px;
- display: flex;
- align-items: center;
- gap: 12px;
- border-right: 1px solid #f0f0f0;
-
- &:last-child {
- border-right: none;
- }
-
- .stat-icon {
- width: 36px;
- height: 36px;
- background: $primary-color-light;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: $primary-color;
- flex-shrink: 0;
- }
-
- .stat-info {
- .stat-label {
- font-size: 12px;
- color: $text-secondary;
- margin-bottom: 4px;
- white-space: nowrap;
- }
-
- .stat-value {
- font-size: 20px;
- font-weight: 600;
- color: $text-primary;
- }
- }
- }
- }
-
- .search-bar {
- margin-bottom: 16px;
- }
-
- .data-table {
- background: #fff;
- border-radius: $radius-lg;
- box-shadow: $shadow-sm;
- overflow: hidden;
-
- .account-cell {
- display: flex;
- align-items: center;
- gap: 10px;
-
- .account-name {
- font-weight: 500;
- color: $text-primary;
- }
- }
-
- .platform-cell {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
-
- .platform-icon {
- width: 20px;
- height: 20px;
- border-radius: 4px;
- }
- }
-
- .increase {
- color: #10b981;
- }
-
- .decrease {
- color: #ef4444;
- }
-
- .update-time {
- font-size: 12px;
- color: $text-secondary;
- }
-
- .status-tag {
- font-size: 12px;
- padding: 2px 8px;
- border-radius: 4px;
-
- &.active {
- color: #10b981;
- background: rgba(16, 185, 129, 0.1);
- }
-
- &.inactive {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.1);
- }
- }
- }
- }
- .account-detail {
- .detail-header {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-bottom: 24px;
-
- .header-info {
- h3 {
- margin: 0 0 8px 0;
- font-size: 18px;
- }
-
- .platform-info {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 14px;
- color: $text-secondary;
-
- .platform-icon {
- width: 18px;
- height: 18px;
- border-radius: 4px;
- }
- }
- }
- }
- .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;
- grid-template-columns: repeat(4, 1fr);
- gap: 16px;
- margin-bottom: 16px;
- .stat-item {
- background: #f8fafc;
- border-radius: 12px;
- padding: 16px;
- text-align: center;
- .stat-value {
- font-size: 20px;
- font-weight: 600;
- color: $primary-color;
- }
- .stat-label {
- font-size: 13px;
- color: $text-secondary;
- margin-top: 4px;
- }
- }
- }
- .detail-export-wrap {
- margin-left: 12px;
- }
- .work-title-cell {
- .work-title-text {
- font-weight: 500;
- color: $text-primary;
- }
- }
- .pagination-wrapper {
- padding-top: 12px;
- display: flex;
- justify-content: flex-end;
- }
- }
- @media (max-width: 1400px) {
- .account-analytics {
- .stats-row {
- grid-template-columns: repeat(4, 1fr);
-
- .stat-card {
- &:nth-child(4) {
- border-right: none;
- }
-
- &:nth-child(n+5) {
- border-top: 1px solid #f0f0f0;
- }
- }
- }
- }
- }
- @media (max-width: 1200px) {
- .account-analytics {
- .filter-bar {
- flex-direction: column;
- align-items: flex-start;
- gap: 12px;
-
- .filter-left {
- flex-wrap: wrap;
- }
- }
-
- .stats-row {
- grid-template-columns: repeat(3, 1fr);
-
- .stat-card {
- &:nth-child(3n) {
- border-right: none;
- }
-
- &:nth-child(n+4) {
- border-top: 1px solid #f0f0f0;
- }
- }
- }
- }
- }
- </style>
|