jihuaqiang преди 2 седмици
родител
ревизия
4e0bb64054

+ 1 - 1
src/components/layout/headerbar.tsx

@@ -20,7 +20,7 @@ const Headerbar = (props: { colorBgContainer: string }) => {
 	}
 
   return (
-    <Header title='票圈内容合作平台' className='bg-[#20214F] px-[0]'>
+    <Header title='票圈内容合作平台' className='bg-[#20214F] !px-1'>
       <div className='text-[#9090a7] flex items-center justify-between h-full px-[20px] '>
         <div className='flex items-center gap-[12px]'>
           <Icon component={LogoIcon} className='text-[40px]' />

+ 0 - 0
src/views/publishContent/weGZH/components/publishPlanModal/index.module.css


+ 175 - 0
src/views/publishContent/weGZH/components/publishPlanModal/index.tsx

@@ -0,0 +1,175 @@
+import React, { useState } from 'react';
+import { Modal, Form, Select, Button, Row, Col, Card, Typography } from 'antd';
+import { CloseOutlined, PlusOutlined, EditOutlined, PlayCircleOutlined } from '@ant-design/icons';
+import VideoSelectModal from '../videoSelectModal'; // Import the new component
+
+const { Option } = Select;
+const { Text } = Typography;
+
+interface VideoItem {
+	id: number;
+	source: string;
+	title: string;
+	thumbnail: string; // URL to thumbnail image
+	spreadEfficiency: number;
+}
+
+interface AddPunlishPlanModalProps {
+	visible: boolean;
+	onCancel: () => void;
+	onOk: (values: any) => void; // Pass form values on OK
+}
+
+const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCancel, onOk }) => {
+	const [form] = Form.useForm();
+	const [selectedVideos, setSelectedVideos] = useState<VideoItem[]>([]); // Start with empty selection
+	const [isVideoSelectVisible, setIsVideoSelectVisible] = useState(false);
+
+	const handleOk = () => {
+		form
+			.validateFields()
+			.then((values) => {
+				// Ensure at least one video is selected before submitting
+				if (selectedVideos.length === 0) {
+					// Optionally show a message to the user
+					console.error('Please select at least one video.');
+					return;
+				}
+				onOk({ ...values, videos: selectedVideos });
+			})
+			.catch((info) => {
+				console.log('Validate Failed:', info);
+			});
+	};
+
+	const removeVideo = (idToRemove: number) => {
+		setSelectedVideos(currentVideos => currentVideos.filter(video => video.id !== idToRemove));
+	};
+
+	const openVideoSelector = () => {
+		setIsVideoSelectVisible(true);
+	};
+
+	const handleVideoSelectionOk = (newlySelectedVideos: VideoItem[]) => {
+		setSelectedVideos(newlySelectedVideos);
+		setIsVideoSelectVisible(false);
+	};
+
+	const handleVideoSelectionCancel = () => {
+		setIsVideoSelectVisible(false);
+	};
+
+	return (
+		<>
+			<Modal
+				title="创建发布计划"
+				open={visible}
+				onCancel={onCancel}
+				width={800} // Adjust width as needed
+				footer={[
+					<Button key="back" onClick={onCancel}>
+						取消
+					</Button>,
+					<Button key="submit" type="primary" onClick={handleOk}>
+						确定
+					</Button>,
+				]}
+			>
+				<Form form={form} layout="vertical">
+					<Row gutter={16}>
+						<Col span={12}>
+							<Form.Item
+								name="publisher"
+								label="发布方"
+								rules={[{ required: true, message: '请选择发布方' }]}
+							>
+								<Select placeholder="选择发布方">
+									<Option value="platform">平台发布</Option>
+									<Option value="user">用户发布</Option>
+								</Select>
+							</Form.Item>
+						</Col>
+						<Col span={12}>
+							<Form.Item
+								name="officialAccount"
+								label="公众号名称"
+								rules={[{ required: true, message: '请选择公众号' }]}
+							>
+								<Select placeholder="选择公众号">
+									<Option value="account1">小慧爱厨房</Option>
+									<Option value="account2">小阳看天下</Option>
+									{/* Add more options as needed */}
+								</Select>
+							</Form.Item>
+						</Col>
+					</Row>
+
+					<Form.Item label="发布场景">
+						<Text>关注回复</Text>
+					</Form.Item>
+
+					<Form.Item label="发布内容" required>
+						<div className="flex flex-wrap gap-4">
+							{selectedVideos.map((video) => (
+								<Card
+									key={video.id}
+									className="w-[220px] relative group"
+									bodyStyle={{ padding: 0 }}
+								>
+									<Button
+										shape="circle"
+										icon={<CloseOutlined />}
+										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"
+										size="small"
+										onClick={() => removeVideo(video.id)}
+									/>
+									<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">
+										<Text type="secondary" className="text-xs">传播效率: {video.spreadEfficiency.toFixed(2)}</Text>
+										<Button
+											icon={<EditOutlined />}
+											className="w-full mt-2"
+											// onClick={() => handleEditTitleCover(video.id)} // Add handler later
+										>
+											编辑标题/封面
+										</Button>
+									</div>
+								</Card>
+							))}
+
+							{/* Add Video Button - Conditionally Rendered */}
+							{selectedVideos.length < 3 && (
+								<div
+									className="w-[220px] h-[316px] flex flex-col justify-center items-center bg-gray-100 border border-dashed border-gray-300 rounded cursor-pointer hover:border-blue-500 hover:text-blue-500"
+									onClick={openVideoSelector} // Open the drawer on click
+								>
+									<PlusOutlined className="text-2xl mb-2" />
+									<Typography.Text>添加视频</Typography.Text>
+								</div>
+							)}
+						</div>
+					</Form.Item>
+				</Form>
+			</Modal>
+
+			{/* Video Selection Drawer */}
+			<VideoSelectModal
+				visible={isVideoSelectVisible}
+				onClose={handleVideoSelectionCancel}
+				onOk={handleVideoSelectionOk}
+				initialSelectedIds={selectedVideos.map(v => v.id)} // Pass current selection IDs
+			/>
+		</>
+	);
+};
+
+export default AddPunlishPlanModal;

