Browse Source

选视频弹窗改为滚动加载,移除排序选项

- 移除排序选项 UI,sort 固定为推荐指数(行为不变)
- 移除分页控件,改用 IntersectionObserver 滚动到底部自动加载下一页
- footer 显示「已加载 X / 共 N 条」,列表底部显示加载中/没有更多了

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
刘立冬 13 hours ago
parent
commit
38dc5208fc

+ 49 - 43
src/views/publishContent/weCom/components/videoSelectModal/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
 import {
 import {
 	Drawer,
 	Drawer,
 	Button,
 	Button,
@@ -6,7 +6,6 @@ import {
 	Input,
 	Input,
 	Card,
 	Card,
 	Typography,
 	Typography,
-	Pagination,
 	Space,
 	Space,
 	message,
 	message,
 	Modal,
 	Modal,
@@ -17,7 +16,6 @@ import http from '@src/http';
 import { getVideoContentListApi } from '@src/http/api';
 import { getVideoContentListApi } from '@src/http/api';
 import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
 import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
 import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
 import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
-import { enumToOptions } from '@src/utils/helper';
 
 
 const { Text, Paragraph } = Typography;
 const { Text, Paragraph } = Typography;
 
 
@@ -39,47 +37,64 @@ export enum VideoSortType {
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [] }) => {
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, onOk, planType, initialSelectedIds = [] }) => {
 	const { videoCategoryOptions } = useVideoCategoryOptions();
 	const { videoCategoryOptions } = useVideoCategoryOptions();
 	const [category, setCategory] = useState<string>();
 	const [category, setCategory] = useState<string>();
-	const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
+	const sort = VideoSortType.推荐指数;
+	const PAGE_SIZE = 10;
 	const [searchTerm, setSearchTerm] = useState<string>('');
 	const [searchTerm, setSearchTerm] = useState<string>('');
 	const [currentPage, setCurrentPage] = useState(1);
 	const [currentPage, setCurrentPage] = useState(1);
-	const [pageSize, setPageSize] = useState(10);
 	const [total, setTotal] = useState(0);
 	const [total, setTotal] = useState(0);
 	const [loading, setLoading] = useState(false);
 	const [loading, setLoading] = useState(false);
+	const [loadingMore, setLoadingMore] = useState(false);
+	const [hasMore, setHasMore] = useState(true);
 	const [videoList, setVideoList] = useState<WeVideoItem[]>([]);
 	const [videoList, setVideoList] = useState<WeVideoItem[]>([]);
 	const [videoListAll, setVideoListAll] = useState<WeVideoItem[]>([]);
 	const [videoListAll, setVideoListAll] = useState<WeVideoItem[]>([]);
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
 	const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
 	const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
+	const sentinelRef = useRef<HTMLDivElement>(null);
+	const loadingMoreRef = useRef(false);
 	const MAX_SELECTION = 3;
 	const MAX_SELECTION = 3;
 
 
-	const getVideoList = async (pageNum?: number, _pageSize?: number) => {
-		setLoading(true);
-		setCurrentPage(pageNum || currentPage);
-		setPageSize(_pageSize || pageSize);
+	const getVideoList = async (pageNum: number, append: boolean) => {
+		if (append) {
+			if (loadingMoreRef.current) return;
+			loadingMoreRef.current = true;
+			setLoadingMore(true);
+		} else {
+			setLoading(true);
+		}
+		setCurrentPage(pageNum);
 
 
 		const requestParams = {
 		const requestParams = {
 			category,
 			category,
 			title: searchTerm,
 			title: searchTerm,
 			sort,
 			sort,
 			type: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
 			type: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
-			pageNum: pageNum || currentPage,
-			pageSize: _pageSize || pageSize,
+			pageNum,
+			pageSize: PAGE_SIZE,
 		};
 		};
 
 
 		const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
 		const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
 			message.error('获取视频列表失败');
 			message.error('获取视频列表失败');
 		}).finally(() => {
 		}).finally(() => {
-			setLoading(false);
+			if (append) {
+				loadingMoreRef.current = false;
+				setLoadingMore(false);
+			} else {
+				setLoading(false);
+			}
 		});
 		});
 		if (res && res.code === 0) {
 		if (res && res.code === 0) {
-			const mappedVideos = (res.data.objs || []).map(video => ({ ...video, scene: videoList.find(v => v.videoId === video.videoId)?.scene || 0 as 0 | 1 }));
-			setVideoList(mappedVideos);
-			setVideoListAll(old => [...old, ...mappedVideos]);
+			const mapped = (res.data.objs || []).map(video => ({ ...video, scene: 0 as 0 | 1 }));
+			setVideoList(prev => append
+				? [...prev, ...mapped.filter(v => !prev.find(p => p.videoId === v.videoId))]
+				: mapped);
+			setVideoListAll(old => [...old, ...mapped.filter(v => !old.find(o => o.videoId === v.videoId))]);
 			setTotal(res.data.totalSize);
 			setTotal(res.data.totalSize);
+			setHasMore(pageNum * PAGE_SIZE < res.data.totalSize);
 		}
 		}
 	}
 	}
 
 
 	useEffect(() => {
 	useEffect(() => {
-		getVideoList();
+		getVideoList(1, false);
 	}, []);
 	}, []);
 
 
 	useEffect(() => {
 	useEffect(() => {
@@ -88,11 +103,21 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 		}
 		}
 	}, [visible, initialSelectedIds]);
 	}, [visible, initialSelectedIds]);
 
 
