feat: 实现时间线拖拽排序功能及PWA支持
Some checks failed
test/timeline-frontend/pipeline/head Something is wrong with the build of this commit
Some checks failed
test/timeline-frontend/pipeline/head Something is wrong with the build of this commit
新增时间线节点的拖拽排序功能,使用dnd-kit库实现可排序网格布局。添加PWA支持,包括Service Worker注册和manifest配置。优化移动端适配,改进批量操作工具栏和撤销/重做功能。 重构用户登录和注册页面,修复登录跳转逻辑。调整画廊视图在不同设备上的显示效果。新增协作成员管理功能,支持批量修改权限。 修复请求错误处理中的跳转逻辑问题,避免重复跳转登录页。优化样式表,增强时间线卡片和图片展示的响应式布局。 新增多个API接口支持批量操作,包括排序、删除和时间修改。引入useBatchSelection和useHistory自定义Hook管理状态。添加UndoRedoToolbar组件提供撤销/重做功能。 实现Service Worker离线缓存策略,支持静态资源和API请求的缓存。新增PWA工具函数处理安装提示和更新检测。优化移动端交互,调整组件布局和操作按钮。
This commit is contained in:
@@ -1,32 +1,19 @@
|
||||
// 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 SortableTimelineGrid from '@/pages/story/components/SortableTimelineGrid';
|
||||
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 { MoreOutlined, PlusOutlined, SyncOutlined, TeamOutlined } 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 { Button, Dropdown, Empty, FloatButton, MenuProps, message, Space, 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);
|
||||
};
|
||||
import CollaboratorModal from './components/CollaboratorModal';
|
||||
|
||||
const Index = () => {
|
||||
const isMobile = useIsMobile();
|
||||
@@ -42,6 +29,7 @@ const Index = () => {
|
||||
'add' | 'edit' | 'addSubItem' | 'editSubItem'
|
||||
>();
|
||||
const [openDetailDrawer, setOpenDetailDrawer] = useState(false);
|
||||
const [openCollaboratorModal, setOpenCollaboratorModal] = useState(false);
|
||||
const [detailItem, setDetailItem] = useState<StoryItem>();
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 30 });
|
||||
const [isRefreshing, setIsRefreshing] = useState(false); // 是否正在刷新最新数据
|
||||
@@ -264,13 +252,45 @@ const Index = () => {
|
||||
|
||||
const groupedItems = groupItemsByDate(items);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
onBack={() => history.push('/story')}
|
||||
title={
|
||||
queryDetailLoading ? '加载中' : `${detail?.title} ${`共${detail?.itemCount ?? 0}个时刻`}`
|
||||
const getExtraContent = () => {
|
||||
if (isMobile) {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'refresh',
|
||||
label: '刷新',
|
||||
icon: <SyncOutlined />,
|
||||
onClick: () => {
|
||||
setItems([]);
|
||||
setPagination({ current: 1, pageSize: 30 });
|
||||
setLoadDirection('refresh');
|
||||
run({ current: 1 });
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (judgePermission(detail?.permissionType ?? null, 'auth')) {
|
||||
menuItems.unshift({
|
||||
key: 'collaborators',
|
||||
label: '协作成员',
|
||||
icon: <TeamOutlined />,
|
||||
onClick: () => setOpenCollaboratorModal(true),
|
||||
});
|
||||
}
|
||||
extra={
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
|
||||
<Button icon={<MoreOutlined />} type="text" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{judgePermission(detail?.permissionType ?? null, 'auth') && (
|
||||
<Button icon={<TeamOutlined />} onClick={() => setOpenCollaboratorModal(true)}>
|
||||
协作成员
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => {
|
||||
@@ -283,7 +303,17 @@ const Index = () => {
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
onBack={() => history.push('/story')}
|
||||
title={
|
||||
queryDetailLoading ? '加载中' : `${detail?.title} ${`共${detail?.itemCount ?? 0}个时刻`}`
|
||||
}
|
||||
extra={getExtraContent()}
|
||||
>
|
||||
<div
|
||||
className="timeline"
|
||||
@@ -305,47 +335,48 @@ const Index = () => {
|
||||
</div>
|
||||
)}
|
||||
{Object.values(groupedItems)
|
||||
.sort((a, b) => b.sortValue - a.sortValue) // 按日期降序排列(最新的在前)
|
||||
.map(({ dateKey, items: dateItems }) => (
|
||||
.sort((a, b) => b.sortValue - a.sortValue)
|
||||
.map(({ dateKey, items: dateItems, sortValue }) => (
|
||||
<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>
|
||||
<SortableTimelineGrid
|
||||
items={dateItems}
|
||||
dateKey={dateKey}
|
||||
sortValue={sortValue}
|
||||
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();
|
||||
}}
|
||||
onOrderChange={(changedDateKey, newItems) => {
|
||||
setItems((prev) => {
|
||||
const updated = [...prev];
|
||||
const startIdx = updated.findIndex(
|
||||
(item) => item.storyItemTime === newItems[0]?.storyItemTime
|
||||
);
|
||||
if (startIdx !== -1) {
|
||||
updated.splice(startIdx, newItems.length, ...newItems);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="load-indicator">加载中...</div>}
|
||||
@@ -498,6 +529,11 @@ const Index = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CollaboratorModal
|
||||
visible={openCollaboratorModal}
|
||||
onCancel={() => setOpenCollaboratorModal(false)}
|
||||
storyId={lineId ?? ''}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user