wip: dnd step

This commit is contained in:
loveuer 2025-03-31 17:53:41 +08:00
parent 1e7127bf67
commit b4e3e7d5c6
18 changed files with 1139 additions and 399 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>u-pipe</title>
</head>
<body>
<div id="root"></div>

View File

@ -10,9 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@mui/styles": "^6.4.8",
"antd": "^5.24.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.4.1",
"zustand": "^5.0.3"
},

949
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,25 @@
import { Layout, Menu } from 'antd';
import { Link } from 'react-router-dom';
import { Layout } from 'antd';
const { Header } = Layout;
// Import the makeStyles function. If you're using @mui/styles, it should be imported like this.
import { makeStyles } from '@mui/styles';
const useStyle = makeStyles({
header: {
padding: 0,
margin: 0,
position: 'static',
top: 0,
display: 'flex',
},
});
const HeaderNav = () => {
const style = useStyle();
return (
<Header style={{ position: 'fixed', zIndex: 1, width: '100%' }}>
<Menu theme="dark" mode="horizontal">
<Menu.Item key="1" icon={<></>}>
<Link to="/"></Link>
</Menu.Item>
<Menu.Item key="2" icon={<></>}>
<Link to="/tasks"></Link>
</Menu.Item>
</Menu>
</Header>
<Header className={style.header}></Header>
);
};

View File

@ -1,19 +1,23 @@
import { makeStyles } from '@mui/styles';
import { Layout, Menu } from 'antd';
import { Link } from 'react-router-dom';
const { Sider } = Layout;
const useStyle = makeStyles({
container: {
padding: 0,
margin: 0,
top: 0,
display: 'flex',
flex: 1,
},
});
const SideNav = () => {
const style = useStyle();
return (
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 64,
}}
>
<Sider className={style.container}>
<Menu theme="dark" mode="inline">
<Menu.Item key="1" icon={<></>}>
<Link to="/"></Link>

6
frontend/src/index.css Normal file
View File

@ -0,0 +1,6 @@
html, body {
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}

View File

@ -0,0 +1,11 @@
import { Status } from "./task";
export interface Step {
id: number;
title: string;
description: string;
created_at: number;
status: Status;
task_id: number;
type: "src" | "dst" | "mid";
}

View File

@ -1,8 +1,9 @@
export type Status = "pending" | "blocked" | "running" | "completed" | "failed";
export interface Task {
id: number;
title: string;
description: string;
created_at: number;
status: "pending" | "blocked" | "running" | "completed" | "failed";
status: Status;
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import AppRoutes from './routes';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(

View File

@ -0,0 +1,71 @@
import React from 'react';
import { makeStyles } from '@mui/styles';
import { Button, Checkbox, Form, FormProps, Input, Space, Typography } from 'antd';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
const useStyle = makeStyles({
container: {
padding: 0,
margin: 0,
top: 0,
display: 'flex',
flex: 1,
height: '100vh',
alignItems: 'center', // 垂直居中对齐
justifyContent: 'center', // 水平居中对齐
backgroundImage: `url('https://cert.umisen.com/api/other/wallpaper')`, // 替换为实际的背景图 URL
backgroundSize: 'cover',
backgroundPosition: 'center',
},
formContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '400px',
padding: '40px',
backdropFilter: 'blur(10px)', // 模糊效果
backgroundColor: 'rgba(255, 255, 255, 0.2)', // 背景颜色
borderRadius: '10px', // 圆角
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)', // 阴影
},
form: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
},
"form-filed": {
width: '100%',
"&:nth-child(n+2)": {
marginTop: '20px',
}
},
input: {
width: '100%',
marginTop: '10px',
}
});
const LoginPage: React.FC = () => {
const classes = useStyle();
return (
<div className={classes.container}>
<div className={classes.formContainer}>
<form className={classes.form}>
<div className={classes['form-filed']}>
<Input className={classes.input} size="large" placeholder="用户名" prefix={<UserOutlined />} />
</div>
<div className={classes['form-filed']}>
<Input className={classes.input} size="large" placeholder="密码" prefix={<LockOutlined />} />
</div>
<div className={classes['form-filed']}>
<Button type='primary' size='large' style={{width: '100%'}}></Button>
</div>
</form>
</div>
</div>
);
};
export default LoginPage;

View File

