feat: 支持视频上传、预览及移动端适配
Some checks failed
test/timeline-frontend/pipeline/head There was a failure building this commit
Some checks failed
test/timeline-frontend/pipeline/head There was a failure building this commit
1. 功能增强: - 支持视频文件的上传、存储及缩略图自动生成 - 新增视频播放组件,支持在画廊和时间线中预览视频 - 引入 STOMP 协议支持 WebSocket 实时通知功能 - 增加分享页面(SSR 友好),支持通过 shareId 访问公开内容 2. 移动端优化: - 新增 BottomNav 底部导航组件,优化移动端交互体验 - 引入 useIsMobile 钩子,实现响应式布局切换 - 优化时间线卡片在小屏幕下的显示效果 3. 架构与组件: - 新增 ClientOnly 组件解决 SSR 激活不一致问题 - 新增 ResponsiveGrid 响应式网格布局组件 - 完善 Nginx 配置,增加 MinIO 对象存储代理 - 优化图片懒加载组件 TimelineImage,支持低分辨率占位图
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
// 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, useRequest } from '@umijs/max';
|
||||
import { history, useParams, useRequest } from '@umijs/max';
|
||||
import { Button, Empty, FloatButton, message, Spin } from 'antd';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from '@umijs/max';
|
||||
import { PullToRefresh } from 'antd-mobile';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import './detail.css';
|
||||
import {judgePermission} from "@/pages/story/utils/utils";
|
||||
|
||||
// 格式化时间数组为易读格式
|
||||
const formatTimeArray = (time: string | number[] | undefined): string => {
|
||||
@@ -28,6 +29,7 @@ const formatTimeArray = (time: string | number[] | undefined): string => {
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { id: lineId } = useParams<{ id: string }>();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [items, setItems] = useState<StoryItem[]>([]);
|
||||
@@ -79,12 +81,15 @@ const Index = () => {
|
||||
setHasMoreNew(true);
|
||||
setLoadDirection('init');
|
||||
setLoading(true);
|
||||
run({ current: 1 });
|
||||
queryDetail();
|
||||
run();
|
||||
}, [lineId]);
|
||||
// 处理响应数据
|
||||
useEffect(() => {
|
||||
if (!response) return;
|
||||
const fetched = response.list || [];
|
||||
// 兼容 response.list 和 response.data.list
|
||||
// @ts-ignore
|
||||
const fetched = response.list || response.data?.list || [];
|
||||
const pageSize = pagination.pageSize;
|
||||
const noMore = !(fetched.length === pageSize);
|
||||
|
||||
@@ -94,6 +99,8 @@ const Index = () => {
|
||||
setHasMoreOld(false);
|
||||
} else if (loadDirection === 'newer') {
|
||||
setHasMoreNew(false);
|
||||
} else if (loadDirection === 'init' || loadDirection === 'refresh') {
|
||||
setItems([]);
|
||||
}
|
||||
setLoading(false);
|
||||
setIsRefreshing(false);
|
||||
@@ -123,13 +130,12 @@ const Index = () => {
|
||||
hasShownNoMoreNewRef.current = true;
|
||||
message.info('没有更多更新内容了');
|
||||
}
|
||||
} else if (loadDirection === 'refresh') {
|
||||
// 刷新操作
|
||||
} else {
|
||||
} else if (loadDirection === 'refresh' || loadDirection === 'init') {
|
||||
setItems(fetched);
|
||||
setCurrentTimeArr([fetched[0]?.storyItemTime, fetched[fetched.length - 1]?.storyItemTime]);
|
||||
if (fetched.length > 0) {
|
||||
setCurrentTimeArr([fetched[0]?.storyItemTime, fetched[fetched.length - 1]?.storyItemTime]);
|
||||
}
|
||||
setHasMoreOld(!noMore);
|
||||
setHasMoreNew(true);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -163,7 +169,7 @@ const Index = () => {
|
||||
setLoadDirection('newer');
|
||||
setIsRefreshing(true);
|
||||
setLoading(true);
|
||||
run({ current: 1 });
|
||||
run({ afterTime: afterTime });
|
||||
}, [loading, hasMoreNew, isRefreshing, items, currentTimeArr, run]);
|
||||
|
||||
// 监听滚动事件
|
||||
@@ -173,10 +179,10 @@ const Index = () => {
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
|
||||
|
||||
// 显示回到顶部按钮
|
||||
setShowScrollTop(scrollTop > 300);
|
||||
|
||||
|
||||
// 接近底部时加载更多
|
||||
if (scrollHeight - scrollTop - clientHeight < 200) {
|
||||
loadOlder();
|
||||
@@ -208,12 +214,13 @@ const Index = () => {
|
||||
|
||||
// 按日期分组items,并在每个组内按时间排序
|
||||
const groupItemsByDate = (items: StoryItem[]) => {
|
||||
const groups: { [key: string]: { dateKey: string; items: StoryItem[]; sortValue: number } } = {};
|
||||
|
||||
items.forEach(item => {
|
||||
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}日`;
|
||||
@@ -224,34 +231,34 @@ const Index = () => {
|
||||
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 => {
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -289,54 +296,61 @@ const Index = () => {
|
||||
}}
|
||||
>
|
||||
{items.length > 0 ? (
|
||||
<>
|
||||
<PullToRefresh onRefresh={loadNewer} disabled={!isMobile}>
|
||||
{hasMoreNew && !isMobile && (
|
||||
<div style={{ textAlign: 'center', padding: '12px 0' }}>
|
||||
<Button onClick={loadNewer} loading={isRefreshing}>
|
||||
加载新内容
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{Object.values(groupedItems)
|
||||
.sort((a, b) => b.sortValue - a.sortValue) // 按日期降序排列(最新的在前)
|
||||
.map(({ dateKey, items: dateItems }) => (
|
||||
<div key={dateKey}>
|
||||
<h2 className="timeline-section-header">{dateKey}</h2>
|
||||
<div className="timeline-grid-wrapper">
|
||||
{dateItems.map((item, index) => {
|
||||
// 调试:确保每个item都有有效的数据
|
||||
if (!item || (!item.id && !item.instanceId)) {
|
||||
console.warn('发现无效的item:', item, 'at index:', index);
|
||||
return null; // 不渲染无效的item
|
||||
}
|
||||
|
||||
return (
|
||||
<TimelineGridItem
|
||||
key={item.id ?? item.instanceId}
|
||||
item={item}
|
||||
handleOption={(
|
||||
item: StoryItem,
|
||||
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem',
|
||||
) => {
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div key={dateKey}>
|
||||
<h2 className="timeline-section-header">{dateKey}</h2>
|
||||
<div className="timeline-grid-wrapper">
|
||||
{dateItems.map((item, index) => {
|
||||
// 调试:确保每个item都有有效的数据
|
||||
if (!item || (!item.id && !item.instanceId)) {
|
||||
console.warn('发现无效的item:', item, 'at index:', index);
|
||||
return null; // 不渲染无效的item
|
||||
}
|
||||
|
||||
return (
|
||||
<TimelineGridItem
|
||||
key={item.id ?? item.instanceId}
|
||||
item={item}
|
||||
handleOption={(
|
||||
item: StoryItem,
|
||||
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem',
|
||||
) => {
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
{loading && <div className="load-indicator">加载中...</div>}
|
||||
{!loading && !hasMoreOld && <div className="no-more-data">已加载全部历史数据</div>}
|
||||
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
{showScrollTop && (
|
||||
<div
|
||||
@@ -355,15 +369,15 @@ const Index = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PullToRefresh>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -372,8 +386,8 @@ const Index = () => {
|
||||
<Spin size="large" />
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
fontSize: '16px',
|
||||
marginTop: 16,
|
||||
fontSize: '16px',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
@@ -381,14 +395,14 @@ const Index = () => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Empty
|
||||
description="暂无时间线数据"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
imageStyle={{
|
||||
height: 60,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Empty
|
||||
description="暂无时间线数据"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
imageStyle={{
|
||||
height: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
@@ -396,22 +410,22 @@ const Index = () => {
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
还没有添加任何时刻
|
||||
</div>
|
||||
</Empty>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
disabled={!judgePermission(detail?.permissionType ?? null, 'edit')}
|
||||
onClick={() => {
|
||||
setCurrentOption('add');
|
||||
setCurrentItem(undefined);
|
||||
setOpenAddItemModal(true);
|
||||
}}
|
||||
>
|
||||
添加第一个时刻
|
||||
</Button>
|
||||
</>
|
||||
还没有添加任何时刻
|
||||
</div>
|
||||
</Empty>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
disabled={!judgePermission(detail?.permissionType ?? null, 'edit')}
|
||||
onClick={() => {
|
||||
setCurrentOption('add');
|
||||
setCurrentItem(undefined);
|
||||
setOpenAddItemModal(true);
|
||||
}}
|
||||
>
|
||||
添加第一个时刻
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user