|
|
@@ -203,42 +203,106 @@
|
|
|
<div class="core-data-content">
|
|
|
<!-- 流量数据卡片 -->
|
|
|
<div class="traffic-data">
|
|
|
- <h4 class="section-title">流量数据</h4>
|
|
|
+ <div class="section-title-row">
|
|
|
+ <h4 class="section-title">流量数据</h4>
|
|
|
+ </div>
|
|
|
<div class="data-cards">
|
|
|
- <div class="data-card highlight">
|
|
|
- <div class="card-label">播放量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.playCount || selectedWork.viewsCount || 0) }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">播放总时长</div>
|
|
|
- <div class="card-value">{{ workDetailData.totalWatchDuration || '0秒' }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">点赞量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.likeCount || selectedWork.likesCount || 0) }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">评论量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.commentCount || selectedWork.commentsCount || 0) }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">收藏量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.collectCount || selectedWork.collectsCount || 0) }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">分享量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.shareCount || selectedWork.sharesCount || 0) }}</div>
|
|
|
- </div>
|
|
|
- <div class="data-card">
|
|
|
- <div class="card-label">涨粉量</div>
|
|
|
- <div class="card-value">{{ formatNumber(workDetailData.fansIncrease || 0) }}</div>
|
|
|
- </div>
|
|
|
+ <!-- 小红书:指标更多 + 支持点选联动趋势图 -->
|
|
|
+ <template v-if="selectedWork.platform === 'xiaohongshu'">
|
|
|
+ <div
|
|
|
+ v-for="item in xhsMetricCards"
|
|
|
+ :key="item.key"
|
|
|
+ class="data-card"
|
|
|
+ :class="{ highlight: activeTrendMetric === item.key }"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric(item.key)"
|
|
|
+ @keyup.enter="setTrendMetric(item.key)"
|
|
|
+ >
|
|
|
+ <div class="card-label">{{ item.label }}</div>
|
|
|
+ <div class="card-value">{{ item.value }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 非小红书:保持原口径 -->
|
|
|
+ <template v-else>
|
|
|
+ <div
|
|
|
+ class="data-card highlight"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('playCount')"
|
|
|
+ @keyup.enter="setTrendMetric('playCount')"
|
|
|
+ >
|
|
|
+ <div class="card-label">播放量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.playCount || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('totalWatchDuration')"
|
|
|
+ @keyup.enter="setTrendMetric('totalWatchDuration')"
|
|
|
+ >
|
|
|
+ <div class="card-label">播放总时长</div>
|
|
|
+ <div class="card-value">{{ workDetailData.totalWatchDuration || '0秒' }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('likeCount')"
|
|
|
+ @keyup.enter="setTrendMetric('likeCount')"
|
|
|
+ >
|
|
|
+ <div class="card-label">点赞量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.likeCount || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('commentCount')"
|
|
|
+ @keyup.enter="setTrendMetric('commentCount')"
|
|
|
+ >
|
|
|
+ <div class="card-label">评论量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.commentCount || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('collectCount')"
|
|
|
+ @keyup.enter="setTrendMetric('collectCount')"
|
|
|
+ >
|
|
|
+ <div class="card-label">收藏量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.collectCount || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('shareCount')"
|
|
|
+ @keyup.enter="setTrendMetric('shareCount')"
|
|
|
+ >
|
|
|
+ <div class="card-label">分享量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.shareCount || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="data-card"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="setTrendMetric('fansIncrease')"
|
|
|
+ @keyup.enter="setTrendMetric('fansIncrease')"
|
|
|
+ >
|
|
|
+ <div class="card-label">涨粉量</div>
|
|
|
+ <div class="card-value">{{ formatNumber(workDetailData.fansIncrease || 0) }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 播放量趋势 -->
|
|
|
<div class="trend-section">
|
|
|
- <h4 class="section-title">播放量趋势</h4>
|
|
|
+ <h4 class="section-title">{{ trendTitle }}</h4>
|
|
|
<div ref="playTrendChartRef" style="height: 300px" v-loading="detailLoading"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -354,34 +418,112 @@ const workList = ref<WorkData[]>([]);
|
|
|
// 详情弹窗相关
|
|
|
const drawerVisible = ref(false);
|
|
|
const selectedWork = ref<WorkData | null>(null);
|
|
|
+// 作品基础信息(来自 works 表 /api/works/:id)
|
|
|
+const selectedWorkBase = ref<any | null>(null);
|
|
|
const activeTab = ref('core');
|
|
|
const detailLoading = ref(false);
|
|
|
|
|
|
// 作品详情数据
|
|
|
interface WorkDetailData {
|
|
|
playCount: number;
|
|
|
+ exposureCount: number;
|
|
|
totalWatchDuration: string;
|
|
|
likeCount: number;
|
|
|
commentCount: number;
|
|
|
collectCount: number;
|
|
|
shareCount: number;
|
|
|
fansIncrease: number;
|
|
|
+ coverClickRate: string;
|
|
|
+ avgWatchDuration: string;
|
|
|
+ completionRate: string;
|
|
|
+ twoSecondExitRate: string;
|
|
|
}
|
|
|
|
|
|
const workDetailData = ref<WorkDetailData>({
|
|
|
playCount: 0,
|
|
|
+ exposureCount: 0,
|
|
|
totalWatchDuration: '0秒',
|
|
|
likeCount: 0,
|
|
|
commentCount: 0,
|
|
|
collectCount: 0,
|
|
|
shareCount: 0,
|
|
|
fansIncrease: 0,
|
|
|
+ coverClickRate: '0',
|
|
|
+ avgWatchDuration: '0',
|
|
|
+ completionRate: '0',
|
|
|
+ twoSecondExitRate: '0',
|
|
|
});
|
|
|
|
|
|
// 播放量趋势图
|
|
|
const playTrendChartRef = ref<HTMLElement>();
|
|
|
let playTrendChart: echarts.ECharts | null = null;
|
|
|
|
|
|
+type TrendMetricKey =
|
|
|
+ | 'exposureCount'
|
|
|
+ | 'playCount'
|
|
|
+ | 'likeCount'
|
|
|
+ | 'commentCount'
|
|
|
+ | 'collectCount'
|
|
|
+ | 'shareCount'
|
|
|
+ | 'coverClickRate'
|
|
|
+ | 'avgWatchDuration'
|
|
|
+ | 'completionRate'
|
|
|
+ | 'twoSecondExitRate'
|
|
|
+ | 'fansIncrease'
|
|
|
+ | 'totalWatchDuration';
|
|
|
+
|
|
|
+const activeTrendMetric = ref<TrendMetricKey>('playCount');
|
|
|
+const workStatsHistory = ref<any[]>([]);
|
|
|
+
|
|
|
+function toIntSafe(v: any): number {
|
|
|
+ const n = Number(String(v ?? '0').replace(/[^\d.-]/g, ''));
|
|
|
+ if (!Number.isFinite(n)) return 0;
|
|
|
+ return Math.max(0, Math.trunc(n));
|
|
|
+}
|
|
|
+
|
|
|
+const trendTitle = computed(() => {
|
|
|
+ if (!selectedWork.value) return '趋势';
|
|
|
+ if (selectedWork.value.platform !== 'xiaohongshu') {
|
|
|
+ const map: Record<TrendMetricKey, string> = {
|
|
|
+ playCount: '播放量趋势',
|
|
|
+ totalWatchDuration: '播放总时长趋势',
|
|
|
+ likeCount: '点赞量趋势',
|
|
|
+ commentCount: '评论量趋势',
|
|
|
+ collectCount: '收藏量趋势',
|
|
|
+ shareCount: '分享量趋势',
|
|
|
+ fansIncrease: '涨粉量趋势',
|
|
|
+ exposureCount: '曝光量趋势',
|
|
|
+ coverClickRate: '封面点击率趋势',
|
|
|
+ avgWatchDuration: '平均观看时长趋势',
|
|
|
+ completionRate: '完播率趋势',
|
|
|
+ twoSecondExitRate: '2s退出率趋势',
|
|
|
+ };
|
|
|
+ return map[activeTrendMetric.value] || '趋势';
|
|
|
+ }
|
|
|
+ const map: Record<TrendMetricKey, string> = {
|
|
|
+ exposureCount: '曝光量趋势',
|
|
|
+ playCount: '播放(阅读)量趋势',
|
|
|
+ likeCount: '点赞量趋势',
|
|
|
+ commentCount: '评论量趋势',
|
|
|
+ collectCount: '收藏量趋势',
|
|
|
+ shareCount: '分享量趋势',
|
|
|
+ coverClickRate: '封面点击率趋势',
|
|
|
+ avgWatchDuration: '平均观看时长趋势',
|
|
|
+ completionRate: '完播率趋势',
|
|
|
+ twoSecondExitRate: '2s退出率趋势',
|
|
|
+ fansIncrease: '涨粉量趋势',
|
|
|
+ totalWatchDuration: '播放总时长趋势',
|
|
|
+ };
|
|
|
+ return map[activeTrendMetric.value] || '趋势';
|
|
|
+});
|
|
|
+
|
|
|
+function setTrendMetric(key: TrendMetricKey) {
|
|
|
+ activeTrendMetric.value = key;
|
|
|
+ if (workStatsHistory.value.length > 0) {
|
|
|
+ updatePlayTrendChart(workStatsHistory.value);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function getPlatformName(platform: PlatformType) {
|
|
|
return PLATFORMS[platform]?.name || platform;
|
|
|
}
|
|
|
@@ -557,70 +699,139 @@ function formatNumber(num: number | null | undefined): string {
|
|
|
return String(num);
|
|
|
}
|
|
|
|
|
|
+function formatDurationSeconds(secLike: any): string {
|
|
|
+ const s = Math.max(0, parseInt(String(secLike ?? '0'), 10) || 0);
|
|
|
+ if (s >= 60) return `${Math.floor(s / 60)}分${s % 60}秒`;
|
|
|
+ return `${s}秒`;
|
|
|
+}
|
|
|
+
|
|
|
+function formatRate(rateLike: any): string {
|
|
|
+ const raw = String(rateLike ?? '0').trim();
|
|
|
+ if (!raw) return '0%';
|
|
|
+ if (raw.includes('%')) return raw;
|
|
|
+ const n = Number(raw);
|
|
|
+ if (Number.isNaN(n)) return raw;
|
|
|
+ // 兼容:有的入库是 0.12(表示 12%),有的是 12(表示 12%)
|
|
|
+ const pct = n <= 1 ? n * 100 : n;
|
|
|
+ return `${pct.toFixed(2).replace(/\.00$/, '')}%`;
|
|
|
+}
|
|
|
+
|
|
|
+function calcDetailRangeDatesFixed14Days(): { start: string; end: string } {
|
|
|
+ const end = dayjs(endDate.value || dayjs().format('YYYY-MM-DD'));
|
|
|
+ const start = end.subtract(13, 'day'); // 近14天(含当天)
|
|
|
+ const publish = dayjs(selectedWork.value?.publishTime);
|
|
|
+ const clampedStart = publish.isValid() && publish.isAfter(start) ? publish : start;
|
|
|
+ return { start: clampedStart.format('YYYY-MM-DD'), end: end.format('YYYY-MM-DD') };
|
|
|
+}
|
|
|
+
|
|
|
+const xhsMetricCards = computed(() => {
|
|
|
+ const d = workDetailData.value;
|
|
|
+ return [
|
|
|
+ { key: 'exposureCount' as const, label: '曝光量', value: formatNumber(d.exposureCount || 0) },
|
|
|
+ { key: 'playCount' as const, label: '播放(阅读)量', value: formatNumber(d.playCount || selectedWork.value?.viewsCount || 0) },
|
|
|
+ { key: 'likeCount' as const, label: '点赞量', value: formatNumber(d.likeCount || selectedWork.value?.likesCount || 0) },
|
|
|
+ { key: 'commentCount' as const, label: '评论量', value: formatNumber(d.commentCount || selectedWork.value?.commentsCount || 0) },
|
|
|
+ { key: 'collectCount' as const, label: '收藏量', value: formatNumber(d.collectCount || selectedWork.value?.collectsCount || 0) },
|
|
|
+ { key: 'shareCount' as const, label: '分享量', value: formatNumber(d.shareCount || selectedWork.value?.sharesCount || 0) },
|
|
|
+ { key: 'coverClickRate' as const, label: '封面点击率', value: formatRate(d.coverClickRate) },
|
|
|
+ { key: 'avgWatchDuration' as const, label: '平均观看时长', value: formatDurationSeconds(d.avgWatchDuration) },
|
|
|
+ { key: 'completionRate' as const, label: '完播率', value: formatRate(d.completionRate) },
|
|
|
+ { key: 'twoSecondExitRate' as const, label: '2s退出率', value: formatRate(d.twoSecondExitRate) },
|
|
|
+ { key: 'fansIncrease' as const, label: '涨粉量', value: formatNumber(d.fansIncrease || 0) },
|
|
|
+ ];
|
|
|
+});
|
|
|
+
|
|
|
// 查看详情
|
|
|
async function handleView(row: WorkData) {
|
|
|
selectedWork.value = row;
|
|
|
+ selectedWorkBase.value = null;
|
|
|
+ activeTrendMetric.value = row.platform === 'xiaohongshu' ? 'exposureCount' : 'playCount';
|
|
|
+ workStatsHistory.value = [];
|
|
|
drawerVisible.value = true;
|
|
|
activeTab.value = 'core';
|
|
|
|
|
|
- // 重置数据
|
|
|
+ // 先用列表行做“瞬时占位”(列表来自区间汇总,可能不等于 works 表累计值)
|
|
|
workDetailData.value = {
|
|
|
playCount: row.viewsCount || 0,
|
|
|
+ exposureCount: 0,
|
|
|
totalWatchDuration: '0秒',
|
|
|
likeCount: row.likesCount || 0,
|
|
|
commentCount: row.commentsCount || 0,
|
|
|
collectCount: row.collectsCount || 0,
|
|
|
shareCount: row.sharesCount || 0,
|
|
|
fansIncrease: 0,
|
|
|
+ coverClickRate: '0',
|
|
|
+ avgWatchDuration: '0',
|
|
|
+ completionRate: '0',
|
|
|
+ twoSecondExitRate: '0',
|
|
|
};
|
|
|
|
|
|
- // 加载作品详情数据
|
|
|
+ // 1) 加载 works 表基础信息(标题、发布时间、累计播放/点赞等)
|
|
|
+ await loadWorkBase(row.id);
|
|
|
+ // 2) 加载 work_day_statistics 历史快照(用于“最新累计值”与趋势)
|
|
|
await loadWorkDetail(row.id);
|
|
|
}
|
|
|
|
|
|
+// 加载作品基础信息(works 表)
|
|
|
+async function loadWorkBase(workId: number) {
|
|
|
+ try {
|
|
|
+ const data = await request.get(`/api/works/${workId}`);
|
|
|
+ if (!data) return;
|
|
|
+ selectedWorkBase.value = data;
|
|
|
+
|
|
|
+ // 基础信息补齐:以 works 表为准(如果 works 缺失字段则回退到列表行)
|
|
|
+ if (selectedWork.value) {
|
|
|
+ selectedWork.value = {
|
|
|
+ ...selectedWork.value,
|
|
|
+ title: data.title || selectedWork.value.title,
|
|
|
+ publishTime: data.publishTime || selectedWork.value.publishTime,
|
|
|
+ coverUrl: data.coverUrl || selectedWork.value.coverUrl,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 顶部卡片:按需求展示 works.yesterday_*(昨日快照)
|
|
|
+ workDetailData.value = {
|
|
|
+ playCount: toIntSafe(data.yesterdayPlayCount ?? 0),
|
|
|
+ exposureCount: toIntSafe(data.yesterdayExposureCount ?? 0),
|
|
|
+ totalWatchDuration: workDetailData.value.totalWatchDuration || '0秒',
|
|
|
+ likeCount: toIntSafe(data.yesterdayLikeCount ?? 0),
|
|
|
+ commentCount: toIntSafe(data.yesterdayCommentCount ?? 0),
|
|
|
+ collectCount: toIntSafe(data.yesterdayCollectCount ?? 0),
|
|
|
+ shareCount: toIntSafe(data.yesterdayShareCount ?? 0),
|
|
|
+ fansIncrease: toIntSafe(data.yesterdayFansIncrease ?? 0),
|
|
|
+ coverClickRate: String(data.yesterdayCoverClickRate ?? '0'),
|
|
|
+ avgWatchDuration: String(data.yesterdayAvgWatchDuration ?? '0'),
|
|
|
+ completionRate: String(data.yesterdayCompletionRate ?? '0'),
|
|
|
+ twoSecondExitRate: String(data.yesterdayTwoSecondExitRate ?? '0'),
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ // works 表请求失败不影响后续趋势展示
|
|
|
+ console.warn('加载作品基础信息失败:', error);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 加载作品详情数据(历史统计数据)
|
|
|
async function loadWorkDetail(workId: number) {
|
|
|
detailLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
- // 计算日期范围:从作品发布时间到结束日期(或今天)
|
|
|
- const publishDate = dayjs(selectedWork.value?.publishTime);
|
|
|
- const queryEndDate = dayjs(endDate.value || dayjs().format('YYYY-MM-DD'));
|
|
|
- const startDateStr = publishDate.isValid() ? publishDate.format('YYYY-MM-DD') : dayjs().subtract(30, 'day').format('YYYY-MM-DD');
|
|
|
- const endDateStr = queryEndDate.format('YYYY-MM-DD');
|
|
|
+ // 趋势固定:近 14 天(按 work_day_statistics 日新增量口径)
|
|
|
+ const { start: startDateStr, end: endDateStr } = calcDetailRangeDatesFixed14Days();
|
|
|
|
|
|
- // 调用接口获取该作品的历史统计数据
|
|
|
+ // 调用接口获取该作品的历史统计数据(work_day_statistics 快照)
|
|
|
const data = await request.get(`/api/work-day-statistics/work/${workId}`, {
|
|
|
params: {
|
|
|
- start_date: startDateStr,
|
|
|
- end_date: endDateStr,
|
|
|
+ // 注意:后端校验参数名为 startDate/endDate
|
|
|
+ startDate: startDateStr,
|
|
|
+ endDate: endDateStr,
|
|
|
},
|
|
|
});
|
|
|
|
|
|
if (data && Array.isArray(data)) {
|
|
|
const workStats = data;
|
|
|
+ workStatsHistory.value = workStats;
|
|
|
|
|
|
- // 计算汇总数据(取最新一条记录的累计值)
|
|
|
- if (workStats.length > 0) {
|
|
|
- const latest = workStats[workStats.length - 1];
|
|
|
- const totalWatchDuration = latest.totalWatchDuration || '0';
|
|
|
- const durationSeconds = parseInt(totalWatchDuration) || 0;
|
|
|
- const durationStr = durationSeconds >= 60
|
|
|
- ? `${Math.floor(durationSeconds / 60)}分${durationSeconds % 60}秒`
|
|
|
- : `${durationSeconds}秒`;
|
|
|
-
|
|
|
- workDetailData.value = {
|
|
|
- playCount: latest.playCount || 0,
|
|
|
- totalWatchDuration: durationStr,
|
|
|
- likeCount: latest.likeCount || 0,
|
|
|
- commentCount: latest.commentCount || 0,
|
|
|
- collectCount: latest.collectCount || 0,
|
|
|
- shareCount: latest.shareCount || 0,
|
|
|
- fansIncrease: latest.fansIncrease || 0,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 绘制播放量趋势图
|
|
|
+ // 绘制趋势图(按天新增量)
|
|
|
await nextTick();
|
|
|
updatePlayTrendChart(workStats);
|
|
|
}
|
|
|
@@ -633,7 +844,7 @@ async function loadWorkDetail(workId: number) {
|
|
|
}
|
|
|
|
|
|
// 更新播放量趋势图
|
|
|
-function updatePlayTrendChart(stats: Array<{ recordDate: string; playCount: number }>) {
|
|
|
+function updatePlayTrendChart(stats: Array<any>) {
|
|
|
if (!playTrendChartRef.value) return;
|
|
|
|
|
|
if (!playTrendChart) {
|
|
|
@@ -646,7 +857,27 @@ function updatePlayTrendChart(stats: Array<{ recordDate: string; playCount: numb
|
|
|
);
|
|
|
|
|
|
const dates = sortedStats.map(s => dayjs(s.recordDate).format('YYYY-MM-DD'));
|
|
|
- const playCounts = sortedStats.map(s => s.playCount || 0);
|
|
|
+
|
|
|
+ const metric = activeTrendMetric.value;
|
|
|
+ const seriesName = trendTitle.value.replace('趋势', '');
|
|
|
+
|
|
|
+ const values = sortedStats.map((s) => {
|
|
|
+ const v = s?.[metric];
|
|
|
+ if (metric === 'coverClickRate' || metric === 'completionRate' || metric === 'twoSecondExitRate') {
|
|
|
+ const raw = String(v ?? '0').trim();
|
|
|
+ if (raw.includes('%')) return Number(raw.replace('%', '')) || 0;
|
|
|
+ const n = Number(raw);
|
|
|
+ if (Number.isNaN(n)) return 0;
|
|
|
+ return n <= 1 ? n * 100 : n;
|
|
|
+ }
|
|
|
+ if (metric === 'avgWatchDuration') {
|
|
|
+ return Math.max(0, parseInt(String(v ?? '0'), 10) || 0);
|
|
|
+ }
|
|
|
+ if (metric === 'totalWatchDuration') {
|
|
|
+ return Math.max(0, parseInt(String(v ?? '0'), 10) || 0);
|
|
|
+ }
|
|
|
+ return Number(v) || 0;
|
|
|
+ });
|
|
|
|
|
|
const option: echarts.EChartsOption = {
|
|
|
tooltip: {
|
|
|
@@ -684,10 +915,10 @@ function updatePlayTrendChart(stats: Array<{ recordDate: string; playCount: numb
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
- name: '播放量',
|
|
|
+ name: seriesName || '趋势',
|
|
|
type: 'line',
|
|
|
smooth: true,
|
|
|
- data: playCounts,
|
|
|
+ data: values,
|
|
|
itemStyle: {
|
|
|
color: '#ff6b9d',
|
|
|
},
|
|
|
@@ -919,12 +1150,27 @@ onMounted(() => {
|
|
|
.traffic-data {
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
+ .section-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
.section-title {
|
|
|
- margin: 0 0 16px 0;
|
|
|
+ margin: 0;
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
color: $text-primary;
|
|
|
}
|
|
|
+
|
|
|
+ .detail-range {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
|
|
|
.data-cards {
|
|
|
display: grid;
|
|
|
@@ -949,6 +1195,7 @@ onMounted(() => {
|
|
|
padding: 20px 16px;
|
|
|
text-align: center;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
&.highlight {
|
|
|
background: #fff5f7;
|