@ -1,72 +0,0 @@
import { Table } from 'antd';
import { Layout } from 'antd';
import useTaskStore from '../services/taskService';
import { Task } from '../interfaces/task';
import { TimePipe } from '../utils/pipe';
import { Button } from 'antd';
import HeaderNav from '../components/HeaderNav';
import SideNav from '../components/SideNav';
const { Content } = Layout;
const columns = [
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '创建日期',
dataIndex: 'created_at',
key: 'created_at',
render: (v: number) => {
return <TimePipe timestamp={v} />;
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
const TaskPage = () => {
const { tasks } = useTaskStore();
function onClickNewTask(e: React.MouseEvent<HTMLElement, MouseEvent>) {
console.log('新建任务', e)
}
return (
<Layout>
<HeaderNav />
<Layout>
<SideNav />
<Content
style={{
margin: '64px 0 0 200px',
padding: 24,
minHeight: 280,
}}
>
{/* 新增的新建任务按钮 */}
<Button onClick={(e) => onClickNewTask(e)}></Button>
<Table<Task>
columns={columns}
dataSource={tasks.map((v) => ({ ...v, key: v.id }))}
expandable={{
expandedRowRender: (v) => <p style={{ margin: 0 }}>{v.description}</p>,
}}
/>
</Content>
</Layout>
</Layout>
);
};
export default TaskPage;

View File

@ -0,0 +1,80 @@
import { makeStyles } from "@mui/styles";
import { Tag } from "antd";
const useStyles = makeStyles({
container: {
height: '200px',
width: '600px',
border: '1px solid black',
margin: "100px",
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
overflowY: 'scroll',
},
names: {
maxWidth: '140px',
minWidth: '140px',
},
phones: {
maxWidth: '140px',
minWidth: '140px',
},
address: {
maxWidth: '140px',
minWidth: '140px',
}
});
export const TestPage = () => {
const style = useStyles();
return (
<div className={style.container}>
<div className={style.names}>
<Tag>name1</Tag>
<Tag>name2</Tag>
<Tag>name3</Tag>
<Tag>name4</Tag>
<Tag>name5</Tag>
<Tag>name6</Tag>
<Tag>name7</Tag>
<Tag>name8</Tag>
<Tag>name9</Tag>
<Tag>name10</Tag>
<Tag>name11</Tag>
<Tag>name12</Tag>
<Tag>name13</Tag>
<Tag>name14</Tag>
<Tag>name15</Tag>
</div>
<div className={style.phones}>
<Tag color="success">phone1</Tag>
<Tag color="success">phone2</Tag>
<Tag color="success">phone3</Tag>
<Tag color="success">phone4</Tag>
<Tag color="success">phone5</Tag>
<Tag color="success">phone6</Tag>
<Tag color="success">phone7</Tag>
<Tag color="success">phone8</Tag>
<Tag color="success">phone9</Tag>
</div>
<div className={style.address}>
<Tag color="error">address1</Tag>
<Tag color="error">address2</Tag>
<Tag color="error">address3</Tag>
<Tag color="error">address4</Tag>
<Tag color="error">address5</Tag>
<Tag color="error">address6</Tag>
<Tag color="error">address7</Tag>
<Tag color="error">address8</Tag>
</div>
<div>
<Tag color="warning">birthday</Tag>
<Tag color="warning">birthday</Tag>
<Tag color="warning">birthday</Tag>
<Tag color="warning">birthday</Tag>
<Tag color="warning">birthday</Tag>
<Tag color="warning">birthday</Tag>
</div>
</div>
);
}

View File

@ -0,0 +1,103 @@
import { Table } from 'antd';
import { Layout } from 'antd';
import useTaskStore from '../../services/taskService';
import { Task, Status as TaskStatus } from '../../interfaces/task';
import { TimePipe } from '../../utils/pipe';
import { Button } from 'antd';
import HeaderNav from '../../components/HeaderNav';
import SideNav from '../../components/SideNav';
import { Status } from './components/TaskStatus';
import { makeStyles } from '@mui/styles';
import { useState } from 'react';
import { TaskCreate } from './components/TaskCreate';
const { Content } = Layout;
const useStyle = makeStyles({
layout: {
minHeight: '100vh', // 设置布局的最小高度为视口高度
},
container: {
padding: 0,
margin: 0,
top: 0,
display: 'flex',
flex: 1,
width: '100%',
flexDirection: 'column', // 垂直方向布局
},
new_task: {
padding: 10,
margin: 10,
display: 'flex',
flexDirection: 'row', // 水平方向布局
},
})
const columns = [
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '创建日期',
dataIndex: 'created_at',
key: 'created_at',
render: (v: number) => {
return <TimePipe timestamp={v} />;
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (v: TaskStatus) => {
return <Status status={v} />;
}
},
];
const TaskPage = () => {
const style = useStyle();
const { tasks } = useTaskStore();
const [open_create, set_open_create] = useState(false); // 控制新建任务弹窗的显示和隐藏
function onClickNewTask(e: React.MouseEvent<HTMLElement, MouseEvent>) {
console.log('新建任务', e)
set_open_create(true) // 显示新建任务弹窗
}
return (
<>
<TaskCreate open={open_create} set_open={set_open_create} />
<Layout className={style.layout}>
<HeaderNav />
<Layout>
<SideNav />
<Content
className={style.container}
>
<div className={style.new_task}>
<Button type='primary' onClick={(e) => onClickNewTask(e)}></Button>
</div>
<Table<Task>
columns={columns}
dataSource={tasks.map((v) => ({ ...v, key: v.id }))}
expandable={{
expandedRowRender: (v) => <p style={{ margin: 0 }}>{v.description}</p>,
}}
/>
</Content>
</Layout>
</Layout>
</>
);
};
export default TaskPage;

View File

@ -0,0 +1,107 @@
import { makeStyles } from "@mui/styles"
import { Modal, Collapse } from "antd"
import { DndContext } from '@dnd-kit/core'
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'
import { arrayMove } from '@dnd-kit/sortable'
import { JSX, useState } from "react"
import { Step } from "../../../interfaces/step";
const useStyle = makeStyles({
footer: {
display: 'flex',
flexDirection: 'row',
}
})
export interface TaskCreateProps {
open: boolean,
set_open: (v: boolean) => void
}
const steps: Step[] = [
{ id: 1, title: '步骤1', type: 'src' } as Step,
{ id: 2, title: '步骤2', type: 'mid' } as Step,
{ id: 2, title: '步骤3', type: 'mid' } as Step,
{ id: 2, title: '步骤4', type: 'mid' } as Step,
{ id: 2, title: '步骤5', type: 'mid' } as Step,
{ id: 2, title: '步骤6', type: 'mid' } as Step,
{ id: 3, title: '步骤7', type: 'dst' } as Step,
]
export const TaskCreate = (props: TaskCreateProps) => {
const style = useStyle();
const [items, setItems] = useState(steps);
const onDragEnd = ({ active, over }) => {
if (active.id !== over?.id) {
setItems(items => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
return (
<div>
<Modal
width={{
xs: '90%',
sm: '80%',
md: '70%',
lg: '60%',
xxl: '50%',
}}
title="新建任务"
open={props.open}
// footer={null}
>
<DndContext onDragEnd={onDragEnd}>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<Collapse>
{items.filter(v => v.type === "mid").map(item => (
<SortableItem key={item.id} id={item.id}>
<Collapse.Panel key={item.id} header={item.title}>
</Collapse.Panel>
</SortableItem>
))}
</Collapse>
</SortableContext>
</DndContext>
</Modal>
</div>
);
};
// Bug fix: Correctly define the type of the children prop to avoid implicit 'any' type
const SortableItem = ({ id, children }: { id: number; children: JSX.Element }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
cursor: 'move',
backgroundColor: 'white',
marginBottom: 8,
borderRadius: 4,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
<div {...listeners} style={{
padding: '8px 16px',
borderRight: '2px solid #1890ff',
display: 'inline-block',
marginRight: 16
}}></div>
{children}
</div>
);
};

View File

@ -0,0 +1,42 @@
import { Tag } from "antd";
import { Status as TaskStatus } from "../../../interfaces/task";
import { LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined, PauseCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
export const Status = ({ status }: { status: TaskStatus }) => {
let tag;
switch (status) {
case "pending":
tag = <Tag icon={<PauseCircleOutlined />} color="default">
</Tag>
break;
case "blocked":
tag = <Tag icon={<ExclamationCircleOutlined />} color="warning">
</Tag>
break;
case "running":
tag = <Tag icon={<LoadingOutlined />} color="processing">
</Tag>
break;
case "completed":
tag = <Tag icon={<CheckCircleOutlined />} color="success">
</Tag>;
break;
case "failed":
tag = <Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
break;
default:
tag = null;
}
return (
<span>
{tag}
</span>
);
};

View File

@ -1,7 +1,9 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HomePage } from './pages/HomePage';
import { AboutPage } from './pages/AboutPage';
import TaskPage from './pages/TaskPage';
import TaskPage from './pages/task/TaskPage';
import LoginPage from './pages/LoginPage';
import { TestPage } from './pages/TestPage';
const AppRoutes = () => {
return (
@ -10,6 +12,8 @@ const AppRoutes = () => {
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/tasks" element={<TaskPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/test" element={<TestPage/>} />
</Routes>
</Router>
);

View File

@ -25,6 +25,27 @@ const useTaskStore = create<TaskState>(() => ({
description: '这是任务2的描述',
created_at: Date.now(),
status: 'running'
},
{
id: 3,
title: '任务3',
description: '这是任务3的描述',
created_at: Date.now(),
status: 'completed'
},
{
id: 4,
title: '任务4',
description: '这是任务4的描述',
created_at: Date.now(),
status: 'failed'
},
{
id: 5,
title: '任务5',
description: '这是任务5的描述',
created_at: Date.now(),
status: 'blocked'
}
],
getList: () => {

View File

@ -1,3 +1,5 @@
import { Status } from "../interfaces/task";
export const TimePipe = (props: { timestamp: number }) => {
const date = new Date(props.timestamp);
return (
@ -5,4 +7,4 @@ export const TimePipe = (props: { timestamp: number }) => {
{date.toLocaleDateString()}
</>
)
}
}