Ver Fonte

计划生成页面完善

jihuaqiang há 2 semanas atrás
pai
commit
ee7fe5280c

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

@@ -2,7 +2,7 @@
 import { Button, Dropdown, Layout, message, Switch} from 'antd';
 import { Button, Dropdown, Layout, message, Switch} from 'antd';
 import useConfigStore from '../../store/config';
 import useConfigStore from '../../store/config';
 import LogoIcon from "@src/assets/images/login/logo.svg?react";
 import LogoIcon from "@src/assets/images/login/logo.svg?react";
-import Icon from "@ant-design/icons";
+import Icon, { UserOutlined } from "@ant-design/icons";
 import { logout } from "../../http/sso";
 import { logout } from "../../http/sso";
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 
 
@@ -26,12 +26,12 @@ const Headerbar = (props: { colorBgContainer: string }) => {
           <Icon component={LogoIcon} className='text-[40px]' />
           <Icon component={LogoIcon} className='text-[40px]' />
           <span className='text-lg font-bold'>票圈内容合作平台</span>
           <span className='text-lg font-bold'>票圈内容合作平台</span>
         </div>
         </div>
-        <div className='flex items-center gap-[10px] cursor-pointer'>
+        <div className='flex items-center gap-[30px] cursor-pointer'>
           <Switch checkedChildren="Light" unCheckedChildren="Dark" defaultChecked onChange={(checked) => setAlgorithm(checked ? 'default' : 'dark')} />
           <Switch checkedChildren="Light" unCheckedChildren="Dark" defaultChecked onChange={(checked) => setAlgorithm(checked ? 'default' : 'dark')} />
 					<Dropdown trigger={['hover']} menu={{items: [{label: (<Button type="primary" onClick={logoutHandle}>退出账号</Button>), key: 'logout'}]}}>
 					<Dropdown trigger={['hover']} menu={{items: [{label: (<Button type="primary" onClick={logoutHandle}>退出账号</Button>), key: 'logout'}]}}>
-						<div className='flex items-center gap-[10px]'>
+						<div className='flex items-center gap-[10px] text-xl'>
 							<div>Yujian Xue</div>
 							<div>Yujian Xue</div>
-							<img src="https://avatars.githubusercontent.com/u/48818060?s=48&v=4" alt="avatar" style={{ width: 40, height: 40 }} />
+							<UserOutlined />
 						</div>
 						</div>
 					</Dropdown>
 					</Dropdown>
         </div>
         </div>

+ 1 - 1
src/views/login/login.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from "react";
 import React, { useState } from "react";
 import { Form, Input, Button, Tabs, message } from "antd";
 import { Form, Input, Button, Tabs, message } from "antd";
-import Icon, { LockOutlined, UserOutlined } from "@ant-design/icons";
+import Icon from "@ant-design/icons";
 import styles from "./login.module.css";
 import styles from "./login.module.css";
 import { sendCode, loginBySendCode, login } from "../../http/sso";
 import { sendCode, loginBySendCode, login } from "../../http/sso";
 import LogoIcon from "@src/assets/images/login/logo.svg?react";
 import LogoIcon from "@src/assets/images/login/logo.svg?react";

+ 247 - 0
src/views/publishContent/weGZH/components/editTitleCoverModal/index.tsx

@@ -0,0 +1,247 @@
+import React, { useState, useEffect } from 'react';
+import {
+	Modal,
+	Form,
+	Radio,
+	Input,
+	Button,
+	Upload,
+	Image,
+	Space,
+	message,
+} from 'antd';
+import { UploadOutlined, PlusOutlined, CheckCircleFilled } from '@ant-design/icons';
+import type { RadioChangeEvent } from 'antd';
+import type { UploadFile, UploadProps } from 'antd/es/upload/interface';
+import { VideoItem } from '../types';
+
+const { TextArea } = Input;
+
+interface EditTitleCoverModalProps {
+	visible: boolean;
+	onCancel: () => void;
+	onOk: (updatedVideoData: Partial<VideoItem>) => void;
+	video: VideoItem | null;
+}
+
+// Mock screenshots for demonstration
+const mockScreenshots = [
+	'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
+	'https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.jpeg',
+	'https://gw.alipayobjects.com/zos/antfincdn/cV16ZqzMjW/photo-1473091540282-9b846e7965e3.jpeg',
+];
+
+const EditTitleCoverModal: React.FC<EditTitleCoverModalProps> = ({ visible, onCancel, onOk, video }) => {
+	const [form] = Form.useForm();
+	const [titleType, setTitleType] = useState<'original' | 'custom'>('original');
+	const [coverType, setCoverType] = useState<'original' | 'screenshot' | 'upload'>('original');
+	const [selectedScreenshot, setSelectedScreenshot] = useState<string | null>(null);
+	const [fileList, setFileList] = useState<UploadFile[]>([]);
+
+	useEffect(() => {
+		if (video && visible) {
+			// Reset form based on incoming video data
+			const hasCustomTitle = video.customTitle && video.customTitle !== video.title;
+			const isCustomCover = video.customThumbnail && video.customThumbnail !== video.thumbnail;
+			const isScreenshotCover = mockScreenshots.includes(video.customThumbnail || '');
+
+			const initialTitleType = hasCustomTitle ? 'custom' : 'original';
+			let initialCoverType: 'original' | 'screenshot' | 'upload' = 'original';
+			if (isCustomCover) {
+				if (isScreenshotCover) {
+					initialCoverType = 'screenshot';
+					setSelectedScreenshot(video.customThumbnail || null);
+					setFileList([]);
+				} else {
+					initialCoverType = 'upload';
+					setSelectedScreenshot(null);
+					// Assume customThumbnail for upload is a URL, create UploadFile structure
+					setFileList([{ uid: '-1', name: 'custom_cover.png', status: 'done', url: video.customThumbnail }]);
+				}
+			} else {
+				setSelectedScreenshot(null);
+				setFileList([]);
+			}
+
+			setTitleType(initialTitleType);
+			setCoverType(initialCoverType);
+			form.setFieldsValue({
+				titleType: initialTitleType,
+				customTitle: hasCustomTitle ? video.customTitle : '',
+				coverType: initialCoverType,
+			});
+		} else {
+			// Reset form when modal closes or no video
+			form.resetFields();
+			setTitleType('original');
+			setCoverType('original');
+			setSelectedScreenshot(null);
+			setFileList([]);
+		}
+	}, [video, visible, form]);
+
+	const handleOk = () => {
+		form.validateFields().then(values => {
+			const updatedData: Partial<VideoItem> = {};
+
+			// Handle Title
+			if (values.titleType === 'custom') {
+				updatedData.customTitle = values.customTitle || '';
+			} else {
+				updatedData.customTitle = video?.title; // Revert to original if selected
+			}
+
+			// Handle Cover
+			if (values.coverType === 'screenshot') {
+				if (!selectedScreenshot) {
+					message.error('请选择一个视频截图作为封面');
+					return;
+				}
+				updatedData.customThumbnail = selectedScreenshot;
+			} else if (values.coverType === 'upload') {
+				if (fileList.length === 0 || !fileList[0].url) {
+					// If using status, check for 'done' status and response URL
+					message.error('请上传自定义封面图片');
+					return;
+				}
+				// Assuming the upload process directly provides the final URL in fileList[0].url
+				// In a real app, you might get this from upload response
+				updatedData.customThumbnail = fileList[0].url;
+			} else { // Original
+				updatedData.customThumbnail = video?.thumbnail; // Revert to original
+			}
+
+			onOk(updatedData);
+		}).catch(info => {
+			console.log('Validate Failed:', info);
+		});
+	};
+
+	const handleUploadChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
+		// Only keep the latest file
+		setFileList(newFileList.slice(-1));
+		// If upload is successful, manually set coverType to 'upload'
+		if (newFileList.length > 0 && newFileList[0].status === 'done') {
+			setCoverType('upload');
+			form.setFieldsValue({ coverType: 'upload' });
+			// In a real app, you would get the URL from the response
+			// For demo: assuming the file object gets a url property after upload
+			if (!newFileList[0].url) {
+				newFileList[0].url = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'; // Placeholder URL
+			}
+		}
+	};
+
+	const handleTitleTypeChange = (e: RadioChangeEvent) => {
+		setTitleType(e.target.value);
+	};
+
+	const handleCoverTypeChange = (e: RadioChangeEvent) => {
+		setCoverType(e.target.value);
+		// Reset other cover selections when type changes
+		if (e.target.value !== 'screenshot') setSelectedScreenshot(null);
+		if (e.target.value !== 'upload') setFileList([]);
+	};
+
+	const handleSelectScreenshot = (imgUrl: string) => {
+		setSelectedScreenshot(imgUrl);
+		setCoverType('screenshot');
+		form.setFieldsValue({ coverType: 'screenshot' });
+		setFileList([]); // Clear upload list if screenshot selected
+	};
+
+	const uploadButton = (
+		<button style={{ border: 0, background: 'none' }} type="button">
+			<PlusOutlined />
+			<div style={{ marginTop: 8 }}>上传封面</div>
+		</button>
+	);
+
+	return (
+		<Modal
+			title="编辑标题/封面"
+			open={visible}
+			onCancel={onCancel}
+			onOk={handleOk}
+			width={600}
+			destroyOnClose
+			zIndex={20}
+			forceRender // Keep form instance even when closed
+		>
+			<Form form={form} layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} labelAlign="left">
+				{/* Title Section */}
+				<Form.Item label="标题" name="titleType" initialValue="original">
+					<Radio.Group onChange={handleTitleTypeChange}>
+						<Radio value="original">原始标题</Radio>
+						<Radio value="custom">自定义标题</Radio>
+					</Radio.Group>
+				</Form.Item>
+
+				{titleType === 'custom' && (
+					<Form.Item
+						label="自定义标题"
+						name="customTitle"
+						rules={[{ required: true, message: '请输入自定义标题' }]}
+					>
+						<TextArea rows={2} maxLength={50} showCount placeholder="请输入标题" />
+					</Form.Item>
+				)}
+
+				{/* Cover Section */}
+				<Form.Item label="封面" name="coverType" initialValue="original">
+					<Radio.Group onChange={handleCoverTypeChange}>
+						<Radio value="original">原始封面</Radio>
+						<Radio value="screenshot">视频截图</Radio>
+						<Radio value="upload">自定义上传</Radio>
+					</Radio.Group>
+				</Form.Item>
+
+				{/* Screenshot Selection */}
+				{coverType === 'screenshot' && (
+					<Form.Item label="视频截图" wrapperCol={{ offset: 4, span: 20 }}>
+						<Space wrap>
+							{mockScreenshots.map((imgUrl, index) => (
+								<div
+									key={index}
+									className={`relative w-24 h-24 border-2 cursor-pointer ${selectedScreenshot === imgUrl ? 'border-blue-500' : 'border-transparent'}`}
+									onClick={() => handleSelectScreenshot(imgUrl)}
+								>
+									<Image width="100%" height="100%" src={imgUrl} preview={false} style={{ objectFit: 'cover' }} />
+									{selectedScreenshot === imgUrl && (
+										<CheckCircleFilled className="absolute top-1 right-1 text-green-500 bg-white rounded-full text-lg" />
+									)}
+								</div>
+							))}
+						</Space>
+					</Form.Item>
+				)}
+
+				{/* Custom Upload */}
+				{coverType === 'upload' && (
+					<Form.Item label="自定义上传" wrapperCol={{ offset: 4, span: 20 }}>
+						<Upload
+							// Replace with your actual upload endpoint
+							action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
+							listType="picture-card"
+							fileList={fileList}
+							onChange={handleUploadChange}
+							maxCount={1}
+							accept=".png,.jpg,.jpeg,.gif"
+							beforeUpload={(file) => {
+								const isLt2M = file.size / 1024 / 1024 < 2;
+								if (!isLt2M) {
+									message.error('图片必须小于 2MB!');
+								}
+								return isLt2M;
+							}}
+						>
+							{fileList.length >= 1 ? null : uploadButton}
+						</Upload>
+					</Form.Item>
+				)}
+			</Form>
+		</Modal>
+	);
+};
+
+export default EditTitleCoverModal; 

