'use client'; 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'; import { VideoAnalysisData } from '@/types'; import { searchVideos, VideoListItem, getVideoAnalysis } from '@/lib/api'; // 搜索类型选项 type SearchType = 'star_id' | 'unique_id' | 'nickname'; const SEARCH_TYPE_OPTIONS = [ { value: 'star_id' as SearchType, label: '星图ID' }, { value: 'unique_id' as SearchType, label: '达人unique_id' }, { value: 'nickname' as SearchType, label: '达人昵称' }, ]; const SEARCH_PLACEHOLDER: Record = { star_id: '请输入星图ID', unique_id: '请输入达人unique_id', nickname: '请输入达人昵称关键词', }; // 格式化数字(千分位,保留2位小数) function formatNumber(num: number | null | undefined): string { if (num === null || num === undefined) return '-'; return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // 格式化整数(千分位) function formatInt(num: number | null | undefined): string { if (num === null || num === undefined) return '-'; return Math.round(num).toLocaleString('zh-CN'); } // 详情弹窗组件 - 使用 memo 避免不必要的重渲染 const DetailModal = memo(function DetailModal({ visible, data, loading, onClose, }: { visible: boolean; data: VideoAnalysisData | null; loading: boolean; onClose: () => void; }) { return ( {loading ? (
) : data ? ( {/* 基础信息 */} {data.base_info.star_nickname || '-'} {data.base_info.star_unique_id || '-'} {data.base_info.vid || '-'} {data.base_info.create_date || '-'} {data.base_info.hot_type || '-'} {data.base_info.industry_id || '-'} {data.base_info.brand_name || data.base_info.brand_id || '-'} {data.base_info.video_url ? ( {data.base_info.title || '查看视频'} ) : ( data.base_info.title || '-' )} {/* 触达指标 */} {formatInt(data.reach_metrics.natural_play_cnt)} {formatInt(data.reach_metrics.heated_play_cnt)} {formatInt(data.reach_metrics.total_play_cnt)} {formatInt(data.reach_metrics.total_interaction_cnt)} {formatInt(data.reach_metrics.digg_cnt)} {formatInt(data.reach_metrics.share_cnt)} {formatInt(data.reach_metrics.comment_cnt)} {/* A3指标 */} {formatInt(data.a3_metrics.total_new_a3_cnt)} {formatInt(data.a3_metrics.heated_new_a3_cnt)} {formatInt(data.a3_metrics.natural_new_a3_cnt)} {/* 搜索指标 */} {formatInt(data.search_metrics.back_search_uv)} {formatInt(data.search_metrics.back_search_cnt)} {formatInt(data.search_metrics.after_view_search_uv)} {formatInt(data.search_metrics.after_view_search_cnt)} {formatNumber(data.search_metrics.estimated_natural_search_uv)} {/* 费用指标 */} {formatNumber(data.cost_metrics.total_cost)} {formatNumber(data.cost_metrics.heated_cost)} {formatNumber(data.cost_metrics.estimated_video_cost)} {/* 成本指标 */} {formatNumber(data.calculated_metrics.estimated_cpm)} {formatNumber(data.calculated_metrics.estimated_natural_cpm)} {formatNumber(data.calculated_metrics.estimated_cp_a3)} {formatNumber(data.calculated_metrics.estimated_natural_cp_a3)} {formatNumber(data.calculated_metrics.estimated_cp_search)} {formatNumber(data.calculated_metrics.estimated_natural_cp_search)} ) : null}
); }); export default function VideoAnalysis() { const [searchType, setSearchType] = useState('star_id'); const [searchValue, setSearchValue] = useState(''); const [loading, setLoading] = useState(false); const [listData, setListData] = useState([]); // 详情弹窗状态 const [detailVisible, setDetailVisible] = useState(false); const [detailLoading, setDetailLoading] = useState(false); const [detailData, setDetailData] = useState(null); // 详情缓存 - 避免重复请求 const [detailCache, setDetailCache] = useState>({}); // 使用 useCallback 包裹搜索处理器 const handleSearch = useCallback(async () => { if (!searchValue.trim()) { message.warning(`请输入${SEARCH_TYPE_OPTIONS.find(o => o.value === searchType)?.label}`); return; } setLoading(true); try { const response = await searchVideos({ type: searchType, value: searchValue.trim(), }); if (response.success) { setListData(response.data as VideoListItem[]); if ((response.data as VideoListItem[]).length === 0) { message.info('未找到相关视频'); } } else { message.error(response.error || '搜索失败'); } } catch (err) { message.error(err instanceof Error ? err.message : '搜索失败'); } finally { setLoading(false); } }, [searchType, searchValue]); // 使用 useCallback 包裹详情查看处理器,带缓存逻辑 const handleViewDetail = useCallback(async (itemId: string) => { // 检查缓存 if (detailCache[itemId]) { setDetailData(detailCache[itemId]); setDetailVisible(true); return; } setDetailVisible(true); setDetailLoading(true); setDetailData(null); try { const response = await getVideoAnalysis(itemId); if (response.success) { setDetailData(response.data); // 缓存结果 setDetailCache(prev => ({ ...prev, [itemId]: response.data })); } else { message.error(response.error || '获取详情失败'); setDetailVisible(false); } } catch (err) { message.error(err instanceof Error ? err.message : '获取详情失败'); setDetailVisible(false); } finally { setDetailLoading(false); } }, [detailCache]); // 关闭详情弹窗的回调 const handleCloseDetail = useCallback(() => { setDetailVisible(false); }, []); // 使用 useMemo 包裹表格列定义,避免每次重建 const columns: ColumnsType = useMemo(() => [ { title: '达人昵称', dataIndex: 'star_nickname', key: 'star_nickname', width: 120, fixed: 'left', render: (text) => text || '-', }, { title: '视频标题', dataIndex: 'title', key: 'title', width: 200, ellipsis: true, render: (text, record) => record.video_url ? ( {text || '查看视频'} ) : ( text || '-' ), }, { title: '发布时间', dataIndex: 'create_date', key: 'create_date', width: 110, render: (text) => (text ? text.split('T')[0] : '-'), }, { title: '爆文类型', dataIndex: 'hot_type', key: 'hot_type', width: 90, render: (text) => text || '-', }, { title: '合作行业', dataIndex: 'industry_id', key: 'industry_id', width: 90, render: (text) => text || '-', }, { title: '合作品牌', dataIndex: 'brand_name', key: 'brand_name', width: 100, render: (text) => text || '-', }, { title: '新增A3', dataIndex: 'total_new_a3_cnt', key: 'total_new_a3_cnt', width: 90, align: 'right', render: (val) => formatInt(val), }, { title: '加热A3', dataIndex: 'heated_new_a3_cnt', key: 'heated_new_a3_cnt', width: 90, align: 'right', render: (val) => formatInt(val), }, { title: '自然A3', dataIndex: 'natural_new_a3_cnt', key: 'natural_new_a3_cnt', width: 90, align: 'right', render: (val) => formatInt(val), }, { title: '预估自然CPM', dataIndex: 'estimated_natural_cpm', key: 'estimated_natural_cpm', width: 110, align: 'right', render: (val) => formatNumber(val), }, { title: '预估CPA3', dataIndex: 'estimated_cp_a3', key: 'estimated_cp_a3', width: 100, align: 'right', render: (val) => formatNumber(val), }, { title: '预估自然CPA3', dataIndex: 'estimated_natural_cp_a3', key: 'estimated_natural_cp_a3', width: 110, align: 'right', render: (val) => formatNumber(val), }, { title: '预估CPsearch', dataIndex: 'estimated_cp_search', key: 'estimated_cp_search', width: 110, align: 'right', render: (val) => formatNumber(val), }, { title: '自然CPsearch', dataIndex: 'estimated_natural_cp_search', key: 'estimated_natural_cp_search', width: 110, align: 'right', render: (val) => formatNumber(val), }, { title: '操作', key: 'action', width: 80, fixed: 'right', render: (_, record) => ( ), }, ], [handleViewDetail]); return (
{/* 搜索区域 */}

KOL 视频分析

setSearchValue(e.target.value)} placeholder={SEARCH_PLACEHOLDER[searchType]} onPressEnter={handleSearch} style={{ flex: 1 }} />
{/* 结果表格 - 启用虚拟滚动 */} `共 ${total} 条`, }} locale={{ emptyText: '暂无数据,请输入搜索条件' }} /> {/* 详情弹窗 */} ); }