Ethanfly 17 часов назад
Родитель
Сommit
d9c9186d10

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

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

+ 6 - 14
client/src/views/Accounts/index.vue

@@ -26,7 +26,7 @@
     <div class="page-card filter-bar">
       <el-select v-model="filter.platform" placeholder="平台" clearable style="width: 150px">
         <el-option
-          v-for="platform in platforms"
+          v-for="platform in supportedPlatforms"
           :key="platform.type"
           :label="platform.name"
           :value="platform.type"
@@ -136,17 +136,11 @@
         <el-form-item label="平台">
           <el-select v-model="addForm.platform" placeholder="选择平台" style="width: 100%">
             <el-option
-              v-for="platform in platforms"
+              v-for="platform in supportedPlatforms"
               :key="platform.type"
               :label="platform.name"
               :value="platform.type"
-              :disabled="!platform.supported"
-            >
-              <span class="platform-option">
-                <span>{{ platform.name }}</span>
-                <el-tag v-if="!platform.supported" size="small" type="info">适配中</el-tag>
-              </span>
-            </el-option>
+            />
           </el-select>
         </el-form-item>
         
@@ -383,7 +377,7 @@ import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
 import { Plus, Refresh, Monitor, Loading, CircleCheck, CircleClose, Setting } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { accountsApi } from '@/api/accounts';
-import { PLATFORMS, PLATFORM_TYPES } from '@media-manager/shared';
+import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformAccount, AccountGroup, PlatformType } from '@media-manager/shared';
 import { useTaskQueueStore } from '@/stores/taskQueue';
 import { useTabsStore } from '@/stores/tabs';
@@ -455,10 +449,8 @@ const browserLoginForm = reactive({
   groupId: undefined as number | undefined,
 });
 