+ 148 - 59
src/views/publishContent/weGZH/components/publishPlanModal/index.tsx

@@ -1,29 +1,34 @@
-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
+import React, { useEffect, useState } from 'react';
+import { Modal, Form, Select, Button, Row, Col, Card, Typography, message } from 'antd';
+import { CloseOutlined, PlusOutlined, EditOutlined, PlayCircleOutlined, CaretRightFilled } from '@ant-design/icons';
+import VideoSelectModal from '../videoSelectModal';
+import EditTitleCoverModal from '../editTitleCoverModal';
+import { VideoItem } from '../types'; // Import from common types
+import { PlanData } from '../..';
 
 
 const { Option } = Select;
 const { Option } = Select;
 const { Text } = Typography;
 const { Text } = Typography;
 
 
-interface VideoItem {
-	id: number;
-	source: string;
-	title: string;
-	thumbnail: string; // URL to thumbnail image
-	spreadEfficiency: number;
-}
-
 interface AddPunlishPlanModalProps {
 interface AddPunlishPlanModalProps {
 	visible: boolean;
 	visible: boolean;
 	onCancel: () => void;
 	onCancel: () => void;
 	onOk: (values: any) => void; // Pass form values on OK
 	onOk: (values: any) => void; // Pass form values on OK
+	actionType: 'add' | 'edit';
+	editPlanData?: PlanData;
 }
 }
 
 
