Browse Source

导出数据

Ethanfly 13 hours ago
parent
commit
e21c187c32

+ 13 - 12
client/package.json

@@ -14,37 +14,38 @@
     "generate-icons": "node scripts/generate-icons.js"
   },
   "dependencies": {
-    "@media-manager/shared": "workspace:*",
-    "vue": "^3.4.15",
-    "vue-router": "^4.2.5",
-    "pinia": "^2.1.7",
-    "element-plus": "^2.5.3",
     "@element-plus/icons-vue": "^2.3.1",
+    "@media-manager/shared": "workspace:*",
     "axios": "^1.6.5",
     "dayjs": "^1.11.10",
     "echarts": "^5.4.3",
+    "element-plus": "^2.5.3",
+    "pinia": "^2.1.7",
+    "vue": "^3.4.15",
     "vue-echarts": "^6.6.8",
-    "vue-i18n": "^9.9.0"
+    "vue-i18n": "^9.9.0",
+    "vue-router": "^4.2.5",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@types/node": "^20.10.6",
+    "@typescript-eslint/eslint-plugin": "^6.18.0",
+    "@typescript-eslint/parser": "^6.18.0",
     "@vitejs/plugin-vue": "^5.0.3",
     "@vue/tsconfig": "^0.5.1",
     "electron": "^28.1.3",
     "electron-builder": "^24.9.1",
+    "eslint": "^8.56.0",
+    "eslint-plugin-vue": "^9.20.0",
     "sass": "^1.69.7",
+    "sharp": "^0.34.5",
     "typescript": "^5.3.3",
     "unplugin-auto-import": "^0.17.3",
     "unplugin-vue-components": "^0.26.0",
     "vite": "^5.0.11",
     "vite-plugin-electron": "^0.28.0",
     "vite-plugin-electron-renderer": "^0.14.5",
-    "vue-tsc": "^1.8.27",
-    "eslint": "^8.56.0",
-    "@typescript-eslint/eslint-plugin": "^6.18.0",
-    "@typescript-eslint/parser": "^6.18.0",
-    "eslint-plugin-vue": "^9.20.0",
-    "sharp": "^0.34.5"
+    "vue-tsc": "^1.8.27"
   },
   "build": {
     "appId": "com.media-manager.app",

+ 42 - 2
client/src/views/Analytics/Account/index.vue

@@ -216,6 +216,11 @@
               {{ btn.label }}
             </el-button>
           </div>
+          <div class="detail-export-wrap">
+            <el-button type="primary" plain size="small" :disabled="!detailDailyData.length" @click="exportDetailDailyData">
+              导出数据
+            </el-button>
+          </div>
         </div>
 
         <!-- 详情 Tab -->
@@ -241,9 +246,10 @@
               </div>
             </div>
 
-            <!-- 每日数据表格 -->
+            <!-- 每日数据表格:时间倒序;收益、推荐量暂未接入先注释 -->
             <el-table :data="detailDailyData" v-loading="detailLoading" stripe>
               <el-table-column prop="date" label="时间" width="120" align="center" />
+              <!-- 收益与推荐量暂未接入,先隐藏
               <el-table-column prop="income" label="收益" width="90" align="center">
                 <template #default="{ row }">
                   <span>{{ row.income ?? 0 }}</span>
@@ -254,6 +260,7 @@
                   <span>{{ row.recommendationCount ?? 0 }}</span>
                 </template>
               </el-table-column>
+              -->
               <el-table-column prop="viewsCount" label="播放(阅读)量" width="130" align="center">
                 <template #default="{ row }">
                   <span>{{ row.viewsCount ?? 0 }}</span>
@@ -345,6 +352,7 @@ import { Search, User, View, ChatDotRound, Star, TrendCharts } from '@element-pl
 import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { ElMessage } from 'element-plus';
+import * as XLSX from 'xlsx';
 import dayjs from 'dayjs';
 import request from '@/api/request';
 import iconDefaultUrl from '@/assets/platforms/default.svg?url';
@@ -738,6 +746,33 @@ function handleDetailQuickDate(type: string) {
   loadAccountDetailData();
 }
 
+// 导出当前时间范围内的每日数据为 xlsx(表头与列表一致)
+function exportDetailDailyData() {
+  if (!detailDailyData.value.length) return;
+  const headers = ['时间', '播放(阅读)量', '评论量', '点赞量', '涨粉量'];
+  const rows = detailDailyData.value.map((row) => [
+    row.date ?? '',
+    row.viewsCount ?? 0,
+    row.commentsCount ?? 0,
+    row.likesCount ?? 0,
+    row.fansIncrease ?? 0,
+  ]);
+  const data = [headers, ...rows];
+  const ws = XLSX.utils.aoa_to_sheet(data);
+  const wb = XLSX.utils.book_new();
+  XLSX.utils.book_append_sheet(wb, ws, '数据详情');
+  const arrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
+  const blob = new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+  const name = (selectedAccount.value?.nickname || '账号').replace(/[/\\?*:"|]/g, '_');
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `${name}_数据详情_${detailStartDate.value}_${detailEndDate.value}.xlsx`;
+  a.click();
+  URL.revokeObjectURL(url);
+  ElMessage.success('导出成功');
+}
+
 // 加载账号详情(汇总 + 每日 + 作品)
 async function loadAccountDetailData() {
   if (!selectedAccount.value) return;
@@ -762,7 +797,8 @@ async function loadAccountDetailData() {
         fansIncrease: data.summary?.fansIncrease ?? 0,
       };
 
-      detailDailyData.value = Array.isArray(data.dailyData) ? data.dailyData : [];
+      const rawDaily = Array.isArray(data.dailyData) ? data.dailyData : [];
+      detailDailyData.value = [...rawDaily].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
       detailWorks.value = Array.isArray(data.works) ? data.works : [];
       // 重新加载详情数据时,重置作品分页
       detailWorksPage.value = 1;
@@ -1068,6 +1104,10 @@ onMounted(() => {
     }
   }
 
+  .detail-export-wrap {
+    margin-left: 12px;
+  }
+
   .work-title-cell {
     .work-title-text {
       font-weight: 500;

+ 42 - 2
client/src/views/Analytics/PlatformDetail/index.vue

@@ -216,6 +216,11 @@
               {{ btn.label }}
             </el-button>
           </div>
+          <div class="detail-export-wrap">
+            <el-button type="primary" plain size="small" :disabled="!detailDailyData.length" @click="exportDetailDailyData">
+              导出数据
+            </el-button>
+          </div>
         </div>
 
         <!-- 详情 Tab -->
@@ -241,9 +246,10 @@
               </div>
             </div>
 
-            <!-- 每日数据表格 -->
+            <!-- 每日数据表格:时间倒序;收益、推荐量暂未接入先注释 -->
             <el-table :data="detailDailyData" v-loading="detailLoading" stripe>
               <el-table-column prop="date" label="时间" width="120" align="center" />
+              <!-- 收益与推荐量暂未接入,先隐藏
               <el-table-column prop="income" label="收益" width="90" align="center">
                 <template #default="{ row }">
                   <span>{{ row.income ?? 0 }}</span>
@@ -254,6 +260,7 @@
                   <span>{{ row.recommendationCount ?? 0 }}</span>
                 </template>
               </el-table-column>
+              -->
               <el-table-column prop="viewsCount" label="播放(阅读)量" width="130" align="center">
                 <template #default="{ row }">
                   <span>{{ row.viewsCount ?? 0 }}</span>
@@ -333,6 +340,7 @@ import { useRoute, useRouter } from 'vue-router';
 import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { ElMessage } from 'element-plus';
+import * as XLSX from 'xlsx';
 import dayjs from 'dayjs';
 import request from '@/api/request';
 
@@ -678,6 +686,33 @@ function handleDetailQuickDate(type: string) {
   loadAccountDetailData();
 }
 
+// 导出当前时间范围内的每日数据为 xlsx(表头与列表一致)
+function exportDetailDailyData() {
+  if (!detailDailyData.value.length) return;
+  const headers = ['时间', '播放(阅读)量', '评论量', '点赞量', '涨粉量'];
+  const rows = detailDailyData.value.map((row) => [
+    row.date ?? '',
+    row.viewsCount ?? 0,
+    row.commentsCount ?? 0,
+    row.likesCount ?? 0,
+    row.fansIncrease ?? 0,
+  ]);
+  const data = [headers, ...rows];
+  const ws = XLSX.utils.aoa_to_sheet(data);
+  const wb = XLSX.utils.book_new();
+  XLSX.utils.book_append_sheet(wb, ws, '数据详情');
+  const arrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
+  const blob = new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+  const name = (selectedAccountDetail.value?.nickname || selectedAccountDetail.value?.username || '账号').replace(/[/\\?*:"|]/g, '_');
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `${name}_数据详情_${detailStartDate.value}_${detailEndDate.value}.xlsx`;
+  a.click();
+  URL.revokeObjectURL(url);
+  ElMessage.success('导出成功');
+}
+
 // 加载账号详情(汇总 + 每日 + 作品)
 async function loadAccountDetailData() {
   if (!selectedAccountDetail.value) return;
@@ -702,7 +737,8 @@ async function loadAccountDetailData() {
         fansIncrease: data.summary?.fansIncrease ?? 0,
       };
 
-      detailDailyData.value = Array.isArray(data.dailyData) ? data.dailyData : [];
+      const rawDaily = Array.isArray(data.dailyData) ? data.dailyData : [];
+      detailDailyData.value = [...rawDaily].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
       detailWorks.value = Array.isArray(data.works) ? data.works : [];
     }
   } catch (error) {
@@ -948,6 +984,10 @@ onMounted(() => {
       }
     }
 
+    .detail-export-wrap {
+      margin-left: 12px;
+    }
+
     .work-title-cell {
       .work-title-text {
         font-weight: 500;

+ 3 - 0
pnpm-lock.yaml

@@ -50,6 +50,9 @@ importers:
       vue-router:
         specifier: ^4.2.5
         version: 4.6.4(vue@3.5.26(typescript@5.9.3))
+      xlsx:
+        specifier: ^0.18.5
+        version: 0.18.5
     devDependencies:
       '@types/node':
         specifier: ^20.10.6