|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { useState, useMemo, useCallback, memo } from 'react';
|
|
|
|
|
import { Table, Input, Select, Button, Card, Space, message, Modal, Descriptions, Spin } from 'antd';
|
|
|
|
|
import { SearchOutlined, EyeOutlined } from '@ant-design/icons';
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
@@ -34,8 +34,8 @@ function formatInt(num: number | null | undefined): string {
|
|
|
|
|
return Math.round(num).toLocaleString('zh-CN');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 详情弹窗组件
|
|
|
|
|
function DetailModal({
|
|
|
|
|
// 详情弹窗组件 - 使用 memo 避免不必要的重渲染
|
|
|
|
|
const DetailModal = memo(function DetailModal({
|
|
|
|
|
visible,
|
|
|
|
|
data,
|
|
|
|
|
loading,
|
|
|
|
@@ -53,18 +53,18 @@ function DetailModal({
|
|
|
|
|
onCancel={onClose}
|
|
|
|
|
footer={null}
|
|
|
|
|
width={900}
|
|
|
|
|
styles={{ body: { maxHeight: '70vh', overflowY: 'auto', userSelect: 'text' } }}
|
|
|
|
|
styles={{ body: { maxHeight: '70vh', overflowY: 'auto', userSelect: 'text', WebkitUserSelect: 'text' } }}
|
|
|
|
|
>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: 50 }}>
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
</div>
|
|
|
|
|
) : data ? (
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%', userSelect: 'text', WebkitUserSelect: 'text' } as React.CSSProperties}>
|
|
|
|
|
{/* 基础信息 */}
|
|
|
|
|
<Descriptions title="基础信息" bordered size="small" column={2}>
|
|
|
|
|
<Descriptions.Item label="达人昵称">{data.base_info.star_nickname || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="达人unique_id">{data.base_info.star_unique_id || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions title="基础信息" bordered size="small" column={2} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="达人昵称"><span style={{ userSelect: 'text' }}>{data.base_info.star_nickname || '-'}</span></Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="达人unique_id"><span style={{ userSelect: 'text' }}>{data.base_info.star_unique_id || '-'}</span></Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="视频ID">{data.base_info.vid || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="发布时间">{data.base_info.create_date || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="爆文类型">{data.base_info.hot_type || '-'}</Descriptions.Item>
|
|
|
|
@@ -82,7 +82,7 @@ function DetailModal({
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
{/* 触达指标 */}
|
|
|
|
|
<Descriptions title="触达指标" bordered size="small" column={4}>
|
|
|
|
|
<Descriptions title="触达指标" bordered size="small" column={4} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="自然曝光数">{formatInt(data.reach_metrics.natural_play_cnt)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="加热曝光数">{formatInt(data.reach_metrics.heated_play_cnt)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="总曝光数">{formatInt(data.reach_metrics.total_play_cnt)}</Descriptions.Item>
|
|
|
|
@@ -93,14 +93,14 @@ function DetailModal({
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
{/* A3指标 */}
|
|
|
|
|
<Descriptions title="A3指标" bordered size="small" column={3}>
|
|
|
|
|
<Descriptions title="A3指标" bordered size="small" column={3} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="新增A3">{formatInt(data.a3_metrics.total_new_a3_cnt)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="加热新增A3">{formatInt(data.a3_metrics.heated_new_a3_cnt)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="自然新增A3">{formatInt(data.a3_metrics.natural_new_a3_cnt)}</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
{/* 搜索指标 */}
|
|
|
|
|
<Descriptions title="搜索指标" bordered size="small" column={3}>
|
|
|
|
|
<Descriptions title="搜索指标" bordered size="small" column={3} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="回搜人数">{formatInt(data.search_metrics.back_search_uv)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="回搜次数">{formatInt(data.search_metrics.back_search_cnt)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="看后搜人数">{formatInt(data.search_metrics.after_view_search_uv)}</Descriptions.Item>
|
|
|
|
@@ -109,14 +109,14 @@ function DetailModal({
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
{/* 费用指标 */}
|
|
|
|
|
<Descriptions title="费用指标" bordered size="small" column={3}>
|
|
|
|
|
<Descriptions title="费用指标" bordered size="small" column={3} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="预估总费用">{formatNumber(data.cost_metrics.total_cost)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="预估加热费用">{formatNumber(data.cost_metrics.heated_cost)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="预估视频采买费用">{formatNumber(data.cost_metrics.estimated_video_cost)}</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
{/* 成本指标 */}
|
|
|
|
|
<Descriptions title="成本指标" bordered size="small" column={3}>
|
|
|
|
|
<Descriptions title="成本指标" bordered size="small" column={3} contentStyle={{ userSelect: 'text', cursor: 'text' }}>
|
|
|
|
|
<Descriptions.Item label="预估CPM">{formatNumber(data.calculated_metrics.estimated_cpm)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="预估自然CPM">{formatNumber(data.calculated_metrics.estimated_natural_cpm)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="预估CPA3">{formatNumber(data.calculated_metrics.estimated_cp_a3)}</Descriptions.Item>
|
|
|
|
@@ -128,7 +128,7 @@ function DetailModal({
|
|
|
|
|
) : null}
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default function VideoAnalysis() {
|
|
|
|
|
const [searchType, setSearchType] = useState<SearchType>('star_id');
|
|
|
|
@@ -141,7 +141,11 @@ export default function VideoAnalysis() {
|
|
|
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
|
|
|
const [detailData, setDetailData] = useState<VideoAnalysisData | null>(null);
|
|
|
|
|
|
|
|
|
|
const handleSearch = async () => {
|
|
|
|
|
// 详情缓存 - 避免重复请求
|
|
|
|
|
const [detailCache, setDetailCache] = useState<Record<string, VideoAnalysisData>>({});
|
|
|
|
|
|
|
|
|
|
// 使用 useCallback 包裹搜索处理器
|
|
|
|
|
const handleSearch = useCallback(async () => {
|
|
|
|
|
if (!searchValue.trim()) {
|
|
|
|
|
message.warning(`请输入${SEARCH_TYPE_OPTIONS.find(o => o.value === searchType)?.label}`);
|
|
|
|
|
return;
|
|
|
|
@@ -167,9 +171,17 @@ export default function VideoAnalysis() {
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [searchType, searchValue]);
|
|
|
|
|
|
|
|
|
|
// 使用 useCallback 包裹详情查看处理器,带缓存逻辑
|
|
|
|
|
const handleViewDetail = useCallback(async (itemId: string) => {
|
|
|
|
|
// 检查缓存
|
|
|
|
|
if (detailCache[itemId]) {
|
|
|
|
|
setDetailData(detailCache[itemId]);
|
|
|
|
|
setDetailVisible(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleViewDetail = async (itemId: string) => {
|
|
|
|
|
setDetailVisible(true);
|
|
|
|
|
setDetailLoading(true);
|
|
|
|
|
setDetailData(null);
|
|
|
|
@@ -178,6 +190,8 @@ export default function VideoAnalysis() {
|
|
|
|
|
const response = await getVideoAnalysis(itemId);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
setDetailData(response.data);
|
|
|
|
|
// 缓存结果
|
|
|
|
|
setDetailCache(prev => ({ ...prev, [itemId]: response.data }));
|
|
|
|
|
} else {
|
|
|
|
|
message.error(response.error || '获取详情失败');
|
|
|
|
|
setDetailVisible(false);
|
|
|
|
@@ -188,10 +202,15 @@ export default function VideoAnalysis() {
|
|
|
|
|
} finally {
|
|
|
|
|
setDetailLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [detailCache]);
|
|
|
|
|
|
|
|
|
|
// 表格列定义
|
|
|
|
|
const columns: ColumnsType<VideoListItem> = [
|
|
|
|
|
// 关闭详情弹窗的回调
|
|
|
|
|
const handleCloseDetail = useCallback(() => {
|
|
|
|
|
setDetailVisible(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 使用 useMemo 包裹表格列定义,避免每次重建
|
|
|
|
|
const columns: ColumnsType<VideoListItem> = useMemo(() => [
|
|
|
|
|
{
|
|
|
|
|
title: '达人昵称',
|
|
|
|
|
dataIndex: 'star_nickname',
|
|
|
|
@@ -322,7 +341,7 @@ export default function VideoAnalysis() {
|
|
|
|
|
</Button>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
], [handleViewDetail]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: 24, maxWidth: 1600, margin: '0 auto' }}>
|
|
|
|
@@ -357,14 +376,15 @@ export default function VideoAnalysis() {
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 结果表格 */}
|
|
|
|
|
{/* 结果表格 - 启用虚拟滚动 */}
|
|
|
|
|
<Card>
|
|
|
|
|
<Table
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={listData}
|
|
|
|
|
rowKey="item_id"
|
|
|
|
|
loading={loading}
|
|
|
|
|
scroll={{ x: 1800 }}
|
|
|
|
|
virtual
|
|
|
|
|
scroll={{ x: 1800, y: 600 }}
|
|
|
|
|
pagination={{
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
@@ -379,7 +399,7 @@ export default function VideoAnalysis() {
|
|
|
|
|
visible={detailVisible}
|
|
|
|
|
data={detailData}
|
|
|
|
|
loading={detailLoading}
|
|
|
|
|
onClose={() => setDetailVisible(false)}
|
|
|
|
|
onClose={handleCloseDetail}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|