Browse Source

Merge branch 'cb'

# Conflicts:
#	client/src/components.d.ts
Ethanfly 12 giờ trước cách đây
mục cha
commit
edf5fa1c16

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

@@ -15,7 +15,25 @@ 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']
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElProgress: typeof import('element-plus/es')['ElProgress']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElRow: typeof import('element-plus/es')['ElRow']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
+    ElTag: typeof import('element-plus/es')['ElTag']
+
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 113 - 6
client/src/layouts/MainLayout.vue

@@ -78,7 +78,7 @@
       <el-menu
         :default-active="activeMenuPath"
         class="sidebar-menu"
-        :collapse="false"
+        :collapse="isCollapsed"
         :collapse-transition="false"
         background-color="transparent"
         text-color="#64748b"
@@ -110,10 +110,24 @@
           <span class="menu-title">定时任务</span>
         </el-menu-item>
         
-        <el-menu-item index="/analytics">
-          <el-icon><TrendCharts /></el-icon>
-          <span class="menu-title">数据分析</span>
-        </el-menu-item>
+        <el-sub-menu index="/analytics">
+          <template #title>
+            <el-icon><TrendCharts /></el-icon>
+            <span class="menu-title">数据分析</span>
+          </template>
+          <el-menu-item index="/analytics/overview">
+            <span class="sub-menu-title">数据总览</span>
+          </el-menu-item>
+          <el-menu-item index="/analytics/platform">
+            <span class="sub-menu-title">平台数据</span>
+          </el-menu-item>
+          <el-menu-item index="/analytics/account">
+            <span class="sub-menu-title">账号数据</span>
+          </el-menu-item>
+          <el-menu-item index="/analytics/work">
+            <span class="sub-menu-title">作品数据</span>
+          </el-menu-item>
+        </el-sub-menu>
         
         <el-menu-item index="/settings" v-if="authStore.isAdmin">
           <el-icon><Setting /></el-icon>
@@ -329,7 +343,10 @@ const pageComponents = shallowRef<Record<string, ReturnType<typeof defineAsyncCo
   '/publish': defineAsyncComponent(() => import('@/views/Publish/index.vue')),
   '/comments': defineAsyncComponent(() => import('@/views/Comments/index.vue')),
   '/schedule': defineAsyncComponent(() => import('@/views/Schedule/index.vue')),
-  '/analytics': defineAsyncComponent(() => import('@/views/Analytics/index.vue')),
+  '/analytics/overview': defineAsyncComponent(() => import('@/views/Analytics/Overview/index.vue')),
+  '/analytics/platform': defineAsyncComponent(() => import('@/views/Analytics/Platform/index.vue')),
+  '/analytics/account': defineAsyncComponent(() => import('@/views/Analytics/Account/index.vue')),
+  '/analytics/work': defineAsyncComponent(() => import('@/views/Analytics/Work/index.vue')),
   '/settings': defineAsyncComponent(() => import('@/views/Settings/index.vue')),
   '/profile': defineAsyncComponent(() => import('@/views/Profile/index.vue')),
 });
@@ -830,6 +847,78 @@ function handleCloseAllTabs() {
         }
       }
     }
+    
+    // 子菜单样式
+    :deep(.el-sub-menu) {
+      .el-sub-menu__title {
+        height: 44px;
+        line-height: 44px;
+        margin-bottom: 4px;
+        border-radius: $radius-base;
+        color: $sidebar-text-color;
+        font-weight: 500;
+        overflow: hidden;
+        position: relative;
+        display: flex;
+        align-items: center;
+        padding: 0 16px !important;
+        transition: padding 0.35s ease, background 0.2s ease, color 0.2s ease;
+        
+        .el-icon {
+          font-size: 18px;
+          width: 18px;
+          height: 18px;
+          min-width: 18px;
+          flex-shrink: 0;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+        
+        .menu-title {
+          white-space: nowrap;
+          overflow: hidden;
+          margin-left: 12px;
+          opacity: 1;
+          display: inline-block;
+          max-width: 150px;
+          transition: opacity 0.2s ease 0.1s, margin-left 0.35s ease, max-width 0.35s ease;
+        }
+        
+        &:hover {
+          background: $bg-base !important;
+          color: $primary-color;
+        }
+      }
+      
+      &.is-active > .el-sub-menu__title {
+        color: $primary-color !important;
+      }
+      
+      .el-menu {
+        background: transparent !important;
+        
+        .el-menu-item {
+          height: 40px;
+          line-height: 40px;
+          padding-left: 48px !important;
+          margin-bottom: 2px;
+          
+          .sub-menu-title {
+            font-size: 13px;
+          }
+          
+          &.is-active {
+            background: $primary-color-light !important;
+            color: $primary-color !important;
+            
+            &::before {
+              display: none;
+            }
+          }
+        }
+      }
+    }
   }
   
   // 折叠状态
@@ -859,6 +948,24 @@ function handleCloseAllTabs() {
           transition: opacity 0.15s ease, margin-left 0.35s ease, max-width 0.35s ease;
         }
       }
+      
+      :deep(.el-sub-menu) {
+        .el-sub-menu__title {
+          padding: 0 15px !important;
+          justify-content: center;
+          
+          .menu-title {
+            opacity: 0;
+            margin-left: 0;
+            max-width: 0;
+            transition: opacity 0.15s ease, margin-left 0.35s ease, max-width 0.35s ease;
+          }
+          
+          .el-sub-menu__icon-arrow {
+            display: none;
+          }
+        }
+      }
     }
   }
 }

+ 23 - 1
client/src/router/index.ts

