index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Drawer,
  4. Button,
  5. Select,
  6. Input,
  7. Card,
  8. Typography,
  9. Pagination,
  10. Space,
  11. message,
  12. Modal,
  13. } from 'antd';
  14. import { CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
  15. import { VideoItem, VideoListResponse } from '../types';
  16. import http from '@src/http';
  17. import { getVideoContentListApi } from '@src/http/api';
  18. import { useVideoCategoryOptions } from '../../hooks/useVideoCategoryOptions';
  19. import { VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
  20. import { GzhPlanType } from '../../hooks/useGzhPlanList';
  21. import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
  22. import { enumToOptions } from '@src/utils/helper';
  23. const { Paragraph, Text } = Typography;
  24. interface VideoSelectModalProps {
  25. planType: GzhPlanType;
  26. visible: boolean;
  27. onClose: () => void;
  28. onOk: (selectedVideos: VideoItem[]) => void;
  29. initialSelectedIds?: number[];
  30. selectedVideos?: VideoItem[];
  31. }
  32. const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [] }) => {
  33. const { videoCategoryOptions } = useVideoCategoryOptions();
  34. const [category, setCategory] = useState<string>();
  35. const [sort, setSort] = useState<VideoSortType>(VideoSortType.平台推荐);
  36. const [searchTerm, setSearchTerm] = useState<string>('');
  37. const [currentPage, setCurrentPage] = useState(1);
  38. const [pageSize, setPageSize] = useState(10);
  39. const [total, setTotal] = useState(0);
  40. const [loading, setLoading] = useState(false);
  41. const [videoList, setVideoList] = useState<VideoItem[]>([]);
  42. const [videoListAll, setVideoListAll] = useState<VideoItem[]>([]);
  43. const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
  44. const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
  45. const MAX_SELECTION = 3;
  46. const getVideoListType = (planType: GzhPlanType) => {
  47. if (planType === GzhPlanType.自动回复) {
  48. return VideoSearchPlanType.自动回复;
  49. } else if (planType === GzhPlanType.公众号推送) {
  50. return VideoSearchPlanType.公众号推送;
  51. } else {
  52. return VideoSearchPlanType.服务号推送;
  53. }
  54. }
  55. const getVideoList = async (pageNum?: number, _pageSize?: number) => {
  56. setLoading(true);
  57. setCurrentPage(pageNum || currentPage);
  58. setPageSize(_pageSize || pageSize);
  59. const res = await http.post<VideoListResponse>(getVideoContentListApi, {
  60. category,
  61. title: searchTerm,
  62. sort,
  63. type: getVideoListType(planType),
  64. pageNum: pageNum || currentPage,
  65. pageSize: _pageSize || pageSize,
  66. }).catch(() => {
  67. message.error('获取视频列表失败');
  68. }).finally(() => {
  69. setLoading(false);
  70. });
  71. if (res && res.code === 0) {
  72. setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
  73. setVideoListAll(old => [...old, ...res.data.objs.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
  74. setTotal(res.data.totalSize);
  75. }
  76. }
  77. useEffect(() => {
  78. if (visible) {
  79. setVideoList(selectedVideos);
  80. setVideoListAll(selectedVideos);
  81. getVideoList(0);
  82. }
  83. }, [visible]);
  84. useEffect(() => {
  85. if (visible) {
  86. setSelectedVideoIds(new Set(initialSelectedIds));
  87. }
  88. }, [visible, initialSelectedIds]);
  89. const handleSearch = () => {
  90. console.log('Searching for:', { category, searchTerm });
  91. const currentPage = 1
  92. setCurrentPage(currentPage);
  93. getVideoList(currentPage);
  94. };
  95. const handleSelectVideo = (videoId: number) => {
  96. setSelectedVideoIds(prev => {
  97. const newSet = new Set(prev);
  98. if (newSet.has(videoId)) {
  99. newSet.delete(videoId);
  100. } else {
  101. if (newSet.size >= MAX_SELECTION) {
  102. message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
  103. return prev;
  104. }
  105. newSet.add(videoId);
  106. }
  107. return newSet;
  108. });
  109. };
  110. const handleOk = () => {
  111. const _selectedVideos = videoListAll.filter(video => selectedVideoIds.has(video.videoId));
  112. onOk(_selectedVideos);
  113. onClose();
  114. };
  115. const playVideo = (video: VideoItem) => {
  116. setPlayingVideo(video);
  117. };
  118. const closeVideoPlayer = () => {
  119. setPlayingVideo(null);
  120. };
  121. return (
  122. <>
  123. <Drawer
  124. title="内容选取"
  125. open={visible}
  126. onClose={onClose}
  127. width={800}
  128. placement="right"
  129. loading={loading}
  130. styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
  131. footer={
  132. <div className="flex justify-between items-center">
  133. <Pagination
  134. current={currentPage}
  135. pageSize={pageSize}
  136. total={total}
  137. onChange={(page, size) => {
  138. setCurrentPage(page);
  139. setPageSize(size);
  140. getVideoList(page, size);
  141. }}
  142. pageSizeOptions={['10', '20', '50']}
  143. size="small"
  144. showSizeChanger
  145. showTotal={(total) => `共 ${total} 条`}
  146. />
  147. <Space>
  148. <Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
  149. <Button onClick={onClose}>取消</Button>
  150. <Button type="primary" onClick={handleOk}>确定</Button>
  151. </Space>
  152. </div>
  153. }
  154. >
  155. <div className="flex flex-wrap gap-4 mb-6">
  156. <div className="flex items-center gap-2">
  157. <span className="text-gray-600">排序选项:</span>
  158. <Select
  159. style={{ width: 120 }}
  160. value={sort}
  161. onChange={setSort}
  162. options={enumToOptions(VideoSortType)}
  163. />
  164. </div>
  165. <div className="flex items-center gap-2">
  166. <span className="text-gray-600">品类:</span>
  167. <Select
  168. placeholder="选择品类"
  169. style={{ width: 160 }}
  170. value={category}
  171. allowClear
  172. onChange={setCategory}
  173. options={videoCategoryOptions.map(option => ({ label: option, value: option }))}
  174. />
  175. </div>
  176. <div className="flex items-center gap-2">
  177. <span className="text-gray-600">视频标题:</span>
  178. <Input
  179. placeholder="搜索视频标题"
  180. style={{ width: 180 }}
  181. value={searchTerm}
  182. onPressEnter={handleSearch}
  183. allowClear
  184. onChange={e => setSearchTerm(e.target.value)}
  185. />
  186. </div>
  187. <Button type="primary" onClick={handleSearch}>搜索</Button>
  188. </div>
  189. <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
  190. {videoList.map((video) => {
  191. const isSelected = selectedVideoIds.has(video.videoId);
  192. const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
  193. return (
  194. <Card
  195. key={video.videoId}
  196. className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
  197. styles={{ body: { padding: 0 } }}
  198. onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
  199. >
  200. <div className="p-2">
  201. <Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
  202. <Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
  203. </div>
  204. <div
  205. className="relative"
  206. style={{ paddingBottom: '79.8%' }}
  207. onClick={(e) => { e.stopPropagation(); playVideo(video); }}
  208. >
  209. <img src={video.customCover || video.cover} alt={video.customTitle || video.title} referrerPolicy="no-referrer" className="absolute inset-0 w-full h-full object-cover" />
  210. <div className="absolute inset-0 flex justify-center items-center cursor-pointer">
  211. <CaretRightFilled className="!text-white text-4xl bg-black/20 rounded-full p-1 pl-2" />
  212. </div>
  213. </div>
  214. <div className="p-3 flex justify-between items-center">
  215. <div className="flex flex-col gap-1">
  216. <Text type="secondary" className="text-xs">平台传播得分: {video.score?.toFixed(2) || '无'}</Text>
  217. <Text type="secondary" className="text-xs">行业裂变率: {video.industryFissionRate?.toFixed(2) || '无'}</Text>
  218. <Text type="secondary" className="text-xs">本渠道传播率: {video.channelFissionRate?.toFixed(2) || '无'}</Text>
  219. </div>
  220. {isSelected ? (
  221. <CheckCircleFilled className="text-green-500 text-xl" />
  222. ) : (
  223. <div className={`w-5 h-5 border-2 ${isDisabled ? 'border-gray-200 bg-gray-100' : 'border-gray-300' } rounded-full`}></div>
  224. )}
  225. </div>
  226. </Card>
  227. );
  228. })}
  229. </div>
  230. </Drawer>
  231. <Modal
  232. open={!!playingVideo}
  233. onCancel={closeVideoPlayer}
  234. title={playingVideo?.title}
  235. footer={null}
  236. destroyOnClose
  237. width={720}
  238. styles={{ body: { padding: 0, background: '#000' } }}
  239. >
  240. {playingVideo && (
  241. <video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.video}>
  242. Your browser does not support the video tag.
  243. </video>
  244. )}
  245. </Modal>
  246. </>
  247. );
  248. };
  249. export default VideoSelectModal;