|
@@ -0,0 +1,196 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import {
|
|
|
+ Drawer,
|
|
|
+ Button,
|
|
|
+ Select,
|
|
|
+ Input,
|
|
|
+ Row,
|
|
|
+ Col,
|
|
|
+ Card,
|
|
|
+ Typography,
|
|
|
+ Pagination,
|
|
|
+ Checkbox,
|
|
|
+ Space,
|
|
|
+ message,
|
|
|
+} from 'antd';
|
|
|
+import { EditOutlined, PlayCircleOutlined, CheckCircleFilled } from '@ant-design/icons';
|
|
|
+
|
|
|
+const { Option } = Select;
|
|
|
+const { Text } = Typography;
|
|
|
+
|
|
|
+// Reusing the VideoItem interface, adjust if needed
|
|
|
+interface VideoItem {
|
|
|
+ id: number;
|
|
|
+ source: string;
|
|
|
+ title: string;
|
|
|
+ thumbnail: string;
|
|
|
+ spreadEfficiency: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface VideoSelectModalProps {
|
|
|
+ visible: boolean;
|
|
|
+ onClose: () => void;
|
|
|
+ onOk: (selectedVideos: VideoItem[]) => void;
|
|
|
+ initialSelectedIds?: number[]; // Pass initially selected IDs if needed
|
|
|
+}
|
|
|
+
|
|
|
+// Mock data for demonstration
|
|
|
+const mockVideos: VideoItem[] = Array.from({ length: 15 }, (_, i) => ({
|
|
|
+ id: i + 100, // Ensure unique IDs
|
|
|
+ source: '票圈 | 3亿人喜欢的视频平台',
|
|
|
+ title: `最美中国——上海之海 ${i + 1},带你领略航拍风景`,
|
|
|
+ thumbnail: 'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
|
|
|
+ spreadEfficiency: 8.56 + i * 0.1,
|
|
|
+}));
|
|
|
+
|
|
|
+const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, initialSelectedIds = [] }) => {
|
|
|
+ const [category, setCategory] = useState<string>();
|
|
|
+ const [searchTerm, setSearchTerm] = useState<string>('');
|
|
|
+ const [currentPage, setCurrentPage] = useState(1);
|
|
|
+ const [pageSize, setPageSize] = useState(10);
|
|
|
+ const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
|
|
|
+ const MAX_SELECTION = 3; // Define the limit
|
|
|
+
|
|
|
+ // Reset selection when modal opens/initial selection changes
|
|
|
+ useEffect(() => {
|
|
|
+ if (visible) {
|
|
|
+ setSelectedVideoIds(new Set(initialSelectedIds));
|
|
|
+ }
|
|
|
+ }, [visible, initialSelectedIds]);
|
|
|
+
|
|
|
+ const handleSearch = () => {
|
|
|
+ // Implement search logic here, fetch videos based on category and searchTerm
|
|
|
+ console.log('Searching for:', { category, searchTerm });
|
|
|
+ setCurrentPage(1); // Reset to first page on search
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSelectVideo = (videoId: number) => {
|
|
|
+ setSelectedVideoIds(prev => {
|
|
|
+ const newSet = new Set(prev);
|
|
|
+ if (newSet.has(videoId)) {
|
|
|
+ newSet.delete(videoId); // Allow deselection anytime
|
|
|
+ } else {
|
|
|
+ // Check limit before adding
|
|
|
+ if (newSet.size >= MAX_SELECTION) {
|
|
|
+ message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
|
|
|
+ return prev; // Return previous state if limit reached
|
|
|
+ }
|
|
|
+ newSet.add(videoId);
|
|
|
+ }
|
|
|
+ return newSet;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleOk = () => {
|
|
|
+ // Find the full video objects corresponding to the selected IDs
|
|
|
+ // In a real app, you might only pass IDs back, or fetch details if needed
|
|
|
+ const selectedVideos = mockVideos.filter(video => selectedVideoIds.has(video.id));
|
|
|
+ onOk(selectedVideos);
|
|
|
+ onClose(); // Close the drawer after confirmation
|
|
|
+ };
|
|
|
+
|
|
|
+ // Paginate mock data for display
|
|
|
+ const paginatedData = mockVideos.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Drawer
|
|
|
+ title="内容选取"
|
|
|
+ open={visible}
|
|
|
+ onClose={onClose}
|
|
|
+ width={720} // Adjust width as needed
|
|
|
+ placement="right"
|
|
|
+ footerStyle={{ textAlign: 'right', padding: '10px 24px' }}
|
|
|
+ footer={
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <Pagination
|
|
|
+ current={currentPage}
|
|
|
+ pageSize={pageSize}
|
|
|
+ total={mockVideos.length} // Replace with actual total count from API
|
|
|
+ onChange={(page, size) => {
|
|
|
+ setCurrentPage(page);
|
|
|
+ setPageSize(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>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {/* Search Filters */}
|
|
|
+ <div className="flex flex-wrap gap-4 mb-6">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-gray-600">品类:</span>
|
|
|
+ <Select
|
|
|
+ placeholder="选择品类"
|
|
|
+ style={{ width: 180 }}
|
|
|
+ value={category}
|
|
|
+ onChange={setCategory}
|
|
|
+ // Add options based on your data
|
|
|
+ options={[{ label: '品类A', value: 'A' }, { label: '品类B', value: 'B' }]}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-gray-600">视频标题:</span>
|
|
|
+ <Input
|
|
|
+ placeholder="搜索视频标题"
|
|
|
+ style={{ width: 200 }}
|
|
|
+ value={searchTerm}
|
|
|
+ onChange={e => setSearchTerm(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Button type="primary" onClick={handleSearch}>搜索</Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Video Grid */}
|
|
|
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
|
|
+ {paginatedData.map((video) => {
|
|
|
+ const isSelected = selectedVideoIds.has(video.id);
|
|
|
+ const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
|
|
|
+ return (
|
|
|
+ <Card
|
|
|
+ key={video.id}
|
|
|
+ className={`relative ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
|
|
|
+ bodyStyle={{ padding: 0 }}
|
|
|
+ onClick={() => !isDisabled && handleSelectVideo(video.id)} // Prevent click if disabled
|
|
|
+ >
|
|
|
+ <div className="p-3">
|
|
|
+ <Text type="secondary" className="text-xs">{video.source}</Text>
|
|
|
+ <Text className="block mt-1 mb-2 leading-tight line-clamp-2" title={video.title}>{video.title}</Text>
|
|
|
+ </div>
|
|
|
+ <div className="relative h-[120px] bg-gray-200">
|
|
|
+ <img src={video.thumbnail} alt={video.title} className="w-full h-full object-cover" />
|
|
|
+ <div className="absolute inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
|
|
+ <PlayCircleOutlined className="text-white text-4xl" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="p-3 flex justify-between items-center">
|
|
|
+ <Text type="secondary" className="text-xs">传播效率: {video.spreadEfficiency.toFixed(2)}</Text>
|
|
|
+ {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>
|
|
|
+ <Button
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ className="w-[calc(100%-1.5rem)] mx-3 mb-3"
|
|
|
+ // onClick={(e) => { e.stopPropagation(); handleEditTitleCover(video.id); }} // Prevent card click
|
|
|
+ >
|
|
|
+ 编辑标题/封面
|
|
|
+ </Button>
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </Drawer>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default VideoSelectModal;
|