2025-08-07 19:48:36 +08:00
|
|
|
// src/pages/story/detail.tsx
|
2025-08-05 19:02:14 +08:00
|
|
|
import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal';
|
|
|
|
|
import TimelineItem from '@/pages/story/components/TimelineItem/TimelineItem';
|
|
|
|
|
import { StoryItem } from '@/pages/story/data';
|
2025-08-07 19:48:36 +08:00
|
|
|
import { queryStoryDetail, queryStoryItem } from '@/pages/story/service';
|
2025-08-05 19:02:14 +08:00
|
|
|
import { PageContainer } from '@ant-design/pro-components';
|
|
|
|
|
import { history, useIntl, useRequest } from '@umijs/max';
|
2025-08-07 19:48:36 +08:00
|
|
|
import { FloatButton, Spin } from 'antd';
|
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
2025-08-05 19:02:14 +08:00
|
|
|
import { useParams } from 'react-router';
|
2025-08-07 19:48:36 +08:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
|
|
|
import { FixedSizeList as List } from 'react-window';
|
2025-08-05 19:02:14 +08:00
|
|
|
import './index.css';
|
|
|
|
|
import useStyles from './style.style';
|
|
|
|
|
|
|
|
|
|
interface TimelineItemProps {
|
2025-08-07 19:48:36 +08:00
|
|
|
children: React.ReactNode;
|
2025-08-05 19:02:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Index = () => {
|
|
|
|
|
const { id: lineId } = useParams<{ id: string }>();
|
|
|
|
|
const { styles } = useStyles();
|
|
|
|
|
const intl = useIntl();
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2025-08-07 19:48:36 +08:00
|
|
|
const [items, setItems] = useState<StoryItem[]>([]);
|
2025-08-05 19:02:14 +08:00
|
|
|
const [loading, setLoading] = useState(false);
|
2025-08-07 19:48:36 +08:00
|
|
|
const [hasMore, setHasMore] = useState(true);
|
2025-08-05 19:02:14 +08:00
|
|
|
const [openAddItemModal, setOpenAddItemModal] = useState(false);
|
|
|
|
|
const [currentItem, setCurrentItem] = useState<StoryItem>();
|
2025-08-07 19:48:36 +08:00
|
|
|
const [currentOption, setCurrentOption] = useState<
|
|
|
|
|
'add' | 'edit' | 'addSubItem' | 'editSubItem'
|
|
|
|
|
>();
|
|
|
|
|
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
2025-08-05 19:02:14 +08:00
|
|
|
|
2025-08-07 19:48:36 +08:00
|
|
|
const { data: response, run } = useRequest(
|
2025-08-05 19:02:14 +08:00
|
|
|
() => {
|
2025-08-07 19:48:36 +08:00
|
|
|
return queryStoryItem({ storyInstanceId: lineId, ...pagination });
|
2025-08-05 19:02:14 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
manual: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-08-07 19:48:36 +08:00
|
|
|
|
2025-08-05 19:02:14 +08:00
|
|
|
const {
|
|
|
|
|
data: detail,
|
|
|
|
|
run: queryDetail,
|
|
|
|
|
loading: queryDetailLoading,
|
|
|
|
|
} = useRequest(() => {
|
|
|
|
|
return queryStoryDetail(lineId ?? '');
|
|
|
|
|
});
|
2025-08-07 19:48:36 +08:00
|
|
|
|
2025-08-05 19:02:14 +08:00
|
|
|
// 初始化加载数据
|
|
|
|
|
useEffect(() => {
|
2025-08-07 19:48:36 +08:00
|
|
|
setItems([]);
|
|
|
|
|
setPagination({ current: 1, pageSize: 10 });
|
|
|
|
|
setHasMore(true);
|
2025-08-05 19:02:14 +08:00
|
|
|
run();
|
|
|
|
|
}, [lineId]);
|
2025-08-07 19:48:36 +08:00
|
|
|
|
|
|
|
|
// 处理响应数据
|
2025-08-05 19:02:14 +08:00
|
|
|
useEffect(() => {
|
2025-08-07 19:48:36 +08:00
|
|
|
if (!response) return;
|
|
|
|
|
|
|
|
|
|
if (pagination.current === 1) {
|
|
|
|
|
// 首页数据
|
|
|
|
|
setItems(response.list || []);
|
|
|
|
|
} else {
|
|
|
|
|
// 追加数据
|
|
|
|
|
setItems(prev => [...prev, ...(response.list || [])]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否还有更多数据
|
|
|
|
|
setHasMore(response.list && response.list.length === pagination.pageSize);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}, [response, pagination]);
|
|
|
|
|
|
|
|
|
|
// 滚动到底部加载更多
|
|
|
|
|
const loadMore = useCallback(() => {
|
|
|
|
|
if (loading || !hasMore) return;
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setPagination(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
current: prev.current + 1
|
2025-08-05 19:02:14 +08:00
|
|
|
}));
|
2025-08-07 19:48:36 +08:00
|
|
|
}, [loading, hasMore]);
|
|
|
|
|
|
|
|
|
|
// 当分页变化时重新请求数据
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (pagination.current > 1) {
|
|
|
|
|
console.log('分页变化')
|
|
|
|
|
run();
|
|
|
|
|
}
|
|
|
|
|
}, [pagination, run]);
|
2025-08-05 19:02:14 +08:00
|
|
|
|
2025-08-07 19:48:36 +08:00
|
|
|
// 渲染单个时间线项的函数
|
|
|
|
|
const renderTimelineItem = useCallback(
|
|
|
|
|
({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={style}>
|
|
|
|
|
<TimelineItem
|
|
|
|
|
item={item}
|
|
|
|
|
handleOption={(
|
|
|
|
|
item: StoryItem,
|
|
|
|
|
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem',
|
|
|
|
|
) => {
|
|
|
|
|
setCurrentItem(item);
|
|
|
|
|
setCurrentOption(option);
|
|
|
|
|
setOpenAddItemModal(true);
|
|
|
|
|
}}
|
|
|
|
|
refresh={() => {
|
|
|
|
|
// 刷新当前页数据
|
|
|
|
|
setPagination(prev => ({ ...prev, current: 1 }));
|
|
|
|
|
run();
|
|
|
|
|
queryDetail();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[items, run, queryDetail],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 处理滚动事件
|
|
|
|
|
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
|
|
|
|
// 当滚动到底部时加载更多
|
|
|
|
|
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
|
|
|
|
loadMore();
|
|
|
|
|
}
|
|
|
|
|
}, [loadMore]);
|
2025-08-05 19:02:14 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<PageContainer
|
|
|
|
|
onBack={() => history.push('/story')}
|
2025-08-07 19:48:36 +08:00
|
|
|
title={
|
|
|
|
|
queryDetailLoading ? '加载中' : `${detail?.title} ${`共${detail?.itemCount ?? 0}个时刻`}`
|
|
|
|
|
}
|
2025-08-05 19:02:14 +08:00
|
|
|
>
|
2025-08-07 19:48:36 +08:00
|
|
|
<div
|
|
|
|
|
className="timeline"
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
style={{
|
|
|
|
|
height: 'calc(100vh - 200px)',
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{items.length > 0 ? (
|
|
|
|
|
<AutoSizer>
|
|
|
|
|
{({ height, width }) => (
|
|
|
|
|
<div
|
|
|
|
|
style={{ height, width, position: 'relative' }}
|
|
|
|
|
onScroll={handleScroll}
|
|
|
|
|
className="timeline-hide-scrollbar"
|
|
|
|
|
>
|
|
|
|
|
<List
|
|
|
|
|
height={height}
|
|
|
|
|
itemCount={items.length}
|
|
|
|
|
itemSize={300} // 根据实际项高度调整
|
|
|
|
|
width={width}
|
|
|
|
|
>
|
|
|
|
|
{renderTimelineItem}
|
|
|
|
|
</List>
|
|
|
|
|
|
|
|
|
|
{/* 加载更多指示器 */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
|
|
|
<Spin />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!hasMore && items.length > 0 && (
|
|
|
|
|
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
|
|
|
|
没有更多内容了
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</AutoSizer>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
|
|
|
|
{loading ? <Spin /> : '暂无时间线数据'}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-08-05 19:02:14 +08:00
|
|
|
</div>
|
2025-08-07 19:48:36 +08:00
|
|
|
|
|
|
|
|
<FloatButton
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setCurrentOption('add');
|
|
|
|
|
setCurrentItem();
|
|
|
|
|
setOpenAddItemModal(true);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
2025-08-05 19:02:14 +08:00
|
|
|
<AddTimeLineItemModal
|
|
|
|
|
visible={openAddItemModal}
|
|
|
|
|
initialValues={currentItem}
|
|
|
|
|
option={currentOption}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
setOpenAddItemModal(false);
|
|
|
|
|
}}
|
|
|
|
|
onOk={() => {
|
|
|
|
|
setOpenAddItemModal(false);
|
2025-08-07 19:48:36 +08:00
|
|
|
// 添加新项后刷新数据
|
|
|
|
|
setPagination(prev => ({ ...prev, current: 1 }));
|
2025-08-05 19:02:14 +08:00
|
|
|
run();
|
2025-08-07 19:48:36 +08:00
|
|
|
queryDetail()
|
2025-08-05 19:02:14 +08:00
|
|
|
}}
|
|
|
|
|
storyId={lineId}
|
|
|
|
|
/>
|
|
|
|
|
</PageContainer>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default Index;
|