diff --git a/src/components/TimelineImage/index.tsx b/src/components/TimelineImage/index.tsx index d86a9b3..3e3ca6c 100644 --- a/src/components/TimelineImage/index.tsx +++ b/src/components/TimelineImage/index.tsx @@ -16,6 +16,7 @@ interface Props { imageInstanceId?: string; imageList?: ImageItem[]; currentIndex?: number; + style?: React.CSSProperties; } const TimelineImage: React.FC = (props) => { @@ -28,6 +29,7 @@ const TimelineImage: React.FC = (props) => { height = 200, imageList = [], currentIndex = 0, + style, } = props; const { imageUrl, loading } = useFetchImageUrl(imageInstanceId ?? ''); @@ -55,6 +57,7 @@ const TimelineImage: React.FC = (props) => { }} height={height} width={width} + style={style} alt={title} fallback={ fallback ?? diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 28c5d41..18d0c66 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -5,6 +5,7 @@ import pages from './zh-CN/pages'; import pwa from './zh-CN/pwa'; import settingDrawer from './zh-CN/settingDrawer'; import settings from './zh-CN/settings'; +import story from "@/locales/zh-CN/story"; export default { 'navBar.lang': '语言', @@ -21,4 +22,5 @@ export default { ...settings, ...pwa, ...component, + ...story, }; diff --git a/src/locales/zh-CN/pages.ts b/src/locales/zh-CN/pages.ts index 55a2147..a266bc6 100644 --- a/src/locales/zh-CN/pages.ts +++ b/src/locales/zh-CN/pages.ts @@ -64,13 +64,4 @@ export default { 'pages.searchTable.tenThousand': '万', 'pages.searchTable.batchDeletion': '批量删除', 'pages.searchTable.batchApproval': '批量审批', - "story.deleteSuccess": "删除成功", - "story.deleteFailed": "删除失败", - "story.deleteConfirm": "确认删除", - "story.deleteConfirmDescription": "确定要删除这个故事项吗?此操作不可撤销。", - "story.yes": "是", - "story.no": "否", - "story.edit": "编辑", - "story.addSubItem": "添加子项", - "story.delete": "删除" }; diff --git a/src/locales/zh-CN/story.ts b/src/locales/zh-CN/story.ts new file mode 100644 index 0000000..4e4055b --- /dev/null +++ b/src/locales/zh-CN/story.ts @@ -0,0 +1,13 @@ +export default { + "story.deleteSuccess": "删除成功", + "story.deleteFailed": "删除失败", + "story.deleteConfirm": "确认删除", + "story.deleteConfirmDescription": "确定要删除这个故事项吗?此操作不可撤销。", + "story.yes": "是", + "story.no": "否", + "story.edit": "编辑", + "story.addSubItem": "添加子项", + "story.delete": "删除", + 'story.showLess': '更少', + 'story.showMore': '更多', +} diff --git a/src/pages/story/components/AddTimeLineItemModal.tsx b/src/pages/story/components/AddTimeLineItemModal.tsx index 9fc9198..507c4f9 100644 --- a/src/pages/story/components/AddTimeLineItemModal.tsx +++ b/src/pages/story/components/AddTimeLineItemModal.tsx @@ -41,7 +41,6 @@ const AddTimeLineItemModal: React.FC = ({ onOk, storyId, initialValues, - storyItemId, option, }) => { const [form] = Form.useForm(); @@ -60,6 +59,7 @@ const AddTimeLineItemModal: React.FC = ({ const searchInputRef = useRef(null); useEffect(() => { + console.log(initialValues) if (initialValues && option.startsWith('edit')) { form.setFieldsValue({ title: initialValues.title, @@ -68,7 +68,7 @@ const AddTimeLineItemModal: React.FC = ({ description: initialValues.description, }); } - }, [initialValues, option]); + }, [initialValues, option, visible]); // 获取图库图片 const fetchGalleryImages = async (page: number = 1, keyword: string = '') => { @@ -80,13 +80,13 @@ const AddTimeLineItemModal: React.FC = ({ pageSize: pageSize, keyword: keyword, }); - const images = response.data.list.map((img: any) => ({ + const images = response?.data?.list.map((img: any) => ({ instanceId: img.instanceId, imageName: img.imageName, url: `/file/image-low-res/${img.instanceId}`, - })); + })) ?? []; setGalleryImages(images); - setTotal(response.data.total); + setTotal(response?.data?.total ?? 0); setCurrentPage(page); } catch (error) { message.error('获取图库图片失败'); @@ -321,6 +321,7 @@ const AddTimeLineItemModal: React.FC = ({ onCancel(); }} width={800} + zIndex={1001} footer={[ - + { + e?.stopPropagation(); + handleDelete(); + }} + okText={intl.formatMessage({ id: 'story.yes' })} + cancelText={intl.formatMessage({ id: 'story.no' })} + > + + } > -

- 描述: {storyItem.description} -

-

- 日期: {storyItem.storyItemTime} -

-

- 位置: {storyItem.location} -

- {/* 封面图 */} - {storyItem.coverInstanceId && ( - <> -

- 封面图: -

- - - )} +
+
+

描述

+

{storyItem.description}

+
- {/* 时刻图库 */} - {imagesList && imagesList.length > 0 && ( - <> -

- 时刻图库: -

-
- {imagesList.map((imageInstanceId, index) => ( - - ))} + + + {/* 时刻图库 */} + {imagesList && imagesList.length > 0 && ( +
+

时刻图库

+
+ {imagesList.map((imageInstanceId, index) => ( +
+ +
+ ))} +
- - )} + )} + - {/* 添加子时间点模态框 */} - setOpenAddSubItemModal(false)} - lineId={storyItem.storyInstanceId} - storyItemId={storyItem.instanceId} - /> - - {/* 编辑主时间点模态框 */} - setOpenEditMainItemModal(false)} - initialValues={storyItem} - /> + {/* 创建和更新信息 */} +
+

记录信息

+
+
+
创建时间
+
{formatDate(storyItem.createTime)}
+
+
+
更新时间
+
{formatDate(storyItem.updateTime)}
+
+
+
更新人
+
系统用户
+
+
+
+
); diff --git a/src/pages/story/detail.tsx b/src/pages/story/detail.tsx index 9efb61b..d1ddd2c 100644 --- a/src/pages/story/detail.tsx +++ b/src/pages/story/detail.tsx @@ -4,33 +4,35 @@ import TimelineItem from '@/pages/story/components/TimelineItem/TimelineItem'; import { StoryItem } from '@/pages/story/data'; import { queryStoryDetail, queryStoryItem } from '@/pages/story/service'; import { PageContainer } from '@ant-design/pro-components'; -import { history, useIntl, useRequest } from '@umijs/max'; -import { FloatButton, Spin } from 'antd'; +import { history, useRequest } from '@umijs/max'; +import { FloatButton, Spin, Empty, Button } from 'antd'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList as List } from 'react-window'; +import { VariableSizeList as List } from 'react-window'; +import { SyncOutlined } from '@ant-design/icons'; import './index.css'; -import useStyles from './style.style'; - -interface TimelineItemProps { - children: React.ReactNode; -} const Index = () => { const { id: lineId } = useParams<{ id: string }>(); - const { styles } = useStyles(); - const intl = useIntl(); const containerRef = useRef(null); + const listRef = useRef(null); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); + 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 [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); + // 存储每个item的高度 + const [itemSizes, setItemSizes] = useState>({}); + // 存储已测量高度的item ID集合 + const measuredItemsRef = useRef>(new Set()); + const [isRefreshing, setIsRefreshing] = useState(false); // 是否正在刷新最新数据 + const [showScrollTop, setShowScrollTop] = useState(false); // 是否显示回到顶部按钮 const { data: response, run } = useRequest( () => { @@ -53,7 +55,11 @@ const Index = () => { useEffect(() => { setItems([]); setPagination({ current: 1, pageSize: 10 }); - setHasMore(true); + setHasMoreOld(true); + setHasMoreNew(true); + // 重置高度缓存 + setItemSizes({}); + measuredItemsRef.current = new Set(); run(); }, [lineId]); @@ -64,74 +70,219 @@ const Index = () => { if (pagination.current === 1) { // 首页数据 setItems(response.list || []); - } else { - // 追加数据 + } else if (pagination.current > 1) { + // 追加更老的数据 setItems(prev => [...prev, ...(response.list || [])]); + } else if (pagination.current < 1) { + // 在前面插入更新的数据 + setItems(prev => [...(response.list || []), ...prev]); + // 保持滚动位置 + setTimeout(() => { + if (listRef.current && response.list) { + listRef.current.scrollToItem(response.list.length, 'start'); + } + }, 0); } // 检查是否还有更多数据 - setHasMore(response.list && response.list.length === pagination.pageSize); + if (pagination.current >= 1) { + // 检查是否有更老的数据 + setHasMoreOld(response.list && response.list.length === pagination.pageSize); + } else if (pagination.current < 1) { + // 检查是否有更新的数据 + setHasMoreNew(response.list && response.list.length === pagination.pageSize); + } + setLoading(false); + setIsRefreshing(false); }, [response, pagination]); - // 滚动到底部加载更多 - const loadMore = useCallback(() => { - if (loading || !hasMore) return; + // 滚动到底部加载更老的数据 + const loadOlder = useCallback(() => { + if (loading || !hasMoreOld || pagination.current < 1) return; setLoading(true); setPagination(prev => ({ ...prev, current: prev.current + 1 })); - }, [loading, hasMore]); + }, [loading, hasMoreOld, pagination]); + + // 滚动到顶部加载更新的数据 + const loadNewer = useCallback(() => { + if (loading || !hasMoreNew || pagination.current > 1 || isRefreshing) return; + + setIsRefreshing(true); + setPagination(prev => ({ + ...prev, + current: prev.current - 1 + })); + }, [loading, hasMoreNew, pagination, isRefreshing]); // 当分页变化时重新请求数据 useEffect(() => { - if (pagination.current > 1) { - console.log('分页变化') + if (pagination.current !== 1) { run(); } }, [pagination, run]); + // 获取item高度的函数 + const getItemSize = useCallback((index: number) => { + const item = items[index]; + if (!item) return 300; // 默认高度 + + // 如果已经测量过该item的高度,则使用缓存的值 + if (itemSizes[item.id]) { + return itemSizes[item.id]; + } + + // 返回默认高度 + return 300; + }, [items, itemSizes]); + + // 当item尺寸发生变化时调用 + const onItemResize = useCallback((itemId: string, height: number) => { + // 只有当高度发生变化时才更新 + if (itemSizes[itemId] !== height) { + setItemSizes(prev => ({ + ...prev, + [itemId]: height + })); + + // 通知List组件重新计算尺寸 + if (listRef.current) { + listRef.current.resetAfterIndex(0); + } + } + }, [itemSizes]); + // 渲染单个时间线项的函数 const renderTimelineItem = useCallback( ({ index, style }: { index: number; style: React.CSSProperties }) => { + // 显示加载指示器的条件 + const showOlderLoading = index === items.length && hasMoreOld && pagination.current >= 1; + const showNewerLoading = index === 0 && hasMoreNew && pagination.current < 1 && isRefreshing; + + if (showOlderLoading || showNewerLoading) { + return ( +
+
+ +
+ {showNewerLoading ? '正在加载更新的内容...' : '正在加载更多内容...'} +
+
+
+ ); + } + const item = items[index]; if (!item) return null; return (
- { - setCurrentItem(item); - setCurrentOption(option); - setOpenAddItemModal(true); +
{ + // 当元素被渲染时测量其实际高度 + if (el && !measuredItemsRef.current.has(item.id)) { + measuredItemsRef.current.add(item.id); + // 使用requestAnimationFrame确保DOM已经渲染完成 + requestAnimationFrame(() => { + if (el) { + const height = el.getBoundingClientRect().height; + onItemResize(item.id, height); + } + }); + } }} - refresh={() => { - // 刷新当前页数据 - setPagination(prev => ({ ...prev, current: 1 })); - run(); - queryDetail(); - }} - /> + > + { + setCurrentItem(item); + setCurrentOption(option); + setOpenAddItemModal(true); + }} + refresh={() => { + // 刷新当前页数据 + setPagination(prev => ({ ...prev, current: 1 })); + // 重置高度测量 + measuredItemsRef.current = new Set(); + run(); + queryDetail(); + }} + /> +
); }, - [items, run, queryDetail], + [items, hasMoreOld, hasMoreNew, pagination, isRefreshing, onItemResize, run, queryDetail], ); // 处理滚动事件 - const handleScroll = useCallback((e: React.UIEvent) => { - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - // 当滚动到底部时加载更多 - if (scrollTop + clientHeight >= scrollHeight - 10) { - loadMore(); + const handleItemsRendered = useCallback(({ visibleStartIndex, visibleStopIndex }) => { + // 当可视区域接近列表顶部时加载更新的数据 + if (visibleStartIndex <= 3 && hasMoreNew && !isRefreshing && pagination.current >= 1) { + loadNewer(); } - }, [loadMore]); + + // 当可视区域接近列表底部时加载更老的数据 + if (visibleStopIndex >= items.length - 3 && hasMoreOld && !loading && pagination.current >= 1) { + loadOlder(); + } + + // 控制回到顶部按钮的显示 + setShowScrollTop(visibleStartIndex > 5); + }, [hasMoreNew, hasMoreOld, isRefreshing, loading, items.length, pagination, loadNewer, loadOlder]); + + // 手动刷新最新数据 + const handleRefresh = () => { + if (isRefreshing) return; + + setIsRefreshing(true); + setPagination(prev => ({ + ...prev, + current: 0 // 使用0作为刷新标识 + })); + }; + + // 回到顶部 + const scrollToTop = () => { + if (listRef.current) { + listRef.current.scrollToItem(0, 'start'); + } + }; + + // 监听滚动事件,动态显示/隐藏提示信息 + useEffect(() => { + const handleScroll = () => { + if (containerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = containerRef.current; + + // 判断是否在顶部 + const isTop = scrollTop === 0; + // 判断是否在底部 + const isBottom = scrollTop + clientHeight >= scrollHeight; + + setHasMoreNew(isTop && hasMoreNew); // 更新顶部提示的显示状态 + setHasMoreOld(isBottom && hasMoreOld); // 更新底部提示的显示状态 + } + }; + + const timelineContainer = containerRef.current; + if (timelineContainer) { + timelineContainer.addEventListener('scroll', handleScroll); + } + + return () => { + if (timelineContainer) { + timelineContainer.removeEventListener('scroll', handleScroll); + } + }; + }, [hasMoreNew, hasMoreOld]); return ( { title={ queryDetailLoading ? '加载中' : `${detail?.title} ${`共${detail?.itemCount ?? 0}个时刻`}` } + extra={ + + } >
{items.length > 0 ? ( - - {({ height, width }) => ( -
+ <> + {/* 顶部提示信息 */} + {!hasMoreNew && pagination.current <= 1 && ( +
+ 已加载全部更新内容 +
+ )} + + + {({ height, width }) => ( = 1 ? 40 : 0)} + itemCount={items.length + (hasMoreOld && pagination.current >= 1 ? 1 : 0) + (hasMoreNew && pagination.current < 1 && isRefreshing ? 1 : 0)} + itemSize={getItemSize} width={width} + onItemsRendered={handleItemsRendered} > {renderTimelineItem} + )} + - {/* 加载更多指示器 */} - {loading && ( -
- -
- )} - - {!hasMore && items.length > 0 && ( -
- 没有更多内容了 -
- )} + {/* 底部提示信息 */} + {!hasMoreOld && pagination.current >= 1 && ( +
+ 已加载全部历史内容
)} - + + {/* 回到顶部按钮 */} + {showScrollTop && ( +
+
+ )} + ) : ( -
- {loading ? : '暂无时间线数据'} +
+ {loading ? ( + <> + +
正在加载时间线数据...
+ + ) : ( + <> + + + + )}
)}
@@ -206,8 +433,10 @@ const Index = () => { setOpenAddItemModal(false); // 添加新项后刷新数据 setPagination(prev => ({ ...prev, current: 1 })); + // 重置高度测量 + measuredItemsRef.current = new Set(); run(); - queryDetail() + queryDetail(); }} storyId={lineId} /> diff --git a/src/pages/story/index.css b/src/pages/story/index.css index 148b9fb..66d00bc 100644 --- a/src/pages/story/index.css +++ b/src/pages/story/index.css @@ -9,12 +9,33 @@ transition: transform 0.3s ease; scrollbar-width: none; -ms-overflow-style: none; + border-bottom: 1px solid #eee; } .timeline::-webkit-scrollbar { display: none; } +.timeline-item-header { + margin-bottom: 10px; +} + +.timeline-item-content p { + line-height: 1.6; + margin-bottom: 10px; +} + +.timeline-item-images { + display: flex; + gap: 10px; +} + +.timeline-image { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 4px; +} /* 主时间线容器 */ .timeline-event-container { position: relative;