ソースを参照

feat: 支持收藏发布

jihuaqiang 1 週間 前
コミット
60a2cfd682

+ 6 - 0
src/http/api.ts

@@ -36,6 +36,7 @@ export const getVideoContentCategoryListApi = `${import.meta.env.VITE_API_URL}/c
 export const getVideoContentCoverFrameListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/videoContentCoverFrameList`
 export const getVideoContentListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/videoContentList`
 export const getUploadVideoContentListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/upload/videoContentList`
+export const getCollectVideoContentListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/collect/videoContentList`
 export const getShareQrPic = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/qw/getSharePic`
 export const getShareQrLink = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/getShareUrlLink`
 
@@ -63,3 +64,8 @@ export const uploadDeleteVideo = `${import.meta.env.VITE_API_URL}/contentPlatfor
 // 设置绑定微信用户
 export const getBindPQUserInfo = `${import.meta.env.VITE_API_URL}/contentPlatform/setting/getBindPQUserInfo`
 export const bindPQUser = `${import.meta.env.VITE_API_URL}/contentPlatform/setting/webLogin`
+
+// 收藏
+export const saveCollectVideoContentApi = `${import.meta.env.VITE_API_URL}/contentPlatform/collectContent/save`
+export const getCollectContentListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/collectContent/list`
+export const clearInvalidCollectContentApi = `${import.meta.env.VITE_API_URL}/contentPlatform/collectContent/clearInvalid`

+ 27 - 3
src/views/contentManage/collectVideos/index.tsx

@@ -1,9 +1,33 @@
-import React, { } from 'react';
+import React from 'react';
+import { VideoSelectContent, VideoLibraryType } from '@src/views/publishContent/weCom/components/videoSelectModal';
+import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+import { Button, message } from 'antd';
+import http from '@src/http';
+import { clearInvalidCollectContentApi } from '@src/http/api';
 
 const CollectVideos: React.FC = () => {
+
+	const handleClearInvalidVideo = async () => {
+		console.log('handleClearInvalidVideo');
+		const res = await http.get<ApiResponse<any>>(clearInvalidCollectContentApi);
+		if (res && res.code === 0) {
+			message.success('清空失效视频成功');
+		} else {
+			message.error('清空失效视频失败');
+		}
+	};
+
 	return (
-		<div>
-			<h1>Collect Videos</h1>
+		<div className="mb-[30px] max-h-[calc(100vh-145px)] bg-white overflow-y-auto">
+			<Button type="primary" onClick={handleClearInvalidVideo} className="!fixed bottom-2 right-10">清空失效视频</Button>
+			<VideoSelectContent
+				planType={VideoSearchPlanType.企微社群}
+				initialSelectedIds={[]}
+				selectedVideos={[]}
+				defaultVideoLibraryType={VideoLibraryType.我的收藏}
+				autoLoad={true}
+				isCollectedPage={true}
+			/>
 		</div>
 	);
 };

+ 2 - 1
src/views/contentManage/uploadVideos/index.tsx

@@ -9,6 +9,7 @@ import { UploadContentResponse } from './type';
 import dayjs from 'dayjs';
 import VideoPlayModal from '@src/views/publishContent/weCom/components/videoPlayModal';
 import { UserInfo } from '../../setting/type';
+import { VideoLibraryType } from '@src/views/publishContent/weCom/components/videoSelectModal';
 // Define a type for the expected API response (adjust if needed based on actual API)
 const TableHeight = window.innerHeight - 380;
 
@@ -174,7 +175,7 @@ const MyVideos: React.FC = () => {
 	}
 
 	const uploadPublishVideo = (record: any) => {
-		return location.href = '/publishContent/wegzh/' + record.videoId;
+		return location.href = '/publishContent/wegzh/' + VideoLibraryType.我的上传 + '/' + record.videoId;
 	}
 
 	// 引导用户去登录

+ 1 - 1
src/views/publishContent/publishContent.router.tsx

@@ -40,7 +40,7 @@ const demoRoutes: AdminRouterItem[] = [
 		},
 		children: [
 			{
-				path: 'wegzh/:code?',
+				path: 'wegzh/:libType?/:code?',
 				element: <LazyComponent Component={WeGZH} />,
 				meta: {
 					label: "公众号",

+ 11 - 1
src/views/publishContent/types.ts

@@ -1,5 +1,14 @@
 import { TagType } from "./weCom/components/videoSelectModal";
 
+export enum CollectedStatusEnum {
+	已收藏 = 1,
+	未收藏 = 0,
+}
+
+export enum VideoStatusEnum {
+	已下架 = 0,
+	正常 = 1,
+}
 export interface VideoItem {
   videoId: number;
   video: string;
@@ -17,6 +26,7 @@ export interface VideoItem {
   recommendScore?: number;
   tags?: TagType[];
 	scene?: 0 | 1;
-	isCollected?: boolean;
+	collect?: CollectedStatusEnum;
+	status?: VideoStatusEnum;
 }
 

+ 271 - 211
src/views/publishContent/weCom/components/videoSelectModal/index.tsx

@@ -15,23 +15,36 @@ import {
 	Tooltip,
 	Tag,
 	Flex,
+	Empty,
 } from 'antd';
 import { CheckCircleFilled, CaretRightFilled, QuestionCircleOutlined, StarOutlined, StarFilled } from '@ant-design/icons';
 import { VideoItem, VideoListResponse } from '@src/views/publishContent/weGZH/components/types';
 import http from '@src/http';
-import { getVideoContentListApi, getUploadVideoContentListApi } from '@src/http/api';
+import { getVideoContentListApi, getUploadVideoContentListApi, getCollectVideoContentListApi, saveCollectVideoContentApi, getCollectContentListApi } from '@src/http/api';
 import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
 import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
 import { enumToOptions } from '@src/utils/helper';
 import { isNil } from 'lodash';
+import { CollectedStatusEnum, VideoStatusEnum } from '@src/views/publishContent/types';
 
 export enum VideoLibraryType {
 	平台视频库 = 0,
 	我的上传 = 1,
+	我的收藏 = 2,
 }
 
 const { Text, Paragraph } = Typography;
 
+interface VideoSelectContentProps {
+	planType: VideoSearchPlanType;
+	initialSelectedIds?: number[];
+	selectedVideos?: VideoItem[];
+	defaultVideoLibraryType?: VideoLibraryType;
+	onVideoListChange?: (videos: VideoItem[]) => void;
+	autoLoad?: boolean;
+	isCollectedPage?: boolean; // 是否是收藏页面
+}
+
 interface VideoSelectModalProps {
 	planType: VideoSearchPlanType;
 	visible: boolean;
@@ -84,11 +97,18 @@ interface GetVideoListParams {
 	_videoLibraryType?: VideoLibraryType;
 }
 
-const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
+export const VideoSelectContent: React.FC<VideoSelectContentProps> = ({
+	planType,
+	initialSelectedIds = [],
+	selectedVideos = [],
+	defaultVideoLibraryType,
+	onVideoListChange,
+	autoLoad = true,
+	isCollectedPage = false,
+}) => {
 	const { videoCategoryOptions } = useVideoCategoryOptions();
 	const [tagOptions, setTagOptions] = useState<TagTypeOptions[]>([]);
 	const [category, setCategory] = useState<string>();
-	// const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
 	const [sortType, setSortType] = useState<SortTypeEnum>(SortTypeEnum.推荐指数);
 	const [recentNotUsed, setRecentNotUsed] = useState<RecentNotUsedType>();
 	const [tags, setTags] = useState<TagType[]>([]);
@@ -110,6 +130,28 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 		}
 	}, [defaultVideoLibraryType]);
 
+	useEffect(() => {
+		if (onVideoListChange) {
+			const selectedVideos = videoListAll.filter(video => selectedVideoIds.has(video.videoId));
+			const uniqueSelectedVideos = selectedVideos.filter((video, index, self) =>
+				index === self.findIndex((t) => t.videoId === video.videoId)
+			);
+			onVideoListChange(uniqueSelectedVideos);
+		}
+	}, [selectedVideoIds, videoListAll]);
+
+	const getVideoListUrl = (videoLibraryType: VideoLibraryType) => {
+		if (videoLibraryType === VideoLibraryType.平台视频库) {
+			return getVideoContentListApi;
+		} else if (videoLibraryType === VideoLibraryType.我的上传) {
+			return getUploadVideoContentListApi;
+		} else if (videoLibraryType === VideoLibraryType.我的收藏 && isCollectedPage) {
+			return getCollectContentListApi;
+		} else {
+			return getCollectVideoContentListApi;
+		}
+	}
+
 	const getVideoList = async ({pageNum, _pageSize, _sortType, _recentNotUsed, _category, _tags, _videoLibraryType}: GetVideoListParams) => {
 		setLoading(true);
 		setCurrentPage(pageNum || currentPage);
@@ -117,7 +159,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 
 		// 根据视频库类型选择不同的API
 		const currentVideoLibraryType = isNil(_videoLibraryType) ? videoLibraryType : _videoLibraryType;
-		const apiUrl = currentVideoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
+		const apiUrl = getVideoListUrl(currentVideoLibraryType);
 
 		const requestParams = {
 			category: isNil(_category) ? category : _category,
@@ -139,6 +181,8 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 			setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
 			setVideoListAll(old => [...old, ...res.data.objs.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
 			setTotal(res.data.totalSize);
+		} else {
+			message.error('获取视频列表失败');
 		}
 	}
 
@@ -152,29 +196,18 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	}
 
 	useEffect(() => {
-		if (visible) {
-			console.log('selectedVideos', selectedVideos);
+		if (autoLoad) {
 			// 初始化时设置 selectedVideos
 			setVideoList(selectedVideos);
 			setVideoListAll(selectedVideos);
 			setTagOptions(enumToOptions(TagType).map(option => ({ label: getTagLabel(option.value as TagType), value: option.value as TagType })));
 			getVideoList({});
-		} else { 
-			setRecentNotUsed(undefined);
-			setTags([]);
-			setVideoLibraryType(defaultVideoLibraryType || VideoLibraryType.平台视频库);
-			setSortType(SortTypeEnum.推荐指数);
-			setSearchTerm('');
-			setCategory(undefined);
-			setCurrentPage(1);
 		}
-	}, [visible]);
+	}, [autoLoad]);
 
 	useEffect(() => {
-		if (visible) {
-			setSelectedVideoIds(new Set(initialSelectedIds));
-		}
-	}, [visible, initialSelectedIds]);
+		setSelectedVideoIds(new Set(initialSelectedIds));
+	}, [initialSelectedIds]);
 
 	const handleSearch = () => {
 		console.log('Searching for:', { category, searchTerm });
@@ -199,17 +232,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 		});
 	};
 
-	const handleOk = () => {
-		const selectedVideos = videoListAll.filter(video => selectedVideoIds.has(video.videoId));
-		// 去重
-		const uniqueSelectedVideos = selectedVideos.filter((video, index, self) =>
-			index === self.findIndex((t) => t.videoId === video.videoId)
-		);
-		console.log('uniqueSelectedVideos', uniqueSelectedVideos);
-		onOk(uniqueSelectedVideos);
-	};
-
-const playVideo = (video: VideoItem) => {
+	const playVideo = (video: VideoItem) => {
 		setPlayingVideo(video);
 	};
 
@@ -224,19 +247,16 @@ const playVideo = (video: VideoItem) => {
 
 	const handleSortTypeChange = (v: RadioChangeEvent) => {
 		setSortType(v.target.value as SortTypeEnum);
-		console.log('handleSortTypeChange', v.target.value);
 		getVideoList({pageNum: 1, _sortType: v.target.value as SortTypeEnum});
 	}
 
 	const onCategoryChange = (v: string) => {
 		setCategory(v);
-		console.log('onCategoryChange', v);
 		getVideoList({pageNum: 1, _category: v || '' as string});
 	}
 
 	const onRecentNotUsedChange = (v: RecentNotUsedType) => {
 		setRecentNotUsed(v);
-		console.log('onRecentNotUsedChange', v);
 		if (isNil(v)) {
 			getVideoList({pageNum: 1, _recentNotUsed: ''});
 		} else {
@@ -250,201 +270,199 @@ const playVideo = (video: VideoItem) => {
 		getVideoList({pageNum: 1, _tags: newTags});
 	}
 
-	const handleCollectVideo = (videoId: number) => {
-		console.log('handleCollectVideo', videoId);
+	const handleCollectVideo = async (videoId: number) => {
 		const video = videoList.find(v => v.videoId === videoId);
 		if (video) {
-			setVideoList(prev => prev.map(v => v.videoId === videoId ? {...v, isCollected: !v.isCollected} : v));
-			message.success(video.isCollected ? '取消收藏' : '收藏成功');
+			const isCollected = video.collect === CollectedStatusEnum.已收藏 ? true : false;
+			const res = await http.post(saveCollectVideoContentApi, {
+				videoId: videoId,
+				collect: isCollected ? CollectedStatusEnum.未收藏 : CollectedStatusEnum.已收藏,
+			});
+			if (res && res.code === 0) {
+				setVideoList(prev => prev.map(v => v.videoId === videoId ? {...v, collect: isCollected ? CollectedStatusEnum.未收藏 : CollectedStatusEnum.已收藏} : v));
+				message.success(isCollected ? '取消收藏' : '收藏成功');
+			} else {
+				message.error('收藏失败');
+			}
 		}
 	}
 
+	const handleCreatePublish = (videoId: number) => {
+		window.location.href = `/publishContent/wegzh/${videoLibraryType}/${videoId}`;
+	}
+
 	return (
 		<>
-			<Drawer
-				title="内容选取"
-				open={visible}
-				onClose={onClose}
-				width={900}
-				placement="right"
-				loading={loading}
-				destroyOnClose
-				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
-				footer={
-					<div className="flex justify-between items-center">
-						<Pagination
-							current={currentPage}
-							pageSize={pageSize}
-							total={total}
-							onChange={(page, size) => {
-								setCurrentPage(page);
-								setPageSize(size);
-								getVideoList({pageNum: page, _pageSize: 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-2 mb-2">
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">视频来源:</span>
-						<Select
-							style={{ width: 120 }}
-							value={videoLibraryType}
-							onChange={handleVideoLibraryTypeChange}
-							options={enumToOptions(VideoLibraryType)}
-						/>
-					</div>
-					
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">品类:</span>
-						<Select
-							placeholder="选择品类"
-							style={{ width: 120 }}
-							value={category}
-							allowClear
-							onChange={onCategoryChange}
-							options={videoCategoryOptions.map(option => ({ label: option, value: option }))}
-						/>
-					</div>
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">近期未使用:</span>
-						<Select
-							placeholder="选择近期未使用"
-							style={{ width: 120 }}
-							value={recentNotUsed}
-							allowClear
-							onChange={onRecentNotUsedChange}
-							options={enumToOptions(RecentNotUsedType)}
-						/>
-					</div>
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">视频标题:</span>
-						<Input
-							placeholder="搜索视频标题"
-							style={{ width: 130 }}
-							value={searchTerm}
-							allowClear
-							onChange={e => setSearchTerm(e.target.value)}
-							onPressEnter={handleSearch}
-						/>
-					</div>
-					<Button type="primary" onClick={handleSearch}>搜索</Button>
+			<div className="flex flex-wrap gap-2 mb-2">
+				{!isCollectedPage && <div className="flex items-center gap-2">
+					<span className="text-gray-600">视频来源:</span>
+					<Select
+						style={{ width: 120 }}
+						value={videoLibraryType}
+						onChange={handleVideoLibraryTypeChange}
+						options={enumToOptions(VideoLibraryType)}
+					/>
+				</div>}
+				
+				<div className="flex items-center gap-2">
+					<span className="text-gray-600">品类:</span>
+					<Select
+						placeholder="选择品类"
+						style={{ width: 120 }}
+						value={category}
+						allowClear
+						onChange={onCategoryChange}
+						options={videoCategoryOptions.map(option => ({ label: option, value: option }))}
+					/>
+				</div>
+				<div className="flex items-center gap-2">
+					<span className="text-gray-600">近期未使用:</span>
+					<Select
+						placeholder="选择近期未使用"
+						style={{ width: 140 }}
+						value={recentNotUsed}
+						allowClear
+						onChange={onRecentNotUsedChange}
+						options={enumToOptions(RecentNotUsedType)}
+					/>
+				</div>
+				<div className="flex items-center gap-2">
+					<span className="text-gray-600">视频标题:</span>
+					<Input
+						placeholder="搜索视频标题"
+						style={{ width: 130 }}
+						value={searchTerm}
+						allowClear
+						onChange={e => setSearchTerm(e.target.value)}
+						onPressEnter={handleSearch}
+					/>
 				</div>
-				<div className="flex">
-					<div className="flex flex-wrap gap-2 mb-6 items-center">
-						<span className="text-gray-600 ">排序选项:</span>
-						<Radio.Group
-							onChange={handleSortTypeChange}
-							value={sortType}
-							buttonStyle="solid"
+				<Button type="primary" onClick={handleSearch}>搜索</Button>
+			</div>
+			<div className="flex">
+				<div className="flex flex-wrap gap-2 mb-6 items-center">
+					<span className="text-gray-600 ">排序选项:</span>
+					<Radio.Group
+						onChange={handleSortTypeChange}
+						value={sortType}
+						buttonStyle="solid"
+					>
+						<Radio.Button value={SortTypeEnum.推荐指数}>推荐指数 <Tooltip title="结合视频在票圈、同业务场景、本账号下的历史表现综合得分"><QuestionCircleOutlined /></Tooltip></Radio.Button>
+						<Radio.Button value={SortTypeEnum.更新时间}>更新时间</Radio.Button>
+					</Radio.Group>
+				</div>
+				<div className="flex flex-wrap gap-2 mb-6 ml-10 items-center">
+					<span className="text-gray-600 ">推荐标签:</span>
+					{tagOptions.map<React.ReactNode>((option) => (
+						<Tag.CheckableTag
+							key={option.value}
+							checked={tags.includes(option.value)}
+							onChange={(checked) => handleTagChange(option.value, checked)}
 						>
-							<Radio.Button value={SortTypeEnum.推荐指数}>推荐指数 <Tooltip title="结合视频在票圈、同业务场景、本账号下的历史表现综合得分"><QuestionCircleOutlined /></Tooltip></Radio.Button>
-							<Radio.Button value={SortTypeEnum.更新时间}>更新时间</Radio.Button>
-						</Radio.Group>
-					</div>
-					<div className="flex flex-wrap gap-2 mb-6 ml-10 items-center">
-						<span className="text-gray-600 ">推荐标签:</span>
-						{tagOptions.map<React.ReactNode>((option) => (
-							<Tag.CheckableTag
-								key={option.value}
-								checked={tags.includes(option.value)}
-								onChange={(checked) => handleTagChange(option.value, checked)}
-							>
-								{option.label}
-							</Tag.CheckableTag>
-						))}
-					</div>
+							{option.label}
+						</Tag.CheckableTag>
+					))}
 				</div>
-				<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
-					{videoList.map((video) => {
-						const isSelected = selectedVideoIds.has(video.videoId);
-						const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
-						return (
-							<Card
-								key={video.videoId}
-								className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-2 !bg-blue-50 shadow-md' : 'hover:shadow-sm'}`}
-								styles={{ body: { padding: 0 } }}
-								onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
+			</div>
+			<div className={`grid gap-4 ${isCollectedPage ? 'grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3'}`}>
+				{ 
+					!loading && total === 0 && (
+						<div className="col-span-full flex justify-center items-center py-8">
+							<Empty description="暂无数据" />
+						</div>
+					)
+				}
+				{videoList.map((video) => {
+					const isSelected = selectedVideoIds.has(video.videoId);
+					const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
+					const isInvalid = video.status === VideoStatusEnum.已下架;
+					
+					return (
+						<Card
+							key={video.videoId}
+							className={`relative ${isDisabled || isInvalid ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-2 !bg-blue-50 shadow-md' : 'hover:shadow-sm'}`}
+							styles={{ body: { padding: 0 } }}
+							onClick={() => !isDisabled && !isInvalid && handleSelectVideo(video.videoId)}
+						>
+							{isInvalid && (
+								<div className="absolute top-0 left-0 w-full h-full bg-black/50 flex justify-center items-center z-10">
+									<Text type="secondary" className="!text-2xl !text-white font-bold">视频已下架</Text>
+								</div>
+							)}
+							<div className="p-3">
+								<Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
+								<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 1, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
+							</div>
+							<div
+								className="relative"
+								style={{ paddingBottom: '79.8%' }}
+								onClick={(e) => { e.stopPropagation(); playVideo(video); }}
 							>
-								<div className="p-3">
-									<Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
-									<Paragraph className="mt-1 !mb-1" ellipsis={{ rows: 1, tooltip: true }} title={video.customTitle || video.title}>{video.customTitle || video.title}</Paragraph>
+								<img src={video.customCover || video.cover} alt={video.customTitle || video.title} 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
-									className="relative"
-									style={{ paddingBottom: '79.8%' }}
-									onClick={(e) => { e.stopPropagation(); playVideo(video); }}
-								>
-									<img src={video.customCover || video.cover} alt={video.customTitle || video.title} 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 flex justify-between items-center">
+								<div className="flex flex-1 flex-col gap-1">
+									<Text type="secondary" className="text-xs">推荐指数: {video.recommendScore?.toFixed(2) || '无'}</Text>
+									<Text type="secondary" className="text-xs">推荐标签: {'tags' in video && video.tags && video.tags.length > 0 ?
+										video.tags.map(tag => <Tag key={tag} className="text-xs">{getTagLabel(tag as TagType)}</Tag>) : '无'}</Text>
 								</div>
-								<div className="p-3 flex justify-between items-center">
-									<div className="flex flex-1 flex-col gap-1">
-										<Text type="secondary" className="text-xs">推荐指数: {video.recommendScore?.toFixed(2) || '无'}</Text>
-										<Text type="secondary" className="text-xs">推荐标签: {'tags' in video && video.tags && video.tags.length > 0 ?
-											video.tags.map(tag => <Tag key={tag} className="text-xs">{getTagLabel(tag as TagType)}</Tag>) : '无'}</Text>
-									</div>
-									<div className="flex flex-col items-center gap-2">
-										{ 
-											video.isCollected ? (
+								<div className="flex flex-col items-center gap-2">
+									{ 
+										video.collect ? (
+											<Flex gap={4} onClick={(e) => { e.stopPropagation(); handleCollectVideo(video.videoId); }}>
+												<StarFilled className="text-xl !text-[#1890ff]" />
+												<Text>收藏</Text>
+											</Flex>
+										) : (
 												<Flex gap={4} onClick={(e) => { e.stopPropagation(); handleCollectVideo(video.videoId); }}>
-													<StarFilled className="text-xl !text-[#1890ff]" />
-													<Text>收藏</Text>
+													<StarOutlined className="text-xl !text-black/50" />
+													<Text className="!text-black/70">收藏</Text>
+												</Flex>
+										)
+									}
+									{ 
+										isCollectedPage ?
+											<Button type="text" onClick={(e) => { e.stopPropagation(); handleCreatePublish(video.videoId); }}>创建发布</Button>
+										: <>
+											{isSelected ? (
+												<Flex gap={4}>
+													<CheckCircleFilled className="text-base !text-[#1890ff]" />
+													<Text className="!text-black/70">已选</Text>
 												</Flex>
 											) : (
-													<Flex gap={4} onClick={(e) => { e.stopPropagation(); handleCollectVideo(video.videoId); }}>
-														<StarOutlined className="text-xl !text-black/50" />
-														<Text className="!text-black/70">收藏</Text>
-													</Flex>
-											)
-										}
-										{isSelected ? (
-											<Flex gap={4}>
-												<CheckCircleFilled className="text-base !text-[#1890ff]" />
-												<Text className="!text-black/70">已选</Text>
-											</Flex>
-										) : (
-											<Flex gap={4} className="items-center">
-												<div className={`w-4 h-4 border-2 ${isDisabled ? 'border-gray-200 bg-gray-100' : 'border-black/50' } rounded-full`}></div>
-												<Text className="!text-black/70">选择</Text>
-											</Flex>
-										)}
-									</div>
-									
+												<Flex gap={4} className="items-center">
+													<div className={`w-4 h-4 border-2 ${isDisabled ? 'border-gray-200 bg-gray-100' : 'border-black/50' } rounded-full`}></div>
+													<Text className="!text-black/70">选择</Text>
+												</Flex>
+											)}
+										</>
+									}
 								</div>
-								{/* {isSelected && (
-									<div className="p-3 flex justify-between items-center relative z-10">
-										<Select
-											placeholder="选择场景"
-											style={{ width: 180 }}
-											defaultValue={video.scene || 0}
-											value={video.scene}
-											onChange={(value) => handleChangeSelectVideo(video.videoId, value)}
-											options={ planType === WeComPlanType.社群 ?
-												[{ label: '群发', value: 0 }, { label: '单发', value: 1 }]
-												: [{ label: '关注回复', value: 0 }]
-											}
-											onClick={(e) => e.stopPropagation()}
-										/>
-									</div>
-								)} */}
-							</Card>
-						);
-					})}
-				</div>
-			</Drawer>
+								
+							</div>
+						</Card>
+					);
+				})}
+			</div>
+			<div className="mt-4 flex justify-center fixed bottom-3 right-50">
+				<Pagination
+					current={currentPage}
+					pageSize={pageSize}
+					total={total}
+					onChange={(page, size) => {
+						setCurrentPage(page);
+						setPageSize(size);
+						getVideoList({pageNum: page, _pageSize: size});
+					}}
+					pageSizeOptions={['10', '20', '50']}
+					size="small"
+					showSizeChanger
+					showTotal={(total) => `共 ${total} 条`}
+				/>
+			</div>
 
 			<Modal
 				open={!!playingVideo}
