|
@@ -26,6 +26,7 @@ import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
|
|
|
import { enumToOptions } from '@src/utils/helper';
|
|
import { enumToOptions } from '@src/utils/helper';
|
|
|
import { isNil } from 'lodash';
|
|
import { isNil } from 'lodash';
|
|
|
import { CollectedStatusEnum, VideoStatusEnum } from '@src/views/publishContent/types';
|
|
import { CollectedStatusEnum, VideoStatusEnum } from '@src/views/publishContent/types';
|
|
|
|
|
+import useLogger from '@src/hooks/useLogger';
|
|
|
|
|
|
|
|
export enum VideoLibraryType {
|
|
export enum VideoLibraryType {
|
|
|
平台视频库 = 0,
|
|
平台视频库 = 0,
|
|
@@ -37,6 +38,9 @@ const { Text, Paragraph } = Typography;
|
|
|
|
|
|
|
|
interface VideoSelectContentProps {
|
|
interface VideoSelectContentProps {
|
|
|
planType: VideoSearchPlanType;
|
|
planType: VideoSearchPlanType;
|
|
|
|
|
+ traceId?: number;
|
|
|
|
|
+ gzhAccountId?: number;
|
|
|
|
|
+ subChannel?: string;
|
|
|
initialSelectedIds?: number[];
|
|
initialSelectedIds?: number[];
|
|
|
selectedVideos?: VideoItem[];
|
|
selectedVideos?: VideoItem[];
|
|
|
defaultVideoLibraryType?: VideoLibraryType;
|
|
defaultVideoLibraryType?: VideoLibraryType;
|
|
@@ -47,6 +51,9 @@ interface VideoSelectContentProps {
|
|
|
|
|
|
|
|
interface VideoSelectModalProps {
|
|
interface VideoSelectModalProps {
|
|
|
planType: VideoSearchPlanType;
|
|
planType: VideoSearchPlanType;
|
|
|
|
|
+ traceId?: number;
|
|
|
|
|
+ subChannel?: string;
|
|
|
|
|
+ gzhAccountId?: number;
|
|
|
visible: boolean;
|
|
visible: boolean;
|
|
|
onClose: () => void;
|
|
onClose: () => void;
|
|
|
onOk: (selectedVideos: VideoItem[]) => void;
|
|
onOk: (selectedVideos: VideoItem[]) => void;
|
|
@@ -105,6 +112,9 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
onVideoListChange,
|
|
onVideoListChange,
|
|
|
autoLoad = true,
|
|
autoLoad = true,
|
|
|
isCollectedPage = false,
|
|
isCollectedPage = false,
|
|
|
|
|
+ traceId = 0,
|
|
|
|
|
+ subChannel = '',
|
|
|
|
|
+ gzhAccountId = 0,
|
|
|
}) => {
|
|
}) => {
|
|
|
const { videoCategoryOptions } = useVideoCategoryOptions();
|
|
const { videoCategoryOptions } = useVideoCategoryOptions();
|
|
|
const [tagOptions, setTagOptions] = useState<TagTypeOptions[]>([]);
|
|
const [tagOptions, setTagOptions] = useState<TagTypeOptions[]>([]);
|
|
@@ -123,6 +133,13 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
|
|
const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
|
|
|
const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultVideoLibraryType || VideoLibraryType.平台视频库);
|
|
const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultVideoLibraryType || VideoLibraryType.平台视频库);
|
|
|
const MAX_SELECTION = 3;
|
|
const MAX_SELECTION = 3;
|
|
|
|
|
+ const { uploadLogVideoPlay, uploadLogVideoPlayEnd, uploadLogVideoCollect, uploadLogVideoListQuery, uploadLogVideoView } = useLogger();
|
|
|
|
|
+ const [requestId, setRequestId] = useState<number>(0);
|
|
|
|
|
+ const [playTime, setPlayTime] = useState<number>(0);
|
|
|
|
|
+ const viewedVideoIdsRef = React.useRef<Set<number>>(new Set());
|
|
|
|
|
+ const imageRefs = React.useRef<Map<number, HTMLImageElement>>(new Map());
|
|
|
|
|
+ const observerRef = React.useRef<IntersectionObserver | null>(null);
|
|
|
|
|
+ const logParamsRef = React.useRef({ traceId, requestId, planType, gzhAccountId, subChannel, uploadLogVideoView });
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (defaultVideoLibraryType) {
|
|
if (defaultVideoLibraryType) {
|
|
@@ -176,6 +193,21 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
message.error('获取视频列表失败');
|
|
message.error('获取视频列表失败');
|
|
|
}).finally(() => {
|
|
}).finally(() => {
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
|
|
|
+ const _requestId = new Date().getTime();
|
|
|
|
|
+ setRequestId(_requestId);
|
|
|
|
|
+ uploadLogVideoListQuery({
|
|
|
|
|
+ traceId: traceId,
|
|
|
|
|
+ requestId: _requestId,
|
|
|
|
|
+ planType: planType,
|
|
|
|
|
+ gzhAccountId: gzhAccountId,
|
|
|
|
|
+ subChannel: subChannel,
|
|
|
|
|
+ category: isNil(_category) ? category : _category,
|
|
|
|
|
+ title: searchTerm,
|
|
|
|
|
+ recentNotUsed: isNil(_recentNotUsed) ? recentNotUsed : _recentNotUsed as RecentNotUsedType,
|
|
|
|
|
+ sortType: isNil(_sortType) ? sortType : _sortType,
|
|
|
|
|
+ tags: isNil(_tags) ? tags : _tags,
|
|
|
|
|
+ videoLibraryType: currentVideoLibraryType,
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
if (res && res.code === 0) {
|
|
if (res && res.code === 0) {
|
|
|
setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
|
|
setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
|
|
@@ -234,10 +266,12 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
|
|
|
|
|
const playVideo = (video: VideoItem) => {
|
|
const playVideo = (video: VideoItem) => {
|
|
|
setPlayingVideo(video);
|
|
setPlayingVideo(video);
|
|
|
|
|
+ uploadLogVideoPlay({videoId: video.videoId, traceId: traceId, requestId: requestId, planType: planType, gzhAccountId: gzhAccountId, subChannel: subChannel});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const closeVideoPlayer = () => {
|
|
const closeVideoPlayer = () => {
|
|
|
setPlayingVideo(null);
|
|
setPlayingVideo(null);
|
|
|
|
|
+ uploadLogVideoPlayEnd({videoId: playingVideo?.videoId || 0, playTime: playTime, traceId: traceId, requestId: requestId, planType: planType, gzhAccountId: gzhAccountId, subChannel: subChannel});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleVideoLibraryTypeChange = (v: VideoLibraryType) => {
|
|
const handleVideoLibraryTypeChange = (v: VideoLibraryType) => {
|
|
@@ -281,6 +315,15 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
if (res && res.code === 0) {
|
|
if (res && res.code === 0) {
|
|
|
setVideoList(prev => prev.map(v => v.videoId === videoId ? {...v, collect: isCollected ? CollectedStatusEnum.未收藏 : CollectedStatusEnum.已收藏} : v));
|
|
setVideoList(prev => prev.map(v => v.videoId === videoId ? {...v, collect: isCollected ? CollectedStatusEnum.未收藏 : CollectedStatusEnum.已收藏} : v));
|
|
|
message.success(isCollected ? '取消收藏' : '收藏成功');
|
|
message.success(isCollected ? '取消收藏' : '收藏成功');
|
|
|
|
|
+ uploadLogVideoCollect({
|
|
|
|
|
+ videoId: videoId,
|
|
|
|
|
+ traceId: traceId,
|
|
|
|
|
+ requestId: requestId,
|
|
|
|
|
+ collect: isCollected ? CollectedStatusEnum.未收藏 : CollectedStatusEnum.已收藏,
|
|
|
|
|
+ planType: planType,
|
|
|
|
|
+ gzhAccountId: gzhAccountId,
|
|
|
|
|
+ subChannel: subChannel,
|
|
|
|
|
+ });
|
|
|
} else {
|
|
} else {
|
|
|
message.error('收藏失败');
|
|
message.error('收藏失败');
|
|
|
}
|
|
}
|
|
@@ -291,6 +334,58 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
window.location.href = `/publishContent/wegzh/${videoLibraryType}/${videoId}`;
|
|
window.location.href = `/publishContent/wegzh/${videoLibraryType}/${videoId}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const handleTimeUpdate = (e: React.ChangeEvent<HTMLVideoElement>) => {
|
|
|
|
|
+ setPlayTime(e.target.currentTime);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新日志参数 ref
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ logParamsRef.current = { traceId, requestId, planType, gzhAccountId, subChannel, uploadLogVideoView };
|
|
|
|
|
+ }, [traceId, requestId, planType, gzhAccountId, subChannel, uploadLogVideoView]);
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化 Intersection Observer
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!observerRef.current) {
|
|
|
|
|
+ observerRef.current = new IntersectionObserver(
|
|
|
|
|
+ (entries) => {
|
|
|
|
|
+ entries.forEach((entry) => {
|
|
|
|
|
+ if (entry.isIntersecting && entry.intersectionRatio >= 1.0) {
|
|
|
|
|
+ // 图片完全进入视口
|
|
|
|
|
+ const videoId = parseInt(entry.target.getAttribute('data-video-id') || '0');
|
|
|
|
|
+ if (videoId && !viewedVideoIdsRef.current.has(videoId)) {
|
|
|
|
|
+ viewedVideoIdsRef.current.add(videoId);
|
|
|
|
|
+ // 使用 ref 中的最新值
|
|
|
|
|
+ const params = logParamsRef.current;
|
|
|
|
|
+ const idx = parseInt(entry.target.getAttribute('data-idx') || '0');
|
|
|
|
|
+ params.uploadLogVideoView({
|
|
|
|
|
+ videoId: videoId,
|
|
|
|
|
+ idx: idx,
|
|
|
|
|
+ traceId: params.traceId,
|
|
|
|
|
+ requestId: params.requestId,
|
|
|
|
|
+ planType: params.planType,
|
|
|
|
|
+ gzhAccountId: params.gzhAccountId,
|
|
|
|
|
+ subChannel: params.subChannel,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ threshold: 1.0, // 完全可见时才触发
|
|
|
|
|
+ rootMargin: '0px',
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ // 组件卸载时断开 observer
|
|
|
|
|
+ if (observerRef.current) {
|
|
|
|
|
+ observerRef.current.disconnect();
|
|
|
|
|
+ observerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
@@ -372,7 +467,7 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
</div>
|
|
</div>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
- {videoList.map((video) => {
|
|
|
|
|
|
|
+ {videoList.map((video, index) => {
|
|
|
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 isInvalid = video.status === VideoStatusEnum.已下架;
|
|
const isInvalid = video.status === VideoStatusEnum.已下架;
|
|
@@ -398,7 +493,30 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
style={{ paddingBottom: '79.8%' }}
|
|
style={{ paddingBottom: '79.8%' }}
|
|
|
onClick={(e) => { e.stopPropagation(); playVideo(video); }}
|
|
onClick={(e) => { e.stopPropagation(); playVideo(video); }}
|
|
|
>
|
|
>
|
|
|
- <img src={video.customCover || video.cover} alt={video.customTitle || video.title} referrerPolicy="no-referrer" className="absolute inset-0 w-full h-full object-cover" />
|
|
|
|
|
|
|
+ <img
|
|
|
|
|
+ ref={(el) => {
|
|
|
|
|
+ if (el) {
|
|
|
|
|
+ imageRefs.current.set(video.videoId, el);
|
|
|
|
|
+ // 当图片元素添加到 DOM 时,立即开始观察
|
|
|
|
|
+ if (observerRef.current) {
|
|
|
|
|
+ observerRef.current.observe(el);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 当图片元素从 DOM 移除时,取消观察
|
|
|
|
|
+ const img = imageRefs.current.get(video.videoId);
|
|
|
|
|
+ if (img && observerRef.current) {
|
|
|
|
|
+ observerRef.current.unobserve(img);
|
|
|
|
|
+ }
|
|
|
|
|
+ imageRefs.current.delete(video.videoId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ data-video-id={video.videoId}
|
|
|
|
|
+ data-idx={index + 1}
|
|
|
|
|
+ src={video.customCover || video.cover}
|
|
|
|
|
+ alt={video.customTitle || video.title}
|
|
|
|
|
+ referrerPolicy="no-referrer"
|
|
|
|
|
+ className="absolute inset-0 w-full h-full object-cover"
|
|
|
|
|
+ />
|
|
|
<div className="absolute inset-0 flex justify-center items-center cursor-pointer">
|
|
<div className="absolute inset-0 flex justify-center items-center cursor-pointer">
|
|
|
<CaretRightFilled className="!text-white text-4xl bg-black/20 rounded-full p-1 pl-2" />
|
|
<CaretRightFilled className="!text-white text-4xl bg-black/20 rounded-full p-1 pl-2" />
|
|
|
</div>
|
|
</div>
|
|
@@ -474,7 +592,7 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
styles={{ body: { padding: 0, background: '#000' } }}
|
|
styles={{ body: { padding: 0, background: '#000' } }}
|
|
|
>
|
|
>
|
|
|
{playingVideo && (
|
|
{playingVideo && (
|
|
|
- <video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.video}>
|
|
|
|
|
|
|
+ <video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.video} onTimeUpdate={handleTimeUpdate}>
|
|
|
Your browser does not support the video tag.
|
|
Your browser does not support the video tag.
|
|
|
</video>
|
|
</video>
|
|
|
)}
|
|
)}
|
|
@@ -483,7 +601,7 @@ export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
|
|
|
|
|
|
|
+const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType, traceId, subChannel, gzhAccountId }) => {
|
|
|
const [currentSelectedVideos, setCurrentSelectedVideos] = useState<VideoItem[]>([]);
|
|
const [currentSelectedVideos, setCurrentSelectedVideos] = useState<VideoItem[]>([]);
|
|
|
|
|
|
|
|
const handleOk = () => {
|
|
const handleOk = () => {
|
|
@@ -514,6 +632,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
>
|
|
>
|
|
|
<VideoSelectContent
|
|
<VideoSelectContent
|
|
|
planType={planType}
|
|
planType={planType}
|
|
|
|
|
+ traceId={traceId}
|
|
|
|
|
+ gzhAccountId={gzhAccountId}
|
|
|
|
|
+ subChannel={subChannel}
|
|
|
initialSelectedIds={initialSelectedIds}
|
|
initialSelectedIds={initialSelectedIds}
|
|
|
selectedVideos={selectedVideos}
|
|
selectedVideos={selectedVideos}
|
|
|
defaultVideoLibraryType={defaultVideoLibraryType}
|
|
defaultVideoLibraryType={defaultVideoLibraryType}
|