Browse Source

Merge branch 'feature_first_pages' of Web/contentCooper into master

jihuaqiang 1 tuần trước cách đây
mục cha
commit
2f951c81ed

+ 1 - 1
index.html

@@ -5,7 +5,7 @@
     <link rel="icon" type="image/svg+xml" href="/logo.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 		<meta name="referrer" content="no-referrer" />
-    <title>票圈对外内容平台</title>
+    <title>票圈内容合作平台</title>
   </head>
   <body>
     <div id="root"></div>

+ 75 - 18
src/views/cooperationAccount/gzh/index.tsx

@@ -1,4 +1,4 @@
-import { Button, Table, Image, Modal, message, Select } from "antd";
+import { Button, Table, Image, Modal, message, Select, Popconfirm, Input } from "antd";
 import type { TableProps } from 'antd';
 import { useState, useEffect, useRef } from "react";
 import { InfoCircleFilled, CheckCircleFilled } from "@ant-design/icons";
@@ -8,7 +8,8 @@ import { accountGetAuthQrCode, accountGetAuthResult, accountGetContentType, acco
 interface DataType {
   id: number;
   name: string;
-  ghId: string;
+	ghId: string;
+	type: AccountType;
   contentType: string;
   createTimestamp: number;
 }
@@ -34,10 +35,16 @@ interface AuthResultData {
   ghId: string;
 }
 
+enum AccountType {
+  公众号 = 0,
+  服务号 = 1
+}
+
 const AccountDialog = ({ title, isOpen, handleOk: parentHandleOk, handleCancel, accountData }: AccountDialogProps) => {
   const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
   const [accountName, setAccountName] = useState<string>("授权成功后展示");
-  const [accountId, setAccountId] = useState<string>("授权成功后展示");
+	const [accountId, setAccountId] = useState<string>("授权成功后展示");
+	const [accountType, setAccountType] = useState<AccountType>(AccountType.公众号);
   const [contentTypes, setContentTypes] = useState<string[]>([]);
   const [selectedContentType, setSelectedContentType] = useState<string>("");
   const [authSuccess, setAuthSuccess] = useState<boolean>(false);
@@ -136,7 +143,8 @@ const AccountDialog = ({ title, isOpen, handleOk: parentHandleOk, handleCancel,
       // 如果是编辑模式,填充账号数据并设置为授权成功状态
       if (accountData) {
         setAccountName(accountData.name);
-        setAccountId(accountData.ghId);
+				setAccountId(accountData.ghId);
+				setAccountType(accountData.type);
         setSelectedContentType(accountData.contentType);
         setAuthSuccess(true); // 编辑模式下直接显示授权成功状态
       }
@@ -155,7 +163,8 @@ const AccountDialog = ({ title, isOpen, handleOk: parentHandleOk, handleCancel,
   const resetState = () => {
     setQrCodeUrl("");
     setAccountName("授权成功后展示");
-    setAccountId("授权成功后展示");
+		setAccountId("授权成功后展示");
+		setAccountType(AccountType.公众号);
     setContentTypes([]);
     setSelectedContentType("");
     setAuthSuccess(false); // 重置授权成功状态
@@ -192,7 +201,8 @@ const AccountDialog = ({ title, isOpen, handleOk: parentHandleOk, handleCancel,
       // 准备请求参数
       const params: any = {
         contentType: selectedContentType,
-        ghId: accountId,
+				ghId: accountId,
+				type: accountType,
         name: accountName
       };
 
@@ -276,6 +286,21 @@ const AccountDialog = ({ title, isOpen, handleOk: parentHandleOk, handleCancel,
       <div className={"flex mb-[20px]"}>
         <div className={"w-[100px]"}>公众号ID</div>
         <div className={"flex-1"}>{accountId}</div>
+			</div>
+			<div className={"flex mb-[20px]"}>
+        <div className={"w-[100px] leading-[30px]"}>公众号类别</div>
+        <div className={"flex-1"}>
+					<Select
+						disabled={!!accountData}
+            value={accountType}
+            style={{ width: 130 }}
+            options={[
+              { label: '公众号', value: AccountType.公众号 },
+              { label: '服务号', value: AccountType.服务号 },
+            ]}
+            onChange={(value) => setAccountType(value)}
+          />
+        </div>
       </div>
       <div className={"flex mb-[20px]"}>
         <div className={"w-[100px] leading-[30px]"}>内容类别</div>
@@ -298,12 +323,14 @@ const Gzh: React.FC = () => {
   const [title, setTitle] = useState('');
   const [authFailed, setAuthFailed] = useState(false);
   const [loading, setLoading] = useState(false);
-  const [data, setData] = useState<DataType[]>([]);
+	const [data, setData] = useState<DataType[]>([]);
+	const [queryName, setQueryName] = useState('');
+	const [isSearch, setIsSearch] = useState(false);
   const [pagination, setPagination] = useState({
     current: 1,
     pageSize: 10,
     total: 0
-  });
+	});
 
   const columns: TableProps<DataType>['columns'] = [
     {
@@ -315,7 +342,15 @@ const Gzh: React.FC = () => {
       title: '账号id',
       dataIndex: 'ghId',
       key: 'ghId',
-    },
+		},
+		{
+			title: '公众号类别',
+			dataIndex: 'type',
+			key: 'type',
+			render: (type) => {
+				return type === AccountType.公众号 ? '公众号' : '服务号';
+			}
+		},
     {
       title: '内容类别',
       dataIndex: 'contentType',
@@ -332,19 +367,26 @@ const Gzh: React.FC = () => {
     {
       key: 'action',
       render: (_, record) => (
-        <>
-          <Button type="link" onClick={() => modifyAccount(record)}>编辑</Button>
-          <Button type="link" danger onClick={() => deleteAccount(record)}>删除</Button>
+				<>
+					<Button type="link" onClick={() => modifyAccount(record)}>编辑</Button>
+					<Popconfirm title="确定删除该公众号吗?" okText="确定" cancelText="取消" onConfirm={() => deleteAccount(record)}>
+						<Button type="link">删除</Button>
+					</Popconfirm>
         </>
       ),
     }
   ]
 
   // 获取账号列表数据
-  const fetchAccountList = async (pageNum = 1, pageSize = 10) => {
+	const fetchAccountList = async (pageNum = 1, pageSize = 10, _queryName = '') => {
+		setIsSearch(_queryName.trim() !== '');
     setLoading(true);
     try {
-      const res = await http.post<AccountListResponse>(accountList, { pageNum, pageSize });
+			const res = await http.post<AccountListResponse>(accountList, {
+				pageNum,
+				pageSize,
+				name: (_queryName || isSearch) ? (_queryName || queryName) : '',
+			});
       if (res.code === 0 && res.data) {
         setData(res.data.objs || []);
         setPagination({
@@ -433,20 +475,35 @@ const Gzh: React.FC = () => {
     setIsOpen(false);
     // 重置当前选中的账号数据
     setCurrentAccount(undefined);
-  }
+	}
+	
+	const handleSearch = () => {
+		fetchAccountList(1, pagination.pageSize, queryName);
+	}
 
   // 当前选中的账号数据
   const [currentAccount, setCurrentAccount] = useState<DataType | undefined>(undefined);
 
   return (
-    <>
+		<>
+			<div className={"text-[20px] font-medium mb-[10px]"}>公众号管理</div>
       <div className={"flex mb-[10px]"}>
-        <div className={"flex-1 leading-[32px]"}>公众号管理</div>
+				<div className={"flex-1 flex gap-[10px]"}>
+					<Input
+						className={"!w-[200px]"}
+						allowClear
+						placeholder="请输入账号名称"
+						value={queryName}
+						onChange={(e) => setQueryName(e.target.value)}
+					/>
+					<Button type="primary" onClick={handleSearch}>搜索</Button>
+				</div>
         <div>
           <Button type="primary" onClick={addAccount} className={"mr-[10px]"}>+ 新建账号</Button>
         </div>
       </div>
-      <Table 
+			<Table 
+				
         columns={columns} 
         dataSource={data} 
         rowKey="id" 

+ 1 - 0
src/views/publishContent/weCom/components/linkDetailModal/index.tsx

@@ -7,6 +7,7 @@ import { getShareQrPic } from '@src/http/api';
 import http from '@src/http';
 import modal from 'antd/es/modal';
 import VideoPlayModal from '../videoPlayModal';
+
 // import QRCode from 'qrcode.react'; // Consider adding this dependency if not present
 
 const { Text } = Typography;

+ 4 - 2
src/views/publishContent/weCom/index.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import { Space, Table, Button, Input, Select, Tabs, message, Spin } from 'antd';
+import { Space, Table, Button, Input, Select, Tabs, message, Spin, Popconfirm } from 'antd';
 import type { TableProps } from 'antd';
 import styles from './index.module.css';
 import { WeComPlan, WeComPlanListResponse, WeComPlanType, WeVideoItem } from './type';
@@ -98,7 +98,9 @@ const WeGZHContent: React.FC = () => {
 					<Button type="link" onClick={() => showQrCodeModal(record.pageUrl)}>二维码</Button>
 					<Button type="link" onClick={() => copyToClipboard(record.pageUrl)}>复制链接</Button>
 					<Button type="link" onClick={() => showDetailModal(record)}>详情</Button>
-					<Button type="link" onClick={() => deletePlan(record)}>删除</Button>
+					<Popconfirm title="确定删除该内容吗?" okText="确定" cancelText="取消" onConfirm={() => deletePlan(record)}>
+						<Button type="link">删除</Button>
+					</Popconfirm>
 				</Space>
 			),
 		},

+ 127 - 21
src/views/publishContent/weGZH/components/PunlishPlanDetailModal/index.tsx

@@ -1,11 +1,19 @@
-import React from 'react';
-import { Modal, Descriptions } from 'antd';
-import { GzhPlanType } from '../../hooks/useGzhPlanList';
-import dayjs from 'dayjs';
+import React, { useState } from 'react';
+import { Modal, Descriptions, Space, Button, Tooltip, message, Table } from 'antd';
+import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
+import { CopyOutlined, PlayCircleOutlined, DownloadOutlined, LinkOutlined, QrcodeOutlined } from '@ant-design/icons';
+import { VideoItem } from '../types';
+import VideoPlayModal from '@src/views/publishContent/weCom/components/videoPlayModal';
+import modal from 'antd/es/modal';
+import http from '@src/http';
+import { getShareQrPic } from '@src/http/api';
+import copy from 'copy-to-clipboard';
+import { Typography } from 'antd';
+const { Text } = Typography;
 interface PunlishPlanDetailModalProps {
   visible: boolean;
   onCancel: () => void;
-  planData: GzhPlanType;
+  planData: GzhPlanDataType;
 }
 
 const PunlishPlanDetailModal: React.FC<PunlishPlanDetailModalProps> = ({
@@ -13,6 +21,95 @@ const PunlishPlanDetailModal: React.FC<PunlishPlanDetailModalProps> = ({
   onCancel,
   planData,
 }) => {
+	const [isVideoPlayModalVisible, setIsVideoPlayModalVisible] = useState(false);
+	const [activeVideo, setActiveVideo] = useState<VideoItem | null>(null);
+
+	const columns = [
+    {
+      title: '视频标题',
+      dataIndex: 'title',
+      key: 'title',
+      render: (title: string) => (
+        <Space>
+          <Text style={{ maxWidth: 200 }} ellipsis={{ tooltip: title }}>{title}</Text>
+          <Tooltip title="复制标题">
+            <Button
+              type="text"
+              icon={<CopyOutlined />}
+              onClick={() => {
+                if (copy(title)) {
+                  message.success('标题已复制');
+                } else {
+                  message.error('复制失败');
+                }
+              }}
+              size="small"
+            />
+          </Tooltip>
+        </Space>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: VideoItem) => (
+        <Space wrap size="small">
+          <Button type="link" icon={<PlayCircleOutlined />} onClick={() => playVideo(record)} size="small">播放</Button>
+          <Button type="link" icon={<DownloadOutlined />} onClick={() => downloadCover(record)} size="small">下载封面</Button>
+          <Button type="link" icon={<LinkOutlined />} onClick={() => copyPushLink(record)} size="small">复制推送链接</Button>
+          <Button type="link" icon={<QrcodeOutlined />} onClick={() => showQRCode(record)} size="small">二维码</Button>
+        </Space>
+      ),
+    },
+	];
+	const playVideo = (video: VideoItem) => {
+		setActiveVideo(video);
+		setIsVideoPlayModalVisible(true);
+	};
+
+	const downloadCover = (video: VideoItem) => {
+		if (video.cover) {
+			const link = document.createElement('a');
+			link.href = video.cover;
+			// Attempt to infer filename, might need refinement
+			const filename = video.cover.substring(video.cover.lastIndexOf('/') + 1) || `cover_${video.videoId}.jpg`;
+			link.download = filename;
+			link.target = '_blank'; // Open in new tab might be safer for some browsers
+			link.rel = 'noopener noreferrer';
+			document.body.appendChild(link);
+			link.click();
+			document.body.removeChild(link);
+		} else {
+			message.warning('No cover image URL available.');
+		}
+	};
+	
+	const copyPushLink = (video: VideoItem) => {
+		// Assuming video object might have a 'pushLink' property added by the parent component
+		const linkToCopy = video.pageUrl || 'Push link not available'; // Updated placeholder
+		if (video.pageUrl && copy(linkToCopy)) {
+			message.success('推送链接已复制');
+		} else if (!video.pageUrl) {
+			message.warning('没有可复制的推送链接');
+		} else {
+			message.error('复制失败');
+		}
+	};
+	
+	const showQRCode = (video: VideoItem) => {
+		http.get<string>(getShareQrPic, {
+			params: {
+				pageUrl: video.pageUrl,
+			}
+		}).then(res => {
+			modal.info({
+				title: '二维码',
+				content: <img src={res.data} alt="二维码" />,
+			});
+		}).catch(err => {
+			message.error(err.msg || '获取二维码失败');
+		});
+	};
 
   return (
     planData && visible && <Modal
@@ -23,23 +120,32 @@ const PunlishPlanDetailModal: React.FC<PunlishPlanDetailModalProps> = ({
       footer={null}
       width={800}
     >
-      <Descriptions column={1} bordered>
-				<Descriptions.Item label="场景">{planData?.scene === 0 ? '关注回复' : '自动回复'}</Descriptions.Item>
+			<Descriptions column={1} className='mb-4'>
+				<Descriptions.Item label="发布计划类型">{planData.type.toString() === GzhPlanType.自动回复 ? '自动回复' : '服务号推送'}</Descriptions.Item>
+				{ planData.type.toString() === GzhPlanType.自动回复 && <Descriptions.Item label="场景">关注回复</Descriptions.Item> }
+				<Descriptions.Item label="发布方式">{planData?.publishStage === 0 ? '平台发布' : '用户获取路径'}</Descriptions.Item>
+				<Descriptions.Item label="视频选取方式">{planData?.selectVideoType === 0 ? '手动选取' : '自动选取'}</Descriptions.Item>
         <Descriptions.Item label="公众号名称">{planData.accountName}</Descriptions.Item>
-        <Descriptions.Item label="视频列表">
-          {planData.videoList?.map((video, index) => (
-            <div key={video.videoId} style={{ marginBottom: '16px' }}>
-              <p>视频 {index + 1}: {video.customTitle || video.title}</p>
-              <img 
-                src={video.customCover || video.cover} 
-                alt={`视频${index + 1}封面`} 
-                style={{ maxWidth: '150px' }} 
-              />
-            </div>
-          ))}
-        </Descriptions.Item>
-        <Descriptions.Item label="发布时间">{dayjs(planData.createTimestamp).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
-        <Descriptions.Item label="发布方">{planData.publishStage === 0 ? '平台发布' : '用户发布'}</Descriptions.Item>
+				{ 
+					planData.videoList?.length > 0 ?
+						<Descriptions.Item label="发布内容" >
+							<Table
+								columns={columns}
+								dataSource={planData.videoList?.map(v => ({ ...v, key: v.videoId }))} // Ensure each item has a unique key
+								pagination={false} // Disable pagination if the list is expected to be short
+								size="small"
+							/>
+							<VideoPlayModal
+								visible={isVideoPlayModalVisible}
+								onClose={() => setIsVideoPlayModalVisible(false)}
+								videoUrl={activeVideo?.video || ''}
+								title={activeVideo?.title || ''}
+							/>
+						</Descriptions.Item>
+						: <Descriptions.Item label="发布内容">
+							<Text className='text-gray-500'>视频由系统自动选取</Text>
+						</Descriptions.Item>
+				}
 			</Descriptions>
     </Modal>
   );

+ 1 - 2
src/views/publishContent/weGZH/components/editTitleCoverModal/index.tsx

@@ -16,7 +16,6 @@ import { VideoItem } from '../types';
 import { getAccessToken } from '@src/http/sso';
 import { adFileUpload, getVideoContentCoverFrameListApi } from '@src/http/api';
 import http from '@src/http';
-import { isNil } from 'lodash-es';
 
 const { TextArea } = Input;
 
@@ -42,7 +41,7 @@ const EditTitleCoverModal: React.FC<EditTitleCoverModalProps> = ({ visible, onCa
 	useEffect(() => {
 		if (video && visible) {
 			// Reset form based on incoming video data
-			const hasCustomTitle = !isNil(video.customTitle) || video.customTitle === '';
+			const hasCustomTitle = video.customTitle && video.customTitle !== '';
 			const isCustomCover = video.customCoverType === 1 || video.customCoverType === 2;
 			const isScreenshotCover = video.customCoverType === 1;
 

+ 142 - 76
src/views/publishContent/weGZH/components/publishPlanModal/index.tsx

@@ -4,7 +4,7 @@ import { CloseOutlined, PlusOutlined, EditOutlined, CaretRightFilled } from '@an
 import VideoSelectModal from '../videoSelectModal';
 import EditTitleCoverModal from '../editTitleCoverModal';
 import { VideoItem } from '../types'; // Import from common types
-import { GzhPlanType } from '../../hooks/useGzhPlanList';
+import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
 import { useAccountOptions } from '../../hooks/useAccountOptions';
 
 const { Option } = Select;
@@ -13,30 +13,58 @@ const { Paragraph } = Typography;
 interface AddPunlishPlanModalProps {
 	visible: boolean;
 	onCancel: () => void;
-	onOk: (values: GzhPlanType) => void; // Pass form values on OK
+	onOk: (values: GzhPlanDataType) => void; // Pass form values on OK
 	actionType: 'add' | 'edit';
-	editPlanData?: GzhPlanType;
+	planType: GzhPlanType;
+	editPlanData?: GzhPlanDataType;
 	isSubmiting?: boolean;
 }
 
-const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSubmiting, onCancel, onOk, actionType, editPlanData }) => {
+const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({
+	visible,
+	isSubmiting,
+	onCancel,
+	onOk,
+	actionType,
+	planType,
+	editPlanData
+}) => {
 	const [form] = Form.useForm();
+	const type = Form.useWatch('type', form);
+	const selectVideoType = Form.useWatch('selectVideoType', form);
 	const [selectedVideos, setSelectedVideos] = useState<VideoItem[]>([]);
 	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
-	const { accountOptions } = useAccountOptions();
+	const { accountOptions, getAccountList } = useAccountOptions();
 
 	useEffect(() => {
 		if (actionType === 'edit') {
-			form.setFieldsValue(editPlanData);
+			form.setFieldsValue({...editPlanData, type: editPlanData?.type.toString()});
 			setSelectedVideos(editPlanData?.videoList || []);
 		} else {
 			setSelectedVideos([]);
-			form.resetFields();
+			form.setFieldsValue({
+				id: undefined,
+				type: planType,
+				publishStage: 1,
+				selectVideoType: 0,
+				accountId: undefined,
+				scene: 0,
+				videoList: []
+			});
 		}
 	}, [actionType, editPlanData, visible]);
 
+	useEffect(() => {
+		getAccountList({accountType: planType});
+	}, [planType]);
+
+	const onTypeChange = (value: string) => {
+		form.setFieldsValue({ accountId: undefined });
+		getAccountList({ accountType: value });
+	}
+
 	const handleOk = () => {
 		form
 			.validateFields()
@@ -46,7 +74,12 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 					message.error('请至少选择一个视频'); // Use Antd message
 					return;
 				}
-				onOk({ ...values, scene: 0, videoList: selectedVideos });
+				const formData = { ...values };
+				if (formData.type === GzhPlanType.自动回复) { 
+					formData.scene = 0;
+				}
+				formData.videoList = selectedVideos;
+				onOk(formData);
 			})
 			.catch((info) => {
 				console.log('Validate Failed:', info);
@@ -112,7 +145,7 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 	return (
 		<>
 			<Modal
-				title="创建发布计划"
+				title={actionType === 'add' ? "创建发布计划" : "编辑发布计划"}
 				open={visible}
 				destroyOnClose
 				onCancel={onCancel}
@@ -134,17 +167,45 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 						hidden
 					>
 					</Form.Item>
+					<Form.Item
+						name="type"
+						label="发布计划类型"
+						layout="horizontal"
+						labelCol={{ span: 4 }}
+						labelAlign='left'
+						required
+					>
+						<Select placeholder="选择计划类型" onChange={onTypeChange} className='!w-50' disabled={actionType === 'edit'}>
+							<Option value={GzhPlanType.自动回复}>自动回复</Option>
+							<Option value={GzhPlanType.服务号推送}>服务号推送</Option>
+						</Select>
+					</Form.Item>
+					<Form.Item
+						name="scene"
+						label="发布场景"
+						layout="horizontal"
+						labelCol={{ span: 4 }}
+						labelAlign='left'
+						initialValue={0}
+						hidden={type !== GzhPlanType.自动回复}
+					>
+						<Select
+							placeholder="发布场景"
+							className='!w-50'
+							options={[{ label: '关注回复', value: 0 }]}>
+						</Select>
+					</Form.Item>
 					<Form.Item
 						name="publishStage"
-						label="发布方"
+						label="发布方"
 						labelCol={{ span: 4 }}
 						labelAlign='left'
 						layout="horizontal"
-						rules={[{ required: true, message: '请选择发布方' }]}
+						rules={[{ required: true, message: '请选择发布方' }]}
 					>
-						<Select placeholder="选择发布方" allowClear className='!w-50'>
-							<Option value={0}>平台发布</Option>
-							<Option value={1}>用户发布</Option>
+						<Select placeholder="选择发布方" allowClear className='!w-50'>
+							<Option value={0} disabled>平台发布</Option>
+							<Option value={1} >用户获取路径</Option>
 						</Select>
 					</Form.Item>
 					<Form.Item
@@ -164,77 +225,82 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 							filterOption={(input, option) =>
 								(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
 							}
-							options={accountOptions.map(option => ({ label: option.name, value: option.id }))}>
+							options={accountOptions?.map(option => ({ label: option.name, value: option.id }))}>
 						</Select>
 					</Form.Item>
+					
 					<Form.Item
-						name="scene"
-						label="发布场景"
-						layout="horizontal"
+						name="selectVideoType"
+						label="视频选取方式"
 						labelCol={{ span: 4 }}
 						labelAlign='left'
-						initialValue={0}
-					>
-						<Select
-							placeholder="发布场景"
-							className='!w-50'
-							options={[{ label: '关注回复', value: 0 }]}>
+						layout="horizontal"
+						rules={[{ required: true, message: '请选择视频选取方式' }]}>
+						<Select placeholder="选择视频选取方式" allowClear className='!w-50'>
+							<Option value={0} >手动选取</Option>
+							<Option value={1} disabled>自动选取</Option>
 						</Select>
 					</Form.Item>
-
 					<Form.Item label="发布内容" required>
-						<div className="flex flex-wrap gap-4">
-							{selectedVideos.map((video) => (
-								<Card
-									key={video.videoId}
-									className="w-[240px] relative group"
-								>
-									<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.videoId)}
-									/>
-									<div className="p-0">
-										<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
-									</div>
-									<div
-										className="relative"
-										style={{ paddingBottom: '79.8%' }}
-										onClick={(e) => {
-											e.stopPropagation(); // Prevent card selection if clicking thumbnail/play
-											playVideo(video);
-										}}
-									>
-										<img src={video.customCover || video.cover} referrerPolicy="no-referrer" className="absolute inset-0 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">
-										<Button
-											icon={<EditOutlined />}
-											className="w-full mt-2"
-											onClick={() => openEditModal(video)} // Open edit modal
+						{ 
+							selectVideoType === 0 ?
+								(<div className="flex flex-wrap gap-4">
+									{selectedVideos.map((video) => (
+										<Card
+											key={video.videoId}
+											className="w-[240px] relative group"
 										>
-											编辑标题/封面
-										</Button>
-									</div>
-								</Card>
-							))}
-
-							{/* Add Video Button - Conditionally Rendered */}
-							{selectedVideos.length < 3 && (
-								<div
-									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"
-									onClick={openVideoSelector} // Open the drawer on click
-								>
-									<PlusOutlined className="text-2xl mb-2" />
-									<Typography.Text>添加视频</Typography.Text>
-								</div>
-							)}
-						</div>
+											<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.videoId)}
+											/>
+											<div className="p-0">
+												<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 2, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
+											</div>
+											<div
+												className="relative"
+												style={{ paddingBottom: '79.8%' }}
+												onClick={(e) => {
+													e.stopPropagation(); // Prevent card selection if clicking thumbnail/play
+													playVideo(video);
+												}}
+											>
+												<img src={video.customCover || video.cover} referrerPolicy="no-referrer" className="absolute inset-0 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">
+												<Button
+													icon={<EditOutlined />}
+													className="w-full mt-2"
+													onClick={() => openEditModal(video)} // Open edit modal
+												>
+													编辑标题/封面
+												</Button>
+											</div>
+										</Card>
+									))}
+		
+									{/* Add Video Button - Conditionally Rendered */}
+									{selectedVideos.length < 3 && (
+										<div
+											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"
+											onClick={openVideoSelector} // Open the drawer on click
+										>
+											<PlusOutlined className="text-2xl mb-2" />
+											<Typography.Text>添加视频</Typography.Text>
+										</div>
+									)}
+								</div>)
+								: (<div>
+								<Paragraph>视频由系统自动选取,不可手动选择</Paragraph>
+							</div>)
+						}
+						
 					</Form.Item>
 				</Form>
 			</Modal>

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

@@ -5,6 +5,7 @@ export interface VideoItem {
 	score: number,
 	title: string,
 	customTitle: string,
+	pageUrl: string,
 	video: string,
 	videoId: number,
 } 

+ 8 - 7
src/views/publishContent/weGZH/hooks/useAccountOptions.ts

@@ -1,6 +1,7 @@
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
 import { getGzhAccountOptionsApi } from '@src/http/api';
 import request from '@src/http/index';
+import { GzhPlanType } from './useGzhPlanList';
 
 interface Account {
   id: string;
@@ -13,11 +14,15 @@ export const useAccountOptions = () => {
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
 
-  const getAccountList = async () => {
+  const getAccountList = async ({accountType}: {accountType?: string}) => {
     try {
       setLoading(true);
       setError(null);
-			const data = await request.get(getGzhAccountOptionsApi);
+			const data = await request.get(getGzhAccountOptionsApi, {
+				params: {
+					accountType: accountType === GzhPlanType.自动回复 ? undefined : GzhPlanType.服务号推送
+				}
+			});
       setAccountOptions(data.data as Account[]);
     } catch (err) {
       setError(err instanceof Error ? err.message : 'Failed to fetch accounts');
@@ -26,10 +31,6 @@ export const useAccountOptions = () => {
     }
   };
 
-  useEffect(() => {
-    getAccountList();
-  }, []);
-
   return {
     accountOptions,
     getAccountList,

+ 19 - 6
src/views/publishContent/weGZH/hooks/useGzhPlanList.ts

@@ -3,13 +3,20 @@ import { getGzhPlanListApi } from '@src/http/api';
 import request from '@src/http/index';
 import { VideoItem } from '../components/types';
 
-export interface GzhPlanType {
+export enum GzhPlanType {
+	自动回复 = '0',
+	服务号推送 = '1',
+}
+
+export interface GzhPlanDataType {
   accountId: number;
   accountName: string;
   createTimestamp: number;
   id: number;
-  publishStage: number;
-  scene: number;
+	publishStage: number;
+	type: GzhPlanType;
+	scene: number;
+	selectVideoType: number;
   title: string[];
 	videoCount: number;
 	videoList: VideoItem[];
@@ -21,17 +28,19 @@ interface GzhPlanListParams {
 	createTimestampStart?: number;
 	publishStage?: number;
 	title?: string;
+	type?: string;
+	selectVideoType?: number;
 	pageNum: number;
 	pageSize: number;
 }
 
 interface GzhPlanListResponse {
-	objs: GzhPlanType[];
+	objs: GzhPlanDataType[];
 	totalSize: number;
 }
 
 export const useGzhPlanList = () => {
-  const [gzhPlanList, setGzhPlanList] = useState<GzhPlanType[]>([]);
+  const [gzhPlanList, setGzhPlanList] = useState<GzhPlanDataType[]>([]);
   const [loading, setLoading] = useState(false);
 	const [error, setError] = useState<string | null>(null);
 	const [totalSize, setTotalSize] = useState(0);
@@ -41,6 +50,8 @@ export const useGzhPlanList = () => {
 		createTimestampEnd,
 		createTimestampStart,
 		publishStage,
+		type = '0',
+		selectVideoType,
 		title,
 		pageNum,
 		pageSize
@@ -53,11 +64,13 @@ export const useGzhPlanList = () => {
 				createTimestampEnd,
 				createTimestampStart,
 				publishStage,
+				type: +type,
+				selectVideoType,
 				title,
 				pageNum,
 				pageSize,
 			});
-      setGzhPlanList(data.data.objs as GzhPlanType[]);
+      setGzhPlanList(data.data.objs as GzhPlanDataType[]);
 			setTotalSize(data.data.totalSize);
     } catch (err) {
       setError(err instanceof Error ? err.message : 'Failed to fetch accounts');

+ 102 - 41
src/views/publishContent/weGZH/index.tsx

@@ -1,12 +1,12 @@
 import React, { useEffect, useState } from 'react';
-import { Space, Table, Button, Input, Select, DatePicker, Tabs, message, Typography, Spin } from 'antd';
+import { Space, Table, Button, Input, Select, DatePicker, Tabs, message, Typography, Spin, Popconfirm } from 'antd';
 import type { TableProps } from 'antd';
 import dayjs, { Dayjs } from 'dayjs';
 import styles from './index.module.css';
 import PunlishPlanModal from './components/publishPlanModal';
 const { RangePicker } = DatePicker;
 import { useAccountOptions } from '@src/views/publishContent/weGZH/hooks/useAccountOptions';
-import { useGzhPlanList, GzhPlanType } from '@src/views/publishContent/weGZH/hooks/useGzhPlanList';
+import { useGzhPlanList, GzhPlanDataType, GzhPlanType } from '@src/views/publishContent/weGZH/hooks/useGzhPlanList';
 import http from '@src/http';
 import { deleteGzhPlanApi, saveGzhPlanApi } from '@src/http/api';
 import PunlishPlanDetailModal from './components/PunlishPlanDetailModal';
@@ -14,6 +14,7 @@ import PunlishPlanDetailModal from './components/PunlishPlanDetailModal';
 const TableHeight = window.innerHeight - 380;
 
 const WeGZHContent: React.FC = () => {
+	const [planType, setPlanType] = useState<GzhPlanType>(GzhPlanType.自动回复);
 	// 状态管理
 	const [selectedAccount, setSelectedAccount] = useState<string>();
 	const [videoTitle, setVideoTitle] = useState<string>('');
@@ -21,17 +22,18 @@ const WeGZHContent: React.FC = () => {
 	const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>();
 	const [isShowAddPunlishPlan, setIsShowAddPunlishPlan] = useState<boolean>(false);
 	const [actionType, setActionType] = useState<'add' | 'edit'>('add');
-	const [editPlanData, setEditPlanData] = useState<GzhPlanType>();
-	const [activeKey, setActiveKey] = useState<string>('1');
+	const [editPlanData, setEditPlanData] = useState<GzhPlanDataType>();
+	const [selectVideoType, setSelectVideoType] = useState<number>();
 	const [isSubmiting, setIsSubmiting] = useState<boolean>(false);
 	const [isLoading, setIsLoading] = useState<boolean>(false);
-	const { accountOptions } = useAccountOptions();
+	const { accountOptions, getAccountList } = useAccountOptions();
 	const { gzhPlanList, getGzhPlanList, totalSize } = useGzhPlanList();
+	const [pageNum, setPageNum] = useState<number>(1);
 
 	const [isShowAddPunlishDetailPlan, setIsShowAddPunlishDetailPlan] = useState<boolean>(false);
 
 	// 表格列配置
-	const columns: TableProps<GzhPlanType>['columns'] = [
+	const columns: TableProps<GzhPlanDataType>['columns'] = [
 		{
 			title: '公众号名称',
 			dataIndex: 'accountName',
@@ -39,7 +41,25 @@ const WeGZHContent: React.FC = () => {
 			width: 160,
 		},
 		{
-			title: '场景',
+			title: '发布方式',
+			dataIndex: 'publishStage',
+			key: 'publishStage',
+			width: 120,
+			render: (_, record) => {
+				return record.publishStage === 0 ? '平台发布' : '用户获取路径';
+			}
+		},
+		{
+			title: '视频选取方式',
+			dataIndex: 'selectVideoType',
+			key: 'selectVideoType',
+			width: 120,
+			render: (_, record) => {
+				return record.selectVideoType === 0 ? '手动选取' : '自动选取';
+			}
+		},
+		{
+			title: '发布场景',
 			dataIndex: 'scene',
 			key: 'scene',
 			width: 120,
@@ -59,7 +79,7 @@ const WeGZHContent: React.FC = () => {
 			key: 'title',
 			ellipsis: true,
 			render: (_, record) => {
-				return record.videoList.map(video => {
+				return record?.videoList?.map(video => {
 					return <Typography.Paragraph style={{ maxWidth: '300px' }} ellipsis={{ rows: 1, tooltip: true }} key={video.videoId}>{video.customTitle || video.title}</Typography.Paragraph>
 				})
 			}
@@ -73,15 +93,7 @@ const WeGZHContent: React.FC = () => {
 				return record.createTimestamp ? dayjs(record.createTimestamp).format('YYYY-MM-DD HH:mm:ss') : '';
 			}
 		},
-		{
-			title: '发布方',
-			dataIndex: 'publishStage',
-			key: 'publishStage',
-			width: 120,
-			render: (_, record) => {
-				return record.publishStage === 0 ? '平台发布' : '用户发布';
-			}
-		},
+		
 		{
 			title: '操作',
 			key: 'action',
@@ -90,13 +102,25 @@ const WeGZHContent: React.FC = () => {
 				<Space size="middle">
 					<Button type="link" onClick={() => editPlan(record)}>编辑</Button>
 					<Button type="link" onClick={() => editPlanDetail(record)}>详情</Button>
-					<Button type="link" onClick={() => deletePlan(record)}>删除</Button>
+					<Popconfirm title="确定删除该计划吗?" okText="确定" cancelText="取消" onConfirm={() => deletePlan(record)}>
+						<Button type="link">删除</Button>
+					</Popconfirm>
+					
 				</Space>
 			),
 		},
 	];
+	const cloumns2: TableProps<GzhPlanDataType>['columns'] = columns.filter(item => item.title !== '发布场景').map(item => { 
+		if (item.title === '公众号名称') { 
+			return {
+				...item,
+				title: '服务号名称',
+			}
+		}
+		return item;
+	});
 
-	const deletePlan = async (record: GzhPlanType) => {
+	const deletePlan = async (record: GzhPlanDataType) => {
 		setIsLoading(true);
 		const res = await http.post(deleteGzhPlanApi, {
 			id: record.id,
@@ -106,8 +130,10 @@ const WeGZHContent: React.FC = () => {
 		if (res?.code === 0) {
 			message.success('删除成功');
 			getGzhPlanList({
+				type: planType,
 				pageNum: 1,
 				pageSize: 10,
+				selectVideoType: selectVideoType,
 			});
 		} else {
 			message.error(res?.msg || '删除失败');
@@ -115,13 +141,13 @@ const WeGZHContent: React.FC = () => {
 		setIsLoading(false);
 	}
 
-	const editPlan = (record: GzhPlanType) => {
+	const editPlan = (record: GzhPlanDataType) => {
 		setEditPlanData(record);
 		setActionType('edit');
 		setIsShowAddPunlishPlan(true);
 	};
 
-	const editPlanDetail = (record: GzhPlanType) => {
+	const editPlanDetail = (record: GzhPlanDataType) => {
 		setEditPlanData(record);
 		setActionType('edit');
 		setIsShowAddPunlishDetailPlan(true);
@@ -133,9 +159,13 @@ const WeGZHContent: React.FC = () => {
 		setIsShowAddPunlishPlan(true);
 	}
 
-	const handleAddPunlishPlan = async (params: GzhPlanType) => {
+	const handleAddPunlishPlan = async (params: GzhPlanDataType) => {
 		setIsSubmiting(true);
-		const res = await http.post<GzhPlanType>(saveGzhPlanApi, params)
+		if (params.type !== planType) { 
+			setPlanType(params.type as GzhPlanType);
+		}
+		
+		const res = await http.post<GzhPlanDataType>(saveGzhPlanApi, {...params, type: +params.type})
 			.catch(err => {
 				message.error(err.msg);
 			})
@@ -144,7 +174,9 @@ const WeGZHContent: React.FC = () => {
 			});
 		if (res?.code === 0) {
 			message.success('发布计划创建成功');
+			
 			getGzhPlanList({
+				type: params.type,
 				pageNum: 1,
 				pageSize: 10,
 			});
@@ -155,19 +187,29 @@ const WeGZHContent: React.FC = () => {
 	}
 
 	useEffect(() => {
+		setSelectedAccount(undefined);
+		setVideoTitle('');
+		setSelectedPublisher(undefined);
+		setSelectVideoType(undefined);
+		setDateRange(undefined);
+		setPageNum(1);
+		getAccountList({ accountType: planType });
 		getGzhPlanList({
+			type: planType,
 			pageNum: 1,
 			pageSize: 10,
 		});
-	}, []);
+	}, [planType]);
 
 	const handleSearch = () => {
 		getGzhPlanList({
 			pageNum: 1,
 			pageSize: 10,
 			title: videoTitle,
+			type: planType,
 			accountId: selectedAccount ? parseInt(selectedAccount) : undefined,
 			publishStage: selectedPublisher,
+			selectVideoType: selectVideoType,
 			createTimestampStart: dateRange?.[0]?.unix() ? dateRange[0].unix() * 1000 : undefined,
 			createTimestampEnd: dateRange?.[1]?.unix() ? dateRange[1].unix() * 1000 : undefined,
 		});
@@ -181,10 +223,10 @@ const WeGZHContent: React.FC = () => {
 				{/* 搜索区域 */}
 				<div className="flex flex-wrap gap-4 mb-3">
 					<div className="flex items-center gap-2">
-						<span className="text-gray-600">公众号名称:</span>
+						<span className="text-gray-600">{ planType === GzhPlanType.自动回复 ? '公众号名称' : '服务号名称' }:</span>
 						<Select
-							placeholder="选择公众号"
-							style={{ width: 200 }}
+							placeholder={planType === GzhPlanType.自动回复 ? '选择公众号' : '选择服务号'}
+							style={{ width: 150 }}
 							value={selectedAccount}
 							onChange={setSelectedAccount}
 							allowClear
@@ -192,7 +234,7 @@ const WeGZHContent: React.FC = () => {
 							filterOption={(input, option) =>
 								(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
 							}
-							options={accountOptions.map(item => ({ label: item.name, value: item.id }))}
+							options={accountOptions?.map(item => ({ label: item.name, value: item.id })) || []}
 						/>
 					</div>
 
@@ -209,16 +251,30 @@ const WeGZHContent: React.FC = () => {
 					</div>
 
 					<div className="flex items-center gap-2">
-						<span className="text-gray-600">发布方:</span>
+						<span className="text-gray-600">发布方:</span>
 						<Select
-							placeholder="选择发布方"
-							style={{ width: 200 }}
+							placeholder="选择发布方"
+							style={{ width: 140 }}
 							value={selectedPublisher}
 							onChange={setSelectedPublisher}
 							allowClear
 							options={[
 								{ label: '平台发布', value: 0 },
-								{ label: '用户发布', value: 1 },
+								{ label: '用户获取路径', value: 1 },
+							]}
+						/>
+					</div>
+					<div className="flex items-center gap-2">
+						<span className="text-gray-600">视频选取方式:</span>
+						<Select
+							placeholder="视频选取方式"
+							style={{ width: 130 }}
+							value={selectVideoType}
+							onChange={setSelectVideoType}
+							allowClear
+							options={[
+								{ label: '手动选取', value: 0 },
+								{ label: '自动选取', value: 1 },
 							]}
 						/>
 					</div>
@@ -226,7 +282,7 @@ const WeGZHContent: React.FC = () => {
 					<div className="flex items-center gap-2">
 						<RangePicker
 							placeholder={['开始时间', '结束时间']}
-							style={{ width: 400 }}
+							style={{ width: 270 }}
 							allowClear
 							value={dateRange}
 							onChange={(dates) => {
@@ -238,15 +294,16 @@ const WeGZHContent: React.FC = () => {
 					<Button type="primary" className="ml-2" onClick={ handleSearch}>搜索</Button>
 				</div>
 				<Tabs
-					defaultActiveKey="1"
+					defaultActiveKey={ GzhPlanType.自动回复}
 					type="card"
 					size="large"
 					className={styles.antTableTab}
 					items={[
-						{ label: '自动回复', key: '1' },
+						{ label: '自动回复', key: GzhPlanType.自动回复 },
+						{ label: '服务号推送', key: GzhPlanType.服务号推送 },
 					]}
-					activeKey={activeKey}
-					onChange={setActiveKey}
+					activeKey={planType}
+					onChange={(key) => setPlanType(key as GzhPlanType)}
 					tabBarExtraContent={
 						{ right: <Button type="primary" onClick={addPunlishPlan}>+ 创建发布</Button> }}
 				/>
@@ -254,17 +311,20 @@ const WeGZHContent: React.FC = () => {
 				<Table
 					rowKey={(record) => record.id}
 					className={styles.antTable}
-					columns={columns}
+					columns={planType === GzhPlanType.自动回复 ? columns : cloumns2}
 					dataSource={gzhPlanList}
 					scroll={{ x: 'max-content', y: TableHeight }}
 					pagination={{
 						total: totalSize,
 						pageSize: 10,
+						current: pageNum,
 						showTotal: (total) => `共 ${total} 条`,
 						onChange: (page) => getGzhPlanList({
 							pageNum: page,
 							pageSize: 10,
 							title: videoTitle,
+							type: planType,
+							selectVideoType: selectVideoType,
 							accountId: selectedAccount ? parseInt(selectedAccount) : undefined,
 							publishStage: selectedPublisher,
 							createTimestampStart: dateRange?.[0]?.unix() ? dateRange[0].unix() * 1000 : undefined,
@@ -274,14 +334,15 @@ const WeGZHContent: React.FC = () => {
 				/>
 				<PunlishPlanModal
 					visible={isShowAddPunlishPlan}
-					onCancel={() => { 
+					onCancel={() => {
 						setEditPlanData(undefined);
 						setIsShowAddPunlishPlan(false);
-					} }
+					}}
 					onOk={handleAddPunlishPlan}
 					actionType={actionType}
 					editPlanData={editPlanData}
 					isSubmiting={isSubmiting}
+					planType={ planType}
 				/>
 				<PunlishPlanDetailModal
 					visible={isShowAddPunlishDetailPlan}
@@ -289,7 +350,7 @@ const WeGZHContent: React.FC = () => {
 						setEditPlanData(undefined);
 						setIsShowAddPunlishDetailPlan(false);
 					}}
-					planData={editPlanData as GzhPlanType}
+					planData={editPlanData as GzhPlanDataType}
 				/>
 			</div>
 		</Spin>