// src/pages/story/detail.tsx import { useIsMobile } from '@/hooks/useIsMobile'; import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal'; import TimelineGridItem from '@/pages/story/components/TimelineGridItem'; import TimelineItemDrawer from '@/pages/story/components/TimelineItemDrawer'; import { StoryItem, StoryItemTimeQueryParams } from '@/pages/story/data'; import { queryStoryDetail, queryStoryItem, removeStoryItem } from '@/pages/story/service'; import { judgePermission } from '@/pages/story/utils/utils'; import { PlusOutlined, SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { history, useParams, useRequest } from '@umijs/max'; import { Button, Empty, FloatButton, message, Spin } from 'antd'; import { PullToRefresh } from 'antd-mobile'; import { useCallback, useEffect, useRef, useState } from 'react'; import './detail.css'; // 格式化时间数组为易读格式 const formatTimeArray = (time: string | number[] | undefined): string => { if (!time) return ''; // 如果是数组格式 [2025, 12, 23, 8, 55, 39] if (Array.isArray(time)) { const [year, month, day, hour, minute, second] = time; return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`; } // 如果已经是字符串格式,直接返回 return String(time); }; const Index = () => { const isMobile = useIsMobile(); const { id: lineId } = useParams<{ id: string }>(); const containerRef = useRef(null); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [hasMoreOld, setHasMoreOld] = useState(true); // 是否有更老的数据 const [hasMoreNew, setHasMoreNew] = useState(true); // 是否有更新的数据 const [openAddItemModal, setOpenAddItemModal] = useState(false); const [currentItem, setCurrentItem] = useState(); const [currentOption, setCurrentOption] = useState< 'add' | 'edit' | 'addSubItem' | 'editSubItem' >(); const [openDetailDrawer, setOpenDetailDrawer] = useState(false); const [detailItem, setDetailItem] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 30 }); const [isRefreshing, setIsRefreshing] = useState(false); // 是否正在刷新最新数据 const [showScrollTop, setShowScrollTop] = useState(false); // 是否显示回到顶部按钮 const [currentTimeArr, setCurrentTimeArr] = useState<[string | undefined, string | undefined]>([ undefined, undefined, ]); const [loadDirection, setLoadDirection] = useState<'init' | 'older' | 'newer' | 'refresh'>( 'init', ); const hasShownNoMoreOldRef = useRef(false); const hasShownNoMoreNewRef = useRef(false); type QueryParams = StoryItemTimeQueryParams & { current?: number; pageSize?: number }; const { data: response, run } = useRequest( (params?: QueryParams) => { return queryStoryItem({ storyInstanceId: lineId, pageSize: pagination.pageSize, ...params }); }, { manual: true, }, ); const { data: detail, run: queryDetail, loading: queryDetailLoading, } = useRequest(() => { return queryStoryDetail(lineId ?? ''); }); // 初始化加载数据 useEffect(() => { setHasMoreOld(true); setHasMoreNew(true); setLoadDirection('init'); setLoading(true); queryDetail(); run(); }, [lineId]); // 处理响应数据 useEffect(() => { if (!response) return; // 兼容 response.list 和 response.data.list // @ts-ignore const fetched = response.list || response.data?.list || []; const pageSize = pagination.pageSize; const noMore = !(fetched.length === pageSize); // 若无新数据则避免触发列表重绘,只更新加载状态 if (!fetched.length) { if (loadDirection === 'older') { setHasMoreOld(false); } else if (loadDirection === 'newer') { setHasMoreNew(false); } else if (loadDirection === 'init' || loadDirection === 'refresh') { setItems([]); } setLoading(false); setIsRefreshing(false); setLoadDirection('init'); return; } if (loadDirection === 'older') { setItems((prev) => { const next = [...prev, ...fetched]; setCurrentTimeArr([next[0]?.storyItemTime, next[next.length - 1]?.storyItemTime]); return next; }); setHasMoreOld(!noMore); if (noMore && !hasShownNoMoreOldRef.current) { hasShownNoMoreOldRef.current = true; message.info('没有更多历史内容了'); } } else if (loadDirection === 'newer') { setItems((prev) => { const next = [...fetched, ...prev]; setCurrentTimeArr([next[0]?.storyItemTime, next[next.length - 1]?.storyItemTime]); return next; }); setHasMoreNew(!noMore); if (noMore && !hasShownNoMoreNewRef.current) { hasShownNoMoreNewRef.current = true; message.info('没有更多更新内容了'); } } else if (loadDirection === 'refresh' || loadDirection === 'init') { setItems(fetched); if (fetched.length > 0) { setCurrentTimeArr([fetched[0]?.storyItemTime, fetched[fetched.length - 1]?.storyItemTime]); } setHasMoreOld(!noMore); } setLoading(false); setIsRefreshing(false); setLoadDirection('init'); }, [response, loadDirection, pagination.pageSize]); // 滚动到底部加载更老的数据 const loadOlder = useCallback(() => { if (loading || !hasMoreOld) { return; } const beforeTime = items[items.length - 1]?.storyItemTime || currentTimeArr[1]; if (!beforeTime) return; const nextPage = pagination.current + 1; setPagination((prev) => ({ ...prev, current: nextPage })); setLoadDirection('older'); setLoading(true); run({ current: nextPage }); }, [loading, hasMoreOld, items, currentTimeArr, pagination.current, run]); // 滚动到顶部加载更新的数据 const loadNewer = useCallback(() => { if (loading || !hasMoreNew || isRefreshing) { return; } const afterTime = items[0]?.storyItemTime || currentTimeArr[0]; if (!afterTime) return; setPagination((prev) => ({ ...prev, current: 1 })); setLoadDirection('newer'); setIsRefreshing(true); setLoading(true); run({ afterTime: afterTime }); }, [loading, hasMoreNew, isRefreshing, items, currentTimeArr, run]); // 监听滚动事件 useEffect(() => { const container = containerRef.current; if (!container) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = container; // 显示回到顶部按钮 setShowScrollTop(scrollTop > 300); // 接近底部时加载更多 if (scrollHeight - scrollTop - clientHeight < 200) { loadOlder(); } }; container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); }, [loadOlder]); // 手动刷新最新数据 const handleRefresh = () => { if (isRefreshing) return; setIsRefreshing(true); setLoadDirection('refresh'); setPagination((prev) => ({ ...prev, current: 1 })); hasShownNoMoreOldRef.current = false; hasShownNoMoreNewRef.current = false; run({ current: 1 }); }; // 回到顶部 const scrollToTop = () => { if (containerRef.current) { containerRef.current.scrollTo({ top: 0, behavior: 'smooth' }); } }; // 按日期分组items,并在每个组内按时间排序 const groupItemsByDate = (items: StoryItem[]) => { const groups: { [key: string]: { dateKey: string; items: StoryItem[]; sortValue: number } } = {}; items.forEach((item) => { let dateKey = ''; let sortValue = 0; if (Array.isArray(item.storyItemTime)) { const [year, month, day] = item.storyItemTime; dateKey = `${year}年${month}月${day}日`; sortValue = new Date(year, month - 1, day).getTime(); } else if (item.storyItemTime) { const dateStr = String(item.storyItemTime); const datePart = dateStr.split(' ')[0]; dateKey = datePart; sortValue = new Date(datePart).getTime(); } if (!groups[dateKey]) { groups[dateKey] = { dateKey, items: [], sortValue }; } groups[dateKey].items.push(item); }); // 对每个日期组内的项目按时间排序(从早到晚) Object.keys(groups).forEach((dateKey) => { groups[dateKey].items.sort((a, b) => { const timeA = getTimeValue(a.storyItemTime); const timeB = getTimeValue(b.storyItemTime); return timeA - timeB; }); }); return groups; }; // 将时间转换为可比较的数值 const getTimeValue = (time: string | number[] | undefined): number => { if (!time) return 0; if (Array.isArray(time)) { const [year, month, day, hour = 0, minute = 0, second = 0] = time; return new Date(year, month - 1, day, hour, minute, second).getTime(); } return new Date(String(time)).getTime(); }; const groupedItems = groupItemsByDate(items); return ( history.push('/story')} title={ queryDetailLoading ? '加载中' : `${detail?.title} ${`共${detail?.itemCount ?? 0}个时刻`}` } extra={ } >
{items.length > 0 ? ( {hasMoreNew && !isMobile && (
)} {Object.values(groupedItems) .sort((a, b) => b.sortValue - a.sortValue) // 按日期降序排列(最新的在前) .map(({ dateKey, items: dateItems }) => (

{dateKey}

{dateItems.map((item, index) => { // 调试:确保每个item都有有效的数据 if (!item || (!item.id && !item.instanceId)) { console.warn('发现无效的item:', item, 'at index:', index); return null; // 不渲染无效的item } return ( { setCurrentItem(item); setCurrentOption(option); setOpenAddItemModal(true); }} onOpenDetail={(item: StoryItem) => { setDetailItem(item); setOpenDetailDrawer(true); }} disableEdit={!judgePermission(detail?.permissionType ?? null, 'edit')} refresh={() => { setPagination((prev) => ({ ...prev, current: 1 })); hasShownNoMoreOldRef.current = false; hasShownNoMoreNewRef.current = false; setLoadDirection('refresh'); run({ current: 1 }); queryDetail(); }} /> ); })}
))} {loading &&
加载中...
} {!loading && !hasMoreOld &&
已加载全部历史数据
} {/* 回到顶部按钮 */} {showScrollTop && (
)}
) : (
{loading ? ( <>
正在加载时间线数据...
) : ( <>
还没有添加任何时刻
)}
)}
{ setCurrentOption('add'); setCurrentItem(undefined); setOpenAddItemModal(true); }} icon={} disabled={!judgePermission(detail?.permissionType ?? null, 'edit')} type="primary" style={{ right: 24, bottom: 24, }} /> { setOpenAddItemModal(false); }} onOk={() => { setOpenAddItemModal(false); // 添加新项后刷新数据 setPagination((prev) => ({ ...prev, current: 1 })); setLoadDirection('refresh'); run({ current: 1 }); queryDetail(); }} storyId={lineId} /> {/* 详情抽屉 - 在外层管理,不影响网格布局 */} {detailItem && ( { // 这里需要实现删除逻辑 try { if (!detailItem.instanceId) return; const response = await removeStoryItem(detailItem.instanceId); if (response.code === 200) { message.success('删除成功'); setOpenDetailDrawer(false); // 刷新数据 setPagination((prev) => ({ ...prev, current: 1 })); setLoadDirection('refresh'); run({ current: 1 }); queryDetail(); } else { message.error('删除失败'); } } catch (error) { message.error('删除失败'); } }} disableEdit={!judgePermission(detail?.permissionType ?? null, 'edit')} handOption={(item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => { setCurrentItem(item); setCurrentOption(option); setOpenDetailDrawer(false); setOpenAddItemModal(true); }} /> )}
); }; export default Index;