2 Коммиты 887373f766 ... 9ed40e814f

Автор SHA1 Сообщение Дата
  Ethanfly 9ed40e814f Merge branch 'main' of http://gitlab.pubdata.cn/hlm/multi-platform-media-manage 1 день назад
  Ethanfly 407b8761ec 账号统计页 1 день назад

+ 1 - 0
client/src/components.d.ts

@@ -32,6 +32,7 @@ declare module 'vue' {
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']

+ 386 - 92
client/src/views/Analytics/Account/index.vue

@@ -107,6 +107,7 @@
             </div>
           </template>
         </el-table-column>
+        <!-- 收益与推荐量暂未接入,先隐藏
         <el-table-column prop="income" label="收益" width="100" align="center">
           <template #default="{ row }">
             <span>{{ row.income ?? '未支持' }}</span>
@@ -117,6 +118,7 @@
             <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>
@@ -162,7 +164,7 @@
     </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 class="detail-header">
@@ -177,32 +179,142 @@
             </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>
+          <el-button type="primary" @click="loadAccountDetailData">查询</el-button>
         </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>
     </el-drawer>
   </div>
@@ -210,18 +322,22 @@
 
 <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, 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 type { PlatformType } from '@media-manager/shared';
-import { useAuthStore } from '@/stores/auth';
 import { ElMessage } from 'element-plus';
 import dayjs from 'dayjs';
 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 chartLoading = ref(false);
 
@@ -259,13 +375,16 @@ const availablePlatforms = computed(() => {
   }));
 });
 