@@ -465,4 +483,46 @@ const playVideo = (video: VideoItem) => {
 	);
 };
 
+const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
+	const [currentSelectedVideos, setCurrentSelectedVideos] = useState<VideoItem[]>([]);
+
+	const handleOk = () => {
+		onOk(currentSelectedVideos);
+	};
+
+	return (
+		<>
+			<Drawer
+				title="内容选取"
+				open={visible}
+				onClose={onClose}
+				width={950}
+				placement="right"
+				destroyOnClose
+				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
+				footer={
+					<div className="flex justify-between items-center">
+						<Space className="justify-between w-full">
+							<Text>已选 {currentSelectedVideos.length} / 3 条视频</Text>
+							<Space>
+								<Button onClick={onClose}>取消</Button>
+								<Button type="primary" onClick={handleOk}>确定</Button>
+							</Space>
+						</Space>
+					</div>
+				}
+			>
+				<VideoSelectContent
+					planType={planType}
+					initialSelectedIds={initialSelectedIds}
+					selectedVideos={selectedVideos}
+					defaultVideoLibraryType={defaultVideoLibraryType}
+					autoLoad={visible}
+					onVideoListChange={setCurrentSelectedVideos}
+				/>
+			</Drawer>
+		</>
+	);
+};
+
 export default VideoSelectModal;

