From 94db0b4d2aa58b6fc328ec9b1ddf6ad5b6e22343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=A6=82=E5=A8=81?= Date: Tue, 23 Sep 2025 13:20:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=87=BA=E3=80=81=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E3=80=81=E6=95=B4=E7=90=86=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 2 +- frontend/src/pages/Flow/Detail.jsx | 133 +++++++++++++++++- .../src/pages/Flow/components/EditNode.jsx | 1 - 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 2346317..54804e0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -293,7 +293,7 @@ def create_app(): 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() diff --git a/frontend/src/pages/Flow/Detail.jsx b/frontend/src/pages/Flow/Detail.jsx index b1b2d1c..b90f385 100644 --- a/frontend/src/pages/Flow/Detail.jsx +++ b/frontend/src/pages/Flow/Detail.jsx @@ -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) => { @@ -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 = () => { - - +
+ +
+ + { + if (key === 'download') downloadAction(); + else if (key === 'upload') uploadAction(); + else if (key === 'format') formatAction(); + }, + items: [ + { key: 'format', label: '整理', icon: }, + { key: 'line', type: 'divider' }, + { key: 'download', label: '导出', icon: }, + { key: 'upload', label: '导入', icon: }, + ] + }}> + + + }>
{ 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; \ No newline at end of file diff --git a/frontend/src/pages/Flow/components/EditNode.jsx b/frontend/src/pages/Flow/components/EditNode.jsx index e479fa3..451f106 100644 --- a/frontend/src/pages/Flow/components/EditNode.jsx +++ b/frontend/src/pages/Flow/components/EditNode.jsx @@ -30,7 +30,6 @@ const EditNode = forwardRef(({ onFinish }, ref) => { return } open={open} onOpenChange={e => setOpen(e)} formRef={formRef}