Ver código fonte

作品数据

Ethanfly 12 horas atrás
pai
commit
49d0043cbf

+ 51 - 34
client/src/views/Analytics/Work/index.vue

@@ -88,12 +88,9 @@
             :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-option label="按发布时间倒序排列" value="publish_desc" />
         </el-select>
         <el-input 
           v-model="searchKeyword" 
@@ -145,11 +142,13 @@
             </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>
@@ -247,8 +246,6 @@ 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);
 
@@ -396,10 +393,17 @@ function handleQuery() {
 async function loadAccountList() {
   try {
     const res = await request.get('/api/accounts');
-    if (res.data.success) {
-      accountList.value = (res.data.data || []).map((a: any) => ({
+    // 新接口:request 已经解包,res 就是账号数组
+    if (Array.isArray(res)) {
+      accountList.value = res.map((a: any) => ({
         id: a.id,
-        nickname: a.nickname || a.username,
+        nickname: a.accountName || a.nickname || a.username,
+      }));
+    } else if ((res as any)?.data?.data) {
+      // 兼容旧格式 { data: { data: [] } }
+      accountList.value = (res as any).data.data.map((a: any) => ({
+        id: a.id,
+        nickname: a.accountName || a.nickname || a.username,
       }));
     }
   } catch (error) {
@@ -427,40 +431,53 @@ async function loadData() {
   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,
-    });
-    
+    const params: Record<string, any> = {
+      startDate: startDate.value,
+      endDate: endDate.value,
+      page: currentPage.value,
+      pageSize: pageSize.value,
+      sortBy: sortBy.value,
+    };
+
     if (selectedPlatform.value) {
-      queryParams.append('platform', selectedPlatform.value);
+      params.platform = selectedPlatform.value;
     }
-    
+
     if (selectedAccounts.value.length > 0) {
-      queryParams.append('account_ids', selectedAccounts.value.join(','));
+      params.accountIds = selectedAccounts.value.join(',');
     }
-    
+
     if (searchKeyword.value) {
-      queryParams.append('keyword', searchKeyword.value);
+      params.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;
+
+    if (selectedGroup.value) {
+      params.groupId = selectedGroup.value;
+    }
+
+    const data = await request.get('/api/work-day-statistics/works', {
+      params,
+    });
+
+    if (data) {
+      workList.value = (data.works || []) as WorkData[];
+      totalWorks.value = data.total || 0;
+
+      if (data.summary) {
+        summaryData.value = {
+          totalWorks: data.summary.totalWorks || 0,
+          recommendCount: data.summary.recommendCount || 0,
+          viewsCount: data.summary.viewsCount || 0,
+          commentsCount: data.summary.commentsCount || 0,
+          sharesCount: data.summary.sharesCount || 0,
+          collectsCount: data.summary.collectsCount || 0,
+          likesCount: data.summary.likesCount || 0,
+        };
       }
     }
   } catch (error) {
     console.error('加载作品数据失败:', error);
+    ElMessage.error('加载作品数据失败,请稍后重试');
   } finally {
     loading.value = false;
   }

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

@@ -348,5 +348,71 @@ router.get(
   })
 );
 
+/**
+ * GET /api/work-day-statistics/works
+ * 获取作品数据列表(用于「作品数据」页)
+ *
+ * 查询参数:
+ * - startDate: 开始日期(必填)
+ * - endDate: 结束日期(必填)
+ * - platform: 平台(可选)
+ * - accountIds: 账号 ID 列表,逗号分隔(可选)
+ * - groupId: 分组 ID(可选)
+ * - keyword: 标题/账号关键字(可选)
+ * - sortBy: 排序字段(publish_desc/publish_asc/views_desc/likes_desc/comments_desc)
+ * - page: 页码(默认 1)
+ * - pageSize: 每页条数(默认 20)
+ */
+router.get(
+  '/works',
+  [
+    query('startDate').notEmpty().withMessage('startDate 不能为空'),
+    query('endDate').notEmpty().withMessage('endDate 不能为空'),
+    query('platform').optional().isString().withMessage('platform 必须是字符串'),
+    query('accountIds').optional().isString().withMessage('accountIds 必须是字符串'),
+    query('groupId').optional().isInt().withMessage('groupId 必须是整数'),
+    query('keyword').optional().isString().withMessage('keyword 必须是字符串'),
+    query('sortBy').optional().isString().withMessage('sortBy 必须是字符串'),
+    query('page').optional().isInt().withMessage('page 必须是整数'),
+    query('pageSize').optional().isInt().withMessage('pageSize 必须是整数'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const {
+      startDate,
+      endDate,
+      platform,
+      accountIds,
+      groupId,
+      keyword,
+      sortBy,
+      page,
+      pageSize,
+    } = req.query;
+
+    const parsedAccountIds =
+      typeof accountIds === 'string' && accountIds.trim()
+        ? accountIds
+            .split(',')
+            .map((id) => Number(id))
+            .filter((id) => !Number.isNaN(id))
+        : undefined;
+
+    const data = await workDayStatisticsService.getWorksAnalytics(req.user!.userId, {
+      startDate: String(startDate),
+      endDate: String(endDate),
+      platform: (platform as string) || undefined,
+      accountIds: parsedAccountIds,
+      groupId: groupId ? Number(groupId) : undefined,
+      keyword: (keyword as string) || undefined,
+      sortBy: (sortBy as any) || undefined,
+      page: page ? Number(page) : undefined,
+      pageSize: pageSize ? Number(pageSize) : undefined,
+    });
+
+    res.json({ success: true, data });
+  })
+);
+
 export default router;
 

+ 192 - 0
server/src/services/WorkDayStatisticsService.ts

@@ -1108,6 +1108,198 @@ export class WorkDayStatisticsService {
   }
 
   /**
+   * 获取作品数据列表(用于「作品数据」页)
+   * 依据 work_day_statistics 进行区间汇总统计
+   */
+  async getWorksAnalytics(
+    userId: number,
+    options: {
+      startDate: string;
+      endDate: string;
+      platform?: string;
+      accountIds?: number[];
+      groupId?: number;
+      keyword?: string;
+      sortBy?: 'publish_desc' | 'publish_asc' | 'views_desc' | 'likes_desc' | 'comments_desc';
+      page?: number;
+      pageSize?: number;
+    }
+  ): Promise<{
+    summary: {
+      totalWorks: number;
+      recommendCount: number;
+      viewsCount: number;
+      commentsCount: number;
+      sharesCount: number;
+      collectsCount: number;
+      likesCount: number;
+    };
+    total: number;
+    works: Array<{
+      id: number;
+      title: string;
+      coverUrl: string;
+      platform: string;
+      accountId: number;
+      accountName: string;
+      accountAvatar: string | null;
+      workType: string;
+      publishTime: string | null;
+      recommendCount: number;
+      viewsCount: number;
+      commentsCount: number;
+      sharesCount: number;
+      collectsCount: number;
+      likesCount: number;
+    }>;
+  }> {
+    const {
+      startDate,
+      endDate,
+      platform,
+      accountIds,
+      groupId,
+      keyword,
+      sortBy = 'publish_desc',
+      page = 1,
+      pageSize = 20,
+    } = options;
+
+    const startDateStr = startDate;
+    const endDateStr = endDate;
+
+    // 基础查询:当前用户的作品
+    const qb = this.workRepository
+      .createQueryBuilder('w')
+      .leftJoin(WorkDayStatistics, 'wds', 'wds.work_id = w.id AND wds.record_date >= :wStart AND wds.record_date <= :wEnd', {
+        wStart: startDateStr,
+        wEnd: endDateStr,
+      })
+      .innerJoin(PlatformAccount, 'pa', 'pa.id = w.accountId')
+      .select('w.id', 'id')
+      .addSelect('w.title', 'title')
+      .addSelect('w.cover_url', 'coverUrl')
+      .addSelect('w.platform', 'platform')
+      .addSelect('w.accountId', 'accountId')
+      .addSelect('pa.accountName', 'accountName')
+      .addSelect('pa.avatarUrl', 'accountAvatar')
+      .addSelect('w.status', 'workType')
+      .addSelect('w.publish_time', 'publishTime')
+      .addSelect('COALESCE(SUM(wds.play_count), 0)', 'viewsCount')
+      .addSelect('COALESCE(SUM(wds.comment_count), 0)', 'commentsCount')
+      .addSelect('COALESCE(SUM(wds.share_count), 0)', 'sharesCount')
+      .addSelect('COALESCE(SUM(wds.collect_count), 0)', 'collectsCount')
+      .addSelect('COALESCE(SUM(wds.like_count), 0)', 'likesCount')
+      .where('w.userId = :userId', { userId });
+
+    if (platform) {
+      qb.andWhere('w.platform = :platform', { platform });
+    }
+    if (accountIds && accountIds.length > 0) {
+      qb.andWhere('w.accountId IN (:...accountIds)', { accountIds });
+    }
+    if (groupId) {
+      qb.andWhere('pa.groupId = :groupId', { groupId });
+    }
+    if (keyword && keyword.trim()) {
+      const kw = `%${keyword.trim()}%`;
+      qb.andWhere('(w.title LIKE :kw OR pa.accountName LIKE :kw)', { kw });
+    }
+
+    qb.groupBy('w.id');
+
+    // 排序:统一按发布时间倒序(最新的在前)
+    qb.orderBy('w.publish_time', 'DESC');
+
+    // 统计总数(作品数)
+    const countQb = this.workRepository
+      .createQueryBuilder('w')
+      .innerJoin(PlatformAccount, 'pa', 'pa.id = w.accountId')
+      .where('w.userId = :userId', { userId });
+
+    if (platform) {
+      countQb.andWhere('w.platform = :platform', { platform });
+    }
+    if (accountIds && accountIds.length > 0) {
+      countQb.andWhere('w.accountId IN (:...accountIds)', { accountIds });
+    }
+    if (groupId) {
+      countQb.andWhere('pa.groupId = :groupId', { groupId });
+    }
+    if (keyword && keyword.trim()) {
+      const kw = `%${keyword.trim()}%`;
+      countQb.andWhere('(w.title LIKE :kw OR pa.accountName LIKE :kw)', { kw });
+    }
+
+    const total = await countQb.getCount();
+
+    // 分页
+    const offset = (page - 1) * pageSize;
+    qb.skip(offset).take(pageSize);
+
+    const rows = await qb.getRawMany();
+
+    let totalViews = 0;
+    let totalComments = 0;
+    let totalShares = 0;
+    let totalCollects = 0;
+    let totalLikes = 0;
+
+    const works = rows.map((row) => {
+      const views = Number(row.viewsCount) || 0;
+      const comments = Number(row.commentsCount) || 0;
+      const shares = Number(row.sharesCount) || 0;
+      const collects = Number(row.collectsCount) || 0;
+      const likes = Number(row.likesCount) || 0;
+
+      totalViews += views;
+      totalComments += comments;
+      totalShares += shares;
+      totalCollects += collects;
+      totalLikes += likes;
+
+      const publishTime =
+        row.publishTime instanceof Date
+          ? row.publishTime.toISOString()
+          : row.publishTime
+          ? String(row.publishTime)
+          : null;
+
+      return {
+        id: Number(row.id),
+        title: row.title || '',
+        coverUrl: row.coverUrl || '',
+        platform: row.platform || '',
+        accountId: Number(row.accountId) || 0,
+        accountName: row.accountName || '',
+        accountAvatar: row.accountAvatar || null,
+        workType: row.workType || '动态',
+        publishTime,
+        recommendCount: 0,
+        viewsCount: views,
+        commentsCount: comments,
+        sharesCount: shares,
+        collectsCount: collects,
+        likesCount: likes,
+      };
+    });
+
+    return {
+      summary: {
+        totalWorks: total,
+        recommendCount: 0,
+        viewsCount: totalViews,
+        commentsCount: totalComments,
+        sharesCount: totalShares,
+        collectsCount: totalCollects,
+        likesCount: totalLikes,
+      },
+      total,
+      works,
+    };
+  }
+
+  /**
    * 获取平台详情数据
    * 包括汇总统计、每日汇总数据和账号列表
    */