+ 7 - 6
src/views/publishContent/weGZH/components/publishPlanModal/index.tsx

@@ -8,6 +8,7 @@ import { VideoItem } from '../types'; // Import from common types
 import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
 import { useAccountOptions } from '../../hooks/useAccountOptions';
 import { VideoLibraryType } from '@src/views/publishContent/weCom/components/videoSelectModal';
+import { isNil } from 'lodash';
 
 const { Option } = Select;
 const { Paragraph } = Typography;
@@ -21,27 +22,27 @@ interface AddPunlishPlanModalProps {
 	editPlanData?: GzhPlanDataType;
 	isSubmiting?: boolean;
 	videoId?: string;
+	defaultLibType?: VideoLibraryType;
 }
 
-const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSubmiting, onCancel, onOk, actionType, planType, editPlanData, videoId }) => {
+const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSubmiting, onCancel, onOk, actionType, planType, editPlanData, videoId, defaultLibType }) => {
 	const [form] = Form.useForm();
 	const type = Form.useWatch('type', form);
-	const { code } = useParams<{ code?: string }>();
 	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 [initialSelectedVideoId, setInitialSelectedVideoId] = useState<string | null>(null); // State for initial video selection
-	const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(VideoLibraryType.平台视频库);
+	const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultLibType || VideoLibraryType.平台视频库);
 	const { accountOptions, getAccountList } = useAccountOptions();
 
 	// 处理code参数
 	useEffect(() => {
-		if (code) {
-			setVideoLibraryType(VideoLibraryType.我的上传);
+		if (!isNil(defaultLibType)) {
+			setVideoLibraryType(defaultLibType);
 		}
-	}, [code]);
+	}, [defaultLibType]);
 
 	useEffect(() => {
 		if (actionType === 'edit') {

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

@@ -1,303 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
-	Drawer,
-	Button,
-	Select,
-	Input,
-	Card,
-	Typography,
-	Pagination,
-	Space,
-	message,
-	Modal,
-} from 'antd';
-import { CheckCircleFilled, CaretRightFilled } from '@ant-design/icons';
-import { VideoItem, VideoListResponse } from '../types';
-import http from '@src/http';
-import { getVideoContentListApi, getUploadVideoContentListApi } from '@src/http/api';
-import { useVideoCategoryOptions } from '../../hooks/useVideoCategoryOptions';
-import { VideoSortType, VideoLibraryType } from '@src/views/publishContent/weCom/components/videoSelectModal';
-import { GzhPlanType } from '../../hooks/useGzhPlanList';
-import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
-import { enumToOptions } from '@src/utils/helper';
-
-const { Paragraph, Text } = Typography;
-
-interface VideoSelectModalProps {
-	planType: GzhPlanType;
-	visible: boolean;
-	onClose: () => void;
-	onOk: (selectedVideos: VideoItem[]) => void;
-	initialSelectedIds?: number[];
-	selectedVideos?: VideoItem[];
-	defaultVideoLibraryType?: VideoLibraryType;
-}
-
-const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
-	const { videoCategoryOptions } = useVideoCategoryOptions();
-	const [category, setCategory] = useState<string>();
-	const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
-	const [searchTerm, setSearchTerm] = useState<string>('');
-	const [currentPage, setCurrentPage] = useState(1);
-	const [pageSize, setPageSize] = useState(10);
-	const [total, setTotal] = useState(0);
-	const [loading, setLoading] = useState(false);
-	const [videoList, setVideoList] = useState<VideoItem[]>([]);
-	const [videoListAll, setVideoListAll] = useState<VideoItem[]>([]);
-	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
-	const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
-	const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(defaultVideoLibraryType || VideoLibraryType.平台视频库);
-	const MAX_SELECTION = 3;
-
-	// 当默认视频来源变化时更新状态
-	useEffect(() => {
-		if (defaultVideoLibraryType) {
-			setVideoLibraryType(defaultVideoLibraryType);
-		}
-	}, [defaultVideoLibraryType]);
-
-	const getVideoListType = (planType: GzhPlanType) => {
-		if (planType === GzhPlanType.自动回复) {
-			return VideoSearchPlanType.自动回复;
-		} else if (planType === GzhPlanType.公众号推送) {
-			return VideoSearchPlanType.公众号推送;
-		} else {
-			return VideoSearchPlanType.服务号推送;
-		}
-	}
-
-	const getVideoList = async (pageNum?: number, _pageSize?: number) => {
-		setLoading(true);
-		setCurrentPage(pageNum || currentPage);
-		setPageSize(_pageSize || pageSize);
-		// 根据视频库类型选择不同的API
-		const apiUrl = videoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
-
-		const requestParams = {
-			category,
-			title: searchTerm,
-			sort,
-			type: getVideoListType(planType),
-			pageNum: pageNum || currentPage,
-			pageSize: _pageSize || pageSize,
-		};
-
-		const res = await http.post<VideoListResponse>(apiUrl, requestParams).catch(() => {
-			message.error('获取视频列表失败');
-		}).finally(() => {
-			setLoading(false);
-		});
-		if (res && res.code === 0) {
-			selectedVideos = selectedVideos.filter(item => item.videoLibraryType === videoLibraryType)
-			setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
-			setVideoListAll(old => [...old, ...res.data.objs.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
-			setTotal(res.data.totalSize);
-		}
-	}
-
-	// 监听视频库类型变化,重新加载数据
-	useEffect(() => {
-		if (visible) {
-			getVideoList();
-		}
-	}, [videoLibraryType, visible]);
-
-	useEffect(() => {
-		if (visible) {
-			setVideoList(selectedVideos);
-			setVideoListAll(selectedVideos);
-			getVideoList(0);
-		}
-	}, [visible]);
-
-	useEffect(() => {
-		if (visible) {
-			setSelectedVideoIds(new Set(initialSelectedIds));
-		}
-	}, [visible, initialSelectedIds]);
-
-	const handleSearch = () => {
-		console.log('Searching for:', { category, searchTerm });
-		const currentPage = 1
-		setCurrentPage(currentPage);
-		getVideoList(currentPage);
-	};
-
-	const handleSelectVideo = (videoId: number) => {
-		setSelectedVideoIds(prev => {
-			const newSet = new Set(prev);
-			if (newSet.has(videoId)) {
-				newSet.delete(videoId);
-			} else {
-				if (newSet.size >= MAX_SELECTION) {
-					message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
-					return prev;
-				}
-				newSet.add(videoId);
-			}
-			return newSet;
-		});
-	};
-
-	const handleOk = () => {
-		const _selectedVideos = videoListAll.filter(video => selectedVideoIds.has(video.videoId));
-		_selectedVideos.forEach(video => {
-			// 加入视频库类型
-			if (!('videoLibraryType' in video)) {
-				video.videoLibraryType = videoLibraryType
-			}
-		});
-		onOk(_selectedVideos);
-		onClose();
-	};
-
-	const playVideo = (video: VideoItem) => {
-		setPlayingVideo(video);
-	};
-
-	const closeVideoPlayer = () => {
-		setPlayingVideo(null);
-	};
-
-	return (
-		<>
-			<Drawer
-				title="选择视频"
-				open={visible}
-				onClose={onClose}
-				width={900}
-				placement="right"
-				loading={loading}
-				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
-				footer={
-					<div className="flex justify-between items-center">
-						<Pagination
-							current={currentPage}
-							pageSize={pageSize}
-							total={total}
-							onChange={(page, size) => {
-								setCurrentPage(page);
-								setPageSize(size);
-								getVideoList(page, 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-2 mb-6">
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">视频来源:</span>
-						<Select
-							style={{ width: 120 }}
-							value={videoLibraryType}
-							onChange={setVideoLibraryType}
-							options={enumToOptions(VideoLibraryType)}
-						/>
-					</div>
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">排序选项:</span>
-						<Select
-							style={{ width: 120 }}
-							value={sort}
-							onChange={setSort}
-							options={enumToOptions(VideoSortType)}
-						/>
-					</div>
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">品类:</span>
-						<Select
-							placeholder="选择品类"
-							style={{ width: 120 }}
-							value={category}
-							allowClear
-							onChange={setCategory}
-							options={videoCategoryOptions.map(option => ({ label: option, value: option }))}
-						/>
-					</div>
-					<div className="flex items-center gap-2">
-						<span className="text-gray-600">视频标题:</span>
-						<Input
-							placeholder="搜索视频标题"
-							style={{ width: 120 }}
-							value={searchTerm}
-							onPressEnter={handleSearch}
-							allowClear
-							onChange={e => setSearchTerm(e.target.value)}
-						/>
-					</div>
-					<Button type="primary" onClick={handleSearch}>搜索</Button>
-				</div>
-
-				<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
-					{videoList.map((video) => {
-						const isSelected = selectedVideoIds.has(video.videoId);
-						const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
-						return (
-							<Card
-								key={video.videoId}
-								className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
-								styles={{ body: { padding: 0 } }}
-								onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
-							>
-								<div className="p-2">
-									<Text type="secondary" className="text-xs">票圈 | 3亿人喜欢的视频平台</Text>
-									<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(); playVideo(video); }}
-								>
-									<img src={video.customCover || video.cover} alt={video.customTitle || video.title} 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 flex justify-between items-center">
-									<div className="flex flex-col gap-1">
-										<Text type="secondary" className="text-xs">推荐指数: {video.recommendScore?.toFixed(2) || '无'}</Text>
-										<Text type="secondary" className="text-xs">平台传播得分: {video.score?.toFixed(2) || '无'}</Text>
-										<Text type="secondary" className="text-xs">行业裂变率: {video.industryFissionRate?.toFixed(2) || '无'}</Text>
-										<Text type="secondary" className="text-xs">本渠道传播率: {video.channelFissionRate?.toFixed(2) || '无'}</Text>
-									</div>
-									{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}
-				styles={{ body: { padding: 0, background: '#000' } }}
-			>
-				{playingVideo && (
-					<video controls autoPlay className="w-full h-auto max-h-[80vh] block" src={playingVideo.video}>
-						Your browser does not support the video tag.
-					</video>
-				)}
-			</Modal>
-		</>
-	);
-};
-
-export default VideoSelectModal;

+ 7 - 4
src/views/publishContent/weGZH/index.tsx

@@ -11,12 +11,13 @@ import { useGzhPlanList, GzhPlanDataType, GzhPlanType } from '@src/views/publish
 import http from '@src/http';
 import { deleteGzhPlanApi, saveGzhPlanApi } from '@src/http/api';
 import PunlishPlanDetailModal from './components/PunlishPlanDetailModal';
+import { VideoLibraryType } from '../weCom/components/videoSelectModal';
 
 const TableHeight = window.innerHeight - 380;
 
 const WeGZHContent: React.FC = () => {
 	const [planType, setPlanType] = useState<GzhPlanType>(GzhPlanType.自动回复);
-	const { code } = useParams<{ code?: string }>();
+	const { code, libType } = useParams<{ code?: string, libType?: string }>();
 	// 状态管理
 	const [selectedAccount, setSelectedAccount] = useState<string>();
 	const [videoTitle, setVideoTitle] = useState<string>('');
@@ -34,14 +35,15 @@ const WeGZHContent: React.FC = () => {
 	const [pageSize, setPageSize] = useState<number>(10);
 	const [isShowAddPunlishDetailPlan, setIsShowAddPunlishDetailPlan] = useState<boolean>(false);
 	const [videoId, setVideoId] = useState<string | undefined>(undefined);
-
+	const [defaultLibType, setDefaultLibType] = useState<VideoLibraryType>();
 	// 处理code参数
 	useEffect(() => {
-		if (code) {
+		if (code && libType) {
 			setVideoId(code);
+			setDefaultLibType(libType ? parseInt(libType) as VideoLibraryType : undefined);
 			setIsShowAddPunlishPlan(true);
 		}
-	}, [code]);
+	}, [code, libType]);
 
 	// 表格列配置
 	const columns: TableProps<GzhPlanDataType>['columns'] = [
@@ -361,6 +363,7 @@ const WeGZHContent: React.FC = () => {
 					isSubmiting={isSubmiting}
 					planType={ planType}
 					videoId={videoId}
+					defaultLibType={defaultLibType}
 				/>
 				<PunlishPlanDetailModal
 					visible={isShowAddPunlishDetailPlan}