+	useEffect(() => {
+		const sentinel = sentinelRef.current;
+		if (!sentinel) return;
+		const observer = new IntersectionObserver((entries) => {
+			if (entries[0].isIntersecting && hasMore && !loadingMoreRef.current && !loading) {
+				getVideoList(currentPage + 1, true);
+			}
+		}, { threshold: 0.1 });
+		observer.observe(sentinel);
+		return () => observer.disconnect();
+	}, [hasMore, loading, currentPage]);
+
 	const handleSearch = () => {
 	const handleSearch = () => {
-		console.log('Searching for:', { category, searchTerm });
-		const newPageNum = 1;
-		setCurrentPage(newPageNum);
-		getVideoList(newPageNum);
+		setHasMore(true);
+		getVideoList(1, false);
 	};
 	};
 
 
 	const handleSelectVideo = (videoId: number) => {
 	const handleSelectVideo = (videoId: number) => {
@@ -162,20 +187,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
 				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
 				footer={
 				footer={
 					<div className="flex justify-between items-center">
 					<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} 条`}
-						/>
+						<Text type="secondary">已加载 {videoList.length} / 共 {total} 条</Text>
 						<Space>
 						<Space>
 							<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
 							<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
 							<Button onClick={onClose}>取消</Button>
 							<Button onClick={onClose}>取消</Button>
@@ -185,15 +197,6 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 				}
 				}
 			>
 			>
 				<div className="flex flex-wrap gap-2 mb-6">
 				<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={sort}
-							onChange={setSort}
-							options={enumToOptions(VideoSortType)}
-						/>
-					</div>
 					<div className="flex items-center gap-2">
 					<div className="flex items-center gap-2">
 						<span className="text-gray-600">品类:</span>
 						<span className="text-gray-600">品类:</span>
 						<Select
 						<Select
@@ -277,6 +280,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 						);
 						);
 					})}
 					})}
 				</div>
 				</div>
+				<div ref={sentinelRef} className="text-center py-4 text-gray-400 text-xs">
+					{loadingMore ? '加载中...' : !hasMore && videoList.length > 0 ? '— 没有更多了 —' : ''}
+				</div>
 			</Drawer>
 			</Drawer>
 
 
 			<Modal
 			<Modal

+ 51 - 42
src/views/publishContent/weGZH/components/videoSelectModal/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
 import {
 import {
 	Drawer,
 	Drawer,
 	Button,
 	Button,
@@ -6,7 +6,6 @@ import {
 	Input,
 	Input,
 	Card,
 	Card,
 	Typography,
 	Typography,
-	Pagination,
 	Space,
 	Space,
 	message,
 	message,
 	Modal,
 	Modal,
@@ -19,7 +18,6 @@ import { useVideoCategoryOptions } from '../../hooks/useVideoCategoryOptions';
 import { VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
 import { VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
 import { GzhPlanType } from '../../hooks/useGzhPlanList';
 import { GzhPlanType } from '../../hooks/useGzhPlanList';
 import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
 import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
-import { enumToOptions } from '@src/utils/helper';
 
 
 const { Paragraph, Text } = Typography;
 const { Paragraph, Text } = Typography;
 
 
@@ -35,16 +33,20 @@ interface VideoSelectModalProps {
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [] }) => {
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [] }) => {
 	const { videoCategoryOptions } = useVideoCategoryOptions();
 	const { videoCategoryOptions } = useVideoCategoryOptions();
 	const [category, setCategory] = useState<string>();
 	const [category, setCategory] = useState<string>();
-	const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
+	const sort = VideoSortType.推荐指数;
+	const PAGE_SIZE = 10;
 	const [searchTerm, setSearchTerm] = useState<string>('');
 	const [searchTerm, setSearchTerm] = useState<string>('');
 	const [currentPage, setCurrentPage] = useState(1);
 	const [currentPage, setCurrentPage] = useState(1);
-	const [pageSize, setPageSize] = useState(10);
 	const [total, setTotal] = useState(0);
 	const [total, setTotal] = useState(0);
 	const [loading, setLoading] = useState(false);
 	const [loading, setLoading] = useState(false);
+	const [loadingMore, setLoadingMore] = useState(false);
+	const [hasMore, setHasMore] = useState(true);
 	const [videoList, setVideoList] = useState<VideoItem[]>([]);
 	const [videoList, setVideoList] = useState<VideoItem[]>([]);
 	const [videoListAll, setVideoListAll] = useState<VideoItem[]>([]);
 	const [videoListAll, setVideoListAll] = useState<VideoItem[]>([]);
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
 	const [selectedVideoIds, setSelectedVideoIds] = useState<Set<number>>(new Set(initialSelectedIds));
 	const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
 	const [playingVideo, setPlayingVideo] = useState<VideoItem | null>(null);
+	const sentinelRef = useRef<HTMLDivElement>(null);
+	const loadingMoreRef = useRef(false);
 	const MAX_SELECTION = 3;
 	const MAX_SELECTION = 3;
 
 
 	const getVideoListType = (planType: GzhPlanType) => {
 	const getVideoListType = (planType: GzhPlanType) => {
@@ -57,29 +59,43 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		}
 		}
 	}
 	}
 
 
-	const getVideoList = async (pageNum?: number, _pageSize?: number) => {
-		setLoading(true);
-		setCurrentPage(pageNum || currentPage);
-		setPageSize(_pageSize || pageSize);
+	const getVideoList = async (pageNum: number, append: boolean) => {
+		if (append) {
+			if (loadingMoreRef.current) return;
+			loadingMoreRef.current = true;
+			setLoadingMore(true);
+		} else {
+			setLoading(true);
+		}
+		setCurrentPage(pageNum);
 
 
 		const requestParams = {
 		const requestParams = {
 			category,
 			category,
 			title: searchTerm,
 			title: searchTerm,
 			sort,
 			sort,
 			type: getVideoListType(planType),
 			type: getVideoListType(planType),
-			pageNum: pageNum || currentPage,
-			pageSize: _pageSize || pageSize,
+			pageNum,
+			pageSize: PAGE_SIZE,
 		};
 		};
 
 
 		const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
 		const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
 			message.error('获取视频列表失败');
 			message.error('获取视频列表失败');
 		}).finally(() => {
 		}).finally(() => {
-			setLoading(false);
+			if (append) {
+				loadingMoreRef.current = false;
+				setLoadingMore(false);
+			} else {
+				setLoading(false);
+			}
 		});
 		});
 		if (res && res.code === 0) {
 		if (res && res.code === 0) {
-			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))]);
+			const items = res.data.objs || [];
+			setVideoList(prev => append
+				? [...prev, ...items.filter(v => !prev.find(p => p.videoId === v.videoId))]
+				: [...selectedVideos, ...items.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
+			setVideoListAll(old => [...old, ...items.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
 			setTotal(res.data.totalSize);
 			setTotal(res.data.totalSize);
+			setHasMore(pageNum * PAGE_SIZE < res.data.totalSize);
 		}
 		}
 	}
 	}
 
 
@@ -87,7 +103,8 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		if (visible) {
 		if (visible) {
 			setVideoList(selectedVideos);
 			setVideoList(selectedVideos);
 			setVideoListAll(selectedVideos);
 			setVideoListAll(selectedVideos);
-			getVideoList(0);
+			setHasMore(true);
+			getVideoList(1, false);
 		}
 		}
 	}, [visible]);
 	}, [visible]);
 
 
@@ -97,11 +114,22 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		}
 		}
 	}, [visible, initialSelectedIds]);
 	}, [visible, initialSelectedIds]);
 
 
+	useEffect(() => {
+		if (!visible) return;
+		const sentinel = sentinelRef.current;
+		if (!sentinel) return;
+		const observer = new IntersectionObserver((entries) => {
+			if (entries[0].isIntersecting && hasMore && !loadingMoreRef.current && !loading) {
+				getVideoList(currentPage + 1, true);
+			}
+		}, { threshold: 0.1 });
+		observer.observe(sentinel);
+		return () => observer.disconnect();
+	}, [visible, hasMore, loading, currentPage]);
+
 	const handleSearch = () => {
 	const handleSearch = () => {
-		console.log('Searching for:', { category, searchTerm });
-		const currentPage = 1
-		setCurrentPage(currentPage);
-		getVideoList(currentPage);
+		setHasMore(true);
+		getVideoList(1, false);
 	};
 	};
 
 
 	const handleSelectVideo = (videoId: number) => {
 	const handleSelectVideo = (videoId: number) => {
@@ -146,20 +174,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
 				styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
 				footer={
 				footer={
 					<div className="flex justify-between items-center">
 					<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} 条`}
-						/>
+						<Text type="secondary">已加载 {videoList.length} / 共 {total} 条</Text>
 						<Space>
 						<Space>
 							<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
 							<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
 							<Button onClick={onClose}>取消</Button>
 							<Button onClick={onClose}>取消</Button>
@@ -169,15 +184,6 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 				}
 				}
 			>
 			>
 				<div className="flex flex-wrap gap-2 mb-6">
 				<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={sort}
-							onChange={setSort}
-							options={enumToOptions(VideoSortType)}
-						/>
-					</div>
 					<div className="flex items-center gap-2">
 					<div className="flex items-center gap-2">
 						<span className="text-gray-600">品类:</span>
 						<span className="text-gray-600">品类:</span>
 						<Select
 						<Select
@@ -245,6 +251,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 						);
 						);
 					})}
 					})}
 				</div>
 				</div>
+				<div ref={sentinelRef} className="text-center py-4 text-gray-400 text-xs">
+					{loadingMore ? '加载中...' : !hasMore && videoList.length > 0 ? '— 没有更多了 —' : ''}
+				</div>
 			</Drawer>
 			</Drawer>
 
 
 			<Modal
 			<Modal