+ 196 - 0
src/views/publishContent/weGZH/components/videoSelectModal/index.tsx

@@ -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;

+ 5 - 0
src/views/publishContent/weGZH/index.module.css

@@ -0,0 +1,5 @@
+.antTable {
+	:global(.ant-table-title) {
+		padding: 0;
+	}
+}

+ 21 - 10
src/views/publishContent/weGZH/index.tsx

@@ -1,11 +1,12 @@
 import React, { useState } from 'react';
 import { Space, Table, Button, Input, Select, DatePicker } from 'antd';
 import type { TableProps } from 'antd';
-
+import styles from './index.module.css';
+import AddPunlishPlanModal from './components/publishPlanModal';
 const { RangePicker } = DatePicker;
 
 interface DataType {
-	key: string;
+	// key: string;
 	officialAccount: string;
 	scene: string;
 	videoCount: number;
@@ -20,6 +21,7 @@ const WeGZHContent: React.FC = () => {
 	const [videoTitle, setVideoTitle] = useState<string>('');
 	const [selectedPublisher, setSelectedPublisher] = useState<string>();
 	const [dateRange, setDateRange] = useState<[string, string]>();
+	const [isShowAddPunlishPlan, setIsShowAddPunlishPlan] = useState<boolean>(false);
 
 	// 表格列配置
 	const columns: TableProps<DataType>['columns'] = [
@@ -69,7 +71,6 @@ const WeGZHContent: React.FC = () => {
 	// 模拟数据
 	const data: DataType[] = [
 		{
-			key: '1',
 			officialAccount: '小慧爱厨房',
 			scene: '关注回复',
 			videoCount: 3,
@@ -78,7 +79,6 @@ const WeGZHContent: React.FC = () => {
 			publisher: '平台发布',
 		},
 		{
-			key: '2',
 			officialAccount: '小阳看天下',
 			scene: '关注回复',
 			videoCount: 1,
@@ -88,13 +88,17 @@ const WeGZHContent: React.FC = () => {
 		},
 	];
 
+	const addPunlishPlan = () => {
+		setIsShowAddPunlishPlan(true);
+	}
+
 	return (
 		<div>
-			<div className="bg-white p-6 rounded-lg">
-				<div className="text-lg font-medium mb-6">公众号内容</div>
+			<div className="bg-white rounded-lg">
+				<div className="text-lg font-medium mb-3">公众号内容</div>
 				
 				{/* 搜索区域 */}
-				<div className="flex flex-wrap gap-4 mb-6">
+				<div className="flex flex-wrap gap-4 mb-3">
 					<div className="flex items-center gap-2">
 						<span className="text-gray-600">公众号名称:</span>
 						<Select
@@ -153,12 +157,14 @@ const WeGZHContent: React.FC = () => {
 				<Table
 					title={() =>
 						<div className="flex justify-between">
-							<div className="bg-[#ffc107] text-white border-none hover:bg-[#ffca2c]">
+							<div className="text-[#333] font-medium px-[45px] py-[15px] rounded-tr-[20px] bg-[#ffc107] text-[16px] border-none hover:bg-[#ffca2c]">
 								自动回复
 							</div>
-							<Button type="primary">+ 创建发布</Button>
+							<Button type="primary" onClick={addPunlishPlan}>+ 创建发布</Button>
 						</div>
-					}	
+					}
+					rowKey={(record) => record.officialAccount}
+					className={styles.antTable}
 					columns={columns}
 					dataSource={data}
 					pagination={{
@@ -167,6 +173,11 @@ const WeGZHContent: React.FC = () => {
 						showTotal: (total) => `共 ${total} 条`,
 					}}
 				/>
+				<AddPunlishPlanModal
+					visible={isShowAddPunlishPlan}
+					onCancel={() => setIsShowAddPunlishPlan(false)}
+					onOk={() => setIsShowAddPunlishPlan(false)}
+				/>
 			</div>
 		</div>
 	);