@@ -60,7 +60,29 @@ const routes: RouteRecordRaw[] = [
       {
         path: 'analytics',
         name: 'Analytics',
-        component: () => import('@/views/Analytics/index.vue'),
+        redirect: '/analytics/overview',
+        children: [
+          {
+            path: 'overview',
+            name: 'AnalyticsOverview',
+            component: () => import('@/views/Analytics/Overview/index.vue'),
+          },
+          {
+            path: 'platform',
+            name: 'AnalyticsPlatform',
+            component: () => import('@/views/Analytics/Platform/index.vue'),
+          },
+          {
+            path: 'account',
+            name: 'AnalyticsAccount',
+            component: () => import('@/views/Analytics/Account/index.vue'),
+          },
+          {
+            path: 'work',
+            name: 'AnalyticsWork',
+            component: () => import('@/views/Analytics/Work/index.vue'),
+          },
+        ],
       },
       {
         path: 'settings',

+ 4 - 1
client/src/stores/tabs.ts

@@ -37,7 +37,10 @@ export const PAGE_CONFIG: Record<string, { title: string; icon: string }> = {
   '/publish': { title: '发布管理', icon: 'Upload' },
   '/comments': { title: '评论管理', icon: 'ChatDotRound' },
   '/schedule': { title: '定时任务', icon: 'Clock' },
-  '/analytics': { title: '数据分析', icon: 'TrendCharts' },
+  '/analytics/overview': { title: '数据总览', icon: 'TrendCharts' },
+  '/analytics/platform': { title: '平台数据', icon: 'TrendCharts' },
+  '/analytics/account': { title: '账号数据', icon: 'TrendCharts' },
+  '/analytics/work': { title: '作品数据', icon: 'TrendCharts' },
   '/settings': { title: '系统设置', icon: 'Setting' },
   '/profile': { title: '个人中心', icon: 'User' },
 };

+ 34 - 0
client/src/styles/index.scss

@@ -135,3 +135,37 @@ html, body {
 .el-table__row:hover > td.el-table__cell {
   background: $primary-color-light !important;
 }
+
+// 侧边栏折叠后的弹出菜单样式
+.el-menu--vertical.el-menu--popup-container {
+  .el-menu--popup {
+    background: #fff !important;
+    border-radius: $radius-lg;
+    box-shadow: $shadow-lg;
+    padding: 6px;
+    min-width: 140px;
+    
+    .el-menu-item {
+      height: 36px;
+      line-height: 36px;
+      border-radius: $radius-base;
+      margin-bottom: 2px;
+      padding: 0 16px !important;
+      color: $text-regular;
+      
+      &:hover {
+        background: $primary-color-light !important;
+        color: $primary-color;
+      }
+      
+      &.is-active {
+        background: $primary-color-light !important;
+        color: $primary-color !important;
+      }
+      
+      .sub-menu-title {
+        font-size: 13px;
+      }
+    }
+  }
+}

+ 794 - 0
client/src/views/Analytics/Account/index.vue

@@ -0,0 +1,794 @@
+<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"
+        />
+        <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"
+        />
+        <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">
+          <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">
+          <el-option label="全部平台" value="" />
+          <el-option 
+            v-for="platform in availablePlatforms" 
+            :key="platform.value" 
+            :label="platform.label" 
+            :value="platform.value"
+          />
+        </el-select>
+        <el-button type="primary" @click="handleQuery">查询</el-button>
+      </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"
+        @clear="handleQuery"
+        @keyup.enter="handleQuery"
+      >
+        <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="50%">
+      <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-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>
+        </div>
+        
+        <!-- 趋势图 -->
+        <div class="detail-chart">
+          <h4>数据趋势</h4>
+          <div ref="accountChartRef" style="height: 300px" v-loading="chartLoading"></div>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import { Search, User, Coin, Pointer, 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();
+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 Object.entries(PLATFORMS).map(([key, value]) => ({
+    value: key as PlatformType,
+    label: value.name,
+  }));
+});
+
+// 平台图标映射
+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',
+};
+
+// 汇总统计
+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} - 数据详情`;
+});
+
+function getPlatformName(platform: PlatformType) {
+  return PLATFORMS[platform]?.name || platform;
+}
+
+function getPlatformIcon(platform: PlatformType) {
+  return platformIcons[platform] || '';
+}
+
+function formatNumber(num: number) {
+  if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
+  return num.toString();
+}
+
+function formatTime(time: string) {
+  if (!time) return '-';
+  return dayjs(time).format('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':
+      startDate.value = today.subtract(3, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+    case 'last7days':
+      startDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+    case 'lastMonth':
+      startDate.value = today.subtract(30, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+  }
+}
+
+// 查询
+function handleQuery() {
+  loadData();
+}
+
+// 加载分组列表
+async function loadGroups() {
+  try {
+    const res = await request.get('/accounts/groups');
+    if (res.data.success) {
+      accountGroups.value = res.data.data || [];
+    }
+  } catch (error) {
+    console.error('加载分组失败:', error);
+  }
+}
+
+// 加载数据
+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,
+    });
+    
+    if (selectedPlatform.value) {
+      queryParams.append('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;
+      }
+    }
+  } catch (error) {
+    console.error('加载账号数据失败:', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 查看详情
+async function handleDetail(row: AccountData) {
+  selectedAccount.value = row;
+  drawerVisible.value = true;
+  
+  await nextTick();
+  loadAccountTrend(row.id);
+}
+
+// 加载账号趋势数据
+async function loadAccountTrend(accountId: number) {
+  if (!accountChartRef.value) return;
+  
+  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 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);
+    }
+  } catch (error) {
+    console.error('加载账号趋势失败:', error);
+  } finally {
+    chartLoading.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 },
+    ],
+  });
+}
+
+// 导出数据
+function handleExport() {
+  ElMessage.info('导出功能开发中');
+}
+
+// 监听抽屉关闭
+watch(drawerVisible, (visible) => {
+  if (!visible && accountChart) {
+    accountChart.dispose();
+    accountChart = null;
+  }
+});
+
+onMounted(() => {
+  // 默认选择昨天
+  handleQuickDate('yesterday');
+  loadGroups();
+  loadData();
+});
+</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-stats {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 16px;
+    margin-bottom: 24px;
+    
+    .stat-item {
+      background: #f8fafc;
+      border-radius: 12px;
+      padding: 16px;
+      text-align: center;
+      
+      .stat-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: $primary-color;
+      }
+      
+      .stat-label {
+        font-size: 13px;
+        color: $text-secondary;
+        margin-top: 4px;
+      }
+    }
+  }
+  
+  .detail-chart {
+    h4 {
+      margin: 0 0 16px 0;
+      font-size: 15px;
+      color: $text-primary;
+    }
+  }
+}
+
+@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>

+ 560 - 0
client/src/views/Analytics/Overview/index.vue

@@ -0,0 +1,560 @@
+<template>
+  <div class="analytics-overview">
+    <!-- 顶部筛选栏 -->
+    <div class="filter-bar">
+      <div class="filter-left">
+        <el-select v-model="selectedGroup" placeholder="全部分组" clearable style="width: 140px">
+          <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: 140px">
+          <el-option label="全部平台" value="" />
+          <el-option 
+            v-for="platform in availablePlatforms" 
+            :key="platform.value" 
+            :label="platform.label" 
+            :value="platform.value"
+          />
+        </el-select>
+        <el-button type="primary" :loading="refreshing" @click="handleRefreshAll">
+          刷新数据
+        </el-button>
+        <span class="filter-tip">
+          提示:昨日数据一般平台会在中午12点到下午2点更新,建议2点后查询
+        </span>
+      </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-label">{{ item.label }}</div>
+        <div class="stat-value" :class="{ 'highlight': item.highlight }">{{ item.value }}</div>
+      </div>
+    </div>
+    
+    <!-- 搜索框 -->
+    <div class="search-bar">
+      <el-input 
+        v-model="searchKeyword" 
+        placeholder="请输入要搜索的账号" 
+        clearable
+        style="width: 300px"
+        @clear="loadData"
+        @keyup.enter="loadData"
+      >
+        <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="180" fixed="left">
+          <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="100" align="center">
+          <template #default="{ row }">
+            <el-tag size="small" :type="getPlatformTagType(row.platform)">
+              {{ getPlatformName(row.platform) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalIncome" label="总收益" width="90" align="center">
+          <template #default="{ row }">
+            <span>{{ row.totalIncome || '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yesterdayIncome" label="昨日收益" width="90" align="center">
+          <template #default="{ row }">
+            <span>{{ row.yesterdayIncome || '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalViews" label="总播放(阅读)" width="110" align="center">
+          <template #default="{ row }">
+            <span>{{ row.totalViews || '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yesterdayViews" label="昨日播放(阅读)" width="120" align="center">
+          <template #default="{ row }">
+            <span>{{ row.yesterdayViews || '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="fansCount" label="粉丝数" width="90" align="center">
+          <template #default="{ row }">
+            <span class="fans-count">{{ formatNumber(row.fansCount || 0) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yesterdayComments" label="昨日评论" width="90" align="center">
+          <template #default="{ row }">
+            <span>{{ row.yesterdayComments ?? 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yesterdayLikes" label="昨日点赞" width="90" align="center">
+          <template #default="{ row }">
+            <span>{{ row.yesterdayLikes ?? 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="yesterdayFansIncrease" label="昨日涨粉" width="90" align="center">
+          <template #default="{ row }">
+            <span :class="{ 'increase': row.yesterdayFansIncrease > 0, 'decrease': row.yesterdayFansIncrease < 0 }">
+              {{ row.yesterdayFansIncrease ?? 0 }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updateTime" label="更新时间" width="140" 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="100" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button 
+              type="primary" 
+              link 
+              :loading="row.refreshing"
+              @click="handleRefreshAccount(row)"
+            >
+              刷新数据
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { Search } 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();
+const loading = ref(false);
+const refreshing = ref(false);
+
+// 筛选条件
+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 Object.entries(PLATFORMS).map(([key, value]) => ({
+    value: key as PlatformType,
+    label: value.name,
+  }));
+});
+
+// 账号数据
+interface AccountData {
+  id: number;
+  nickname: string;
+  username: string;
+  avatarUrl: string;
+  platform: PlatformType;
+  groupId?: number;
+  fansCount: number;
+  totalIncome: number | null;
+  yesterdayIncome: number | null;
+  totalViews: number | null;
+  yesterdayViews: number | null;
+  yesterdayComments: number;
+  yesterdayLikes: number;
+  yesterdayFansIncrease: number;
+  updateTime: string;
+  status: string;
+  refreshing?: boolean;
+}
+
+const accounts = ref<AccountData[]>([]);
+
+// 汇总统计
+interface SummaryData {
+  totalAccounts: number;
+  totalIncome: number;
+  yesterdayIncome: number;
+  totalViews: number;
+  yesterdayViews: number;
+  totalFans: number;
+  yesterdayComments: number;
+  yesterdayLikes: number;
+  yesterdayFansIncrease: number;
+}
+
+const summaryData = ref<SummaryData>({
+  totalAccounts: 0,
+  totalIncome: 0,
+  yesterdayIncome: 0,
+  totalViews: 0,
+  yesterdayViews: 0,
+  totalFans: 0,
+  yesterdayComments: 0,
+  yesterdayLikes: 0,
+  yesterdayFansIncrease: 0,
+});
+
+// 统计卡片数据
+const summaryStats = computed(() => [
+  { label: '账号总数', value: summaryData.value.totalAccounts },
+  { label: '总收益(元)', value: summaryData.value.totalIncome },
+  { label: '昨日收益(元)', value: summaryData.value.yesterdayIncome },
+  { label: '总播放(阅读)', value: summaryData.value.totalViews },
+  { label: '昨日播放(阅读)', value: summaryData.value.yesterdayViews },
+  { label: '总粉丝', value: summaryData.value.totalFans, highlight: true },
+  { label: '昨日评论', value: summaryData.value.yesterdayComments },
+  { label: '昨日点赞', value: summaryData.value.yesterdayLikes },
+  { label: '昨日涨粉', value: summaryData.value.yesterdayFansIncrease },
+]);
+
+// 过滤后的账号列表
+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;
+});
+
+function getPlatformName(platform: PlatformType) {
+  return PLATFORMS[platform]?.name || platform;
+}
+
+function getPlatformTagType(platform: PlatformType) {
+  const typeMap: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'> = {
+    douyin: 'danger',
+    xiaohongshu: 'danger',
+    bilibili: 'primary',
+    kuaishou: 'warning',
+    weixin: 'success',
+  };
+  return typeMap[platform] || 'info';
+}
+
+function formatNumber(num: number) {
+  if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
+  return num.toString();
+}
+
+function formatTime(time: string) {
+  if (!time) return '-';
+  return dayjs(time).format('MM-DD HH:mm');
+}
+
+// 加载分组列表
+async function loadGroups() {
+  try {
+    const res = await request.get('/accounts/groups');
+    if (res.data.success) {
+      accountGroups.value = res.data.data || [];
+    }
+  } catch (error) {
+    console.error('加载分组失败:', error);
+  }
+}
+
+// 加载数据
+async function loadData() {
+  const userId = authStore.user?.id;
+  if (!userId) return;
+  
+  loading.value = true;
+  
+  try {
+    const queryParams = new URLSearchParams({
+      user_id: userId.toString(),
+    });
+    
+    const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/overview?${queryParams}`);
+    const result = await response.json();
+    
+    if (result.success && result.data) {
+      accounts.value = result.data.accounts || [];
+      summaryData.value = result.data.summary || summaryData.value;
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 刷新所有数据
+async function handleRefreshAll() {
+  refreshing.value = true;
+  try {
+    await loadData();
+    ElMessage.success('数据刷新成功');
+  } finally {
+    refreshing.value = false;
+  }
+}
+
+// 刷新单个账号数据
+async function handleRefreshAccount(account: AccountData) {
+  account.refreshing = true;
+  try {
+    const userId = authStore.user?.id;
+    if (!userId) return;
+    
+    const queryParams = new URLSearchParams({
+      user_id: userId.toString(),
+      account_id: account.id.toString(),
+    });
+    
+    const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/refresh_account?${queryParams}`, {
+      method: 'POST',
+    });
+    const result = await response.json();
+    
+    if (result.success) {
+      // 更新账号数据
+      Object.assign(account, result.data);
+      ElMessage.success('账号数据刷新成功');
+    } else {
+      ElMessage.error(result.error || '刷新失败');
+    }
+  } catch (error) {
+    ElMessage.error('刷新失败');
+  } finally {
+    account.refreshing = false;
+  }
+}
+
+// 导出数据
+function handleExport() {
+  ElMessage.info('导出功能开发中');
+}
+
+onMounted(() => {
+  loadGroups();
+  loadData();
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/variables.scss' as *;
+
+.analytics-overview {
+  .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-tip {
+        font-size: 12px;
+        color: #f56c6c;
+        margin-left: 8px;
+      }
+    }
+  }
+  
+  .stats-row {
+    display: grid;
+    grid-template-columns: repeat(9, 1fr);
+    gap: 0;
+    margin-bottom: 20px;
+    background: #fff;
+    border-radius: $radius-lg;
+    box-shadow: $shadow-sm;
+    overflow: hidden;
+    
+    .stat-card {
+      padding: 20px 16px;
+      text-align: center;
+      border-right: 1px solid #f0f0f0;
+      
+      &:last-child {
+        border-right: none;
+      }
+      
+      .stat-label {
+        font-size: 12px;
+        color: $text-secondary;
+        margin-bottom: 8px;
+        white-space: nowrap;
+      }
+      
+      .stat-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: $text-primary;
+        
+        &.highlight {
+          color: $primary-color;
+        }
+      }
+    }
+  }
+  
+  .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;
+      }
+    }
+    
+    .fans-count {
+      color: $primary-color;
+      font-weight: 500;
+    }
+    
+    .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);
+      }
+    }
+  }
+}
+
+@media (max-width: 1600px) {
+  .analytics-overview {
+    .stats-row {
+      grid-template-columns: repeat(5, 1fr);
+      
+      .stat-card {
+        &:nth-child(5) {
+          border-right: none;
+        }
+        
+        &:nth-child(n+6) {
+          border-top: 1px solid #f0f0f0;
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 1200px) {
+  .analytics-overview {
+    .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;
+        }
+      }
+    }
+    
+    .filter-bar {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+      
+      .filter-left {
+        flex-wrap: wrap;
+        
+        .filter-tip {
+          width: 100%;
+          margin-left: 0;
+          margin-top: 8px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 536 - 0
client/src/views/Analytics/Platform/index.vue

@@ -0,0 +1,536 @@
+<template>
+  <div class="platform-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"
+        />
+        <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"
+        />
+        <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-button type="primary" @click="handleQuery">查询</el-button>
+      </div>
+      <div class="filter-right">
+        <el-button @click="handleExport">导出数据</el-button>
+      </div>
+    </div>
+    
+    <!-- 数据表格 -->
+    <div class="data-table">
+      <el-table :data="platformData" v-loading="loading" stripe>
+        <el-table-column label="平台" min-width="150">
+          <template #default="{ row }">
+            <div class="platform-cell">
+              <img :src="getPlatformIcon(row.platform)" class="platform-icon" :alt="row.platform" />
+              <span class="platform-name">{{ getPlatformName(row.platform) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="income" label="收益" width="120" align="center">
+          <template #default="{ row }">
+            <span>{{ row.income ?? 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="recommendCount" label="推荐量" width="120" align="center">
+          <template #default="{ row }">
+            <span>{{ row.recommendCount ?? '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="viewsCount" label="阅读(播放)量" width="140" align="center">
+          <template #default="{ row }">
+            <span>{{ row.viewsCount ?? '未支持' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="commentsCount" label="评论量" width="100" align="center">
+          <template #default="{ row }">
+            <span>{{ row.commentsCount ?? 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="likesCount" label="点赞量" width="100" align="center">
+          <template #default="{ row }">
+            <span>{{ row.likesCount ?? 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="fansIncrease" label="涨粉量" width="100" 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="140" align="center">
+          <template #default="{ row }">
+            <span class="update-time">{{ formatTime(row.updateTime) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="100" 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="60%">
+      <div v-if="selectedPlatform" class="platform-detail">
+        <!-- 统计概览 -->
+        <div class="detail-stats">
+          <div class="stat-item">
+            <div class="stat-value">{{ formatNumber(selectedPlatform.fansCount || 0) }}</div>
+            <div class="stat-label">总粉丝</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ formatNumber(selectedPlatform.viewsCount || 0) }}</div>
+            <div class="stat-label">总播放</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ formatNumber(selectedPlatform.likesCount || 0) }}</div>
+            <div class="stat-label">总点赞</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ formatNumber(selectedPlatform.commentsCount || 0) }}</div>
+            <div class="stat-label">总评论</div>
+          </div>
+        </div>
+        
+        <!-- 趋势图 -->
+        <div class="detail-chart">
+          <h4>数据趋势</h4>
+          <div ref="detailChartRef" style="height: 300px" v-loading="chartLoading"></div>
+        </div>
+        
+        <!-- 该平台账号列表 -->
+        <div class="detail-accounts">
+          <h4>平台账号</h4>
+          <el-table :data="platformAccounts" size="small">
+            <el-table-column label="账号" min-width="150">
+              <template #default="{ row }">
+                <div class="account-cell">
+                  <el-avatar :size="28" :src="row.avatarUrl">{{ row.nickname?.[0] }}</el-avatar>
+                  <span>{{ row.nickname || row.username }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="fansCount" label="粉丝" width="100" align="center">
+              <template #default="{ row }">{{ formatNumber(row.fansCount || 0) }}</template>
+            </el-table-column>
+            <el-table-column prop="viewsCount" label="播放" width="100" align="center">
+              <template #default="{ row }">{{ formatNumber(row.viewsCount || 0) }}</template>
+            </el-table-column>
+            <el-table-column prop="likesCount" label="点赞" width="100" align="center">
+              <template #default="{ row }">{{ formatNumber(row.likesCount || 0) }}</template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue';
+import * as echarts from 'echarts';
+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';
+
+const PYTHON_API_URL = 'http://localhost:5005';
+
+const authStore = useAuthStore();
+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 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',
+};
+
+// 平台数据
+interface PlatformData {
+  platform: PlatformType;
+  income: number | null;
+  recommendCount: number | null;
+  viewsCount: number | null;
+  commentsCount: number;
+  likesCount: number;
+  fansIncrease: number;
+  fansCount: number;
+  updateTime: string;
+}
+
+const platformData = ref<PlatformData[]>([]);
+
+// 抽屉相关
+const drawerVisible = ref(false);
+const selectedPlatform = ref<PlatformData | null>(null);
+const platformAccounts = ref<any[]>([]);
+const detailChartRef = ref<HTMLElement>();
+let detailChart: echarts.ECharts | null = null;
+
+const drawerTitle = computed(() => {
+  if (!selectedPlatform.value) return '平台详情';
+  return `${getPlatformName(selectedPlatform.value.platform)} - 数据详情`;
+});
+
+function getPlatformName(platform: PlatformType) {
+  return PLATFORMS[platform]?.name || platform;
+}
+
+function getPlatformIcon(platform: PlatformType) {
+  return platformIcons[platform] || '';
+}
+
+function formatNumber(num: number) {
+  if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
+  return num.toString();
+}
+
+function formatTime(time: string) {
+  if (!time) return '-';
+  return dayjs(time).format('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':
+      startDate.value = today.subtract(3, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+    case 'last7days':
+      startDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+    case 'lastMonth':
+      startDate.value = today.subtract(30, 'day').format('YYYY-MM-DD');
+      endDate.value = today.subtract(1, 'day').format('YYYY-MM-DD');
+      break;
+  }
+}
+
+// 查询
+function handleQuery() {
+  loadData();
+}
+
+// 加载数据
+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 response = await fetch(`${PYTHON_API_URL}/work_day_statistics/platforms?${queryParams}`);
+    const result = await response.json();
+    
+    if (result.success && result.data) {
+      platformData.value = result.data;
+    }
+  } catch (error) {
+    console.error('加载平台数据失败:', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 查看详情
+async function handleDetail(row: PlatformData) {
+  selectedPlatform.value = row;
+  drawerVisible.value = true;
+  
+  await nextTick();
+  loadPlatformDetail(row.platform);
+}
+
+// 加载平台详情
+async function loadPlatformDetail(platform: PlatformType) {
+  const userId = authStore.user?.id;
+  if (!userId) return;
+  
+  chartLoading.value = true;
+  
+  try {
+    const queryParams = new URLSearchParams({
+      user_id: userId.toString(),
+      platform: platform,
+      start_date: startDate.value,
+      end_date: endDate.value,
+    });
+    
+    const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/platform_detail?${queryParams}`);
+    const result = await response.json();
+    
+    if (result.success && result.data) {
+      platformAccounts.value = result.data.accounts || [];
+      
+      // 更新图表
+      if (result.data.trend) {
+        updateDetailChart(result.data.trend);
+      }
+    }
+  } catch (error) {
+    console.error('加载平台详情失败:', error);
+  } finally {
+    chartLoading.value = false;
+  }
+}
+
+// 更新详情图表
+function updateDetailChart(trendData: { dates: string[]; fans: number[]; views: number[]; likes: number[] }) {
+  if (!detailChartRef.value) return;
+  
+  if (!detailChart) {
+    detailChart = echarts.init(detailChartRef.value);
+  }
+  
+  detailChart.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 },
+    ],
+  });
+}
+
+// 导出数据
+function handleExport() {
+  ElMessage.info('导出功能开发中');
+}
+
+// 监听抽屉关闭
+watch(drawerVisible, (visible) => {
+  if (!visible && detailChart) {
+    detailChart.dispose();
+    detailChart = null;
+  }
+});
+
+onMounted(() => {
+  // 默认选择昨天
+  handleQuickDate('yesterday');
+  loadData();
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/variables.scss' as *;
+
+.platform-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;
+        margin-left: 8px;
+      }
+    }
+  }
+  
+  .data-table {
+    background: #fff;
+    border-radius: $radius-lg;
+    box-shadow: $shadow-sm;
+    overflow: hidden;
+    
+    .platform-cell {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .platform-icon {
+        width: 24px;
+        height: 24px;
+        border-radius: 4px;
+      }
+      
+      .platform-name {
+        font-weight: 500;
+        color: $text-primary;
+      }
+    }
+    
+    .increase {
+      color: #10b981;
+    }
+    
+    .decrease {
+      color: #ef4444;
+    }
+    
+    .update-time {
+      font-size: 12px;
+      color: $text-secondary;
+    }
+  }
+}
+
+.platform-detail {
+  .detail-stats {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 16px;
+    margin-bottom: 24px;
+    
+    .stat-item {
+      background: #f8fafc;
+      border-radius: 12px;
+      padding: 16px;
+      text-align: center;
+      
+      .stat-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: $primary-color;
+      }
+      
+      .stat-label {
+        font-size: 13px;
+        color: $text-secondary;
+        margin-top: 4px;
+      }
+    }
+  }
+  
+  .detail-chart {
+    margin-bottom: 24px;
+    
+    h4 {
+      margin: 0 0 16px 0;
+      font-size: 15px;
+      color: $text-primary;
+    }
+  }
+  
+  .detail-accounts {
+    h4 {
+      margin: 0 0 16px 0;
+      font-size: 15px;
+      color: $text-primary;
+    }
+    
+    .account-cell {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+  }
+}
+
+@media (max-width: 1200px) {
+  .platform-analytics {
+    .filter-bar {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+      
+      .filter-left {
+        flex-wrap: wrap;
+        
+        .quick-btns {
+          width: 100%;
+          margin-left: 0;
+          margin-top: 8px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 759 - 0
client/src/views/Analytics/Work/index.vue

@@ -0,0 +1,759 @@
+<template>
+  <div class="work-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"
+        />
+        <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"
+        />
+        <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="selectedAccounts" 
+          multiple 
+          collapse-tags 
+          collapse-tags-tooltip
+          placeholder="选择账号" 
+          style="width: 160px"
+        >
+          <el-option 
+            v-for="account in accountList" 
+            :key="account.id" 
+            :label="account.nickname" 
+            :value="account.id"
+          />
+        </el-select>
+        <el-button type="primary" @click="handleQuery">查询</el-button>
+      </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="filter-bar secondary">
+      <div class="filter-left">
+        <el-select v-model="selectedGroup" placeholder="全部" clearable style="width: 120px">
+          <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">
+          <el-option label="全部平台" value="" />
+          <el-option 
+            v-for="platform in availablePlatforms" 
+            :key="platform.value" 
+            :label="platform.label" 
+            :value="platform.value"
+          />
+        </el-select>
+        <el-select v-model="sortBy" style="width: 160px">
+          <el-option label="按发布时间顺序排列" value="publish_desc" />
+          <el-option label="按发布时间倒序排列" value="publish_asc" />
+          <el-option label="按阅读量排序" value="views_desc" />
+          <el-option label="按点赞量排序" value="likes_desc" />
+          <el-option label="按评论量排序" value="comments_desc" />
+        </el-select>
+        <el-input 
+          v-model="searchKeyword" 
+          placeholder="请输入要搜索的作品标题" 
+          clearable
+          style="width: 240px"
+          @clear="handleQuery"
+          @keyup.enter="handleQuery"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+    
+    <!-- 数据表格 -->
+    <div class="data-table">
+      <el-table :data="workList" v-loading="loading" stripe>
+        <el-table-column label="账号" width="150">
+          <template #default="{ row }">
+            <div class="account-cell">
+              <el-avatar :size="32" :src="row.accountAvatar">
+                {{ row.accountName?.[0] }}
+              </el-avatar>
+              <span class="account-name">{{ row.accountName }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="平台" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag size="small" :type="getPlatformTagType(row.platform)">
+              {{ getPlatformName(row.platform) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="标题" min-width="300">
+          <template #default="{ row }">
+            <div class="title-cell">
+              <div class="work-title">{{ row.title }}</div>
+              <div class="work-stats">
+                <span class="stat-item">推荐 <em>{{ row.recommendCount ?? '--' }}</em></span>
+                <span class="stat-item">阅读 <em>{{ row.viewsCount ?? 0 }}</em></span>
+                <span class="stat-item">评论 <em>{{ row.commentsCount ?? 0 }}</em></span>
+                <span class="stat-item">分享 <em>{{ row.sharesCount ?? 0 }}</em></span>
+                <span class="stat-item">收藏 <em>{{ row.collectsCount ?? 0 }}</em></span>
+                <span class="stat-item">点赞 <em>{{ row.likesCount ?? 0 }}</em></span>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="workType" label="类型" width="80" align="center">
+          <template #default="{ row }">
+            <span>{{ row.workType || '动态' }}</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-column label="操作" width="80" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleView(row)">
+              查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <!-- 分页 -->
+      <div class="pagination-wrapper">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :total="totalWorks"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleQuery"
+          @current-change="handleQuery"
+        />
+      </div>
+    </div>
+    
+    <!-- 作品详情抽屉 -->
+    <el-drawer v-model="drawerVisible" title="作品详情" size="50%">
+      <div v-if="selectedWork" class="work-detail">
+        <!-- 作品基本信息 -->
+        <div class="detail-header">
+          <el-image 
+            :src="selectedWork.coverUrl" 
+            class="work-cover"
+            fit="cover"
+          >
+            <template #error>
+              <div class="cover-placeholder">
+                <el-icon :size="32"><Picture /></el-icon>
+              </div>
+            </template>
+          </el-image>
+          <div class="header-info">
+            <h3>{{ selectedWork.title }}</h3>
+            <div class="meta-info">
+              <el-tag size="small">{{ getPlatformName(selectedWork.platform) }}</el-tag>
+              <span class="publish-time">发布于 {{ formatTime(selectedWork.publishTime) }}</span>
+            </div>
+          </div>
+        </div>
+        
+        <!-- 数据统计 -->
+        <div class="detail-stats">
+          <div class="stat-item">
+            <div class="stat-value">{{ selectedWork.viewsCount || 0 }}</div>
+            <div class="stat-label">阅读</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ selectedWork.likesCount || 0 }}</div>
+            <div class="stat-label">点赞</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ selectedWork.commentsCount || 0 }}</div>
+            <div class="stat-label">评论</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ selectedWork.collectsCount || 0 }}</div>
+            <div class="stat-label">收藏</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ selectedWork.sharesCount || 0 }}</div>
+            <div class="stat-label">分享</div>
+          </div>
+        </div>
+        
+        <!-- 作品内容 -->
+        <div class="detail-content" v-if="selectedWork.content">
+          <h4>作品内容</h4>
+          <div class="content-text">{{ selectedWork.content }}</div>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { Search, Picture, Document, View, ChatDotRound, Share, Star, Pointer } 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();
+const loading = ref(false);
+
+// 日期筛选
+const startDate = ref(dayjs().subtract(30, 'day').format('YYYY-MM-DD'));
+const endDate = ref(dayjs().format('YYYY-MM-DD'));
+const activeQuickBtn = ref('lastMonth');
+
+// 快捷日期按钮
+const quickDateBtns = [
+  { label: '近三天', value: 'last3days' },
+  { label: '近七天', value: 'last7days' },
+  { label: '近一个月', value: 'lastMonth' },
+];
+
+// 账号选择
+const selectedAccounts = ref<number[]>([]);
+const accountList = ref<{ id: number; nickname: string }[]>([]);
+
+// 分组和平台筛选
+const selectedGroup = ref<number | ''>('');
+const selectedPlatform = ref<PlatformType | ''>('');
+const sortBy = ref('publish_desc');
+const searchKeyword = ref('');
+
+// 分组列表
+interface AccountGroup {
+  id: number;
+  name: string;
+}
+const accountGroups = ref<AccountGroup[]>([]);
+
+// 可用平台
+const availablePlatforms = computed(() => {
+  return Object.entries(PLATFORMS).map(([key, value]) => ({
+    value: key as PlatformType,
+    label: value.name,
+  }));
+});
+
+// 分页
+const currentPage = ref(1);
+const pageSize = ref(20);
+const totalWorks = ref(0);
+
+// 汇总统计
+const summaryData = ref({
+  totalWorks: 0,
+  recommendCount: 0,
+  viewsCount: 0,
+  commentsCount: 0,
+  sharesCount: 0,
+  collectsCount: 0,
+  likesCount: 0,
+});
+
+// 统计卡片数据
+const summaryStats = computed(() => [
+  { label: '作品总数', value: summaryData.value.totalWorks, icon: Document },
+  { 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.sharesCount, icon: Share },
+  { label: '收藏量', value: summaryData.value.collectsCount, icon: Star },
+  { label: '点赞量', value: summaryData.value.likesCount, icon: Pointer },
+]);
+
+// 作品数据
+interface WorkData {
+  id: number;
+  title: string;
+  coverUrl: string;
+  content: string;
+  platform: PlatformType;
+  accountId: number;
+  accountName: string;
+  accountAvatar: string;
+  workType: string;
+  publishTime: string;
+  recommendCount: number | null;
+  viewsCount: number;
+  commentsCount: number;
+  sharesCount: number;
+  collectsCount: number;
+  likesCount: number;
+}
+
+const workList = ref<WorkData[]>([]);
+
+// 抽屉相关
+const drawerVisible = ref(false);
+const selectedWork = ref<WorkData | null>(null);
+
+function getPlatformName(platform: PlatformType) {
+  return PLATFORMS[platform]?.name || platform;
+}
+
+function getPlatformTagType(platform: PlatformType) {
+  const typeMap: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'> = {
+    douyin: 'danger',
+    xiaohongshu: 'danger',
+    bilibili: 'primary',
+    kuaishou: 'warning',
+    weixin: 'success',
+  };
+  return typeMap[platform] || 'info';
+}
+
+function formatTime(time: string) {
+  if (!time) return '-';
+  return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+}
+
+// 快捷日期选择
+function handleQuickDate(type: string) {
+  activeQuickBtn.value = type;
+  const today = dayjs();
+  
+  switch (type) {
+    case 'last3days':
+      startDate.value = today.subtract(3, 'day').format('YYYY-MM-DD');
+      endDate.value = today.format('YYYY-MM-DD');
+      break;
+    case 'last7days':
+      startDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
+      endDate.value = today.format('YYYY-MM-DD');
+      break;
+    case 'lastMonth':
+      startDate.value = today.subtract(30, 'day').format('YYYY-MM-DD');
+      endDate.value = today.format('YYYY-MM-DD');
+      break;
+  }
+}
+
+// 查询
+function handleQuery() {
+  loadData();
+}
+
+// 加载账号列表
+async function loadAccountList() {
+  try {
+    const res = await request.get('/accounts');
+    if (res.data.success) {
+      accountList.value = (res.data.data || []).map((a: any) => ({
+        id: a.id,
+        nickname: a.nickname || a.username,
+      }));
+    }
+  } catch (error) {
+    console.error('加载账号列表失败:', error);
+  }
+}
+
+// 加载分组列表
+async function loadGroups() {
+  try {
+    const res = await request.get('/accounts/groups');
+    if (res.data.success) {
+      accountGroups.value = res.data.data || [];
+    }
+  } catch (error) {
+    console.error('加载分组失败:', error);
+  }
+}
+
+// 加载数据
+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,
+      page: currentPage.value.toString(),
+      page_size: pageSize.value.toString(),
+      sort_by: sortBy.value,
+    });
+    
+    if (selectedPlatform.value) {
+      queryParams.append('platform', selectedPlatform.value);
+    }
+    
+    if (selectedAccounts.value.length > 0) {
+      queryParams.append('account_ids', selectedAccounts.value.join(','));
+    }
+    
+    if (searchKeyword.value) {
+      queryParams.append('keyword', searchKeyword.value);
+    }
+    
+    const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/works?${queryParams}`);
+    const result = await response.json();
+    
+    if (result.success && result.data) {
+      workList.value = result.data.works || [];
+      totalWorks.value = result.data.total || 0;
+      
+      if (result.data.summary) {
+        summaryData.value = result.data.summary;
+      }
+    }
+  } catch (error) {
+    console.error('加载作品数据失败:', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 查看详情
+function handleView(row: WorkData) {
+  selectedWork.value = row;
+  drawerVisible.value = true;
+}
+
+// 导出数据
+function handleExport() {
+  ElMessage.info('导出功能开发中');
+}
+
+onMounted(() => {
+  loadAccountList();
+  loadGroups();
+  loadData();
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/variables.scss' as *;
+
+.work-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;
+    
+    &.secondary {
+      margin-bottom: 16px;
+      padding: 12px 20px;
+    }
+    
+    .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;
+        }
+      }
+    }
+  }
+  
+  .data-table {
+    background: #fff;
+    border-radius: $radius-lg;
+    box-shadow: $shadow-sm;
+    overflow: hidden;
+    
+    .account-cell {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      
+      .account-name {
+        font-weight: 500;
+        color: $text-primary;
+      }
+    }
+    
+    .title-cell {
+      .work-title {
+        font-weight: 500;
+        color: $primary-color;
+        margin-bottom: 6px;
+        cursor: pointer;
+        
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+      
+      .work-stats {
+        display: flex;
+        gap: 16px;
+        font-size: 12px;
+        color: $text-secondary;
+        
+        .stat-item {
+          em {
+            font-style: normal;
+            color: $primary-color;
+            margin-left: 2px;
+          }
+        }
+      }
+    }
+    
+    .publish-time {
+      font-size: 13px;
+      color: $text-secondary;
+    }
+    
+    .pagination-wrapper {
+      padding: 16px 20px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+
+.work-detail {
+  .detail-header {
+    display: flex;
+    gap: 16px;
+    margin-bottom: 24px;
+    
+    .work-cover {
+      width: 120px;
+      height: 120px;
+      border-radius: 8px;
+      flex-shrink: 0;
+      
+      .cover-placeholder {
+        width: 100%;
+        height: 100%;
+        background: #f3f4f6;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #9ca3af;
+      }
+    }
+    
+    .header-info {
+      flex: 1;
+      
+      h3 {
+        margin: 0 0 12px 0;
+        font-size: 16px;
+        line-height: 1.5;
+      }
+      
+      .meta-info {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        
+        .publish-time {
+          font-size: 13px;
+          color: $text-secondary;
+        }
+      }
+    }
+  }
+  
+  .detail-stats {
+    display: grid;
+    grid-template-columns: repeat(5, 1fr);
+    gap: 16px;
+    margin-bottom: 24px;
+    
+    .stat-item {
+      background: #f8fafc;
+      border-radius: 12px;
+      padding: 16px;
+      text-align: center;
+      
+      .stat-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: $primary-color;
+      }
+      
+      .stat-label {
+        font-size: 13px;
+        color: $text-secondary;
+        margin-top: 4px;
+      }
+    }
+  }
+  
+  .detail-content {
+    h4 {
+      margin: 0 0 12px 0;
+      font-size: 15px;
+      color: $text-primary;
+    }
+    
+    .content-text {
+      font-size: 14px;
+      line-height: 1.8;
+      color: $text-regular;
+      white-space: pre-wrap;
+    }
+  }
+}
+
+@media (max-width: 1400px) {
+  .work-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) {
+  .work-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>

+ 39 - 21
client/src/views/Analytics/index.vue

@@ -123,37 +123,51 @@ const trendData = ref<TrendData | null>(null);
 const platformComparison = ref<PlatformComparison[]>([]);
 
 // 从趋势数据计算统计摘要
+// 注意:数据库存储的是累积值,所以区间增量 = 最后一天 - 第一天
 const summaryItems = computed(() => {
   const data = trendData.value;
   if (!data || data.dates.length === 0) {
     return [
       { label: '总粉丝', value: 0 },
       { label: '粉丝增量', value: 0 },
-      { label: '播放', value: 0 },
-      { label: '点赞', value: 0 },
-      { label: '评论', value: 0 },
-      { label: '收藏', value: 0 },
+      { label: '播放增量', value: 0 },
+      { label: '点赞增量', value: 0 },
+      { label: '评论增量', value: 0 },
+      { label: '收藏增量', value: 0 },
     ];
   }
   
+  const len = data.fans.length;
+  
   // 取最后一天的粉丝数作为当前粉丝数
-  const latestFans = data.fans[data.fans.length - 1] || 0;
-  // 计算粉丝增量(最后一天 - 第一天)
+  const latestFans = data.fans[len - 1] || 0;
+  // 取第一天的值
   const firstFans = data.fans[0] || 0;
+  const firstViews = data.views[0] || 0;
+  const firstLikes = data.likes[0] || 0;
+  const firstComments = data.comments[0] || 0;
+  const firstCollects = data.collects[0] || 0;
+  
+  // 取最后一天的值
+  const lastViews = data.views[len - 1] || 0;
+  const lastLikes = data.likes[len - 1] || 0;
+  const lastComments = data.comments[len - 1] || 0;
+  const lastCollects = data.collects[len - 1] || 0;
+  
+  // 计算区间增量(累积值:最后一天 - 第一天)
   const fansIncrease = latestFans - firstFans;
-  // 计算区间内的总和
-  const totalViews = data.views.reduce((sum, v) => sum + v, 0);
-  const totalLikes = data.likes.reduce((sum, v) => sum + v, 0);
-  const totalComments = data.comments.reduce((sum, v) => sum + v, 0);
-  const totalCollects = data.collects.reduce((sum, v) => sum + v, 0);
+  const viewsIncrease = lastViews - firstViews;
+  const likesIncrease = lastLikes - firstLikes;
+  const commentsIncrease = lastComments - firstComments;
+  const collectsIncrease = lastCollects - firstCollects;
   
   return [
     { label: '总粉丝', value: latestFans },
     { label: '粉丝增量', value: fansIncrease },
-    { label: '总播放', value: totalViews },
-    { label: '总点赞', value: totalLikes },
-    { label: '总评论', value: totalComments },
-    { label: '总收藏', value: totalCollects },
+    { label: '播放增量', value: viewsIncrease },
+    { label: '点赞增量', value: likesIncrease },
+    { label: '评论增量', value: commentsIncrease },
+    { label: '收藏增量', value: collectsIncrease },
   ];
 });
 
@@ -171,13 +185,15 @@ async function loadTrendData() {
   const userId = authStore.user?.id;
   if (!userId || !dateRange.value) return;
   
-  // 计算天数
+  // 获取实际的日期范围
   const [start, end] = dateRange.value;
-  const days = dayjs(end).diff(dayjs(start), 'day') + 1;
+  const startDate = dayjs(start).format('YYYY-MM-DD');
+  const endDate = dayjs(end).format('YYYY-MM-DD');
   
   const queryParams = new URLSearchParams({
     user_id: userId.toString(),
-    days: Math.min(days, 30).toString(),  // 最大30天
+    start_date: startDate,
+    end_date: endDate,
   });
   
   const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/trend?${queryParams}`);
