Compare commits

...

2 Commits

Author SHA1 Message Date
李如威 94db0b4d2a feat: 导出、导入、整理排序 2025-09-23 13:20:16 +08:00
李如威 c1f62e75ae featL 处理优化信息 2025-09-23 10:12:14 +08:00
3 changed files with 130 additions and 10 deletions

View File

@ -286,14 +286,14 @@ def create_app():
async def flows_list(user: DB.User = Depends(get_current_user)):
global_tasks = app.state.tasks
global_status = app.state.task_nodes
db_filter = { "id_deleted": 0 }
db_filter = { "is_deleted": 0 }
if user.level != 0:
work_ids = await DB.WorkUserRelation.filter(user_id=user.id).values_list("work_id", flat=True)
db_filter["id__in": work_ids if work_ids else [0]]
work_list = await DB.Work.filter(**db_filter).order_by("-id")
work_ids = [x.id for x in work_list]
work_content_list = await DB.WorkContent.filter(work_id__in=work_ids).all()
sub_logs = await DB.WorkLogs.filter(work_id__in=work_ids).annotate(max_id=Max("id")).values_list("max_id", flat=True)
sub_logs = await DB.WorkLogs.filter(work_id__in=work_ids).group_by('work_id').annotate(max_id=Max("id")).values_list("max_id", flat=True)
work_logs_list = await DB.WorkLogs.filter(id__in=sub_logs).all()
user_list = await DB.User.filter(id__in=list(set([x.user_id for x in work_content_list]))).all()

View File

@ -1,4 +1,4 @@
import { App, Button, Card, Col, message, Modal, Row, Space, Typography } from "antd"
import { App, Button, Card, Col, Dropdown, message, Modal, Row, Space, Typography } from "antd"
import { useNavigate, useParams } from "react-router-dom";
import { useCallback, useEffect, useRef, useState } from 'react';
import {
@ -18,6 +18,7 @@ import { flowCreate, flowDetail, flowUpdate } from "../../api/flow";
import EditNode from "./components/EditNode";
import CustomNode from "./components/CustomNode";
import { ProForm, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
import { DownloadOutlined, UploadOutlined, EllipsisOutlined, ApartmentOutlined } from "@ant-design/icons";
const COLORS = {
first: "#89A8B2",
@ -62,6 +63,7 @@ const FlowDetail = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [record, setRecord] = useState({});
const editRef = useRef(null);
const inputRef = useRef(null);
const controlRef = useRef(null);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const onBeforeDelete = async (params) => {
@ -116,7 +118,7 @@ const FlowDetail = () => {
sourcePosition: 'right',
targetPosition: 'left',
type: e.type,
data: { label: e.title, style: { backgroundColor: COLORS[e.type] }, position: POSITION[e.type] },
data: { label: e?.title || '开始' , style: { backgroundColor: COLORS[e.type] }, position: POSITION[e.type] },
detail: e
})
if (e.edge) {
@ -126,9 +128,9 @@ const FlowDetail = () => {
});
}
});
setNodes(tempNodes);
setEdges(tempEdges);
controlRef?.current?.setNodes(tempNodes);
controlRef?.current?.setEdges(tempEdges);
await controlRef?.current?.fitView();
} else {
message.error(res.message);
}
@ -176,6 +178,75 @@ const FlowDetail = () => {
message.warning("请检查基础信息");
}
}
const downloadAction = () => {
modal.confirm({
title: '导出',
content: '导出当前工作配置?',
okText: '导出',
cancelText: '取消',
onOk: () => {
try {
const filename = new Date().getTime() + '.json'
const jsonStr = JSON.stringify({ nodes, edges }, null, 4)
const blob = new Blob([jsonStr], { type: "application/json;charset=utf-8" });
// IE / Edge (old) fallback
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename);
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (error) {
console.error("导出失败", error);
message.error("导出失败");
}
}
})
}
const uploadAction = () => {
inputRef?.current?.click();
}
const onFileInputChange = (event) => {
const fileInput = event.target;
if (fileInput?.files?.length === 0) return;
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const text = e.target.result;
const jsonData = JSON.parse(text);
controlRef?.current?.setNodes(jsonData.nodes);
controlRef?.current?.setEdges(jsonData.edges);
} catch (err) {
console.error("JSON 解析失败:", err);
}
};
reader.readAsText(file, "utf-8");
fileInput.value = "";
}
const formatAction = async () => {
const result = groupNodes(nodes, edges);
let y = 0;
let x = 0;
result?.forEach(rows => {
rows.forEach((e) => {
e.position = { x, y }
controlRef?.current?.updateNode(e.id);
x += 180
})
y += 80;
x = 0;
});
await controlRef?.current?.fitView();
}
useEffect(() => {
loadDetail();
@ -185,8 +256,26 @@ const FlowDetail = () => {
<Row gutter={[16, 0]}>
<Col span={18} >
<Card title="工作" extra={<Space>
<Button onClick={() => navigate("/flow")}>取消</Button>
<Button type='primary' onClick={() => onSave()}>{isCreate ? '创建':'保存'}</Button>
<div style={{ display: 'none' }}>
<input ref={inputRef} type="file" accept=".json" onChange={onFileInputChange} />
</div>
<Button onClick={() => navigate("/flow")}>返回</Button>
<Dropdown menu={{
onClick: ({ key }) => {
if (key === 'download') downloadAction();
else if (key === 'upload') uploadAction();
else if (key === 'format') formatAction();
},
items: [
{ key: 'format', label: '整理', icon: <ApartmentOutlined /> },
{ key: 'line', type: 'divider' },
{ key: 'download', label: '导出', icon: <DownloadOutlined /> },
{ key: 'upload', label: '导入', icon: <UploadOutlined /> },
]
}}>
<Button><EllipsisOutlined /></Button>
</Dropdown>
<Button type='primary' onClick={() => onSave()}>{isCreate ? '创建' : '保存'}</Button>
</Space>}>
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
<ReactFlow
@ -256,4 +345,36 @@ const ControlButtons = ({ onLoad }) => {
return <></>
}
function groupNodes(nodes, edges) {
const nodeMap = Object.fromEntries(nodes.map((n) => [n.id, n]));
const graph = {};
nodes.forEach((n) => (graph[n.id] = new Set()));
edges.forEach(({ source, target }) => {
graph[source].add(target);
graph[target].add(source);
});
const visited = new Set();
const result = [];
const dfs = (nodeId, group) => {
visited.add(nodeId);
group.push(nodeMap[nodeId]);
graph[nodeId].forEach((nei) => {
if (!visited.has(nei)) dfs(nei, group);
});
};
nodes.forEach((n) => {
if (!visited.has(n.id)) {
const group = [];
dfs(n.id, group);
result.push(group);
}
});
return result;
}
export default FlowDetail;

View File

@ -30,7 +30,6 @@ const EditNode = forwardRef(({ onFinish }, ref) => {
return <ModalForm
title="编辑"
trigger={<></>}
open={open}
onOpenChange={e => setOpen(e)}
formRef={formRef}