From 8b043a58a12d6f128687da2631489a61719debf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=A6=82=E5=A8=81?= Date: Tue, 9 Sep 2025 23:25:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 + backend/app/main.py | 187 ++++++++++-------- backend/app/models/db/__init__.py | 1 + backend/app/models/db/user.py | 1 + backend/app/models/db/work.py | 49 +++++ backend/app/models/schemas/system.py | 4 +- backend/app/nodes/cmd_node.py | 4 +- frontend/index.html | 2 +- frontend/src/App.css | 50 ++--- frontend/src/context/AuthContext.jsx | 19 +- frontend/src/index.css | 12 -- frontend/src/main.jsx | 20 +- frontend/src/pages/Flow/index.jsx | 107 ++++++++++ frontend/src/pages/Home.jsx | 8 +- frontend/src/pages/Layout.css | 23 +++ frontend/src/pages/Layout.jsx | 71 +++++-- frontend/src/pages/Loading.jsx | 2 +- frontend/src/pages/User/Profile.jsx | 5 + .../src/pages/{User.jsx => User/index.jsx} | 0 frontend/src/routes.jsx | 9 +- 20 files changed, 407 insertions(+), 176 deletions(-) create mode 100644 backend/app/models/db/work.py create mode 100644 frontend/src/pages/Flow/index.jsx create mode 100644 frontend/src/pages/Layout.css create mode 100644 frontend/src/pages/User/Profile.jsx rename frontend/src/pages/{User.jsx => User/index.jsx} (100%) diff --git a/README.md b/README.md index 791de5d..189bf4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ ## 环境准备 +```shell +# python +nvm local 3.13.0 +python -m venv venv +source venv/bin/activate + +# node +nvm use v20.18.0 +``` \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index a36095e..eb9fbce 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,3 +1,4 @@ +import asyncio import uvicorn import uuid import time @@ -23,9 +24,7 @@ scheduler = AsyncIOScheduler() def clear_session(): TTL = 60 * 60 * 24 # 1天有效期 now = time.time() - expired_keys = [ - token for token, (expires_at, _) in sessions.items() if expires_at + TTL < now - ] + expired_keys = [token for token, (expires_at, _) in sessions.items() if expires_at + TTL < now] for token in expired_keys: sessions.pop(token, None) @@ -49,12 +48,12 @@ async def is_init(): @asynccontextmanager async def lifespan(app: FastAPI): + tasks = {} + app.state.tasks = tasks # 数据库 if await is_init(): init_config = await load_init_config() - await init_tortoise( - init_config["db_type"], init_config, init_config.get("admin_password") - ) + await init_tortoise(init_config["db_type"], init_config, init_config.get("admin_password")) logger.info("初始化数据库") # 定时器 scheduler.add_job(clear_session, "interval", seconds=30, misfire_grace_time=10) @@ -63,6 +62,9 @@ async def lifespan(app: FastAPI): yield + for _, v in tasks.items(): + v.cancel() + scheduler.shutdown() logger.info("关闭定时器") @@ -108,9 +110,7 @@ def create_app(): init_config["admin_password"] = request.admin_password init_config["is_init"] = True - init_success = await init_tortoise( - request.db_type, init_config, request.admin_password - ) + init_success = await init_tortoise(request.db_type, init_config, request.admin_password) if init_success: await save_init_config(init_config) @@ -127,16 +127,12 @@ def create_app(): response_model=LoginResponse, ) async def login(login_info: LoginRequest): - user = await DB.User.filter( - username=login_info.username, password=login_info.get_md5_password() - ).first() + user = await DB.User.filter(username=login_info.username, password=login_info.get_md5_password(), is_deleted=0).first() if user: token = uuid.uuid4().hex sessions[token] = time.time(), user logger.info(sessions) - return LoginResponse( - data={"token": token, "user": user.json(del_columns=["password"])} - ) + return LoginResponse(data={"token": token, "user": user.json(del_columns=["password"])}) return LoginResponse(success=False, message="帐号或密码错误") @app.post( @@ -160,79 +156,112 @@ def create_app(): return LoginResponse(data=user.json(del_columns=["password"])) @app.post( - "/api/run", tags=["Flow"], description="执行命令", response_model=BaseResponse + "/api/run", + tags=["Flow"], + description="执行命令", + response_model=BaseResponse, ) async def flow_run(id: int): - nodes = [] - git_node_0 = F.GitNode() - git_node_0.set_id("01K3JHSEGBHHAVQ92RAH8M6GPX") - nodes.append(git_node_0) + task_id = uuid.uuid4().hex - git_node_1 = F.GitNode() - git_node_1.set_id("01K3JHSHKESMWYEGSY4VNCJY4D") - nodes.append(git_node_1) + async def _task(task_id): + try: + nodes = [] + node_0 = F.GitNode() + node_0.set_id("01K3JHSEGBHHAVQ92RAH8M6GPX") + nodes.append(node_0) + node_1 = F.GitNode() + node_1.set_id("01K3JHSHKESMWYEGSY4VNCJY4D") + nodes.append(node_1) + node_2 = F.GitNode() + node_2.set_id("01K3T4GTWZJF3RT5FFH25D3T9C") + nodes.append(node_2) + node_3 = F.CmdNode() + node_3.set_id("01K3T4S2HN97T1B0TPFBCFSB00") + nodes.append(node_3) - cmd_node_2 = F.CmdNode() - cmd_node_2.set_id("01K3JHSMYM584C63QB74G3Q6X4") - nodes.append(cmd_node_2) + logger.debug(app.state.tasks) - cmd_node_3 = F.CmdNode() - cmd_node_3.set_id("01K3JHS5HTY78Z4EYWRFW5TA77") - nodes.append(cmd_node_3) + # 设置依赖关系: node0 - "true" >> node1 - "true" >> node2 + for i in range(len(nodes) - 1): + nodes[i] - "true" >> nodes[i + 1] - # 设置依赖关系: node0 - "true" >> node1 - "true" >> node2 - for i in range(len(nodes) - 1): - nodes[i] - "true" >> nodes[i + 1] + flow = F.AsyncFlow(node_0) + flow.set_params( + { + "01K3JHSEGBHHAVQ92RAH8M6GPX": { + "host": "58.214.239.10", + "user": "dgx07", + "password": "f=bHt/.7kRo:KIXq", + "port": 6378, + "sudo": True, + "git_path": "/data/wrok-flow/dbpai-web", + "git_user": "liruwei", + "git_password": "Wsad.8246", + "git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-web.git", + "git_branch": "pai-beta-build", + "git_clone_type": "http/https", + "git_clone_depth": 1, + }, + "01K3JHSHKESMWYEGSY4VNCJY4D": { + "host": "58.214.239.10", + "user": "dgx07", + "password": "f=bHt/.7kRo:KIXq", + "port": 6378, + "sudo": True, + "git_path": "/data/wrok-flow/dbpai-server", + "git_user": "liruwei", + "git_password": "Wsad.8246", + "git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-server.git", + "git_branch": "v3.4.2", + "git_clone_type": "http/https", + "git_clone_depth": 1, + }, + "01K3T4GTWZJF3RT5FFH25D3T9C": { + "host": "58.214.239.10", + "user": "dgx07", + "password": "f=bHt/.7kRo:KIXq", + "port": 6378, + "sudo": True, + "git_path": "/data/wrok-flow/dbpai-jenkins", + "git_user": "liruwei", + "git_password": "Wsad.8246", + "git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-jenkins.git", + "git_branch": "v3.4.2", + "git_clone_type": "http/https", + "git_clone_depth": 1, + }, + "01K3T4S2HN97T1B0TPFBCFSB00": { + "host": "58.214.239.10", + "user": "dgx07", + "password": "f=bHt/.7kRo:KIXq", + "port": 6378, + "sudo": True, + "cmd": "\n".join( + [ + "cd /data/wrok-flow/dbpai-jenkins", + "git log", + ] + ), + }, + } + ) + shared = {} + await flow.run_async(shared) + app.state.tasks.pop(task_id, None) + except asyncio.CancelledError: + logger.debug(f"任务被取消: {task_id}") + app.state.tasks.pop(task_id, None) - flow = F.AsyncFlow(git_node_0) - - flow.set_params( - { - "01K3JHSEGBHHAVQ92RAH8M6GPX": { - "git_path": "/Users/liruwei/Documents/code/project/dabai/dbpai-web", - "git_user": "liruwei", - "git_password": "Wsad.8246", - "git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-web.git", - "git_branch": "v3.4.2", - "git_clone_type": "http/https", - "git_clone_depth": 0, - }, - "01K3JHSHKESMWYEGSY4VNCJY4D": { - "git_path": "/Users/liruwei/Documents/code/project/dabai/dbpai-server", - "git_user": "liruwei", - "git_password": "Wsad.8246", - "git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-server.git", - "git_branch": "v3.4.2", - "git_clone_type": "http/https", - "git_clone_depth": 0, - }, - "01K3JHSMYM584C63QB74G3Q6X4": { - "cmd": "\n".join( - [ - "cd /Users/liruwei/Documents/code/project/dabai", - "cp ./dbpai-web/public/dbpai-chat-widget.min.js ./dbpai-server/src/static", - "echo '开始打包'", - "sh build-dbpai-jenkisn.sh all 2> /dev/null", - "echo '打包结束'", - "pwd", - "cd ./dbpai-jenkins", - "git add .", - "git commit -m 'feat: build'", - "git push", - ] - ) - }, - "01K3JHS5HTY78Z4EYWRFW5TA77": { - "cmd": "\n".join(["pwd"]), - }, - } - ) - shared = {} - await flow.run_async(shared) - - logger.info(shared) + app.state.tasks[task_id] = asyncio.create_task(_task(task_id)) + return BaseResponse(data=task_id) + @app.post("/api/stop", tags=["Flow"], description="停止执行指令", response_model=BaseResponse) + async def stop_run(task_id: str): + task = app.state.tasks.pop(task_id, None) + if task: + task.cancel() return BaseResponse() return app diff --git a/backend/app/models/db/__init__.py b/backend/app/models/db/__init__.py index db523d3..f9f1d0a 100644 --- a/backend/app/models/db/__init__.py +++ b/backend/app/models/db/__init__.py @@ -1,2 +1,3 @@ from .system import * from .user import * +from .work import * \ No newline at end of file diff --git a/backend/app/models/db/user.py b/backend/app/models/db/user.py index 4ceee3a..6c8a348 100644 --- a/backend/app/models/db/user.py +++ b/backend/app/models/db/user.py @@ -5,6 +5,7 @@ class User(BaseModel): username = fields.CharField(max_length=50, null=True) password = fields.CharField(max_length=128, null=True) level = fields.IntField(default=1, null=True, description="0:超级用户 1:普通用户") + is_deleted = fields.IntField(default=0, null=True) class Meta: table = "tb_users" diff --git a/backend/app/models/db/work.py b/backend/app/models/db/work.py new file mode 100644 index 0000000..783fed0 --- /dev/null +++ b/backend/app/models/db/work.py @@ -0,0 +1,49 @@ +from tortoise import fields +from .system import BaseModel +from enum import Enum + +class DBAnalysisFileStatus(str, Enum): + PROCESSING = "PROCESSING" # 处理中 + ERROR = "ERROR" # 处理失败 + DONE = "DONE" # 处理完成 + + +class Work(BaseModel): + uuid = fields.CharField(max_length=128, index=True) + creator_id = fields.IntField(null=True) + work_title = fields.CharField(max_length=128, null=True) + work_desc = fields.CharField(max_length=255, null=True) + is_deleted = fields.IntField(default=0, null=True) + + class Meta: + table = "tb_work" + + +class WorkLogsStatus(str, Enum): + DEFAULT = "DEFAULT" + RUNNING = "RUNNING" + SUCCESS = "SUCCESS" + FAILED = "FAILED" + + +class WorkLogs(BaseModel): + user_id = fields.IntField(null=True) + work_id = fields.IntField(index=True) + status = fields.CharField(max_length=128, null=True) + result = fields.JSONField(default={}) + work_title = fields.CharField(max_length=128, null=True) + work_desc = fields.CharField(max_length=255, null=True) + is_deleted = fields.IntField(default=0) + + class Meta: + table = "tb_work_logs" + + +class WorkContent(BaseModel): + user_id = fields.IntField(null=True) + work_id = fields.IntField(index=True) + content = fields.JSONField(default={}) + is_deleted = fields.IntField(default=0) + + class Meta: + table = "tb_work_content" diff --git a/backend/app/models/schemas/system.py b/backend/app/models/schemas/system.py index 7a38a23..f2f37f4 100644 --- a/backend/app/models/schemas/system.py +++ b/backend/app/models/schemas/system.py @@ -1,11 +1,11 @@ from pydantic import BaseModel - +from typing import Any class BaseResponse(BaseModel): + data: Any | None = None success: bool = True message: str | None = None - class InitConfigRequest(BaseModel): db_type: str = "sqlite" admin_password: str = "admin" diff --git a/backend/app/nodes/cmd_node.py b/backend/app/nodes/cmd_node.py index 48394c5..ec9c034 100644 --- a/backend/app/nodes/cmd_node.py +++ b/backend/app/nodes/cmd_node.py @@ -1,4 +1,5 @@ from core.pocket_flow import AsyncNode +from core.logger import logger import asyncio import asyncssh @@ -22,7 +23,6 @@ async def async_run_cmd( text = line if not isinstance(text, str): text = line.decode() - print(text) yield stream_type, text.rstrip() if not host: @@ -127,8 +127,10 @@ class CmdNode(AsyncNode): async for stream_type, line in async_run_cmd(**prep_res): if stream_type == "stderr": err.append(str(line)) + logger.debug(f'err: {line}') else: out.append(str(line)) + logger.debug(f"out: {line}") err_str = "\n".join(err) out_str = "\n".join(out) diff --git a/frontend/index.html b/frontend/index.html index 67cb05e..ddb4168 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + 工作流
diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..4a58288 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,42 +1,14 @@ +html, +body, #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + height: 100%; + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +div.no-select { + user-select: none; +} \ No newline at end of file diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 143b213..1fb4e6e 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,20 +1,25 @@ import { createContext, useContext, useEffect, useState } from "react"; import { login as loginToServer, currentUser } from "../api/user"; const AuthContext = createContext(null); - +import { useNavigate } from 'react-router-dom'; export function AuthProvider({ children }) { const token = localStorage.getItem('_token') const [user, setUser] = useState(null); + const navigate = useNavigate(); const [loading, setLoading] = useState(token?.length > 0); const getUserInfoFromToken = async () => { - const res = await currentUser(); - if (res?.success) { - setUser({ - token: token, - ...res?.data - }) + try { + const res = await currentUser(); + if (res?.success) { + setUser({ + token: token, + ...res?.data + }) + } + } catch (error) { + navigate("/login") } setLoading(false); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..793243b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -13,14 +13,6 @@ -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} body { margin: 0; @@ -49,10 +41,6 @@ button { button:hover { border-color: #646cff; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} @media (prefers-color-scheme: light) { :root { diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index d3ef1e1..7834dfc 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -10,18 +10,12 @@ import { ProConfigProvider } from "@ant-design/pro-components"; createRoot(document.getElementById('root')).render( - - - - - - - + + + + + + + ) diff --git a/frontend/src/pages/Flow/index.jsx b/frontend/src/pages/Flow/index.jsx new file mode 100644 index 0000000..7ec3ea2 --- /dev/null +++ b/frontend/src/pages/Flow/index.jsx @@ -0,0 +1,107 @@ +import { ProTable } from "@ant-design/pro-components" +import { Button, Card, Modal, Popconfirm, Space, Steps } from "antd" +import { useAuth } from "../../context/AuthContext"; +import { LoadingOutlined } from "@ant-design/icons"; +import { useState } from "react"; +export default () => { + const { user } = useAuth(); + const run = async record => { + + } + const columns = [ + { + dataIndex: 'id', + title: "序号", + }, + { + dataIndex: 'title', + title: "工作流", + }, + { + dataIndex: 'status', + title: "状态", + render: (_, record) => { + return + } + }, + { + dataIndex: 'user', + title: '最后执行用户', + }, + { + dataIndex: 'time', + title: '最后执行时间', + }, + { + dataIndex: 'result', + title: '最后执行结果', + }, + { + title: '操作', + valueType: 'option', + key: 'option', + width: user.level === 0 ? 120 : 60, + fixed: 'right', + render: (_, record) => [ + run(record)}> + + , + user?.level === 0 && , + user?.level === 0 && , + ] + } + ]; + return ( + + + + + ) +} + +const RunningStatus = ({record}) => { + const [open, setOpen] = useState(false); + return <> + setOpen(true)}> + + + 执行中 + + + setOpen(false)} + footer={[]} + > + In Progress, + description: 'This is a description.', + }, + { + title: 'Waiting', + description: 'This is a description.', + }, + ]} + /> + + +} \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index aa2c146..63a5f03 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,5 +1,7 @@ +import { Button, Card } from "antd" + export default () => { - return <> -

