index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import React, { useEffect, useState } from 'react';
  2. import { Modal, Form, Select, Button, Card, Typography, message } from 'antd';
  3. import { CloseOutlined, PlusOutlined, EditOutlined, CaretRightFilled } from '@ant-design/icons';
  4. import VideoSelectModal from '../videoSelectModal';
  5. import EditTitleCoverModal from '../editTitleCoverModal';
  6. import { VideoItem } from '../types'; // Import from common types
  7. import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
  8. import { useAccountOptions } from '../../hooks/useAccountOptions';
  9. const { Option } = Select;
  10. const { Paragraph } = Typography;
  11. interface AddPunlishPlanModalProps {
  12. visible: boolean;
  13. onCancel: () => void;
  14. onOk: (values: GzhPlanDataType) => void; // Pass form values on OK
  15. actionType: 'add' | 'edit';
  16. planType: GzhPlanType;
  17. editPlanData?: GzhPlanDataType;
  18. isSubmiting?: boolean;
  19. }
  20. const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({
  21. visible,
  22. isSubmiting,
  23. onCancel,
  24. onOk,
  25. actionType,
  26. planType,
  27. editPlanData
  28. }) => {
  29. const [form] = Form.useForm();
  30. const type = Form.useWatch('type', form);
  31. const selectVideoType = Form.useWatch('selectVideoType', form);
  32. const [selectedVideos, setSelectedVideos] = useState<VideoItem[]>([]);
  33. const [isVideoSelectVisible, setIsVideoSelectVisible] = useState(false);
  34. const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null); // State for video player modal
  35. const [editingVideo, setEditingVideo] = useState<VideoItem | null>(null); // State for editing modal
  36. const { accountOptions, getAccountList } = useAccountOptions();
  37. useEffect(() => {
  38. if (actionType === 'edit') {
  39. form.setFieldsValue({...editPlanData, type: editPlanData?.type.toString()});
  40. setSelectedVideos(editPlanData?.videoList || []);
  41. } else {
  42. setSelectedVideos([]);
  43. form.setFieldsValue({
  44. id: undefined,
  45. type: planType,
  46. publishStage: 1,
  47. selectVideoType: 0,
  48. accountId: undefined,
  49. scene: 0,
  50. videoList: []
  51. });
  52. }
  53. }, [actionType, editPlanData, visible]);
  54. useEffect(() => {
  55. getAccountList({accountType: planType});
  56. }, [planType]);
  57. const onTypeChange = (value: string) => {
  58. form.setFieldsValue({ accountId: undefined });
  59. getAccountList({ accountType: value });
  60. }
  61. const handleOk = () => {
  62. form
  63. .validateFields()
  64. .then((values) => {
  65. // Ensure at least one video is selected before submitting
  66. if (selectedVideos.length === 0) {
  67. message.error('请至少选择一个视频'); // Use Antd message
  68. return;
  69. }
  70. const formData = { ...values };
  71. if (formData.type === GzhPlanType.自动回复) {
  72. formData.scene = 0;
  73. }
  74. formData.videoList = selectedVideos;
  75. onOk(formData);
  76. })
  77. .catch((info) => {
  78. console.log('Validate Failed:', info);
  79. });
  80. };
  81. const removeVideo = (idToRemove: number) => {
  82. setSelectedVideos(currentVideos => currentVideos.filter(video => video.videoId !== idToRemove));
  83. };
  84. const openVideoSelector = () => {
  85. setIsVideoSelectVisible(true);
  86. };
  87. const handleVideoSelectionOk = (newlySelectedVideos: VideoItem[]) => {
  88. // Merge existing custom data with newly selected videos
  89. const currentCustomData = new Map<number, Partial<VideoItem>>();
  90. selectedVideos.forEach(v => {
  91. if (v.videoId) {
  92. currentCustomData.set(v.videoId, { customTitle: v.customTitle, customCover: v.customCover, customCoverType: v.customCoverType });
  93. }
  94. });
  95. const mergedVideos = newlySelectedVideos.map(newVideo => {
  96. const customData = currentCustomData.get(newVideo.videoId);
  97. return { ...newVideo, ...customData };
  98. });
  99. setSelectedVideos(mergedVideos);
  100. setIsVideoSelectVisible(false);
  101. };
  102. const handleVideoSelectionCancel = () => {
  103. setIsVideoSelectVisible(false);
  104. };
  105. const playVideo = (video: VideoItem) => {
  106. setPlayingVideo(video);
  107. };
  108. const closeVideoPlayer = () => {
  109. setPlayingVideo(null);
  110. };
  111. const openEditModal = (video: VideoItem) => {
  112. setEditingVideo(video);
  113. };
  114. const handleEditOk = (updatedData: Partial<VideoItem>) => {
  115. console.log('updatedData', updatedData);
  116. setSelectedVideos(currentVideos =>
  117. currentVideos.map(v =>
  118. v.videoId === editingVideo?.videoId ? { ...v, ...updatedData } : v
  119. )
  120. );
  121. setEditingVideo(null); // Close modal
  122. };
  123. const handleEditCancel = () => {
  124. setEditingVideo(null);
  125. };
  126. return (
  127. <>
  128. <Modal
  129. title={actionType === 'add' ? "创建发布计划" : "编辑发布计划"}
  130. open={visible}
  131. destroyOnClose
  132. onCancel={onCancel}
  133. width={900} // Adjust width as needed
  134. footer={[
  135. <Button key="back" onClick={onCancel}>
  136. 取消
  137. </Button>,
  138. <Button key="submit" type="primary" loading={isSubmiting} onClick={handleOk}>
  139. 确定
  140. </Button>,
  141. ]}
  142. zIndex={10}
  143. >
  144. <Form form={form} layout="vertical">
  145. <Form.Item
  146. name="id"
  147. label="计划ID"
  148. hidden
  149. >
  150. </Form.Item>
  151. <Form.Item
  152. name="type"
  153. label="发布计划类型"
  154. layout="horizontal"
  155. labelCol={{ span: 4 }}
  156. labelAlign='left'
  157. required
  158. >
  159. <Select placeholder="选择计划类型" onChange={onTypeChange} className='!w-50' disabled={actionType === 'edit'}>
  160. <Option value={GzhPlanType.自动回复}>自动回复</Option>
  161. <Option value={GzhPlanType.服务号推送}>服务号推送</Option>
  162. </Select>
  163. </Form.Item>
  164. <Form.Item
  165. name="scene"
  166. label="发布场景"
  167. layout="horizontal"
  168. labelCol={{ span: 4 }}
  169. labelAlign='left'
  170. initialValue={0}
  171. hidden={type !== GzhPlanType.自动回复}
  172. >
  173. <Select
  174. placeholder="发布场景"
  175. className='!w-50'
  176. options={[{ label: '关注回复', value: 0 }]}>
  177. </Select>
  178. </Form.Item>
  179. <Form.Item
  180. name="publishStage"
  181. label="发布方式"
  182. labelCol={{ span: 4 }}
  183. labelAlign='left'
  184. layout="horizontal"
  185. rules={[{ required: true, message: '请选择发布方式' }]}
  186. >
  187. <Select placeholder="选择发布方式" allowClear className='!w-50'>
  188. <Option value={0} disabled={type !== GzhPlanType.自动回复}>平台发布</Option>
  189. <Option value={1} >用户获取路径</Option>
  190. </Select>
  191. </Form.Item>
  192. <Form.Item
  193. name="accountId"
  194. label="公众号名称"
  195. labelCol={{ span: 4 }}
  196. labelAlign='left'
  197. layout="horizontal"
  198. rules={[{ required: true, message: '请选择公众号' }]}
  199. >
  200. <Select
  201. placeholder="选择公众号"
  202. allowClear
  203. disabled={actionType === 'edit'}
  204. className='!w-50'
  205. showSearch
  206. filterOption={(input, option) =>
  207. (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  208. }
  209. options={accountOptions?.map(option => ({ label: option.name, value: option.id }))}>
  210. </Select>
  211. </Form.Item>
  212. <Form.Item
  213. name="selectVideoType"
  214. label="视频选取方式"
  215. labelCol={{ span: 4 }}
  216. labelAlign='left'
  217. layout="horizontal"
  218. rules={[{ required: true, message: '请选择视频选取方式' }]}>
  219. <Select placeholder="选择视频选取方式" allowClear className='!w-50'>
  220. <Option value={0} >手动选取</Option>
  221. <Option value={1} disabled>自动选取</Option>
  222. </Select>
  223. </Form.Item>
  224. <Form.Item label="发布内容" required>
  225. {
  226. selectVideoType === 0 ?
  227. (<div className="flex flex-wrap gap-4">
  228. {selectedVideos.map((video) => (
  229. <Card
  230. key={video.videoId}
  231. className="w-[240px] relative group"
  232. >
  233. <Button
  234. shape="circle"
  235. icon={<CloseOutlined />}
  236. className="!absolute top-1 right-1 z-10 bg-gray-400 bg-opacity-50 border-none text-white hidden group-hover:inline-flex justify-center items-center"
  237. size="small"
  238. onClick={() => removeVideo(video.videoId)}
  239. />
  240. <div className="p-0">
  241. <Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
  242. </div>
  243. <div
  244. className="relative"
  245. style={{ paddingBottom: '79.8%' }}
  246. onClick={(e) => {
  247. e.stopPropagation(); // Prevent card selection if clicking thumbnail/play
  248. playVideo(video);
  249. }}
  250. >
  251. <img src={video.customCover || video.cover} referrerPolicy="no-referrer" className="absolute inset-0 w-full h-full object-cover" />
  252. <div className="absolute inset-0 flex justify-center items-center cursor-pointer">
  253. <CaretRightFilled className="!text-white text-4xl bg-black/20 rounded-full p-1 pl-2" />
  254. </div>
  255. </div>
  256. <div className="p-3">
  257. <Button
  258. icon={<EditOutlined />}
  259. className="w-full mt-2"
  260. onClick={() => openEditModal(video)} // Open edit modal
  261. >
  262. 编辑标题/封面
  263. </Button>
  264. </div>
  265. </Card>
  266. ))}
  267. {/* Add Video Button - Conditionally Rendered */}
  268. {selectedVideos.length < 3 && (
  269. <div
  270. className="w-[240px] h-[316px] flex flex-col justify-center items-center border border-dashed border-gray-300 rounded cursor-pointer dark:border-gray-600 hover:border-blue-500 hover:text-blue-500"
  271. onClick={openVideoSelector} // Open the drawer on click
  272. >
  273. <PlusOutlined className="text-2xl mb-2" />
  274. <Typography.Text>添加视频</Typography.Text>
  275. </div>
  276. )}
  277. </div>)
  278. : (<div>
  279. <Paragraph>视频由系统自动选取,不可手动选择</Paragraph>
  280. </div>)
  281. }
  282. </Form.Item>
  283. </Form>
  284. </Modal>
  285. {/* Video Selection Drawer */}
  286. <VideoSelectModal
  287. visible={isVideoSelectVisible}
  288. onClose={handleVideoSelectionCancel}
  289. onOk={handleVideoSelectionOk}
  290. selectedVideos={selectedVideos}
  291. initialSelectedIds={selectedVideos.map(v => v.videoId)} // Pass current selection IDs
  292. />
  293. {/* Video Player Modal */}
  294. <Modal
  295. open={!!playingVideo}
  296. onCancel={closeVideoPlayer}
  297. title={playingVideo?.customTitle || playingVideo?.title}
  298. footer={null}
  299. destroyOnClose // Unmount video element when closed
  300. width={720} // Adjust as needed
  301. styles={{ body: { padding: 0, background: '#000' } }}
  302. zIndex={20}
  303. >
  304. {playingVideo && (
  305. <video
  306. controls
  307. autoPlay
  308. className="w-full h-auto max-h-[80vh] block"
  309. src={playingVideo.video}
  310. >
  311. Your browser does not support the video tag.
  312. </video>
  313. )}
  314. </Modal>
  315. {/* Edit Title/Cover Modal */}
  316. <EditTitleCoverModal
  317. visible={!!editingVideo}
  318. onCancel={handleEditCancel}
  319. onOk={handleEditOk}
  320. video={editingVideo}
  321. />
  322. </>
  323. );
  324. };
  325. export default AddPunlishPlanModal;