|
@@ -8,15 +8,17 @@ import {
|
|
|
Typography,
|
|
Typography,
|
|
|
Space,
|
|
Space,
|
|
|
Spin,
|
|
Spin,
|
|
|
- Switch,
|
|
|
|
|
Pagination,
|
|
Pagination,
|
|
|
message,
|
|
message,
|
|
|
Modal,
|
|
Modal,
|
|
|
|
|
+ Tag,
|
|
|
|
|
+ Popover,
|
|
|
|
|
+ Descriptions,
|
|
|
} from 'antd';
|
|
} from 'antd';
|
|
|
import { CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
|
|
import { CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
|
|
|
import { VideoListResponse } from '@src/views/publishContent/weGZH/components/types';
|
|
import { VideoListResponse } from '@src/views/publishContent/weGZH/components/types';
|
|
|
import http from '@src/http';
|
|
import http from '@src/http';
|
|
|
-import { getVideoContentListApi, getDemandVideoContentListApi } from '@src/http/api';
|
|
|
|
|
|
|
+import { getVideoContentListApi } from '@src/http/api';
|
|
|
import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
|
|
import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
|
|
|
import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
|
|
import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
|
|
|
|
|
|
|
@@ -26,6 +28,46 @@ console.log('[VideoSelectModal] MODE=', import.meta.env.MODE, 'IS_NON_PROD=', IS
|
|
|
|
|
|
|
|
type LoadMode = 'replace' | 'append' | 'jump';
|
|
type LoadMode = 'replace' | 'append' | 'jump';
|
|
|
|
|
|
|
|
|
|
+const SOURCE_OPTIONS = [
|
|
|
|
|
+ { label: '全部', value: '' },
|
|
|
|
|
+ { label: '先验', value: 'prior' },
|
|
|
|
|
+ { label: '后验', value: 'posterior' },
|
|
|
|
|
+ { label: '全局热门', value: 'hot' },
|
|
|
|
|
+];
|
|
|
|
|
+const SOURCE_LABEL: Record<string, string> = { prior: '先验', posterior: '后验', hot: '热门' };
|
|
|
|
|
+const SOURCE_COLOR: Record<string, string> = { prior: 'blue', posterior: 'green', hot: 'orange' };
|
|
|
|
|
+
|
|
|
|
|
+const renderDemandDetail = (video: WeVideoItem) => (
|
|
|
|
|
+ <div style={{ maxWidth: 460 }}>
|
|
|
|
|
+ <Descriptions size="small" column={1} bordered labelStyle={{ width: 110 }}>
|
|
|
|
|
+ <Descriptions.Item label="来源">{video.source ? SOURCE_LABEL[video.source] || video.source : '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="人群分组">{video.crowdSegment || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求策略">{video.demandStrategy || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="点位类型">{video.pointType || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="标准元素">{video.standardElement || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="维度">{video.dimension || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="分类">{video.categoryName || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求id">{video.demandId || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求内容id">{video.demandContentId || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求内容标题">{video.demandContentTitle || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求内容主题">{video.demandContentTopic || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="人群包">{video.crowdPackage || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="转化目标">{video.conversionTarget || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="合作伙伴">{video.partner || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="账号">{video.account || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="场景值">{video.sceneValue || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求过滤排序">{video.demandFilterSortStrategy || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="需求类型">{video.demandType || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="驱动维度时间">{video.driveDimensionTime || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="实验id">{video.experimentId || '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="score">{video.score != null ? video.score.toFixed(4) : '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="行业裂变率">{video.industryFissionRate != null ? video.industryFissionRate.toFixed(4) : '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="本渠道裂变率">{video.channelFissionRate != null ? video.channelFissionRate.toFixed(4) : '-'}</Descriptions.Item>
|
|
|
|
|
+ <Descriptions.Item label="推荐指数">{video.recommendScore != null ? video.recommendScore.toFixed(4) : '-'}</Descriptions.Item>
|
|
|
|
|
+ </Descriptions>
|
|
|
|
|
+ </div>
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
interface VideoSelectModalProps {
|
|
interface VideoSelectModalProps {
|
|
|
planType: WeComPlanType;
|
|
planType: WeComPlanType;
|
|
|
visible: boolean;
|
|
visible: boolean;
|
|
@@ -58,7 +100,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
|
|
const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
|
|
|
const [viewingPage, setViewingPage] = useState(1);
|
|
const [viewingPage, setViewingPage] = useState(1);
|
|
|
const [pageAnchors, setPageAnchors] = useState<Map<number, number>>(new Map()); // videoId -> page
|
|
const [pageAnchors, setPageAnchors] = useState<Map<number, number>>(new Map()); // videoId -> page
|
|
|
- const [useDemandRecall, setUseDemandRecall] = useState(false); // 测试入口:群体特征召回
|
|
|
|
|
|
|
+ const [source, setSource] = useState<string>(''); // 测试入口:数据来源筛选(prior/posterior/hot/空)
|
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
|
const pageObserverRef = useRef<IntersectionObserver | null>(null);
|
|
const pageObserverRef = useRef<IntersectionObserver | null>(null);
|
|
|
const passedPagesRef = useRef<Set<number>>(new Set());
|
|
const passedPagesRef = useRef<Set<number>>(new Set());
|
|
@@ -87,12 +129,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
setCurrentPage(pageNum);
|
|
setCurrentPage(pageNum);
|
|
|
|
|
|
|
|
const type = planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复;
|
|
const type = planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复;
|
|
|
- const apiUrl = useDemandRecall ? getDemandVideoContentListApi : getVideoContentListApi;
|
|
|
|
|
- const requestParams = useDemandRecall
|
|
|
|
|
- ? { title: searchTerm, sort, type, pageNum, pageSize: PAGE_SIZE }
|
|
|
|
|
- : { category, title: searchTerm, sort, type, pageNum, pageSize: PAGE_SIZE };
|
|
|
|
|
|
|
+ const requestParams = { category, title: searchTerm, sort, type, pageNum, pageSize: PAGE_SIZE, ...(source ? { source } : {}) };
|
|
|
|
|
|
|
|
- const res = await http.post<VideoListResponse>(apiUrl, requestParams).catch(() => {
|
|
|
|
|
|
|
+ const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
|
|
|
message.error('获取视频列表失败');
|
|
message.error('获取视频列表失败');
|
|
|
}).finally(() => {
|
|
}).finally(() => {
|
|
|
if (isAppend) {
|
|
if (isAppend) {
|
|
@@ -211,15 +250,14 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
getVideoList(1, 'replace');
|
|
getVideoList(1, 'replace');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const handleToggleDemandRecall = (checked: boolean) => {
|
|
|
|
|
- setUseDemandRecall(checked);
|
|
|
|
|
|
|
+ const handleChangeSource = (value: string) => {
|
|
|
|
|
+ setSource(value);
|
|
|
setHasMore(true);
|
|
setHasMore(true);
|
|
|
setVideoList([]);
|
|
setVideoList([]);
|
|
|
setViewingPage(1);
|
|
setViewingPage(1);
|
|
|
setPageAnchors(new Map());
|
|
setPageAnchors(new Map());
|
|
|
passedPagesRef.current.clear();
|
|
passedPagesRef.current.clear();
|
|
|
drawerBodyRef.current?.scrollTo({ top: 0 });
|
|
drawerBodyRef.current?.scrollTo({ top: 0 });
|
|
|
- // 切换后重新请求(用最新 state 值)
|
|
|
|
|
setTimeout(() => getVideoListRef.current?.(1, 'replace'), 0);
|
|
setTimeout(() => getVideoListRef.current?.(1, 'replace'), 0);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -336,8 +374,13 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
<Button type="primary" loading={loading} onClick={handleSearch}>搜索</Button>
|
|
<Button type="primary" loading={loading} onClick={handleSearch}>搜索</Button>
|
|
|
{IS_NON_PROD && (
|
|
{IS_NON_PROD && (
|
|
|
<div className="flex items-center gap-2 ml-auto">
|
|
<div className="flex items-center gap-2 ml-auto">
|
|
|
- <span className="text-gray-600">群体召回(测试):</span>
|
|
|
|
|
- <Switch checked={useDemandRecall} onChange={handleToggleDemandRecall} />
|
|
|
|
|
|
|
+ <span className="text-gray-600">来源(测试):</span>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ value={source}
|
|
|
|
|
+ style={{ width: 110 }}
|
|
|
|
|
+ onChange={handleChangeSource}
|
|
|
|
|
+ options={SOURCE_OPTIONS}
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
@@ -348,7 +391,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
const isSelected = selectedVideoIds.has(video.videoId);
|
|
const isSelected = selectedVideoIds.has(video.videoId);
|
|
|
const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
|
|
const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
|
|
|
const startsPage = pageAnchors.get(video.videoId);
|
|
const startsPage = pageAnchors.get(video.videoId);
|
|
|
- return (
|
|
|
|
|
|
|
+ const cardNode = (
|
|
|
<Card
|
|
<Card
|
|
|
key={video.videoId}
|
|
key={video.videoId}
|
|
|
{...(startsPage ? { 'data-page-anchor': startsPage } : {})}
|
|
{...(startsPage ? { 'data-page-anchor': startsPage } : {})}
|
|
@@ -356,6 +399,15 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
styles={{ body: { padding: 0 } }}
|
|
styles={{ body: { padding: 0 } }}
|
|
|
onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
|
|
onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
|
|
|
>
|
|
>
|
|
|
|
|
+ {video.source && (
|
|
|
|
|
+ <Tag
|
|
|
|
|
+ color={SOURCE_COLOR[video.source]}
|
|
|
|
|
+ className="!absolute !mr-0 z-10"
|
|
|
|
|
+ style={{ top: 8, left: 8 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {SOURCE_LABEL[video.source] || video.source}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ )}
|
|
|
<div className="p-3">
|
|
<div className="p-3">
|
|
|
<Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
|
|
<Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
|
|
|
<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.title}>{video.title}</Paragraph>
|
|
<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.title}>{video.title}</Paragraph>
|
|
@@ -396,6 +448,20 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
)} */}
|
|
)} */}
|
|
|
</Card>
|
|
</Card>
|
|
|
);
|
|
);
|
|
|
|
|
+ if (IS_NON_PROD) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Popover
|
|
|
|
|
+ key={video.videoId}
|
|
|
|
|
+ trigger="hover"
|
|
|
|
|
+ placement="right"
|
|
|
|
|
+ mouseEnterDelay={0.3}
|
|
|
|
|
+ content={renderDemandDetail(video)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {cardNode}
|
|
|
|
|
+ </Popover>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ return cardNode;
|
|
|
})}
|
|
})}
|
|
|
</div>
|
|
</div>
|
|
|
<div ref={sentinelRef} className="text-center py-4 text-gray-400 text-xs">
|
|
<div ref={sentinelRef} className="text-center py-4 text-gray-400 text-xs">
|