| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- import React, { useState, useEffect } from 'react';
- import {
- Drawer,
- Button,
- Select,
- Input,
- Card,
- Typography,
- Pagination,
- Space,
- message,
- Modal,
- Radio,
- RadioChangeEvent,
- Tooltip,
- Tag,
- } from 'antd';
- 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,
- 我的上传 = 1,
- }
- const { Text, Paragraph } = Typography;
- interface VideoSelectModalProps {
- planType: WeComPlanType | GzhPlanType;
- visible: boolean;
- onClose: () => void;
- onOk: (selectedVideos: (VideoItem | WeVideoItem)[]) => void;
- initialSelectedIds?: number[];
- selectedVideos?: (VideoItem | WeVideoItem)[];
- defaultVideoLibraryType?: VideoLibraryType;
- }
- export enum VideoSortType {
- 平台推荐 = 0,
- 行业裂变率 = 1,
- 本渠道传播率 = 2,
- 推荐指数 = 3,
- }
- 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 [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 | VideoItem)[]>([]);
- const [videoListAll, setVideoListAll] = useState<(WeVideoItem | VideoItem)[]>([]);
- const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
- const [playingVideo, setPlayingVideo] = useState<WeVideoItem | VideoItem | null>(null);
- const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultVideoLibraryType || VideoLibraryType.平台视频库);
- const MAX_SELECTION = 3;
- 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 currentVideoLibraryType = isNil(_videoLibraryType) ? videoLibraryType : _videoLibraryType;
- const apiUrl = currentVideoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
- const requestParams = {
- category: isNil(_category) ? category : _category,
- title: searchTerm,
- recentNotUsed: isNil(_recentNotUsed) ? recentNotUsed : _recentNotUsed,
- sortType: isNil(_sortType) ? sortType : _sortType,
- tags: isNil(_tags) ? tags : _tags,
- type: getVideoListType(planType),
- pageNum: pageNum || currentPage,
- pageSize: _pageSize || pageSize,
- };
- const res = await http.post<VideoListResponse>(apiUrl, requestParams).catch(() => {
- message.error('获取视频列表失败');
- }).finally(() => {
- setLoading(false);
- });
- if (res && res.code === 0) {
- 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(() => {
- if (visible) {
- console.log('selectedVideos', selectedVideos);
- // 初始化时设置 selectedVideos
- setVideoList(selectedVideos);
- setVideoListAll(selectedVideos);
- getVideoList({});
- }
- }, [visible]);
- useEffect(() => {
- if (visible) {
- setSelectedVideoIds(new Set(initialSelectedIds));
- }
- }, [visible, initialSelectedIds]);
- const handleSearch = () => {
- console.log('Searching for:', { category, searchTerm });
- const newPageNum = 1;
- setCurrentPage(newPageNum);
- getVideoList({pageNum: newPageNum});
- };
- const handleSelectVideo = (videoId: number) => {
- setSelectedVideoIds(prev => {
- const newSet = new Set(prev);
- if (newSet.has(videoId)) {
- newSet.delete(videoId);
- } else {
- if (newSet.size >= MAX_SELECTION) {
- message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
- return prev;
- }
- newSet.add(videoId);
- }
- return newSet;
- });
- };
- const handleOk = () => {
- const selectedVideos = videoListAll.filter(video => selectedVideoIds.has(video.videoId));
- // 去重
- 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 | VideoItem) => {
- setPlayingVideo(video);
- };
- const closeVideoPlayer = () => {
- setPlayingVideo(null);
- };
- 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 (
- <>
- <Drawer
- title="内容选取"
- open={visible}
- onClose={onClose}
- width={900}
- placement="right"
- loading={loading}
- destroyOnClose
- styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
- footer={
- <div className="flex justify-between items-center">
- <Pagination
- current={currentPage}
- pageSize={pageSize}
- total={total}
- onChange={(page, size) => {
- setCurrentPage(page);
- setPageSize(size);
- getVideoList({pageNum: page, _pageSize: size});
- }}
- pageSizeOptions={['10', '20', '50']}
- size="small"
- showSizeChanger
- showTotal={(total) => `共 ${total} 条`}
- />
- <Space>
- <Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
- <Button onClick={onClose}>取消</Button>
- <Button type="primary" onClick={handleOk}>确定</Button>
- </Space>
- </div>
- }
- >
- <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={handleVideoLibraryTypeChange}
- options={enumToOptions(VideoLibraryType)}
- />
- </div>
-
- <div className="flex items-center gap-2">
- <span className="text-gray-600">品类:</span>
- <Select
- placeholder="选择品类"
- style={{ width: 120 }}
- value={category}
- allowClear
- 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: 130 }}
- value={searchTerm}
- allowClear
- onChange={e => setSearchTerm(e.target.value)}
- onPressEnter={handleSearch}
- />
- </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);
- const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
- return (
- <Card
- key={video.videoId}
- className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2 bg-blue-50 shadow-md' : 'hover:shadow-sm'}`}
- styles={{ body: { padding: 0 } }}
- onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
- >
- <div className="p-3">
- <Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
- <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.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-1 flex-col gap-1">
- <Text type="secondary" className="text-xs">推荐指数: {video.recommendScore?.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 ? (
- <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>
- )}
- </div>
- {/* {isSelected && (
- <div className="p-3 flex justify-between items-center relative z-10">
- <Select
- placeholder="选择场景"
- style={{ width: 180 }}
- defaultValue={video.scene || 0}
- value={video.scene}
- onChange={(value) => handleChangeSelectVideo(video.videoId, value)}
- options={ planType === WeComPlanType.社群 ?
- [{ label: '群发', value: 0 }, { label: '单发', value: 1 }]
- : [{ label: '关注回复', value: 0 }]
- }
- onClick={(e) => e.stopPropagation()}
- />
- </div>
- )} */}
- </Card>
- );
- })}
- </div>
- </Drawer>
- <Modal
- open={!!playingVideo}
- onCancel={closeVideoPlayer}
- title={playingVideo?.title}
- footer={null}
- destroyOnClose
- width={720}
- styles={{ body: { padding: 0, background: '#000' } }}
- >
- {playingVideo && (
- <video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.video}>
- Your browser does not support the video tag.
- </video>
- )}
- </Modal>
- </>
- );
- };
- export default VideoSelectModal;
|