-const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCancel, onOk }) => {
+const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCancel, onOk, actionType, editPlanData }) => {
 	const [form] = Form.useForm();
 	const [form] = Form.useForm();
-	const [selectedVideos, setSelectedVideos] = useState<VideoItem[]>([]); // Start with empty selection
+	const [selectedVideos, setSelectedVideos] = useState<VideoItem[]>([]);
 	const [isVideoSelectVisible, setIsVideoSelectVisible] = useState(false);
 	const [isVideoSelectVisible, setIsVideoSelectVisible] = useState(false);
+	const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null); // State for video player modal
+	const [editingVideo, setEditingVideo] = useState<VideoItem | null>(null); // State for editing modal
+
+	useEffect(() => {
+		if (actionType === 'edit') {
+			form.setFieldsValue(editPlanData);
+		}
+	}, [actionType, editPlanData]);
 
 
 	const handleOk = () => {
 	const handleOk = () => {
 		form
 		form
@@ -31,11 +36,16 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 			.then((values) => {
 			.then((values) => {
 				// Ensure at least one video is selected before submitting
 				// Ensure at least one video is selected before submitting
 				if (selectedVideos.length === 0) {
 				if (selectedVideos.length === 0) {
-					// Optionally show a message to the user
-					console.error('Please select at least one video.');
+					message.error('请至少选择一个视频'); // Use Antd message
 					return;
 					return;
 				}
 				}
-				onOk({ ...values, videos: selectedVideos });
+				// Prepare data with potentially updated titles/thumbnails
+				const videosToSubmit = selectedVideos.map(v => ({
+					...v,
+					title: v.customTitle || v.title,
+					thumbnail: v.customThumbnail || v.thumbnail,
+				}));
+				onOk({ ...values, videos: videosToSubmit });
 			})
 			})
 			.catch((info) => {
 			.catch((info) => {
 				console.log('Validate Failed:', info);
 				console.log('Validate Failed:', info);
@@ -51,7 +61,20 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 	};
 	};
 
 
 	const handleVideoSelectionOk = (newlySelectedVideos: VideoItem[]) => {
 	const handleVideoSelectionOk = (newlySelectedVideos: VideoItem[]) => {
-		setSelectedVideos(newlySelectedVideos);
+		// Merge existing custom data with newly selected videos
+		const currentCustomData = new Map<number, Partial<VideoItem>>();
+		selectedVideos.forEach(v => {
+			if (v.customTitle || v.customThumbnail) {
+				currentCustomData.set(v.id, { customTitle: v.customTitle, customThumbnail: v.customThumbnail });
+			}
+		});
+
+		const mergedVideos = newlySelectedVideos.map(newVideo => {
+			const customData = currentCustomData.get(newVideo.id);
+			return { ...newVideo, ...customData };
+		});
+
+		setSelectedVideos(mergedVideos);
 		setIsVideoSelectVisible(false);
 		setIsVideoSelectVisible(false);
 	};
 	};
 
 
@@ -59,13 +82,38 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 		setIsVideoSelectVisible(false);
 		setIsVideoSelectVisible(false);
 	};
 	};
 
 
+	const playVideo = (video: VideoItem) => {
+		setPlayingVideo(video);
+	};
+
+	const closeVideoPlayer = () => {
+		setPlayingVideo(null);
+	};
+
+	const openEditModal = (video: VideoItem) => {
+		setEditingVideo(video);
+	};
+
+	const handleEditOk = (updatedData: Partial<VideoItem>) => {
+		setSelectedVideos(currentVideos =>
+			currentVideos.map(v =>
+				v.id === editingVideo?.id ? { ...v, ...updatedData } : v
+			)
+		);
+		setEditingVideo(null); // Close modal
+	};
+
+	const handleEditCancel = () => {
+		setEditingVideo(null);
+	};
+
 	return (
 	return (
 		<>
 		<>
 			<Modal
 			<Modal
 				title="创建发布计划"
 				title="创建发布计划"
 				open={visible}
 				open={visible}
 				onCancel={onCancel}
 				onCancel={onCancel}
-				width={800} // Adjust width as needed
+				width={900} // Adjust width as needed
 				footer={[
 				footer={[
 					<Button key="back" onClick={onCancel}>
 					<Button key="back" onClick={onCancel}>
 						取消
 						取消
@@ -74,37 +122,43 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 						确定
 						确定
 					</Button>,
 					</Button>,
 				]}
 				]}
+				zIndex={10}
+				destroyOnClose
 			>
 			>
 				<Form form={form} layout="vertical">
 				<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="发布场景">
+					<Form.Item
+						name="publisher"
+						label="发布方"
+						labelCol={{ span: 4 }}
+						labelAlign='left'
+						layout="horizontal"
+						rules={[{ required: true, message: '请选择发布方' }]}
+					>
+						<Select placeholder="选择发布方" allowClear className='!w-50'>
+							<Option value="platform">平台发布</Option>
+							<Option value="user">用户发布</Option>
+						</Select>
+					</Form.Item>
+					<Form.Item
+						name="officialAccount"
+						label="公众号名称"
+						labelCol={{ span: 4 }}
+						labelAlign='left'
+						layout="horizontal"
+						rules={[{ required: true, message: '请选择公众号' }]}
+					>
+						<Select placeholder="选择公众号" allowClear className='!w-50'>
+							<Option value="account1">小慧爱厨房</Option>
+							<Option value="account2">小阳看天下</Option>
+							{/* Add more options as needed */}
+						</Select>
+					</Form.Item>
+					<Form.Item
+						label="发布场景"
+						layout="horizontal"
+						labelCol={{ span: 4 }}
+						labelAlign='left'
+					>
 						<Text>关注回复</Text>
 						<Text>关注回复</Text>
 					</Form.Item>
 					</Form.Item>
 
 
@@ -113,24 +167,28 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 							{selectedVideos.map((video) => (
 							{selectedVideos.map((video) => (
 								<Card
 								<Card
 									key={video.id}
 									key={video.id}
-									className="w-[220px] relative group"
-									bodyStyle={{ padding: 0 }}
+									className="w-[240px] relative group"
 								>
 								>
 									<Button
 									<Button
 										shape="circle"
 										shape="circle"
 										icon={<CloseOutlined />}
 										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"
+										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"
 										size="small"
 										onClick={() => removeVideo(video.id)}
 										onClick={() => removeVideo(video.id)}
 									/>
 									/>
-									<div className="p-3">
+									<div className="p-0">
 										<Text type="secondary" className="text-xs">{video.source}</Text>
 										<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>
+										<Text className="block mt-1 mb-2 leading-tight line-clamp-2" title={video.customTitle || video.title}>{video.customTitle || video.title}</Text>
 									</div>
 									</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 className="relative h-[120px] bg-gray-200 cursor-pointer group/thumb"
+										 onClick={(e) => {
+											 e.stopPropagation(); // Prevent card selection if clicking thumbnail/play
+											 playVideo(video);
+										 }}
+									>
+										<img src={video.customThumbnail || video.thumbnail} alt={video.customTitle || video.title} className="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>
 									</div>
 									<div className="p-3">
 									<div className="p-3">
@@ -138,7 +196,7 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 										<Button
 										<Button
 											icon={<EditOutlined />}
 											icon={<EditOutlined />}
 											className="w-full mt-2"
 											className="w-full mt-2"
-											// onClick={() => handleEditTitleCover(video.id)} // Add handler later
+											onClick={() => openEditModal(video)} // Open edit modal
 										>
 										>
 											编辑标题/封面
 											编辑标题/封面
 										</Button>
 										</Button>
@@ -149,7 +207,7 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 							{/* Add Video Button - Conditionally Rendered */}
 							{/* Add Video Button - Conditionally Rendered */}
 							{selectedVideos.length < 3 && (
 							{selectedVideos.length < 3 && (
 								<div
 								<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"
+									className="w-[240px] 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
 									onClick={openVideoSelector} // Open the drawer on click
 								>
 								>
 									<PlusOutlined className="text-2xl mb-2" />
 									<PlusOutlined className="text-2xl mb-2" />
@@ -168,6 +226,37 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, onCa
 				onOk={handleVideoSelectionOk}
 				onOk={handleVideoSelectionOk}
 				initialSelectedIds={selectedVideos.map(v => v.id)} // Pass current selection IDs
 				initialSelectedIds={selectedVideos.map(v => v.id)} // Pass current selection IDs
 			/>
 			/>
+
+			{/* Video Player Modal */}
+			<Modal
+				open={!!playingVideo}
+				onCancel={closeVideoPlayer}
+				title={playingVideo?.customTitle || playingVideo?.title}
+				footer={null}
+				destroyOnClose // Unmount video element when closed
+				width={720} // Adjust as needed
+				bodyStyle={{ padding: 0, background: '#000' }}
+				zIndex={20}
+			>
+				{playingVideo && (
+					<video
+						controls
+						autoPlay
+						className="w-full h-auto max-h-[80vh] block"
+						src={playingVideo.videoUrl}
+					>
+						Your browser does not support the video tag.
+					</video>
+				)}
+			</Modal>
+
+			{/* Edit Title/Cover Modal */}
+			<EditTitleCoverModal
+				visible={!!editingVideo}
+				onCancel={handleEditCancel}
+				onOk={handleEditOk}
+				video={editingVideo}
+			/>
 		</>
 		</>
 	);
 	);
 };
 };

+ 10 - 0
src/views/publishContent/weGZH/components/types.ts

@@ -0,0 +1,10 @@
+export interface VideoItem {
+	id: number;
+	source: string;
+	title: string;
+	thumbnail: string; // URL to original thumbnail image
+	spreadEfficiency: number;
+	videoUrl: string;
+	customTitle?: string; // Optional custom title
+	customThumbnail?: string; // Optional URL to custom thumbnail image
+} 

+ 126 - 118
src/views/publishContent/weGZH/components/videoSelectModal/index.tsx

@@ -12,35 +12,29 @@ import {
 	Checkbox,
 	Checkbox,
 	Space,
 	Space,
 	message,
 	message,
+	Modal,
 } from 'antd';
 } from 'antd';
-import { EditOutlined, PlayCircleOutlined, CheckCircleFilled } from '@ant-design/icons';
+import { EditOutlined, PlayCircleOutlined, CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
+import { VideoItem } from '../types';
 
 
 const { Option } = Select;
 const { Option } = Select;
 const { Text } = Typography;
 const { Text } = Typography;
 
 
-// Reusing the VideoItem interface, adjust if needed
-interface VideoItem {
-	id: number;
-	source: string;
-	title: string;
-	thumbnail: string;
-	spreadEfficiency: number;
-}
-
 interface VideoSelectModalProps {
 interface VideoSelectModalProps {
 	visible: boolean;
 	visible: boolean;
 	onClose: () => void;
 	onClose: () => void;
 	onOk: (selectedVideos: VideoItem[]) => void;
 	onOk: (selectedVideos: VideoItem[]) => void;
-	initialSelectedIds?: number[]; // Pass initially selected IDs if needed
+	initialSelectedIds?: number[];
 }
 }
 
 
-// Mock data for demonstration
+// Mock data for demonstration - Ensure videoUrl is added
 const mockVideos: VideoItem[] = Array.from({ length: 15 }, (_, i) => ({
 const mockVideos: VideoItem[] = Array.from({ length: 15 }, (_, i) => ({
-	id: i + 100, // Ensure unique IDs
+	id: i + 100,
 	source: '票圈 | 3亿人喜欢的视频平台',
 	source: '票圈 | 3亿人喜欢的视频平台',
 	title: `最美中国——上海之海 ${i + 1},带你领略航拍风景`,
 	title: `最美中国——上海之海 ${i + 1},带你领略航拍风景`,
 	thumbnail: 'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
 	thumbnail: 'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
 	spreadEfficiency: 8.56 + i * 0.1,
 	spreadEfficiency: 8.56 + i * 0.1,
+	videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4', // Example video URL
 }));
 }));
 
 
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, initialSelectedIds = [] }) => {
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, initialSelectedIds = [] }) => {
@@ -49,9 +43,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	const [currentPage, setCurrentPage] = useState(1);
 	const [currentPage, setCurrentPage] = useState(1);
 	const [pageSize, setPageSize] = useState(10);
 	const [pageSize, setPageSize] = useState(10);
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
-	const MAX_SELECTION = 3; // Define the limit
+	const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
+	const MAX_SELECTION = 3;
 
 
-	// Reset selection when modal opens/initial selection changes
 	useEffect(() => {
 	useEffect(() => {
 		if (visible) {
 		if (visible) {
 			setSelectedVideoIds(new Set(initialSelectedIds));
 			setSelectedVideoIds(new Set(initialSelectedIds));
@@ -59,21 +53,19 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	}, [visible, initialSelectedIds]);
 	}, [visible, initialSelectedIds]);
 
 
 	const handleSearch = () => {
 	const handleSearch = () => {
-		// Implement search logic here, fetch videos based on category and searchTerm
 		console.log('Searching for:', { category, searchTerm });
 		console.log('Searching for:', { category, searchTerm });
-		setCurrentPage(1); // Reset to first page on search
+		setCurrentPage(1);
 	};
 	};
 
 
 	const handleSelectVideo = (videoId: number) => {
 	const handleSelectVideo = (videoId: number) => {
 		setSelectedVideoIds(prev => {
 		setSelectedVideoIds(prev => {
 			const newSet = new Set(prev);
 			const newSet = new Set(prev);
 			if (newSet.has(videoId)) {
 			if (newSet.has(videoId)) {
-				newSet.delete(videoId); // Allow deselection anytime
+				newSet.delete(videoId);
 			} else {
 			} else {
-				// Check limit before adding
 				if (newSet.size >= MAX_SELECTION) {
 				if (newSet.size >= MAX_SELECTION) {
 					message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
 					message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
-					return prev; // Return previous state if limit reached
+					return prev;
 				}
 				}
 				newSet.add(videoId);
 				newSet.add(videoId);
 			}
 			}
@@ -82,114 +74,130 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	};
 	};
 
 
 	const handleOk = () => {
 	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));
 		const selectedVideos = mockVideos.filter(video => selectedVideoIds.has(video.id));
 		onOk(selectedVideos);
 		onOk(selectedVideos);
-		onClose(); // Close the drawer after confirmation
+		onClose();
+	};
+
+	const playVideo = (video: VideoItem) => {
+		setPlayingVideo(video);
+	};
+
+	const closeVideoPlayer = () => {
+		setPlayingVideo(null);
 	};
 	};
 
 
-	// Paginate mock data for display
 	const paginatedData = mockVideos.slice((currentPage - 1) * pageSize, currentPage * pageSize);
 	const paginatedData = mockVideos.slice((currentPage - 1) * pageSize, currentPage * pageSize);
 
 
 	return (
 	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)}
-					/>
+		<>
+			<Drawer
+				title="内容选取"
+				open={visible}
+				onClose={onClose}
+				width={800}
+				placement="right"
+				footerStyle={{ textAlign: 'right', padding: '10px 24px' }}
+				footer={
+					<div className="flex justify-between items-center">
+						<Pagination
+							current={currentPage}
+							pageSize={pageSize}
+							total={mockVideos.length}
+							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>
+				}
+			>
+				<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}
+							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>
 				</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
+				<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-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
+								bodyStyle={{ padding: 0 }}
+								onClick={() => !isDisabled && handleSelectVideo(video.id)}
 							>
 							>
-								编辑标题/封面
-							</Button>
-						</Card>
-					);
-				})}
-			</div>
-		</Drawer>
+								<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 group/thumb"
+									onClick={(e) => { e.stopPropagation(); playVideo(video); }}
+								>
+									<img src={video.thumbnail} alt={video.title} className="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">
+									<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>
+							</Card>
+						);
+					})}
+				</div>
+			</Drawer>
+
+			<Modal
+				open={!!playingVideo}
+				onCancel={closeVideoPlayer}
+				title={playingVideo?.title}
+				footer={null}
+				destroyOnClose
+				width={720}
+				bodyStyle={{ padding: 0, background: '#000' }}
+			>
+				{playingVideo && (
+					<video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.videoUrl}>
+						Your browser does not support the video tag.
+					</video>
+				)}
+			</Modal>
+		</>
 	);
 	);
 };
 };
 
 

