feat: 实现时间线拖拽排序功能及PWA支持
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:
2026-02-24 10:33:10 +08:00
parent 5139817b3c
commit 97a5ad3a00
24 changed files with 3012 additions and 247 deletions

View File

@@ -10,7 +10,8 @@ import {
PlayCircleOutlined,
} from '@ant-design/icons';
import { Button, Checkbox, Dropdown, Menu, Spin } from 'antd';
import React, { FC, useCallback } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useIsMobile } from '@/hooks/useIsMobile';
import '../index.css';
interface GridViewProps {
@@ -38,7 +39,29 @@ const GridView: FC<GridViewProps> = ({
loadingMore,
onScroll,
}) => {
const isMobile = useIsMobile();
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const getImageSize = useCallback(() => {
if (isMobile) {
// Mobile: 3 columns for small, 2 columns for large
const padding = 16; // page padding
const gap = 8; // grid gap
if (viewMode === 'large') {
const size = (windowWidth - padding * 2 - gap) / 2;
return { width: size, height: size };
}
// default small
const size = (windowWidth - padding * 2 - gap * 2) / 3;
return { width: size, height: size };
}
switch (viewMode) {
case 'small':
return { width: 150, height: 150 };
@@ -47,7 +70,7 @@ const GridView: FC<GridViewProps> = ({
default:
return { width: 150, height: 150 };
}
}, [viewMode]);
}, [viewMode, isMobile, windowWidth]);
const imageSize = getImageSize();
@@ -103,9 +126,19 @@ const GridView: FC<GridViewProps> = ({
<div
className={viewMode === 'small' ? 'small-grid-view' : 'large-grid-view'}
onScroll={onScroll}
style={isMobile ? {
display: 'grid',
gridTemplateColumns: viewMode === 'large' ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
gap: '8px',
paddingBottom: '20px'
} : {}}
>
{imageList.map((item: ImageItem, index: number) => (
<div key={item.instanceId} className="image-card">
<div
key={item.instanceId}
className="image-card"
style={isMobile ? { width: '100%', margin: 0 } : {}}
>
{batchMode && (
<Checkbox
className="image-checkbox"
@@ -116,7 +149,7 @@ const GridView: FC<GridViewProps> = ({
<div
className="image-wrapper"
style={{
width: imageSize.width,
width: isMobile ? '100%' : imageSize.width,
height: imageSize.height,
backgroundImage: `url(${getImageUrl(item, false)})`,
backgroundSize: 'cover',
@@ -130,7 +163,7 @@ const GridView: FC<GridViewProps> = ({
onClick={() => !batchMode && onPreview(index)}
>
{(item.duration || item.thumbnailInstanceId) && (
<PlayCircleOutlined style={{ fontSize: '32px', color: 'rgba(255,255,255,0.8)' }} />
<PlayCircleOutlined style={{ fontSize: isMobile ? '24px' : '32px', color: 'rgba(255,255,255,0.8)' }} />
)}
{item.duration && (
<span
@@ -154,7 +187,7 @@ const GridView: FC<GridViewProps> = ({
{item.imageName}
</div>
<Dropdown overlay={getImageMenu(item)} trigger={['click']}>
<Button type="text" icon={<MoreOutlined />} />
<Button type="text" icon={<MoreOutlined />} size={isMobile ? "small" : "middle"} />
</Dropdown>
</div>
</div>