|
|
@@ -10,14 +10,20 @@ import {
|
|
|
Space,
|
|
|
message,
|
|
|
Modal,
|
|
|
+ Radio,
|
|
|
+ RadioChangeEvent,
|
|
|
+ Tooltip,
|
|
|
+ Tag,
|
|
|
} from 'antd';
|
|
|
-import { CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
|
|
|
-import { VideoListResponse } from '@src/views/publishContent/weGZH/components/types';
|
|
|
+import { CheckCircleFilled, CaretRightFilled, QuestionCircleOutlined } from '@ant-design/icons';
|
|
|
+import { VideoItem, VideoListResponse } from '@src/views/publishContent/weGZH/components/types';
|
|
|
import http from '@src/http';
|
|
|
import { getVideoContentListApi, getUploadVideoContentListApi } from '@src/http/api';
|
|
|
import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
|
|
|
import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
|
|
|
import { enumToOptions } from '@src/utils/helper';
|
|
|
+import { isNil } from 'lodash';
|
|
|
+import { GzhPlanType } from '@src/views/publishContent/weGZH/hooks/useGzhPlanList';
|
|
|
|
|
|
export enum VideoLibraryType {
|
|
|
平台视频库 = 0,
|
|
|
@@ -27,11 +33,13 @@ export enum VideoLibraryType {
|
|
|
const { Text, Paragraph } = Typography;
|
|
|
|
|
|
interface VideoSelectModalProps {
|
|
|
- planType: WeComPlanType;
|
|
|
+ planType: WeComPlanType | GzhPlanType;
|
|
|
visible: boolean;
|
|
|
onClose: () => void;
|
|
|
- onOk: (selectedVideos: WeVideoItem[]) => void;
|
|
|
+ onOk: (selectedVideos: (VideoItem | WeVideoItem)[]) => void;
|
|
|
initialSelectedIds?: number[];
|
|
|
+ selectedVideos?: (VideoItem | WeVideoItem)[];
|
|
|
+ defaultVideoLibraryType?: VideoLibraryType;
|
|
|
}
|
|
|
|
|
|
export enum VideoSortType {
|
|
|
@@ -41,35 +49,91 @@ export enum VideoSortType {
|
|
|
推荐指数 = 3,
|
|
|
}
|
|
|
|
|
|
-const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [] }) => {
|
|
|
+export enum RecentNotUsedType {
|
|
|
+ 历史 = 0,
|
|
|
+ 近30天 = 1,
|
|
|
+ 近14天 = 2,
|
|
|
+ 近7天 = 3,
|
|
|
+ 近3天 = 4,
|
|
|
+}
|
|
|
+
|
|
|
+export enum SortTypeEnum {
|
|
|
+ 推荐指数 = 0,
|
|
|
+ 更新时间 = 1,
|
|
|
+}
|
|
|
+
|
|
|
+export const Tags = [
|
|
|
+ '票圈受欢迎',
|
|
|
+ '同类用户喜欢',
|
|
|
+ '你的用户爱看',
|
|
|
+ '猜TA想看',
|
|
|
+];
|
|
|
+
|
|
|
+interface GetVideoListParams {
|
|
|
+ pageNum?: number;
|
|
|
+ _pageSize?: number;
|
|
|
+ _sortType?: SortTypeEnum;
|
|
|
+ _recentNotUsed?: RecentNotUsedType;
|
|
|
+ _category?: string;
|
|
|
+ _tags?: string[];
|
|
|
+ _videoLibraryType?: VideoLibraryType;
|
|
|
+}
|
|
|
+
|
|
|
+const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
|
|
|
const { videoCategoryOptions } = useVideoCategoryOptions();
|
|
|
const [category, setCategory] = useState<string>();
|
|
|
- const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
|
|
|
+ // const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
|
|
|
+ const [sortType, setSortType] = useState<SortTypeEnum>(SortTypeEnum.推荐指数);
|
|
|
+ const [recentNotUsed, setRecentNotUsed] = useState<RecentNotUsedType>(RecentNotUsedType.历史);
|
|
|
+ const [tags, setTags] = useState<string[]>([]);
|
|
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
const [pageSize, setPageSize] = useState(10);
|
|
|
const [total, setTotal] = useState(0);
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
- const [videoList, setVideoList] = useState<WeVideoItem[]>([]);
|
|
|
- const [videoListAll, setVideoListAll] = useState<WeVideoItem[]>([]);
|
|
|
+ const [videoList, setVideoList] = useState<(WeVideoItem | VideoItem)[]>([]);
|
|
|
+ const [videoListAll, setVideoListAll] = useState<(WeVideoItem | VideoItem)[]>([]);
|
|
|
const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
|
|
|
- const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
|
|
|
- const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(VideoLibraryType.平台视频库);
|
|
|
+ const [playingVideo, setPlayingVideo] = useState<WeVideoItem | VideoItem | null>(null);
|
|
|
+ const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultVideoLibraryType || VideoLibraryType.平台视频库);
|
|
|
const MAX_SELECTION = 3;
|
|
|
|
|
|
- const getVideoList = async (pageNum?: number, _pageSize?: number) => {
|
|
|
+ const getVideoListType = (planType: GzhPlanType | WeComPlanType) => {
|
|
|
+ if (planType === GzhPlanType.自动回复) {
|
|
|
+ return VideoSearchPlanType.自动回复;
|
|
|
+ } else if (planType === GzhPlanType.公众号推送) {
|
|
|
+ return VideoSearchPlanType.公众号推送;
|
|
|
+ } else if (planType === GzhPlanType.服务号推送) {
|
|
|
+ return VideoSearchPlanType.服务号推送;
|
|
|
+ } else if (planType === WeComPlanType.社群) {
|
|
|
+ return VideoSearchPlanType.企微社群;
|
|
|
+ } else {
|
|
|
+ return VideoSearchPlanType.企微自动回复;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (defaultVideoLibraryType) {
|
|
|
+ setVideoLibraryType(defaultVideoLibraryType);
|
|
|
+ }
|
|
|
+ }, [defaultVideoLibraryType]);
|
|
|
+
|
|
|
+ const getVideoList = async ({pageNum, _pageSize, _sortType, _recentNotUsed, _category, _tags, _videoLibraryType}: GetVideoListParams) => {
|
|
|
setLoading(true);
|
|
|
setCurrentPage(pageNum || currentPage);
|
|
|
setPageSize(_pageSize || pageSize);
|
|
|
|
|
|
// 根据视频库类型选择不同的API
|
|
|
- const apiUrl = videoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
|
|
|
+ const currentVideoLibraryType = isNil(_videoLibraryType) ? videoLibraryType : _videoLibraryType;
|
|
|
+ const apiUrl = currentVideoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
|
|
|
|
|
|
const requestParams = {
|
|
|
- category,
|
|
|
+ category: isNil(_category) ? category : _category,
|
|
|
title: searchTerm,
|
|
|
- sort,
|
|
|
- type: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
|
|
|
+ recentNotUsed: isNil(_recentNotUsed) ? recentNotUsed : _recentNotUsed,
|
|
|
+ sortType: isNil(_sortType) ? sortType : _sortType,
|
|
|
+ tags: isNil(_tags) ? tags : _tags,
|
|
|
+ type: getVideoListType(planType),
|
|
|
pageNum: pageNum || currentPage,
|
|
|
pageSize: _pageSize || pageSize,
|
|
|
};
|
|
|
@@ -80,21 +144,21 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
setLoading(false);
|
|
|
});
|
|
|
if (res && res.code === 0) {
|
|
|
- const mappedVideos = (res.data.objs || []).map(video => ({ ...video, scene: videoList.find(v => v.videoId === video.videoId)?.scene || 0 as 0 | 1 }));
|
|
|
- setVideoList(mappedVideos);
|
|
|
- setVideoListAll(old => [...old, ...mappedVideos]);
|
|
|
+ setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
|
|
|
+ setVideoListAll(old => [...old, ...res.data.objs.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
|
|
|
setTotal(res.data.totalSize);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 监听视频库类型变化,重新加载数据
|
|
|
useEffect(() => {
|
|
|
- getVideoList();
|
|
|
- }, [videoLibraryType]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- getVideoList();
|
|
|
- }, []);
|
|
|
+ if (visible) {
|
|
|
+ console.log('selectedVideos', selectedVideos);
|
|
|
+ // 初始化时设置 selectedVideos
|
|
|
+ setVideoList(selectedVideos);
|
|
|
+ setVideoListAll(selectedVideos);
|
|
|
+ getVideoList({});
|
|
|
+ }
|
|
|
+ }, [visible]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (visible) {
|
|
|
@@ -106,7 +170,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
console.log('Searching for:', { category, searchTerm });
|
|
|
const newPageNum = 1;
|
|
|
setCurrentPage(newPageNum);
|
|
|
- getVideoList(newPageNum);
|
|
|
+ getVideoList({pageNum: newPageNum});
|
|
|
};
|
|
|
|
|
|
const handleSelectVideo = (videoId: number) => {
|
|
|
@@ -131,10 +195,11 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
const uniqueSelectedVideos = selectedVideos.filter((video, index, self) =>
|
|
|
index === self.findIndex((t) => t.videoId === video.videoId)
|
|
|
);
|
|
|
+ console.log('uniqueSelectedVideos', uniqueSelectedVideos);
|
|
|
onOk(uniqueSelectedVideos);
|
|
|
};
|
|
|
|
|
|
- const playVideo = (video: WeVideoItem) => {
|
|
|
+ const playVideo = (video: WeVideoItem | VideoItem) => {
|
|
|
setPlayingVideo(video);
|
|
|
};
|
|
|
|
|
|
@@ -142,26 +207,33 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
setPlayingVideo(null);
|
|
|
};
|
|
|
|
|
|
- // const handleChangeSelectVideo = (videoId: number, scene: number) => {
|
|
|
- // setVideoList((prev: WeVideoItem[]) => {
|
|
|
- // const newList = prev.map(video => {
|
|
|
- // if (video.videoId === videoId) {
|
|
|
- // return { ...video, scene: scene as 0 | 1 };
|
|
|
- // }
|
|
|
- // return video;
|
|
|
- // });
|
|
|
- // return newList;
|
|
|
- // });
|
|
|
- // setVideoListAll((prev: WeVideoItem[]) => {
|
|
|
- // const newList = prev.map(video => {
|
|
|
- // if (video.videoId === videoId) {
|
|
|
- // return { ...video, scene: scene as 0 | 1 };
|
|
|
- // }
|
|
|
- // return video;
|
|
|
- // });
|
|
|
- // return newList;
|
|
|
- // });
|
|
|
- // }
|
|
|
+ const handleVideoLibraryTypeChange = (v: VideoLibraryType) => {
|
|
|
+ setVideoLibraryType(v);
|
|
|
+ getVideoList({pageNum: 1, _videoLibraryType: v});
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleSortTypeChange = (v: RadioChangeEvent) => {
|
|
|
+ setSortType(v.target.value as SortTypeEnum);
|
|
|
+ console.log('handleSortTypeChange', v.target.value);
|
|
|
+ getVideoList({pageNum: 1, _sortType: v.target.value as SortTypeEnum});
|
|
|
+ }
|
|
|
+
|
|
|
+ const onCategoryChange = (v: string) => {
|
|
|
+ setCategory(v);
|
|
|
+ console.log('onCategoryChange', v);
|
|
|
+ getVideoList({pageNum: 1, _category: v || '' as string});
|
|
|
+ }
|
|
|
+
|
|
|
+ const onRecentNotUsedChange = (v: RecentNotUsedType) => {
|
|
|
+ setRecentNotUsed(v);
|
|
|
+ getVideoList({pageNum: 1, _recentNotUsed: v});
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleTagChange = (tag: string, checked: boolean) => {
|
|
|
+ const newTags = checked ? [...tags, tag] : tags.filter(t => t !== tag);
|
|
|
+ setTags(newTags);
|
|
|
+ getVideoList({pageNum: 1, _tags: newTags});
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
@@ -183,7 +255,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
onChange={(page, size) => {
|
|
|
setCurrentPage(page);
|
|
|
setPageSize(size);
|
|
|
- getVideoList(page, size);
|
|
|
+ getVideoList({pageNum: page, _pageSize: size});
|
|
|
}}
|
|
|
pageSizeOptions={['10', '20', '50']}
|
|
|
size="small"
|
|
|
@@ -198,25 +270,17 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
</div>
|
|
|
}
|
|
|
>
|
|
|
- <div className="flex flex-wrap gap-2 mb-6">
|
|
|
+ <div className="flex flex-wrap gap-2 mb-2">
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-gray-600">视频来源:</span>
|
|
|
<Select
|
|
|
style={{ width: 120 }}
|
|
|
value={videoLibraryType}
|
|
|
- onChange={setVideoLibraryType}
|
|
|
+ onChange={handleVideoLibraryTypeChange}
|
|
|
options={enumToOptions(VideoLibraryType)}
|
|
|
/>
|
|
|
</div>
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <span className="text-gray-600">排序选项:</span>
|
|
|
- <Select
|
|
|
- style={{ width: 120 }}
|
|
|
- value={sort}
|
|
|
- onChange={setSort}
|
|
|
- options={enumToOptions(VideoSortType)}
|
|
|
- />
|
|
|
- </div>
|
|
|
+
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-gray-600">品类:</span>
|
|
|
<Select
|
|
|
@@ -224,15 +288,24 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
style={{ width: 120 }}
|
|
|
value={category}
|
|
|
allowClear
|
|
|
- onChange={setCategory}
|
|
|
+ onChange={onCategoryChange}
|
|
|
options={videoCategoryOptions.map(option => ({ label: option, value: option }))}
|
|
|
/>
|
|
|
</div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-gray-600">近期未使用:</span>
|
|
|
+ <Select
|
|
|
+ style={{ width: 120 }}
|
|
|
+ value={recentNotUsed}
|
|
|
+ onChange={onRecentNotUsedChange}
|
|
|
+ options={enumToOptions(RecentNotUsedType)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-gray-600">视频标题:</span>
|
|
|
<Input
|
|
|
placeholder="搜索视频标题"
|
|
|
- style={{ width: 120 }}
|
|
|
+ style={{ width: 130 }}
|
|
|
value={searchTerm}
|
|
|
allowClear
|
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
|
@@ -241,7 +314,31 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
</div>
|
|
|
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
|
|
</div>
|
|
|
-
|
|
|
+ <div className="flex">
|
|
|
+ <div className="flex flex-wrap gap-2 mb-6 items-center">
|
|
|
+ <span className="text-gray-600 ">排序选项:</span>
|
|
|
+ <Radio.Group
|
|
|
+ onChange={handleSortTypeChange}
|
|
|
+ value={sortType}
|
|
|
+ buttonStyle="solid"
|
|
|
+ >
|
|
|
+ <Radio.Button value={SortTypeEnum.推荐指数}>推荐指数 <Tooltip title="结合视频在票圈、同业务场景、本账号下的历史表现综合得分"><QuestionCircleOutlined /></Tooltip></Radio.Button>
|
|
|
+ <Radio.Button value={SortTypeEnum.更新时间}>更新时间</Radio.Button>
|
|
|
+ </Radio.Group>
|
|
|
+ </div>
|
|
|
+ <div className="flex flex-wrap gap-2 mb-6 ml-10 items-center">
|
|
|
+ <span className="text-gray-600 ">推荐标签:</span>
|
|
|
+ {Tags.map<React.ReactNode>((tag) => (
|
|
|
+ <Tag.CheckableTag
|
|
|
+ key={tag}
|
|
|
+ checked={tags.includes(tag)}
|
|
|
+ onChange={(checked) => handleTagChange(tag, checked)}
|
|
|
+ >
|
|
|
+ {tag}
|
|
|
+ </Tag.CheckableTag>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
|
|
{videoList.map((video) => {
|
|
|
const isSelected = selectedVideoIds.has(video.videoId);
|
|
|
@@ -255,26 +352,25 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
|
|
|
>
|
|
|
<div className="p-3">
|
|
|
<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: 1, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
|
|
|
</div>
|
|
|
<div
|
|
|
className="relative"
|
|
|
style={{ paddingBottom: '79.8%' }}
|
|
|
onClick={(e) => { e.stopPropagation(); playVideo(video); }}
|
|
|
>
|
|
|
- <img src={video.cover} alt={video.title} referrerPolicy="no-referrer" className="absolute inset-0 w-full h-full object-cover" />
|
|
|
+ <img 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">
|
|
|
<CaretRightFilled className="!text-white text-4xl bg-black/20 rounded-full p-1 pl-2" />
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="p-3 flex justify-between items-center">
|
|
|
- <div className="flex flex-col gap-1">
|
|
|
+ <div className="flex flex-1 flex-col gap-1">
|
|
|
<Text type="secondary" className="text-xs">推荐指数: {video.recommendScore?.toFixed(2) || '无'}</Text>
|
|
|
- <Text type="secondary" className="text-xs">平台传播得分: {video.score?.toFixed(2) || '无'}</Text>
|
|
|
- <Text type="secondary" className="text-xs">行业裂变率: {video.industryFissionRate?.toFixed(2) || '无'}</Text>
|
|
|
- <Text type="secondary" className="text-xs">本渠道传播率: {video.channelFissionRate?.toFixed(2) || '无'}</Text>
|
|
|
+ <Text type="secondary" className="text-xs">推荐标签: {'tags' in video && video.tags && video.tags.length > 0 ?
|
|
|
+ video.tags.map(tag => <Tag key={tag} className="text-xs">{tag}</Tag>) : '无'}</Text>
|
|
|
</div>
|
|
|
- {isSelected ? (
|
|
|
+ {isSelected ? (
|
|
|
<CheckCircleFilled className="text-green-500 text-xl" />
|
|
|
) : (
|
|
|
<div className={`w-5 h-5 border-2 ${isDisabled ? 'border-gray-200 bg-gray-100' : 'border-gray-300' } rounded-full`}></div>
|