-// 平台图标映射
+// 平台图标映射(与平台数据页保持一致,使用本地 SVG,避免外链被拦截)
+const iconDefault = iconDefaultUrl;
 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(() => [
   { 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.commentsCount, icon: ChatDotRound },
   { label: '点赞量', value: summaryData.value.likesCount, icon: Star },
@@ -345,12 +466,52 @@ const drawerTitle = computed(() => {
   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) {
   return PLATFORMS[platform]?.name || platform;
 }
 
 function getPlatformIcon(platform: PlatformType) {
-  return platformIcons[platform] || '';
+  return platformIcons[platform] || iconDefault;
 }
 
 function formatNumber(num: number) {
@@ -360,7 +521,10 @@ function formatNumber(num: number) {
 
 function formatTime(time: string) {
   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');
       break;
   }
+
+  // 选择快捷日期后,直接刷新数据列表,无需再点击“查询”
+  loadData();
 }
 
 // 查询
@@ -401,8 +568,11 @@ function handleQuery() {
 async function loadGroups() {
   try {
     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) {
     console.error('加载分组失败:', error);
@@ -411,34 +581,43 @@ async function loadGroups() {
 
 // 加载数据
 async function loadData() {
-  const userId = authStore.user?.id;
-  if (!userId) return;
-  
   loading.value = true;
   
   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) {
-      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) {
     console.error('加载账号数据失败:', error);
+    ElMessage.error('加载账号数据失败,请稍后重试');
   } finally {
     loading.value = false;
   }
@@ -448,8 +627,17 @@ async function loadData() {
 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);
 }
 
@@ -460,21 +648,23 @@ async function loadAccountTrend(accountId: number) {
   chartLoading.value = true;
   
   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) {
     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[] }) {
   if (!accountChartRef.value) return;
@@ -529,10 +785,31 @@ watch(drawerVisible, (visible) => {
 });
 
 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();
-  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>
 
@@ -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;
     grid-template-columns: repeat(4, 1fr);
     gap: 16px;
-    margin-bottom: 24px;
-    
+    margin-bottom: 16px;
+
     .stat-item {
       background: #f8fafc;
       border-radius: 12px;
       padding: 16px;
       text-align: center;
-      
+
       .stat-value {
-        font-size: 24px;
+        font-size: 20px;
         font-weight: 600;
         color: $primary-color;
       }
-      
+
       .stat-label {
         font-size: 13px;
         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;
     }
   }

+ 4 - 1
client/src/views/Analytics/Overview/index.vue

@@ -292,7 +292,10 @@ function formatNumber(num: number) {
 
 function formatTime(time: string) {
   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');
 }
 
 // 加载分组列表

+ 4 - 1
client/src/views/Analytics/Platform/index.vue

@@ -239,7 +239,10 @@ function formatNumber(num: number) {
 
 function formatTime(time: string) {
   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');
 }
 
 // 快捷日期选择

+ 403 - 9
client/src/views/Analytics/PlatformDetail/index.vue

@@ -161,6 +161,160 @@
         </el-table-column>
       </el-table>
     </div>
+
+    <!-- 账号详情抽屉(直接在平台详情页弹出) -->
+    <el-drawer v-model="accountDetailVisible" :title="accountDetailTitle" size="70%">
+      <div v-if="selectedAccountDetail" class="account-detail">
+        <!-- 账号基本信息 -->
+        <div class="detail-header">
+          <el-avatar :size="64" :src="selectedAccountDetail.avatarUrl">
+            {{ selectedAccountDetail.nickname?.[0] || selectedAccountDetail.username?.[0] }}
+          </el-avatar>
+          <div class="header-info">
+            <h3>{{ selectedAccountDetail.nickname || selectedAccountDetail.username }}</h3>
+            <div class="platform-info">
+              <span>{{ getPlatformName(selectedAccountDetail.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"
+          />
+          <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>
+          <el-button type="primary" @click="loadAccountDetailData">查询</el-button>
+        </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>
+    </el-drawer>
   </div>
 </template>
 
@@ -248,6 +402,88 @@ const accountList = ref<Array<{
   updateTime: string;
 }>>([]);
 
+// 单个账号详情(在本页抽屉中展示)
+const accountDetailVisible = ref(false);
+const selectedAccountDetail = ref<{
+  id: number;
+  nickname: string;
+  username: string;
+  avatarUrl: string | null;
+  platform: string;
+} | null>(null);
+
+const accountDetailTitle = computed(() => {
+  if (!selectedAccountDetail.value) return '账号详情';
+  return `${selectedAccountDetail.value.nickname || selectedAccountDetail.value.username} - 数据详情`;
+});
+
+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;
+}>>([]);
+
+// 根据当前 startDate / endDate 自动同步快捷按钮高亮状态
+function syncQuickBtnByRange() {
+  const today = dayjs();
+  const s = dayjs(startDate.value);
+  const e = dayjs(endDate.value);
+
+  // 只处理“单日”场景,区间场景不高亮任何快捷按钮
+  if (!s.isValid() || !e.isValid() || !s.isSame(e, 'day')) {
+    activeQuickBtn.value = '';
+    return;
+  }
+
+  const diff = today.startOf('day').diff(s.startOf('day'), 'day');
+  if (diff === 1) {
+    // 昨天
+    activeQuickBtn.value = 'yesterday';
+  } else if (diff === 2) {
+    // 前天
+    activeQuickBtn.value = 'beforeYesterday';
+  } else if (diff === 2 && today.isSame(e.add(2, 'day'), 'day')) {
+    // 近三天(含今天):start = today - 2, end = today
+    activeQuickBtn.value = 'last3days';
+  } else {
+    // 其他情况不自动匹配
+    activeQuickBtn.value = '';
+  }
+}
+
 function getPlatformName(platform: string) {
   return PLATFORMS[platform as PlatformType]?.name || platform;
 }
@@ -257,6 +493,14 @@ function formatNumber(num: number) {
   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;
@@ -382,14 +626,88 @@ function handleExport() {
 
 // 账号详情
 function handleAccountDetail(row: any) {
-  router.push({
-    name: 'AnalyticsAccount',
-    query: {
-      accountId: row.id,
-      startDate: startDate.value,
-      endDate: endDate.value,
-    },
-  });
+  selectedAccountDetail.value = {
+    id: row.id,
+    nickname: row.nickname,
+    username: row.username,
+    avatarUrl: row.avatarUrl,
+    platform: row.platform,
+  };
+  accountDetailVisible.value = true;
+
+  // 初始化详情时间范围与快捷按钮
+  detailStartDate.value = startDate.value;
+  detailEndDate.value = endDate.value;
+  detailActiveQuickBtn.value = activeQuickBtn.value;
+  detailActiveTab.value = 'data';
+
+  loadAccountDetailData();
+}
+
+// 账号详情:快捷日期选择
+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 (!selectedAccountDetail.value) return;
+
+  detailLoading.value = true;
+  try {
+    const data = await request.get('/api/work-day-statistics/account-detail', {
+      params: {
+        accountId: selectedAccountDetail.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('[PlatformDetail] 加载账号详情失败:', error);
+    ElMessage.error('加载账号详情失败,请稍后重试');
+  } finally {
+    detailLoading.value = false;
+  }
 }
 
 // 初始化数据加载
@@ -409,7 +727,10 @@ function initDataLoad() {
     if (route.query.endDate) {
       endDate.value = route.query.endDate as string;
     }
-    
+
+    // 同步一次快捷日期按钮的高亮状态,避免始终显示“昨天”
+    syncQuickBtnByRange();
+
     console.log('[PlatformDetail] 准备加载数据, selectedPlatform:', selectedPlatform.value, 'startDate:', startDate.value, 'endDate:', endDate.value);
     
     // 使用 nextTick 确保响应式更新完成
@@ -558,5 +879,78 @@ onMounted(() => {
       color: $text-secondary;
     }
   }
+
+  .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 {
+          margin-top: 4px;
+          font-size: 14px;
+          color: $text-secondary;
+        }
+      }
+    }
+
+    .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;
+        }
+      }
+    }
+
+    .work-title-cell {
+      .work-title-text {
+        font-weight: 500;
+        color: $text-primary;
+      }
+    }
+  }
 }
 </style>

+ 5 - 1
client/src/views/Analytics/Work/index.vue

@@ -359,7 +359,11 @@ function getPlatformTagType(platform: PlatformType) {
 
 function formatTime(time: string) {
   if (!time) return '-';
-  return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+  const d = dayjs(time);
+  if (!d.isValid()) return time;
+  const nowYear = dayjs().year();
+  // 今年:01-22 10:22,往年:2025-12-22 10:22
+  return d.year() === nowYear ? d.format('MM-DD HH:mm') : d.format('YYYY-MM-DD HH:mm');
 }
 
 // 快捷日期选择

+ 25 - 25
server/python/export_platform_statistics_xlsx.py

@@ -78,53 +78,53 @@ def _safe_str(v) -> str:
 
 
 def _format_update_time(value: str) -> str:
-  """
-  格式化更新时间为 "MM-DD HH:mm" 格式。
-  如果已经是该格式,直接返回;否则尝试解析并转换。
+  """\
+  格式化更新时间为统一的人类可读格式:
+  - 如果是今年:MM-DD HH:mm
+  - 如果是往年:YYYY-MM-DD HH:mm\
   """
   if not value:
     return ""
 
   s = str(value).strip()
-  
-  # 如果已经是 "MM-DD HH:mm" 格式,直接返回
-  if len(s) == 14 and s[2] == '-' and s[5] == ' ' and s[8] == ':':
-    return s
-  
-  # 尝试解析 ISO 格式或其他常见格式
+
+  # 尝试解析 ISO 或常见时间格式
   try:
-    # 处理 ISO 格式(如 "2026-01-28T12:22:00Z")
     if s.endswith("Z"):
       s_clean = s[:-1]
     else:
       s_clean = s
     s_clean = s_clean.replace(" ", "T")
     dt = datetime.fromisoformat(s_clean)
-    return dt.strftime("%m-%d %H:%M")
+    now_year = datetime.now().year
+    if dt.year == now_year:
+      return dt.strftime("%m-%d %H:%M")
+    return dt.strftime("%Y-%m-%d %H:%M")
   except Exception:
     pass
-  
-  # 如果无法解析,尝试提取日期和时间部分
-  # 格式可能是 "YYYY-MM-DD HH:mm:ss" 或类似
+
+  # 尝试从 "YYYY-MM-DD HH:mm:ss" 中拆分
   try:
-    # 尝试匹配 "YYYY-MM-DD HH:mm:ss" 格式
     if len(s) >= 16:
-      parts = s.split(' ')
+      parts = s.split(" ")
       if len(parts) >= 2:
-        date_part = parts[0]  # "YYYY-MM-DD"
-        time_part = parts[1]  # "HH:mm:ss" 或 "HH:mm"
-        date_parts = date_part.split('-')
-        time_parts = time_part.split(':')
+        date_part = parts[0]
+        time_part = parts[1]
+        date_parts = date_part.split("-")
+        time_parts = time_part.split(":")
         if len(date_parts) >= 3 and len(time_parts) >= 2:
-          month = date_parts[1]
-          day = date_parts[2]
+          year = int(date_parts[0])
+          month = date_parts[1].zfill(2)
+          day = date_parts[2].zfill(2)
           hour = time_parts[0].zfill(2)
           minute = time_parts[1].zfill(2)
-          return f"{month}-{day} {hour}:{minute}"
+          now_year = datetime.now().year
+          if year == now_year:
+            return f"{month}-{day} {hour}:{minute}"
+          return f"{year}-{month}-{day} {hour}:{minute}"
   except Exception:
     pass
-  
-  # 如果都失败了,返回原字符串
+
   return s
 
 

+ 34 - 12
server/python/export_work_day_overview_xlsx.py

@@ -97,32 +97,54 @@ def _safe_str(v) -> str:
   return "".join(ch for ch in s if not (0xD800 <= ord(ch) <= 0xDFFF))
 
 
-def _format_date_only(value: str) -> str:
-  """
-  将各种时间字符串格式化为 YYYY-MM-DD,仅保留日期部分。
-  如果无法解析,则尽量取前 10 位作为日期返回。
+def _format_datetime_pretty(value: str) -> str:
+  """\
+  将时间字符串格式化为统一的人类可读格式:
+  - 如果是今年:MM-DD HH:mm
+  - 如果是往年:YYYY-MM-DD HH:mm
+  解析失败时,尽量返回原始字符串。\
   """
   if not value:
     return ""
 
   s = str(value).strip()
-  # 先尝试 ISO 格式解析
+
+  # 尝试按 ISO 格式解析("2026-01-28T12:22:00Z" / "2026-01-28 12:22:00" 等)
   try:
-    # 兼容 "...Z" 结尾
     if s.endswith("Z"):
       s_clean = s[:-1]
     else:
       s_clean = s
-    # 如果中间是 ' ',替换成 'T'
     s_clean = s_clean.replace(" ", "T")
     dt = datetime.fromisoformat(s_clean)
-    return dt.strftime("%Y-%m-%d")
+    now_year = datetime.now().year
+    if dt.year == now_year:
+      return dt.strftime("%m-%d %H:%M")
+    return dt.strftime("%Y-%m-%d %H:%M")
   except Exception:
     pass
 
-  # 退化处理:如果长度>=10,优先取前 10 位
-  if len(s) >= 10:
-    return s[:10]
+  # 退化处理:尝试从类似 "YYYY-MM-DD HH:mm:ss" 的字符串中拆分
+  try:
+    if len(s) >= 16:
+      parts = s.split(" ")
+      if len(parts) >= 2:
+        date_part = parts[0]
+        time_part = parts[1]
+        date_parts = date_part.split("-")
+        time_parts = time_part.split(":")
+        if len(date_parts) >= 3 and len(time_parts) >= 2:
+          year = int(date_parts[0])
+          month = date_parts[1].zfill(2)
+          day = date_parts[2].zfill(2)
+          hour = time_parts[0].zfill(2)
+          minute = time_parts[1].zfill(2)
+          now_year = datetime.now().year
+          if year == now_year:
+            return f"{month}-{day} {hour}:{minute}"
+          return f"{year}-{month}-{day} {hour}:{minute}"
+  except Exception:
+    pass
 
   return s
 
@@ -175,7 +197,7 @@ def build_xlsx(accounts):
       _safe_int(a.get("yesterdayComments")) or 0,
       _safe_int(a.get("yesterdayLikes")) or 0,
       _safe_int(a.get("yesterdayFansIncrease")) or 0,
-      _safe_str(_format_date_only(a.get("updateTime"))),
+      _safe_str(_format_datetime_pretty(a.get("updateTime"))),
     ])
 
   int_cols = {"C", "D", "E", "F", "G", "H"}

BIN
server/python/platforms/__pycache__/baijiahao.cpython-311.pyc


BIN
server/python/platforms/__pycache__/weixin.cpython-311.pyc


BIN
server/python/platforms/__pycache__/xiaohongshu.cpython-311.pyc


+ 64 - 0
server/src/routes/workDayStatistics.ts

@@ -284,5 +284,69 @@ router.get(
   })
 );
 
+/**
+ * GET /api/work-day-statistics/accounts
+ * 获取账号维度的区间统计数据(供「账号数据」页使用)
+ *
+ * 查询参数:
+ * - startDate: 开始日期(必填)
+ * - endDate: 结束日期(必填)
+ * - platform: 平台(可选)
+ * - groupId: 分组ID(可选)
+ */
+router.get(
+  '/accounts',
+  [
+    query('startDate').notEmpty().withMessage('startDate 不能为空'),
+    query('endDate').notEmpty().withMessage('endDate 不能为空'),
+    query('platform').optional().isString().withMessage('platform 必须是字符串'),
+    query('groupId').optional().isInt().withMessage('groupId 必须是整数'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { startDate, endDate, platform } = req.query;
+    const groupId = req.query.groupId ? Number(req.query.groupId) : undefined;
+
+    const data = await workDayStatisticsService.getAccountsAnalytics(req.user!.userId, {
+      startDate: startDate as string,
+      endDate: endDate as string,
+      platform: (platform as string) || undefined,
+      groupId,
+    });
+
+    res.json({ success: true, data });
+  })
+);
+
+/**
+ * GET /api/work-day-statistics/account-detail
+ * 获取单个账号的详情数据(账号维度)
+ *
+ * 查询参数:
+ * - accountId: 账号 ID(必填)
+ * - startDate: 开始日期(必填)
+ * - endDate: 结束日期(必填)
+ */
+router.get(
+  '/account-detail',
+  [
+    query('accountId').notEmpty().withMessage('accountId 不能为空'),
+    query('startDate').notEmpty().withMessage('startDate 不能为空'),
+    query('endDate').notEmpty().withMessage('endDate 不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { accountId, startDate, endDate } = req.query;
+    const userId = req.user!.userId;
+
+    const data = await workDayStatisticsService.getAccountDetail(userId, Number(accountId), {
+      startDate: startDate as string,
+      endDate: endDate as string,
+    });
+
+    res.json({ success: true, data });
+  })
+);
+
 export default router;
 

+ 415 - 2
server/src/services/WorkDayStatisticsService.ts

@@ -703,6 +703,411 @@ export class WorkDayStatisticsService {
   }
 
   /**
+   * 获取账号维度的区间统计数据
+   * 用于「账号数据」页,支持按日期区间、平台、分组筛选
+   */
+  async getAccountsAnalytics(
+    userId: number,
+    options: {
+      startDate: string;
+      endDate: string;
+      platform?: string;
+      groupId?: number;
+    }
+  ): Promise<{
+    accounts: Array<{
+      id: number;
+      nickname: string;
+      username: string;
+      avatarUrl: string | null;
+      platform: string;
+      groupId: number | null;
+      income: number | null;
+      recommendationCount: number | null;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+      fansCount: number;
+      updateTime: string;
+      status: string;
+    }>;
+    summary: {
+      totalAccounts: number;
+      totalIncome: number;
+      recommendationCount: number | null;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+      totalFans: number;
+    };
+  }> {
+    const { startDate, endDate, platform, groupId } = options;
+
+    // 只查询支持的平台:抖音、百家号、视频号、小红书
+    const allowedPlatforms = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
+
+    const accountQuery = this.accountRepository
+      .createQueryBuilder('pa')
+      .where('pa.userId = :userId', { userId })
+      .andWhere('pa.platform IN (:...allowedPlatforms)', { allowedPlatforms });
+
+    if (platform) {
+      accountQuery.andWhere('pa.platform = :platform', { platform });
+    }
+    if (groupId) {
+      accountQuery.andWhere('pa.groupId = :groupId', { groupId });
+    }
+
+    const accounts = await accountQuery.getMany();
+    if (accounts.length === 0) {
+      return {
+        accounts: [],
+        summary: {
+          totalAccounts: 0,
+          totalIncome: 0,
+          recommendationCount: null,
+          viewsCount: 0,
+          commentsCount: 0,
+          likesCount: 0,
+          fansIncrease: 0,
+          totalFans: 0,
+        },
+      };
+    }
+
+    const accountIds = accounts.map(a => a.id);
+
+    // 使用 user_day_statistics 统计区间内的播放/评论/点赞/涨粉等
+    const statsRows = await this.userDayStatisticsRepository
+      .createQueryBuilder('uds')
+      .select('uds.account_id', 'accountId')
+      .addSelect('COALESCE(SUM(uds.play_count), 0)', 'viewsCount')
+      .addSelect('COALESCE(SUM(uds.comment_count), 0)', 'commentsCount')
+      .addSelect('COALESCE(SUM(uds.like_count), 0)', 'likesCount')
+      .addSelect('COALESCE(SUM(uds.fans_increase), 0)', 'fansIncrease')
+      .addSelect('MAX(uds.updated_at)', 'latestUpdateTime')
+      .where('uds.account_id IN (:...accountIds)', { accountIds })
+      .andWhere('DATE(uds.record_date) >= :startDate', { startDate })
+      .andWhere('DATE(uds.record_date) <= :endDate', { endDate })
+      .groupBy('uds.account_id')
+      .getRawMany();
+
+    const statMap = new Map<
+      number,
+      {
+        viewsCount: number;
+        commentsCount: number;
+        likesCount: number;
+        fansIncrease: number;
+        latestUpdateTime: Date | null;
+      }
+    >();
+
+    for (const row of statsRows || []) {
+      const accountId = Number(row.accountId) || 0;
+      if (!accountId) continue;
+      statMap.set(accountId, {
+        viewsCount: Number(row.viewsCount) || 0,
+        commentsCount: Number(row.commentsCount) || 0,
+        likesCount: Number(row.likesCount) || 0,
+        fansIncrease: Number(row.fansIncrease) || 0,
+        latestUpdateTime: row.latestUpdateTime ? new Date(row.latestUpdateTime) : null,
+      });
+    }
+
+    const resultAccounts: Array<{
+      id: number;
+      nickname: string;
+      username: string;
+      avatarUrl: string | null;
+      platform: string;
+      groupId: number | null;
+      income: number | null;
+      recommendationCount: number | null;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+      fansCount: number;
+      updateTime: string;
+      status: string;
+    }> = [];
+
+    let totalViews = 0;
+    let totalComments = 0;
+    let totalLikes = 0;
+    let totalFansIncrease = 0;
+    let totalFans = 0;
+
+    for (const account of accounts) {
+      const stat =
+        statMap.get(account.id) ?? {
+          viewsCount: 0,
+          commentsCount: 0,
+          likesCount: 0,
+          fansIncrease: 0,
+          latestUpdateTime: account.updatedAt ?? null,
+        };
+
+      const fansCount = account.fansCount || 0;
+      const updateTime = stat.latestUpdateTime
+        ? this.formatUpdateTime(stat.latestUpdateTime)
+        : this.formatUpdateTime(account.updatedAt ?? new Date());
+
+      resultAccounts.push({
+        id: account.id,
+        nickname: account.accountName || '',
+        username: account.accountId || '',
+        avatarUrl: account.avatarUrl,
+        platform: account.platform,
+        groupId: account.groupId ?? null,
+        income: null,
+        recommendationCount: null,
+        viewsCount: stat.viewsCount,
+        commentsCount: stat.commentsCount,
+        likesCount: stat.likesCount,
+        fansIncrease: stat.fansIncrease,
+        fansCount,
+        updateTime,
+        status: account.status,
+      });
+
+      totalViews += stat.viewsCount;
+      totalComments += stat.commentsCount;
+      totalLikes += stat.likesCount;
+      totalFansIncrease += stat.fansIncrease;
+      totalFans += fansCount;
+    }
+
+    return {
+      accounts: resultAccounts,
+      summary: {
+        totalAccounts: accounts.length,
+        totalIncome: 0,
+        recommendationCount: null,
+        viewsCount: totalViews,
+        commentsCount: totalComments,
+        likesCount: totalLikes,
+        fansIncrease: totalFansIncrease,
+        totalFans,
+      },
+    };
+  }
+
+  /**
+   * 获取单个账号的详情数据
+   * 包括:汇总统计、每日数据、作品列表
+   * 说明:
+   * - 收益、推荐量目前数据库尚未接入,统一返回 0
+   */
+  async getAccountDetail(
+    userId: number,
+    accountId: number,
+    options: {
+      startDate: string;
+      endDate: string;
+    }
+  ): Promise<{
+    summary: {
+      income: number;
+      recommendationCount: number;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+    };
+    dailyData: Array<{
+      date: string;
+      income: number;
+      recommendationCount: number;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+    }>;
+    works: 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 { startDate, endDate } = options;
+    const account = await this.accountRepository.findOne({
+      where: { id: accountId, userId },
+    });
+
+    if (!account) {
+      // 账号不存在或不属于该用户,返回空数据
+      return {
+        summary: {
+          income: 0,
+          recommendationCount: 0,
+          viewsCount: 0,
+          commentsCount: 0,
+          likesCount: 0,
+          fansIncrease: 0,
+        },
+        dailyData: [],
+        works: [],
+      };
+    }
+
+    const startDateStr = startDate;
+    const endDateStr = endDate;
+
+    // 1. 每日数据:直接从 user_day_statistics 获取指定账号的每日记录
+    const udsRows = await this.userDayStatisticsRepository
+      .createQueryBuilder('uds')
+      .select('uds.record_date', 'recordDate')
+      .addSelect('uds.play_count', 'viewsCount')
+      .addSelect('uds.comment_count', 'commentsCount')
+      .addSelect('uds.like_count', 'likesCount')
+      .addSelect('uds.fans_increase', 'fansIncrease')
+      .where('uds.account_id = :accountId', { accountId })
+      .andWhere('DATE(uds.record_date) >= :startDate', { startDate: startDateStr })
+      .andWhere('DATE(uds.record_date) <= :endDate', { endDate: endDateStr })
+      .orderBy('uds.record_date', 'ASC')
+      .getRawMany();
+
+    const dailyMap = new Map<string, { views: number; comments: number; likes: number; fansIncrease: number }>();
+    for (const row of udsRows || []) {
+      if (!row.recordDate) continue;
+      let dateKey: string;
+      if (row.recordDate instanceof Date) {
+        const y = row.recordDate.getFullYear();
+        const m = String(row.recordDate.getMonth() + 1).padStart(2, '0');
+        const d = String(row.recordDate.getDate()).padStart(2, '0');
+        dateKey = `${y}-${m}-${d}`;
+      } else {
+        dateKey = String(row.recordDate).slice(0, 10);
+      }
+
+      dailyMap.set(dateKey, {
+        views: Number(row.viewsCount) || 0,
+        comments: Number(row.commentsCount) || 0,
+        likes: Number(row.likesCount) || 0,
+        fansIncrease: Number(row.fansIncrease) || 0,
+      });
+    }
+
+    const dStart = new Date(startDateStr);
+    const dEnd = new Date(endDateStr);
+    const cursor = new Date(dStart);
+
+    const dailyData: Array<{
+      date: string;
+      income: number;
+      recommendationCount: number;
+      viewsCount: number;
+      commentsCount: number;
+      likesCount: number;
+      fansIncrease: number;
+    }> = [];
+
+    let totalViews = 0;
+    let totalComments = 0;
+    let totalLikes = 0;
+    let totalFansIncrease = 0;
+
+    while (cursor <= dEnd) {
+      const dateKey = this.formatDate(cursor);
+      const value = dailyMap.get(dateKey) ?? {
+        views: 0,
+        comments: 0,
+        likes: 0,
+        fansIncrease: 0,
+      };
+
+      dailyData.push({
+        date: dateKey,
+        income: 0,
+        recommendationCount: 0,
+        viewsCount: value.views,
+        commentsCount: value.comments,
+        likesCount: value.likes,
+        fansIncrease: value.fansIncrease,
+      });
+
+      totalViews += value.views;
+      totalComments += value.comments;
+      totalLikes += value.likes;
+      totalFansIncrease += value.fansIncrease;
+
+      cursor.setDate(cursor.getDate() + 1);
+    }
+
+    // 2. 作品列表:按作品聚合 work_day_statistics 区间内的数据
+    const worksRows = await this.workRepository
+      .createQueryBuilder('w')
+      .leftJoin(WorkDayStatistics, 'wds', 'wds.work_id = w.id AND wds.record_date >= :wStart AND wds.record_date <= :wEnd', {
+        wStart: startDateStr,
+        wEnd: endDateStr,
+      })
+      .select('w.id', 'id')
+      .addSelect('w.title', 'title')
+      .addSelect('w.cover_url', 'coverUrl')
+      .addSelect('w.platform', 'platform')
+      .addSelect('w.publish_time', 'publishTime')
+      .addSelect('COALESCE(SUM(wds.play_count), 0)', 'viewsCount')
+      .addSelect('COALESCE(SUM(wds.comment_count), 0)', 'commentsCount')
+      .addSelect('COALESCE(SUM(wds.share_count), 0)', 'sharesCount')
+      .addSelect('COALESCE(SUM(wds.collect_count), 0)', 'collectsCount')
+      .addSelect('COALESCE(SUM(wds.like_count), 0)', 'likesCount')
+      .where('w.userId = :userId', { userId })
+      .andWhere('w.accountId = :accountId', { accountId })
+      .groupBy('w.id')
+      .orderBy('w.publish_time', 'DESC')
+      .getRawMany();
+
+    const works = (worksRows || []).map((row) => {
+      const publishTime =
+        row.publishTime instanceof Date
+          ? row.publishTime.toISOString()
+          : row.publishTime
+          ? String(row.publishTime)
+          : null;
+
+      return {
+        id: Number(row.id),
+        title: row.title || '',
+        coverUrl: row.coverUrl || '',
+        platform: row.platform || '',
+        publishTime,
+        // 推荐量目前没有独立字段,统一返回 0
+        recommendCount: 0,
+        viewsCount: Number(row.viewsCount) || 0,
+        commentsCount: Number(row.commentsCount) || 0,
+        sharesCount: Number(row.sharesCount) || 0,
+        collectsCount: Number(row.collectsCount) || 0,
+        likesCount: Number(row.likesCount) || 0,
+      };
+    });
+
+    return {
+      summary: {
+        income: 0,
+        recommendationCount: 0,
+        viewsCount: totalViews,
+        commentsCount: totalComments,
+        likesCount: totalLikes,
+        fansIncrease: totalFansIncrease,
+      },
+      dailyData,
+      works,
+    };
+  }
+
+  /**
    * 获取平台详情数据
    * 包括汇总统计、每日汇总数据和账号列表
    */
@@ -982,13 +1387,21 @@ export class WorkDayStatisticsService {
   }
 
   /**
-   * 格式化更新时间为 "MM-DD HH:mm" 格式
+   * 格式化更新时间为统一的人类可读格式:
+   * - 如果是今年:MM-DD HH:mm(例如:01-22 10:22)
+   * - 如果是往年:YYYY-MM-DD HH:mm(例如:2025-12-22 10:22)
    */
   private formatUpdateTime(date: Date): string {
+    const y = date.getFullYear();
+    const nowYear = new Date().getFullYear();
     const month = String(date.getMonth() + 1).padStart(2, '0');
     const day = String(date.getDate()).padStart(2, '0');
     const hours = String(date.getHours()).padStart(2, '0');
     const minutes = String(date.getMinutes()).padStart(2, '0');
-    return `${month}-${day} ${hours}:${minutes}`;
+
+    if (y === nowYear) {
+      return `${month}-${day} ${hours}:${minutes}`;
+    }
+    return `${y}-${month}-${day} ${hours}:${minutes}`;
   }
 }