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

@@ -1,5 +1,6 @@
import { Footer } from '@/components';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import { loginUser } from '@/services/user/api';
import { CommonResponse } from '@/types/common';
import {
AlipayCircleOutlined,
LockOutlined,
@@ -14,14 +15,19 @@ import {
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { FormattedMessage, Helmet, SelectLang, useIntl, useModel, history, useRequest } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import {
FormattedMessage,
Helmet,
history,
SelectLang,
useIntl,
useModel,
useRequest,
} from '@umijs/max';
import { Alert, message } from 'antd';
import { createStyles } from 'antd-style';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import Settings from '../../../../config/defaultSettings';
import { loginUser } from '@/services/user/api';
import { CommonResponse } from '@/types/common';
const useStyles = createStyles(({ token }) => {
return {
@@ -103,33 +109,35 @@ const Login: React.FC = () => {
const intl = useIntl();
// 使用元组参数签名以匹配 useRequest 重载,避免被分页重载推断
const { loading: submitting, run: login } = useRequest<CommonResponse<UserLoginResult>, [UserLoginParams]>(
(params: UserLoginParams): Promise<CommonResponse<UserLoginResult>> => loginUser(params),
{
manual: true,
formatResult: (res) => res,
onSuccess: async (response: CommonResponse<UserLoginResult>, params: [UserLoginParams]) => {
console.log('登录成功 - response:', response, 'params:', params);
const [loginParams] = params;
const logStatus = { type: loginParams.loginType };
if (response.code === 200) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
// await fetchUserInfo();
localStorage.setItem('timeline_user', JSON.stringify(response.data))
const urlParams = new URL(window.location.href).searchParams;
window.location.href = urlParams.get('redirect')?.split('?redirect=')[1] || '/';
return;
}
console.log(response.message);
// 如果失败去设置用户错误信息
setUserLoginState(response.message as any);
const { loading: submitting, run: login } = useRequest<
CommonResponse<UserLoginResult>,
[UserLoginParams]
>((params: UserLoginParams): Promise<CommonResponse<UserLoginResult>> => loginUser(params), {
manual: true,
formatResult: (res) => res,
onSuccess: async (response: CommonResponse<UserLoginResult>, params: [UserLoginParams]) => {
console.log('登录成功 - response:', response, 'params:', params);
const [loginParams] = params;
const logStatus = { type: loginParams.loginType };
if (response.code === 200) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
// await fetchUserInfo();
localStorage.setItem('timeline_user', JSON.stringify(response.data));
const urlParams = new URL(window.location.href).searchParams;
// 修复:直接使用 redirect 参数,如果不存在则跳转到首页
const redirect = urlParams.get('redirect');
window.location.href = redirect || '/';
return;
}
console.log(response.message);
// 如果失败去设置用户错误信息
setUserLoginState(response.message as any);
},
);
});
const handleSubmit = async (values: API.LoginParams) => {
await login(values as UserLoginParams);
@@ -174,9 +182,9 @@ const Login: React.FC = () => {
/>,
<ActionIcons key="icons" />,
]}*/
onFinish={async (values) => {
await login({ ...values, loginType: type } as UserLoginParams);
}}
onFinish={async (values) => {
await login({ ...values, loginType: type } as UserLoginParams);
}}
>
{/*<Tabs
activeKey={type}

View File

@@ -3,11 +3,15 @@ import { createStyles } from 'antd-style';
const useStyles = createStyles(() => {
return {
registerResult: {
width: '800px',
width: '100%',
maxWidth: '800px',
minHeight: '400px',
margin: 'auto',
padding: '80px',
padding: '40px 20px',
background: 'none',
'@media screen and (min-width: 768px)': {
padding: '80px',
},
},
anticon: {
fontSize: '64px',

View File

@@ -1,13 +1,11 @@
import { registerUser } from '@/services/user/api';
import { CommonResponse } from '@/types/common';
import { history, Link, useRequest } from '@umijs/max';
import { Button, Col, Form, Input, message, Popover, Progress, Row, Select, Space } from 'antd';
import { Button, Form, Input, message, Popover, Progress, Select, Space } from 'antd';
import type { Store } from 'antd/es/form/interface';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import type { StateType } from './service';
import { fakeRegister } from './service';
import useStyles from './style.style';
import { registerUser } from '@/services/user/api';
import { CommonResponse } from '@/types/common';
const FormItem = Form.Item;
const { Option } = Select;
@@ -23,6 +21,7 @@ const passwordProgressMap: {
};
const Register: FC = () => {
const { styles } = useStyles();
const isMobile = useIsMobile();
const [count, setCount]: [number, any] = useState(0);
const [open, setVisible]: [boolean, any] = useState(false);
const [prefix, setPrefix]: [string, any] = useState('86');
@@ -95,7 +94,7 @@ const Register: FC = () => {
},
},
);
const onFinish = (values: Store) => {
// 将表单数据映射为后端需要的格式
const registerParams = {
@@ -202,7 +201,7 @@ const Register: FC = () => {
overlayStyle={{
width: 240,
}}
placement="right"
placement={isMobile ? 'top' : 'right'}
open={open}
>
<FormItem