@@ -206,13 +222,15 @@ async function loadPlatformData() {
   const userId = authStore.user?.id;
   if (!userId || !dateRange.value) return;
   
-  // 计算天数
+  // 获取实际的日期范围
   const [start, end] = dateRange.value;
-  const days = dayjs(end).diff(dayjs(start), 'day') + 1;
+  const startDate = dayjs(start).format('YYYY-MM-DD');
+  const endDate = dayjs(end).format('YYYY-MM-DD');
   
   const queryParams = new URLSearchParams({
     user_id: userId.toString(),
-    days: Math.min(days, 30).toString(),
+    start_date: startDate,
+    end_date: endDate,
   });
   
   const response = await fetch(`${PYTHON_API_URL}/work_day_statistics/platforms?${queryParams}`);

+ 88 - 45
server/python/app.py

@@ -716,11 +716,13 @@ def save_work_day_statistics():
 @app.route("/work_day_statistics/trend", methods=["GET"])
 def get_statistics_trend():
     """
-    获取数据趋势(用于 Dashboard 数据看板)
+    获取数据趋势(用于 Dashboard 数据看板 和 数据分析页面
     
     查询参数:
         user_id: 用户ID (必填)
-        days: 天数 (可选,默认7天,最大30天)
+        days: 天数 (可选,默认7天,最大30天) - 与 start_date/end_date 二选一
+        start_date: 开始日期 (可选,格式 YYYY-MM-DD)
+        end_date: 结束日期 (可选,格式 YYYY-MM-DD)
         account_id: 账号ID (可选,不填则查询所有账号)
     
     响应:
@@ -739,7 +741,9 @@ def get_statistics_trend():
     """
     try:
         user_id = request.args.get("user_id")
-        days = min(int(request.args.get("days", 7)), 30)  # 最大30天
+        days = request.args.get("days")
+        start_date = request.args.get("start_date")
+        end_date = request.args.get("end_date")
         account_id = request.args.get("account_id")
         
         if not user_id:
@@ -773,9 +777,18 @@ def get_statistics_trend():
                         FROM work_day_statistics wds
                         INNER JOIN works w ON wds.work_id = w.id
                         WHERE w.userId = %s
-                          AND wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
                 """
-                params = [user_id, days]
+                params = [user_id]
+                
+                # 支持两种日期筛选方式:start_date/end_date 或 days
+                if start_date and end_date:
+                    sql += " AND wds.record_date >= %s AND wds.record_date <= %s"
+                    params.extend([start_date, end_date])
+                else:
+                    # 默认使用 days 参数
+                    days_value = min(int(days or 7), 30)
+                    sql += " AND wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
+                    params.append(days_value)
                 
                 if account_id:
                     sql += " AND w.accountId = %s"
@@ -862,7 +875,9 @@ def get_statistics_by_platform():
     
     查询参数:
         user_id: 用户ID (必填)
-        days: 天数 (可选,默认30天,最大30天)
+        days: 天数 (可选,默认30天,最大30天) - 与 start_date/end_date 二选一
+        start_date: 开始日期 (可选,格式 YYYY-MM-DD)
+        end_date: 结束日期 (可选,格式 YYYY-MM-DD)
     
     响应:
     {
@@ -883,7 +898,9 @@ def get_statistics_by_platform():
     """
     try:
         user_id = request.args.get("user_id")
-        days = min(int(request.args.get("days", 30)), 30)
+        days = request.args.get("days")
+        start_date = request.args.get("start_date")
+        end_date = request.args.get("end_date")
         
         if not user_id:
             return jsonify({"success": False, "error": "缺少 user_id 参数"}), 400
@@ -894,52 +911,78 @@ def get_statistics_by_platform():
                 # 简化查询:按平台分组获取统计数据
                 # 1. 从 platform_accounts 获取当前粉丝数(注意:字段名是下划线命名 fans_count, user_id)
                 # 2. 从 work_day_statistics 获取播放量等累计数据
-                sql = """
+                
+                # 根据日期参数构建日期条件
+                if start_date and end_date:
+                    date_condition = "wds.record_date >= %s AND wds.record_date <= %s"
+                    date_params = [start_date, end_date]
+                else:
+                    days_value = min(int(days or 30), 30)
+                    date_condition = "wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
+                    date_params = [days_value]
+                
+                # 注意:数据库存储的是累积值,所以区间增量 = 最后一天 - 第一天
+                sql = f"""
                     SELECT 
                         pa.platform,
                         pa.fans_count as current_fans,
-                        COALESCE(stats.total_views, 0) as viewsCount,
-                        COALESCE(stats.total_likes, 0) as likesCount,
-                        COALESCE(stats.total_comments, 0) as commentsCount,
-                        COALESCE(stats.total_collects, 0) as collectsCount,
-                        COALESCE(fans_change.earliest_fans, pa.fans_count) as earliest_fans
+                        COALESCE(last_day.views, 0) - COALESCE(first_day.views, 0) as viewsIncrease,
+                        COALESCE(last_day.likes, 0) - COALESCE(first_day.likes, 0) as likesIncrease,
+                        COALESCE(last_day.comments, 0) - COALESCE(first_day.comments, 0) as commentsIncrease,
+                        COALESCE(last_day.collects, 0) - COALESCE(first_day.collects, 0) as collectsIncrease,
+                        COALESCE(first_day.fans, pa.fans_count) as earliest_fans
                     FROM platform_accounts pa
                     LEFT JOIN (
-                        -- 获取区间内的累计数据(按账号汇总)
+                        -- 获取每个账号在区间内第一天的累积值
                         SELECT 
-                            w.accountId,
-                            SUM(wds.play_count) as total_views,
-                            SUM(wds.like_count) as total_likes,
-                            SUM(wds.comment_count) as total_comments,
-                            SUM(wds.collect_count) as total_collects
-                        FROM work_day_statistics wds
-                        INNER JOIN works w ON wds.work_id = w.id
-                        WHERE w.userId = %s
-                          AND wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
-                        GROUP BY w.accountId
-                    ) stats ON pa.id = stats.accountId
+                            accountId, fans, views, likes, comments, collects
+                        FROM (
+                            SELECT 
+                                w.accountId,
+                                MAX(wds.fans_count) as fans,
+                                SUM(wds.play_count) as views,
+                                SUM(wds.like_count) as likes,
+                                SUM(wds.comment_count) as comments,
+                                SUM(wds.collect_count) as collects,
+                                wds.record_date,
+                                ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date ASC) as rn
+                            FROM work_day_statistics wds
+                            INNER JOIN works w ON wds.work_id = w.id
+                            WHERE w.userId = %s
+                              AND {date_condition}
+                            GROUP BY w.accountId, wds.record_date
+                        ) ranked
+                        WHERE rn = 1
+                    ) first_day ON pa.id = first_day.accountId
                     LEFT JOIN (
-                        -- 获取区间内最早一天的粉丝数
+                        -- 获取每个账号在区间内最后一天的累积值
                         SELECT 
-                            w.accountId,
-                            MAX(wds.fans_count) as earliest_fans
-                        FROM work_day_statistics wds
-                        INNER JOIN works w ON wds.work_id = w.id
-                        WHERE w.userId = %s
-                          AND wds.record_date = (
-                              SELECT MIN(wds2.record_date) 
-                              FROM work_day_statistics wds2 
-                              INNER JOIN works w2 ON wds2.work_id = w2.id 
-                              WHERE w2.userId = %s
-                                AND wds2.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
-                          )
-                        GROUP BY w.accountId
-                    ) fans_change ON pa.id = fans_change.accountId
+                            accountId, fans, views, likes, comments, collects
+                        FROM (
+                            SELECT 
+                                w.accountId,
+                                MAX(wds.fans_count) as fans,
+                                SUM(wds.play_count) as views,
+                                SUM(wds.like_count) as likes,
+                                SUM(wds.comment_count) as comments,
+                                SUM(wds.collect_count) as collects,
+                                wds.record_date,
+                                ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date DESC) as rn
+                            FROM work_day_statistics wds
+                            INNER JOIN works w ON wds.work_id = w.id
+                            WHERE w.userId = %s
+                              AND {date_condition}
+                            GROUP BY w.accountId, wds.record_date
+                        ) ranked
+                        WHERE rn = 1
+                    ) last_day ON pa.id = last_day.accountId
                     WHERE pa.user_id = %s
                     ORDER BY current_fans DESC
                 """
                 
-                cursor.execute(sql, [user_id, days, user_id, user_id, days, user_id])
+                # 构建参数列表:first_day子查询(user_id + date_params) + last_day子查询(user_id + date_params) + 主查询(user_id)
+                params = [user_id] + date_params + [user_id] + date_params + [user_id]
+                cursor.execute(sql, params)
                 results = cursor.fetchall()
                 
                 # 构建响应数据
@@ -953,10 +996,10 @@ def get_statistics_by_platform():
                         "platform": row['platform'],
                         "fansCount": current_fans,
                         "fansIncrease": fans_increase,
-                        "viewsCount": int(row['viewsCount'] or 0),
-                        "likesCount": int(row['likesCount'] or 0),
-                        "commentsCount": int(row['commentsCount'] or 0),
-                        "collectsCount": int(row['collectsCount'] or 0),
+                        "viewsCount": int(row['viewsIncrease'] or 0),  # 区间增量
+                        "likesCount": int(row['likesIncrease'] or 0),  # 区间增量
+                        "commentsCount": int(row['commentsIncrease'] or 0),  # 区间增量
+                        "collectsCount": int(row['collectsIncrease'] or 0),  # 区间增量
                     })
                 
                 print(f"[PlatformStats] 返回 {len(platform_data)} 个平台的数据")

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