|
@@ -8,6 +8,7 @@ import {
|
|
|
Typography,
|
|
Typography,
|
|
|
Space,
|
|
Space,
|
|
|
Spin,
|
|
Spin,
|
|
|
|
|
+ Pagination,
|
|
|
message,
|
|
message,
|
|
|
Modal,
|
|
Modal,
|
|
|
} from 'antd';
|
|
} from 'antd';
|
|
@@ -22,6 +23,8 @@ import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
|
|
|
|
|
|
|
|
const { Paragraph, Text } = Typography;
|
|
const { Paragraph, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
+type LoadMode = 'replace' | 'append' | 'jump';
|
|
|
|
|
+
|
|
|
interface VideoSelectModalProps {
|
|
interface VideoSelectModalProps {
|
|
|
planType: GzhPlanType;
|
|
planType: GzhPlanType;
|
|
|
visible: boolean;
|
|
visible: boolean;
|
|
@@ -46,12 +49,17 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
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 [viewingPage, setViewingPage] = useState(1);
|
|
|
|
|
+ const [pageAnchors, setPageAnchors] = useState<Map<number, number>>(new Map()); // videoId -> page
|
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
|
|
|
+ const pageObserverRef = useRef<IntersectionObserver | null>(null);
|
|
|
|
|
+ const passedPagesRef = useRef<Set<number>>(new Set());
|
|
|
|
|
+ const drawerBodyRef = useRef<HTMLElement | null>(null);
|
|
|
const loadingMoreRef = useRef(false);
|
|
const loadingMoreRef = useRef(false);
|
|
|
const hasMoreRef = useRef(true);
|
|
const hasMoreRef = useRef(true);
|
|
|
const loadingRef = useRef(false);
|
|
const loadingRef = useRef(false);
|
|
|
const currentPageRef = useRef(1);
|
|
const currentPageRef = useRef(1);
|
|
|
- const getVideoListRef = useRef<(pageNum: number, append: boolean) => Promise<void>>();
|
|
|
|
|
|
|
+ const getVideoListRef = useRef<(pageNum: number, mode: LoadMode) => Promise<void>>();
|
|
|
const MAX_SELECTION = 3;
|
|
const MAX_SELECTION = 3;
|
|
|
|
|
|
|
|
useEffect(() => { hasMoreRef.current = hasMore; }, [hasMore]);
|
|
useEffect(() => { hasMoreRef.current = hasMore; }, [hasMore]);
|
|
@@ -68,8 +76,9 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const getVideoList = async (pageNum: number, append: boolean) => {
|
|
|
|
|
- if (append) {
|
|
|
|
|
|
|
+ const getVideoList = async (pageNum: number, mode: LoadMode) => {
|
|
|
|
|
+ const isAppend = mode === 'append';
|
|
|
|
|
+ if (isAppend) {
|
|
|
if (loadingMoreRef.current) return;
|
|
if (loadingMoreRef.current) return;
|
|
|
loadingMoreRef.current = true;
|
|
loadingMoreRef.current = true;
|
|
|
setLoadingMore(true);
|
|
setLoadingMore(true);
|
|
@@ -90,7 +99,7 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
|
|
const res = await http.post<VideoListResponse>(getVideoContentListApi, requestParams).catch(() => {
|
|
|
message.error('获取视频列表失败');
|
|
message.error('获取视频列表失败');
|
|
|
}).finally(() => {
|
|
}).finally(() => {
|
|
|
- if (append) {
|
|
|
|
|
|
|
+ if (isAppend) {
|
|
|
loadingMoreRef.current = false;
|
|
loadingMoreRef.current = false;
|
|
|
setLoadingMore(false);
|
|
setLoadingMore(false);
|
|
|
} else {
|
|
} else {
|
|
@@ -99,9 +108,18 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
});
|
|
});
|
|
|
if (res && res.code === 0) {
|
|
if (res && res.code === 0) {
|
|
|
const items = res.data.objs || [];
|
|
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))]);
|
|
|
|
|
|
|
+ if (mode === 'append') {
|
|
|
|
|
+ setVideoList(prev => [...prev, ...items.filter(v => !prev.find(p => p.videoId === v.videoId))]);
|
|
|
|
|
+ if (items.length > 0) {
|
|
|
|
|
+ setPageAnchors(prev => new Map(prev).set(items[0].videoId, pageNum));
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (mode === 'jump') {
|
|
|
|
|
+ setVideoList([...items]);
|
|
|
|
|
+ setPageAnchors(items.length > 0 ? new Map([[items[0].videoId, pageNum]]) : new Map());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setVideoList([...selectedVideos, ...items.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
|
|
|
|
|
+ setPageAnchors(items.length > 0 ? new Map([[items[0].videoId, pageNum]]) : new Map());
|
|
|
|
|
+ }
|
|
|
setVideoListAll(old => [...old, ...items.filter(v => !old.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);
|
|
setHasMore(pageNum * PAGE_SIZE < res.data.totalSize);
|
|
@@ -115,7 +133,10 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
setVideoList(selectedVideos);
|
|
setVideoList(selectedVideos);
|
|
|
setVideoListAll(selectedVideos);
|
|
setVideoListAll(selectedVideos);
|
|
|
setHasMore(true);
|
|
setHasMore(true);
|
|
|
- getVideoList(1, false);
|
|
|
|
|
|
|
+ setViewingPage(1);
|
|
|
|
|
+ setPageAnchors(new Map());
|
|
|
|
|
+ passedPagesRef.current.clear();
|
|
|
|
|
+ getVideoList(1, 'replace');
|
|
|
}
|
|
}
|
|
|
}, [visible]);
|
|
}, [visible]);
|
|
|
|
|
|
|
@@ -131,15 +152,72 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
if (!node) return;
|
|
if (!node) return;
|
|
|
observerRef.current = new IntersectionObserver((entries) => {
|
|
observerRef.current = new IntersectionObserver((entries) => {
|
|
|
if (entries[0].isIntersecting && hasMoreRef.current && !loadingMoreRef.current && !loadingRef.current) {
|
|
if (entries[0].isIntersecting && hasMoreRef.current && !loadingMoreRef.current && !loadingRef.current) {
|
|
|
- getVideoListRef.current?.(currentPageRef.current + 1, true);
|
|
|
|
|
|
|
+ getVideoListRef.current?.(currentPageRef.current + 1, 'append');
|
|
|
}
|
|
}
|
|
|
}, { rootMargin: '2000px' });
|
|
}, { rootMargin: '2000px' });
|
|
|
observerRef.current.observe(node);
|
|
observerRef.current.observe(node);
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
+ const ensureObserver = useCallback((body: HTMLElement) => {
|
|
|
|
|
+ pageObserverRef.current?.disconnect();
|
|
|
|
|
+ pageObserverRef.current = new IntersectionObserver((entries) => {
|
|
|
|
|
+ entries.forEach(entry => {
|
|
|
|
|
+ const pageStr = (entry.target as HTMLElement).dataset.pageAnchor;
|
|
|
|
|
+ if (!pageStr) return;
|
|
|
|
|
+ const page = Number(pageStr);
|
|
|
|
|
+ const rootTop = entry.rootBounds?.top ?? 0;
|
|
|
|
|
+ if (entry.boundingClientRect.top < rootTop) {
|
|
|
|
|
+ passedPagesRef.current.add(page);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ passedPagesRef.current.delete(page);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ const passed = passedPagesRef.current;
|
|
|
|
|
+ const max = passed.size > 0 ? Math.max(...Array.from(passed)) : 1;
|
|
|
|
|
+ setViewingPage(prev => prev === max ? prev : max);
|
|
|
|
|
+ }, { root: body, threshold: [0, 1] });
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const captureDrawerBody = useCallback((node: HTMLDivElement | null) => {
|
|
|
|
|
+ if (!node) return;
|
|
|
|
|
+ const body = node.closest('.ant-drawer-body') as HTMLElement | null;
|
|
|
|
|
+ if (body) {
|
|
|
|
|
+ drawerBodyRef.current = body;
|
|
|
|
|
+ ensureObserver(body);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [ensureObserver]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const obs = pageObserverRef.current;
|
|
|
|
|
+ const body = drawerBodyRef.current;
|
|
|
|
|
+ if (!obs || !body) return;
|
|
|
|
|
+ obs.disconnect();
|
|
|
|
|
+ passedPagesRef.current.clear();
|
|
|
|
|
+ body.querySelectorAll<HTMLElement>('[data-page-anchor]').forEach(el => obs.observe(el));
|
|
|
|
|
+ }, [videoList, pageAnchors]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ pageObserverRef.current?.disconnect();
|
|
|
|
|
+ };
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const jumpToPage = (page: number) => {
|
|
|
|
|
+ setHasMore(true);
|
|
|
|
|
+ setVideoList([]);
|
|
|
|
|
+ setPageAnchors(new Map());
|
|
|
|
|
+ passedPagesRef.current.clear();
|
|
|
|
|
+ setViewingPage(page);
|
|
|
|
|
+ drawerBodyRef.current?.scrollTo({ top: 0 });
|
|
|
|
|
+ getVideoList(page, 'jump');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleSearch = () => {
|
|
const handleSearch = () => {
|
|
|
setHasMore(true);
|
|
setHasMore(true);
|
|
|
- getVideoList(1, false);
|
|
|
|
|
|
|
+ setViewingPage(1);
|
|
|
|
|
+ setPageAnchors(new Map());
|
|
|
|
|
+ passedPagesRef.current.clear();
|
|
|
|
|
+ getVideoList(1, 'replace');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleSelectVideo = (videoId: number) => {
|
|
const handleSelectVideo = (videoId: number) => {
|
|
@@ -182,8 +260,19 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
placement="right"
|
|
placement="right"
|
|
|
styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
|
|
styles={{ footer: { textAlign: 'right', padding: '10px 24px' } }}
|
|
|
footer={
|
|
footer={
|
|
|
- <div className="flex justify-between items-center">
|
|
|
|
|
- <Text type="secondary">已加载 {videoList.length} / 共 {total} 条</Text>
|
|
|
|
|
|
|
+ <div className="flex justify-between items-center gap-2">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <Text type="secondary">已加载 {videoList.length} / 共 {total} 条</Text>
|
|
|
|
|
+ {total > PAGE_SIZE && (
|
|
|
|
|
+ <Pagination
|
|
|
|
|
+ simple
|
|
|
|
|
+ current={viewingPage}
|
|
|
|
|
+ total={total}
|
|
|
|
|
+ pageSize={PAGE_SIZE}
|
|
|
|
|
+ onChange={jumpToPage}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
<Space>
|
|
<Space>
|
|
|
<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
|
|
<Text>已选 {selectedVideoIds.size} / {MAX_SELECTION} 条视频</Text>
|
|
|
<Button onClick={onClose}>取消</Button>
|
|
<Button onClick={onClose}>取消</Button>
|
|
@@ -219,13 +308,15 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<Spin spinning={loading}>
|
|
<Spin spinning={loading}>
|
|
|
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
|
|
|
|
|
|
+ <div ref={captureDrawerBody} className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
|
|
{videoList.map((video) => {
|
|
{videoList.map((video) => {
|
|
|
const isSelected = selectedVideoIds.has(video.videoId);
|
|
const isSelected = selectedVideoIds.has(video.videoId);
|
|
|
const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
|
|
const isDisabled = !isSelected && selectedVideoIds.size >= MAX_SELECTION;
|
|
|
|
|
+ const startsPage = pageAnchors.get(video.videoId);
|
|
|
return (
|
|
return (
|
|
|
<Card
|
|
<Card
|
|
|
key={video.videoId}
|
|
key={video.videoId}
|
|
|
|
|
+ {...(startsPage ? { 'data-page-anchor': startsPage } : {})}
|
|
|
className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
|
|
className={`relative ${isDisabled ? 'opacity-50' : 'cursor-pointer'} ${isSelected ? 'border-blue-500 border-2' : ''}`}
|
|
|
styles={{ body: { padding: 0 } }}
|
|
styles={{ body: { padding: 0 } }}
|
|
|
onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
|
|
onClick={() => !isDisabled && handleSelectVideo(video.videoId)}
|