-const platforms = PLATFORM_TYPES.map(type => PLATFORMS[type]);
-
-// 只显示已支持的平台
-const supportedPlatforms = platforms.filter(p => p.supported);
+// 平台选项(统一配置:小红书、抖音、视频号、百家号)
+const supportedPlatforms = AVAILABLE_PLATFORM_TYPES.map(type => PLATFORMS[type]);
 
 const filter = reactive({
   platform: '',

+ 16 - 12
client/src/views/Analytics/Account/index.vue

@@ -11,6 +11,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => endDate ? date > new Date(endDate) : false"
+          @change="handleQuery"
         />
         <span class="filter-label">结束时间</span>
         <el-date-picker
@@ -20,6 +22,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => startDate ? date < new Date(startDate) : false"
+          @change="handleQuery"
         />
         <div class="quick-btns">
           <el-button 
@@ -32,7 +36,7 @@
             {{ btn.label }}
           </el-button>
         </div>
-        <el-select v-model="selectedGroup" placeholder="全部分组" clearable style="width: 120px">
+        <el-select v-model="selectedGroup" placeholder="全部分组" clearable style="width: 120px" @change="handleQuery">
           <el-option label="全部分组" value="" />
           <el-option 
             v-for="group in accountGroups" 
@@ -41,7 +45,7 @@
             :value="group.id" 
           />
         </el-select>
-        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px">
+        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px" @change="handleQuery">
           <el-option label="全部平台" value="" />
           <el-option 
             v-for="platform in availablePlatforms" 
@@ -50,7 +54,6 @@
             :value="platform.value"
           />
         </el-select>
-        <el-button type="primary" @click="handleQuery">查询</el-button>
       </div>
       <div class="filter-right">
         <el-button @click="handleExport">导出数据</el-button>
@@ -77,8 +80,6 @@
         placeholder="请输入要搜索的账号" 
         clearable
         style="width: 300px"
-        @clear="handleQuery"
-        @keyup.enter="handleQuery"
       >
         <template #prefix>
           <el-icon><Search /></el-icon>
@@ -119,7 +120,7 @@
           </template>
         </el-table-column>
         -->
-        <el-table-column prop="viewsCount" label="阅读(播放)量" width="130" align="center">
+        <el-table-column prop="viewsCount" label="播放(阅读)量" width="130" align="center">
           <template #default="{ row }">
             <span>{{ row.viewsCount ?? 0 }}</span>
           </template>
@@ -190,6 +191,8 @@
             format="YYYY-MM-DD"
             value-format="YYYY-MM-DD"
             style="width: 140px"
+            :disabled-date="(date: Date) => detailEndDate ? date > new Date(detailEndDate) : false"
+            @change="loadAccountDetailData"
           />
           <span class="filter-label">结束时间</span>
           <el-date-picker
@@ -199,6 +202,8 @@
             format="YYYY-MM-DD"
             value-format="YYYY-MM-DD"
             style="width: 140px"
+            :disabled-date="(date: Date) => detailStartDate ? date < new Date(detailStartDate) : false"
+            @change="loadAccountDetailData"
           />
           <div class="quick-btns">
             <el-button
@@ -211,7 +216,6 @@
               {{ btn.label }}
             </el-button>
           </div>
-          <el-button type="primary" @click="loadAccountDetailData">查询</el-button>
         </div>
 
         <!-- 详情 Tab -->
@@ -325,7 +329,7 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue';
 import { useRoute } from 'vue-router';
 import * as echarts from 'echarts';
 import { Search, User, View, ChatDotRound, Star, TrendCharts } from '@element-plus/icons-vue';
-import { PLATFORMS } from '@media-manager/shared';
+import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { ElMessage } from 'element-plus';
 import dayjs from 'dayjs';
@@ -367,11 +371,11 @@ interface AccountGroup {
 }
 const accountGroups = ref<AccountGroup[]>([]);
 
-// 可用平台
+// 可用平台(统一配置:小红书、抖音、视频号、百家号)
 const availablePlatforms = computed(() => {
-  return Object.entries(PLATFORMS).map(([key, value]) => ({
-    value: key as PlatformType,
-    label: value.name,
+  return AVAILABLE_PLATFORM_TYPES.map(key => ({
+    value: key,
+    label: PLATFORMS[key].name,
   }));
 });
 

+ 7 - 10
client/src/views/Analytics/Overview/index.vue

@@ -151,7 +151,7 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue';
 import { Search } from '@element-plus/icons-vue';
-import { PLATFORMS } from '@media-manager/shared';
+import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { useAuthStore } from '@/stores/auth';
 import { useServerStore } from '@/stores/server';
@@ -176,10 +176,9 @@ interface AccountGroup {
 }
 const accountGroups = ref<AccountGroup[]>([]);
 
-// 可用平台(只显示抖音、百家号、视频号和小红书
+// 可用平台(统一配置:小红书、抖音、视频号、百家号
 const availablePlatforms = computed(() => {
-  const allowedPlatforms: PlatformType[] = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
-  return allowedPlatforms.map(key => ({
+  return AVAILABLE_PLATFORM_TYPES.map(key => ({
     value: key,
     label: PLATFORMS[key].name,
   }));
@@ -246,10 +245,9 @@ const summaryStats = computed(() => [
   { label: '昨日涨粉', value: summaryData.value.yesterdayFansIncrease || 0 },
 ]);
 
-// 过滤后的账号列表(只显示抖音、百家号、视频号和小红书
+// 过滤后的账号列表(只显示统一配置的平台:小红书、抖音、视频号、百家号
 const filteredAccounts = computed(() => {
-  const allowedPlatforms: PlatformType[] = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
-  let result = accounts.value.filter(a => allowedPlatforms.includes(a.platform));
+  let result = accounts.value.filter(a => AVAILABLE_PLATFORM_TYPES.includes(a.platform));
   
   if (selectedGroup.value) {
     result = result.filter(a => a.groupId === selectedGroup.value);
@@ -324,10 +322,9 @@ async function loadData() {
     });
 
     if (data) {
-      // 确保只保留支持的平台
-      const allowedPlatforms: PlatformType[] = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
+      // 确保只保留统一配置的平台
       accounts.value = (data.accounts || []).filter((a: AccountData) => 
-        allowedPlatforms.includes(a.platform)
+        AVAILABLE_PLATFORM_TYPES.includes(a.platform)
       );
       
       // 使用后端返回的汇总数据

+ 5 - 2
client/src/views/Analytics/Platform/index.vue

@@ -11,6 +11,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => endDate ? date > new Date(endDate) : false"
+          @change="handleQuery"
         />
         <span class="filter-label">结束时间</span>
         <el-date-picker
@@ -20,6 +22,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => startDate ? date < new Date(startDate) : false"
+          @change="handleQuery"
         />
         <div class="quick-btns">
           <el-button 
@@ -32,7 +36,6 @@
             {{ btn.label }}
           </el-button>
         </div>
-        <el-button type="primary" @click="handleQuery">查询</el-button>
       </div>
       <div class="filter-right">
         <el-button @click="handleExport">导出数据</el-button>
@@ -50,7 +53,7 @@
             </div>
           </template>
         </el-table-column>
-        <el-table-column prop="viewsCount" label="阅读(播放)量" width="140" align="center">
+        <el-table-column prop="viewsCount" label="播放(阅读)量" width="140" align="center">
           <template #default="{ row }">
             <span>{{ row.viewsCount ?? '未支持' }}</span>
           </template>

+ 15 - 12
client/src/views/Analytics/PlatformDetail/index.vue

@@ -15,6 +15,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => endDate ? date > new Date(endDate) : false"
+          @change="handleQuery"
         />
         <span class="filter-label">结束时间</span>
         <el-date-picker
@@ -24,6 +26,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => startDate ? date < new Date(startDate) : false"
+          @change="handleQuery"
         />
         <div class="quick-btns">
           <el-button 
@@ -36,7 +40,7 @@
             {{ btn.label }}
           </el-button>
         </div>
-        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px">
+        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px" @change="handleQuery">
           <el-option label="全部平台" value="" />
           <el-option 
             v-for="platform in availablePlatforms" 
@@ -45,7 +49,6 @@
             :value="platform.value"
           />
         </el-select>
-        <el-button type="primary" @click="handleQuery">查询</el-button>
       </div>
       <div class="filter-right">
         <!-- <el-button @click="handleViewReport">查看报表</el-button>
@@ -188,6 +191,8 @@
             format="YYYY-MM-DD"
             value-format="YYYY-MM-DD"
             style="width: 140px"
+            :disabled-date="(date: Date) => detailEndDate ? date > new Date(detailEndDate) : false"
+            @change="loadAccountDetailData"
           />
           <span class="filter-label">结束时间</span>
           <el-date-picker
@@ -197,6 +202,8 @@
             format="YYYY-MM-DD"
             value-format="YYYY-MM-DD"
             style="width: 140px"
+            :disabled-date="(date: Date) => detailStartDate ? date < new Date(detailStartDate) : false"
+            @change="loadAccountDetailData"
           />
           <div class="quick-btns">
             <el-button
@@ -209,7 +216,6 @@
               {{ btn.label }}
             </el-button>
           </div>
-          <el-button type="primary" @click="loadAccountDetailData">查询</el-button>
         </div>
 
         <!-- 详情 Tab -->
@@ -324,7 +330,7 @@ console.log('[PlatformDetail] ========== 组件脚本开始执行 ==========');
 
 import { ref, computed, onMounted, nextTick, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import { PLATFORMS } from '@media-manager/shared';
+import { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { ElMessage } from 'element-plus';
 import dayjs from 'dayjs';
@@ -359,15 +365,12 @@ const quickDateBtns = [
   { label: '近一个月', value: 'lastMonth' },
 ];
 
-// 可用平台(只保留与总览列表一致的 4 个:抖音、百家号、视频号、小红书
+// 可用平台(统一配置:小红书、抖音、视频号、百家号
 const availablePlatforms = computed(() => {
-  const allowed: PlatformType[] = ['douyin', 'baijiahao', 'weixin_video', 'xiaohongshu'];
-  return Object.entries(PLATFORMS)
-    .filter(([key]) => allowed.includes(key as PlatformType))
-    .map(([key, value]) => ({
-      value: key,
-      label: value.name,
-    }));
+  return AVAILABLE_PLATFORM_TYPES.map(key => ({
+    value: key,
+    label: PLATFORMS[key].name,
+  }));
 });
 
 // 汇总数据

+ 35 - 10
client/src/views/Analytics/Work/index.vue

@@ -11,6 +11,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => endDate ? date > new Date(endDate) : false"
+          @change="handleFilterChange"
         />
         <span class="filter-label">结束时间</span>
         <el-date-picker
@@ -20,6 +22,8 @@
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 140px"
+          :disabled-date="(date: Date) => startDate ? date < new Date(startDate) : false"
+          @change="handleFilterChange"
         />
         <div class="quick-btns">
           <el-button 
@@ -39,6 +43,7 @@
           collapse-tags-tooltip
           placeholder="选择账号" 
           style="width: 160px"
+          @change="handleFilterChange"
         >
           <el-option 
             v-for="account in accountList" 
@@ -47,7 +52,6 @@
             :value="account.id"
           />
         </el-select>
-        <el-button type="primary" @click="handleQuery">查询</el-button>
       </div>
       <div class="filter-right">
         <el-button @click="handleExport">导出数据</el-button>
@@ -70,7 +74,7 @@
     <!-- 第二行筛选 -->
     <div class="filter-bar secondary">
       <div class="filter-left">
-        <el-select v-model="selectedGroup" placeholder="全部" clearable style="width: 120px">
+        <el-select v-model="selectedGroup" placeholder="全部" clearable style="width: 120px" @change="handleFilterChange">
           <el-option label="全部" value="" />
           <el-option 
             v-for="group in accountGroups" 
@@ -79,7 +83,7 @@
             :value="group.id" 
           />
         </el-select>
-        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px">
+        <el-select v-model="selectedPlatform" placeholder="全部平台" clearable style="width: 120px" @change="handleFilterChange">
           <el-option label="全部平台" value="" />
           <el-option 
             v-for="platform in availablePlatforms" 
@@ -97,8 +101,9 @@
           placeholder="请输入要搜索的作品标题" 
           clearable
           style="width: 240px"
-          @clear="handleQuery"
-          @keyup.enter="handleQuery"
+          @input="handleSearchInput"
+          @clear="handleFilterChange"
+          @keyup.enter="handleFilterChange"
         >
           <template #prefix>
             <el-icon><Search /></el-icon>
@@ -154,6 +159,7 @@
             <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)">
@@ -161,6 +167,7 @@
             </el-button>
           </template>
         </el-table-column>
+        -->
       </el-table>
       
       <!-- 分页 -->
@@ -239,7 +246,7 @@
 <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 { PLATFORMS, AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import type { PlatformType } from '@media-manager/shared';
 import { useAuthStore } from '@/stores/auth';
 import { ElMessage } from 'element-plus';
@@ -278,11 +285,11 @@ interface AccountGroup {
 }
 const accountGroups = ref<AccountGroup[]>([]);
 
-// 可用平台
+// 可用平台(统一配置:小红书、抖音、视频号、百家号)
 const availablePlatforms = computed(() => {
-  return Object.entries(PLATFORMS).map(([key, value]) => ({
-    value: key as PlatformType,
-    label: value.name,
+  return AVAILABLE_PLATFORM_TYPES.map(key => ({
+    value: key,
+    label: PLATFORMS[key].name,
   }));
 });
 
@@ -382,6 +389,7 @@ function handleQuickDate(type: string) {
       endDate.value = today.format('YYYY-MM-DD');
       break;
   }
+  handleQuery();
 }
 
 // 查询
@@ -389,6 +397,23 @@ function handleQuery() {
   loadData();
 }
 
+// 平台/分组/日期/账号变更时重置页码并查询
+function handleFilterChange() {
+  currentPage.value = 1;
+  handleQuery();
+}
+
+// 搜索框防抖(300ms 后触发查询)
+let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
+function handleSearchInput() {
+  if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
+  searchDebounceTimer = setTimeout(() => {
+    currentPage.value = 1;
+    handleQuery();
+    searchDebounceTimer = null;
+  }, 300);
+}
+
 // 加载账号列表
 async function loadAccountList() {
   try {

+ 3 - 2
client/src/views/Works/index.vue

@@ -363,7 +363,7 @@ import { ElMessageBox } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import request from '@/api/request';
 import { accountsApi } from '@/api/accounts';
-import { PLATFORMS, PLATFORM_TYPES, WS_EVENTS } from '@media-manager/shared';
+import { PLATFORMS, AVAILABLE_PLATFORM_TYPES, WS_EVENTS } from '@media-manager/shared';
 import type { Work, WorkStats, PlatformAccount, PlatformType, Comment } from '@media-manager/shared';
 import { useServerStore } from '@/stores/server';
 import { useAuthStore } from '@/stores/auth';
@@ -425,8 +425,9 @@ const commentsPagination = reactive({
   total: 0,
 });
 
+// 平台选项(统一配置:小红书、抖音、视频号、百家号)
 const platforms = computed(() => 
-  PLATFORM_TYPES.map(type => PLATFORMS[type]).filter(p => p.supported)
+  AVAILABLE_PLATFORM_TYPES.map(type => PLATFORMS[type])
 );
 
 const filter = reactive({

+ 1 - 1
server/python/export_platform_statistics_xlsx.py

@@ -45,7 +45,7 @@ except Exception as e:  # pragma: no cover
 
 HEADERS = [
   "平台",
-  "阅读(播放量",
+  "播放(阅读)量",
   "评论量",
   "点赞量",
   "涨粉量",

+ 3 - 3
server/python/export_work_day_overview_xlsx.py

@@ -51,8 +51,8 @@ except Exception as e:  # pragma: no cover - 仅作为运行时保护
 HEADERS = [
   "账号",
   "平台",
-  "总播放量",
-  "昨日播放量",
+  "总播放(阅读)量",
+  "昨日播放(阅读)量",
   "粉丝数",
   "昨日评论",
   "昨日点赞",
@@ -60,7 +60,7 @@ HEADERS = [
   "更新时间",
 ]
 
-COL_WIDTHS = [22, 12, 12, 12, 10, 10, 10, 10, 20]
+COL_WIDTHS = [22, 12, 16, 18, 10, 10, 10, 10, 20]
 
 
 def _safe_int(v):

+ 2 - 2
server/src/services/SystemService.ts

@@ -1,6 +1,6 @@
 import { AppDataSource, SystemConfig, User, PlatformAccount, PublishTask } from '../models/index.js';
 import type { SystemConfig as SystemConfigType } from '@media-manager/shared';
-import { PLATFORM_TYPES } from '@media-manager/shared';
+import { AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import { wsManager } from '../websocket/index.js';
 
 interface UpdateConfigParams {
@@ -32,7 +32,7 @@ export class SystemService {
       allowRegistration: configMap.get('allow_registration') === 'true',
       defaultUserRole: configMap.get('default_user_role') || 'operator',
       maxUploadSize: 4096 * 1024 * 1024, // 4GB
-      supportedPlatforms: PLATFORM_TYPES,
+      supportedPlatforms: AVAILABLE_PLATFORM_TYPES,
     };
   }
 

+ 11 - 0
shared/src/constants/platforms.ts

@@ -177,6 +177,17 @@ export const PLATFORMS: Record<PlatformType, PlatformInfo> = {
 export const PLATFORM_TYPES = Object.keys(PLATFORMS) as PlatformType[];
 
 /**
+ * 可用平台类型(统一配置:仅展示小红书、抖音、视频号、百家号)
+ * 用于平台筛选、下拉选项等所有平台选项卡
+ */
+export const AVAILABLE_PLATFORM_TYPES: PlatformType[] = [
+  'xiaohongshu',  // 小红书
+  'douyin',       // 抖音
+  'weixin_video', // 视频号
+  'baijiahao',    // 百家号
+];
+
+/**
  * 获取平台信息
  */
 export function getPlatformInfo(platform: PlatformType): PlatformInfo {