+ 23 - 8
src/views/publishContent/weGZH/index.tsx

@@ -2,10 +2,10 @@ import React, { useState } from 'react';
 import { Space, Table, Button, Input, Select, DatePicker } from 'antd';
 import { Space, Table, Button, Input, Select, DatePicker } from 'antd';
 import type { TableProps } from 'antd';
 import type { TableProps } from 'antd';
 import styles from './index.module.css';
 import styles from './index.module.css';
-import AddPunlishPlanModal from './components/publishPlanModal';
+import PunlishPlanModal from './components/publishPlanModal';
 const { RangePicker } = DatePicker;
 const { RangePicker } = DatePicker;
 
 
-interface DataType {
+export interface PlanData {
 	// key: string;
 	// key: string;
 	officialAccount: string;
 	officialAccount: string;
 	scene: string;
 	scene: string;
@@ -22,9 +22,11 @@ const WeGZHContent: React.FC = () => {
 	const [selectedPublisher, setSelectedPublisher] = useState<string>();
 	const [selectedPublisher, setSelectedPublisher] = useState<string>();
 	const [dateRange, setDateRange] = useState<[string, string]>();
 	const [dateRange, setDateRange] = useState<[string, string]>();
 	const [isShowAddPunlishPlan, setIsShowAddPunlishPlan] = useState<boolean>(false);
 	const [isShowAddPunlishPlan, setIsShowAddPunlishPlan] = useState<boolean>(false);
+	const [actionType, setActionType] = useState<'add' | 'edit'>('add');
+	const [editPlanData, setEditPlanData] = useState<PlanData>();
 
 
 	// 表格列配置
 	// 表格列配置
-	const columns: TableProps<DataType>['columns'] = [
+	const columns: TableProps<PlanData>['columns'] = [
 		{
 		{
 			title: '公众号名称',
 			title: '公众号名称',
 			dataIndex: 'officialAccount',
 			dataIndex: 'officialAccount',
@@ -61,15 +63,21 @@ const WeGZHContent: React.FC = () => {
 			key: 'action',
 			key: 'action',
 			render: (_, record) => (
 			render: (_, record) => (
 				<Space size="middle">
 				<Space size="middle">
-					<Button type="link">编辑</Button>
+					<Button type="link" onClick={() => editPlan(record)}>编辑</Button>
 					<Button type="link">详情</Button>
 					<Button type="link">详情</Button>
 				</Space>
 				</Space>
 			),
 			),
 		},
 		},
 	];
 	];
 
 
+	const editPlan = (record: PlanData) => {
+		setEditPlanData(record);
+		setActionType('edit');
+		setIsShowAddPunlishPlan(true);
+	};
+
 	// 模拟数据
 	// 模拟数据
-	const data: DataType[] = [
+	const data: PlanData[] = [
 		{
 		{
 			officialAccount: '小慧爱厨房',
 			officialAccount: '小慧爱厨房',
 			scene: '关注回复',
 			scene: '关注回复',
@@ -89,12 +97,13 @@ const WeGZHContent: React.FC = () => {
 	];
 	];
 
 
 	const addPunlishPlan = () => {
 	const addPunlishPlan = () => {
+		setActionType('add');
 		setIsShowAddPunlishPlan(true);
 		setIsShowAddPunlishPlan(true);
 	}
 	}
 
 
 	return (
 	return (
 		<div>
 		<div>
-			<div className="bg-white rounded-lg">
+			<div className="rounded-lg">
 				<div className="text-lg font-medium mb-3">公众号内容</div>
 				<div className="text-lg font-medium mb-3">公众号内容</div>
 				
 				
 				{/* 搜索区域 */}
 				{/* 搜索区域 */}
@@ -106,6 +115,7 @@ const WeGZHContent: React.FC = () => {
 							style={{ width: 200 }}
 							style={{ width: 200 }}
 							value={selectedAccount}
 							value={selectedAccount}
 							onChange={setSelectedAccount}
 							onChange={setSelectedAccount}
+							allowClear
 							options={[
 							options={[
 								{ label: '小慧爱厨房', value: '小慧爱厨房' },
 								{ label: '小慧爱厨房', value: '小慧爱厨房' },
 								{ label: '小阳看天下', value: '小阳看天下' },
 								{ label: '小阳看天下', value: '小阳看天下' },
@@ -119,6 +129,7 @@ const WeGZHContent: React.FC = () => {
 							placeholder="搜索视频标题"
 							placeholder="搜索视频标题"
 							style={{ width: 200 }}
 							style={{ width: 200 }}
 							value={videoTitle}
 							value={videoTitle}
+							allowClear
 							onChange={e => setVideoTitle(e.target.value)}
 							onChange={e => setVideoTitle(e.target.value)}
 						/>
 						/>
 					</div>
 					</div>
@@ -130,6 +141,7 @@ const WeGZHContent: React.FC = () => {
 							style={{ width: 200 }}
 							style={{ width: 200 }}
 							value={selectedPublisher}
 							value={selectedPublisher}
 							onChange={setSelectedPublisher}
 							onChange={setSelectedPublisher}
+							allowClear
 							options={[
 							options={[
 								{ label: '平台发布', value: 'platform' },
 								{ label: '平台发布', value: 'platform' },
 								{ label: '用户发布', value: 'user' },
 								{ label: '用户发布', value: 'user' },
@@ -141,6 +153,7 @@ const WeGZHContent: React.FC = () => {
 						<RangePicker
 						<RangePicker
 							placeholder={['开始时间', '结束时间']}
 							placeholder={['开始时间', '结束时间']}
 							style={{ width: 400 }}
 							style={{ width: 400 }}
+							allowClear
 							onChange={(dates, dateStrings) => {
 							onChange={(dates, dateStrings) => {
 								if (dates) {
 								if (dates) {
 									setDateRange([dateStrings[0], dateStrings[1]]);
 									setDateRange([dateStrings[0], dateStrings[1]]);
@@ -157,7 +170,7 @@ const WeGZHContent: React.FC = () => {
 				<Table
 				<Table
 					title={() =>
 					title={() =>
 						<div className="flex justify-between">
 						<div className="flex justify-between">
-							<div className="text-[#333] font-medium px-[45px] py-[15px] rounded-tr-[20px] bg-[#ffc107] text-[16px] border-none hover:bg-[#ffca2c]">
+							<div className="text-[#333] font-medium px-[45px] py-[10px] rounded-tr-[20px] bg-[#ffc107] dark:bg-[#333] text-[16px] border-none hover:bg-[#ffca2c]">
 								自动回复
 								自动回复
 							</div>
 							</div>
 							<Button type="primary" onClick={addPunlishPlan}>+ 创建发布</Button>
 							<Button type="primary" onClick={addPunlishPlan}>+ 创建发布</Button>
@@ -173,10 +186,12 @@ const WeGZHContent: React.FC = () => {
 						showTotal: (total) => `共 ${total} 条`,
 						showTotal: (total) => `共 ${total} 条`,
 					}}
 					}}
 				/>
 				/>
-				<AddPunlishPlanModal
+				<PunlishPlanModal
 					visible={isShowAddPunlishPlan}
 					visible={isShowAddPunlishPlan}
 					onCancel={() => setIsShowAddPunlishPlan(false)}
 					onCancel={() => setIsShowAddPunlishPlan(false)}
 					onOk={() => setIsShowAddPunlishPlan(false)}
 					onOk={() => setIsShowAddPunlishPlan(false)}
+					actionType={actionType}
+					editPlanData={editPlanData}
 				/>
 				/>
 			</div>
 			</div>
 		</div>
 		</div>

+ 1 - 0
tailwind.config.js

@@ -1,5 +1,6 @@
 /** @type {import('tailwindcss').Config} */
 /** @type {import('tailwindcss').Config} */
 export default {
 export default {
+		darkMode: 'selector',
     content: [
     content: [
       "./index.html",
       "./index.html",
       "./src/**/*.{js,ts,jsx,tsx}",
       "./src/**/*.{js,ts,jsx,tsx}",