Home

- + return + + } \ No newline at end of file diff --git a/frontend/src/pages/Layout.css b/frontend/src/pages/Layout.css new file mode 100644 index 0000000..3fab40f --- /dev/null +++ b/frontend/src/pages/Layout.css @@ -0,0 +1,23 @@ +.page-layout { + min-width: 100vw; + min-height: 100vh; + + .logo { + height: 40px; + width: calc(100% - 16px); + background-color: rgba(255, 255, 255, 0.1); + margin: 8px 8px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.5); + font-size: 30px; + font-weight: bold; + overflow: hidden; + white-space: nowrap; + } +} + +.page-content { + background-color: #f2f3f5; +} diff --git a/frontend/src/pages/Layout.jsx b/frontend/src/pages/Layout.jsx index 730f319..388cd84 100644 --- a/frontend/src/pages/Layout.jsx +++ b/frontend/src/pages/Layout.jsx @@ -1,28 +1,28 @@ import React, { useState } from "react"; import { Outlet, useNavigate, useLocation } from "react-router-dom"; -import { Layout, Menu, Button, theme, Flex } from "antd"; -import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, VideoCameraOutlined, UploadOutlined, LoginOutlined } from "@ant-design/icons"; +import { Layout, Menu, Button, Flex, Dropdown, Steps } from "antd"; +import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LoginOutlined, ProfileOutlined, NodeExpandOutlined } from "@ant-design/icons"; import { useAuth } from "../context/AuthContext"; +import './Layout.css'; const { Header, Sider, Content } = Layout; export default function LayoutPage() { - const { logout } = useAuth(); - const [collapsed, setCollapsed] = useState(false); + const { logout, user } = useAuth(); + const [collapsed, setCollapsed] = useState(true); const navigate = useNavigate(); const location = useLocation(); const menuItems = [ - { key: "1", icon: , label: "首页", path: "/welcome" }, - { key: "2", icon: , label: "用户", path: "/user" }, + { key: "1", icon: , label: "工作台", path: "/flow" }, ]; - const selectedKey = menuItems.find(item => location.pathname.startsWith(item.path))?.key || "1"; + const selectedKey = menuItems.find(item => location.pathname.startsWith(item.path))?.key || ""; return ( - + -
+ - +
); } + +const Logo = ({ collapsed }) => { + return
+ {collapsed ? "F" : "FLOW"} +
+} + +const HeaderRight = ({ logout, navigate, user }) => { + const items = [ + { + label: '用户信息', + key: '2', + icon: + }, + { + type: 'divider', + }, + { + label: '退出登录', + key: '1', + icon: + }, + ]; + const onClick = ({ key }) => { + if (key === '1') { + logout(); + } + if (key === '2') { + navigate('/user/detail') + } + } + return ( + + e.preventDefault()}> + + + + ) +} diff --git a/frontend/src/pages/Loading.jsx b/frontend/src/pages/Loading.jsx index 6b1c232..a74364f 100644 --- a/frontend/src/pages/Loading.jsx +++ b/frontend/src/pages/Loading.jsx @@ -8,7 +8,7 @@ export default () => { const navigate = useNavigate(); useEffect(() => { - user && navigate("/welcome") + user && navigate("/flow") !user && !loading && navigate("/login") }, [user]) diff --git a/frontend/src/pages/User/Profile.jsx b/frontend/src/pages/User/Profile.jsx new file mode 100644 index 0000000..0c8532b --- /dev/null +++ b/frontend/src/pages/User/Profile.jsx @@ -0,0 +1,5 @@ +export default () => { + return <> +

Profile

+ +} \ No newline at end of file diff --git a/frontend/src/pages/User.jsx b/frontend/src/pages/User/index.jsx similarity index 100% rename from frontend/src/pages/User.jsx rename to frontend/src/pages/User/index.jsx diff --git a/frontend/src/routes.jsx b/frontend/src/routes.jsx index 5fa7418..89935c7 100644 --- a/frontend/src/routes.jsx +++ b/frontend/src/routes.jsx @@ -4,6 +4,8 @@ import Loading from './pages/Loading'; import Home from "./pages/Home"; import Login from "./pages/Login"; import User from "./pages/User"; +import Profile from "./pages/User/Profile"; +import Flow from "./pages/Flow"; import LayoutPage from "./pages/Layout"; @@ -15,7 +17,7 @@ function PrivateRoute() { const routes = [ // 登录页公开访问 - { path: "/", element: }, + { path: "/", element: }, { path: "/login", element: }, // 需要登录才能访问的路由 @@ -25,8 +27,9 @@ const routes = [ { element: , children: [ - { path: "/welcome", element: }, - { path: "/User", element: }, + { path: "/flow", element: }, + { path: "/user", element: }, + { path: "/user/detail", element: } ] } ],