feat: 创建路由
This commit is contained in:
parent
70fcc0f410
commit
8b043a58a1
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
|
@ -23,9 +24,7 @@ scheduler = AsyncIOScheduler()
|
||||||
def clear_session():
|
def clear_session():
|
||||||
TTL = 60 * 60 * 24 # 1天有效期
|
TTL = 60 * 60 * 24 # 1天有效期
|
||||||
now = time.time()
|
now = time.time()
|
||||||
expired_keys = [
|
expired_keys = [token for token, (expires_at, _) in sessions.items() if expires_at + TTL < now]
|
||||||
token for token, (expires_at, _) in sessions.items() if expires_at + TTL < now
|
|
||||||
]
|
|
||||||
for token in expired_keys:
|
for token in expired_keys:
|
||||||
sessions.pop(token, None)
|
sessions.pop(token, None)
|
||||||
|
|
||||||
|
|
@ -49,12 +48,12 @@ async def is_init():
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
tasks = {}
|
||||||
|
app.state.tasks = tasks
|
||||||
# 数据库
|
# 数据库
|
||||||
if await is_init():
|
if await is_init():
|
||||||
init_config = await load_init_config()
|
init_config = await load_init_config()
|
||||||
await init_tortoise(
|
await init_tortoise(init_config["db_type"], init_config, init_config.get("admin_password"))
|
||||||
init_config["db_type"], init_config, init_config.get("admin_password")
|
|
||||||
)
|
|
||||||
logger.info("初始化数据库")
|
logger.info("初始化数据库")
|
||||||
# 定时器
|
# 定时器
|
||||||
scheduler.add_job(clear_session, "interval", seconds=30, misfire_grace_time=10)
|
scheduler.add_job(clear_session, "interval", seconds=30, misfire_grace_time=10)
|
||||||
|
|
@ -63,6 +62,9 @@ async def lifespan(app: FastAPI):
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
for _, v in tasks.items():
|
||||||
|
v.cancel()
|
||||||
|
|
||||||
scheduler.shutdown()
|
scheduler.shutdown()
|
||||||
logger.info("关闭定时器")
|
logger.info("关闭定时器")
|
||||||
|
|
||||||
|
|
@ -108,9 +110,7 @@ def create_app():
|
||||||
init_config["admin_password"] = request.admin_password
|
init_config["admin_password"] = request.admin_password
|
||||||
init_config["is_init"] = True
|
init_config["is_init"] = True
|
||||||
|
|
||||||
init_success = await init_tortoise(
|
init_success = await init_tortoise(request.db_type, init_config, request.admin_password)
|
||||||
request.db_type, init_config, request.admin_password
|
|
||||||
)
|
|
||||||
|
|
||||||
if init_success:
|
if init_success:
|
||||||
await save_init_config(init_config)
|
await save_init_config(init_config)
|
||||||
|
|
@ -127,16 +127,12 @@ def create_app():
|
||||||
response_model=LoginResponse,
|
response_model=LoginResponse,
|
||||||
)
|
)
|
||||||
async def login(login_info: LoginRequest):
|
async def login(login_info: LoginRequest):
|
||||||
user = await DB.User.filter(
|
user = await DB.User.filter(username=login_info.username, password=login_info.get_md5_password(), is_deleted=0).first()
|
||||||
username=login_info.username, password=login_info.get_md5_password()
|
|
||||||
).first()
|
|
||||||
if user:
|
if user:
|
||||||
token = uuid.uuid4().hex
|
token = uuid.uuid4().hex
|
||||||
sessions[token] = time.time(), user
|
sessions[token] = time.time(), user
|
||||||
logger.info(sessions)
|
logger.info(sessions)
|
||||||
return LoginResponse(
|
return LoginResponse(data={"token": token, "user": user.json(del_columns=["password"])})
|
||||||
data={"token": token, "user": user.json(del_columns=["password"])}
|
|
||||||
)
|
|
||||||
return LoginResponse(success=False, message="帐号或密码错误")
|
return LoginResponse(success=False, message="帐号或密码错误")
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
|
|
@ -160,79 +156,112 @@ def create_app():
|
||||||
return LoginResponse(data=user.json(del_columns=["password"]))
|
return LoginResponse(data=user.json(del_columns=["password"]))
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/api/run", tags=["Flow"], description="执行命令", response_model=BaseResponse
|
"/api/run",
|
||||||
|
tags=["Flow"],
|
||||||
|
description="执行命令",
|
||||||
|
response_model=BaseResponse,
|
||||||
)
|
)
|
||||||
async def flow_run(id: int):
|
async def flow_run(id: int):
|
||||||
nodes = []
|
|
||||||
|
|
||||||
git_node_0 = F.GitNode()
|
task_id = uuid.uuid4().hex
|
||||||
git_node_0.set_id("01K3JHSEGBHHAVQ92RAH8M6GPX")
|
|
||||||
nodes.append(git_node_0)
|
|
||||||
|
|
||||||
git_node_1 = F.GitNode()
|
async def _task(task_id):
|
||||||
git_node_1.set_id("01K3JHSHKESMWYEGSY4VNCJY4D")
|
try:
|
||||||
nodes.append(git_node_1)
|
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()
|
logger.debug(app.state.tasks)
|
||||||
cmd_node_2.set_id("01K3JHSMYM584C63QB74G3Q6X4")
|
|
||||||
nodes.append(cmd_node_2)
|
|
||||||
|
|
||||||
cmd_node_3 = F.CmdNode()
|
# 设置依赖关系: node0 - "true" >> node1 - "true" >> node2
|
||||||
cmd_node_3.set_id("01K3JHS5HTY78Z4EYWRFW5TA77")
|
for i in range(len(nodes) - 1):
|
||||||
nodes.append(cmd_node_3)
|
nodes[i] - "true" >> nodes[i + 1]
|
||||||
|
|
||||||
# 设置依赖关系: node0 - "true" >> node1 - "true" >> node2
|
flow = F.AsyncFlow(node_0)
|
||||||
for i in range(len(nodes) - 1):
|
flow.set_params(
|
||||||
nodes[i] - "true" >> nodes[i + 1]
|
{
|
||||||
|
"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)
|
app.state.tasks[task_id] = asyncio.create_task(_task(task_id))
|
||||||
|
return BaseResponse(data=task_id)
|
||||||
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.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 BaseResponse()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
from .system import *
|
from .system import *
|
||||||
from .user import *
|
from .user import *
|
||||||
|
from .work import *
|
||||||
|
|
@ -5,6 +5,7 @@ class User(BaseModel):
|
||||||
username = fields.CharField(max_length=50, null=True)
|
username = fields.CharField(max_length=50, null=True)
|
||||||
password = fields.CharField(max_length=128, null=True)
|
password = fields.CharField(max_length=128, null=True)
|
||||||
level = fields.IntField(default=1, null=True, description="0:超级用户 1:普通用户")
|
level = fields.IntField(default=1, null=True, description="0:超级用户 1:普通用户")
|
||||||
|
is_deleted = fields.IntField(default=0, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = "tb_users"
|
table = "tb_users"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
class BaseResponse(BaseModel):
|
class BaseResponse(BaseModel):
|
||||||
|
data: Any | None = None
|
||||||
success: bool = True
|
success: bool = True
|
||||||
message: str | None = None
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class InitConfigRequest(BaseModel):
|
class InitConfigRequest(BaseModel):
|
||||||
db_type: str = "sqlite"
|
db_type: str = "sqlite"
|
||||||
admin_password: str = "admin"
|
admin_password: str = "admin"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from core.pocket_flow import AsyncNode
|
from core.pocket_flow import AsyncNode
|
||||||
|
from core.logger import logger
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncssh
|
import asyncssh
|
||||||
|
|
||||||
|
|
@ -22,7 +23,6 @@ async def async_run_cmd(
|
||||||
text = line
|
text = line
|
||||||
if not isinstance(text, str):
|
if not isinstance(text, str):
|
||||||
text = line.decode()
|
text = line.decode()
|
||||||
print(text)
|
|
||||||
yield stream_type, text.rstrip()
|
yield stream_type, text.rstrip()
|
||||||
|
|
||||||
if not host:
|
if not host:
|
||||||
|
|
@ -127,8 +127,10 @@ class CmdNode(AsyncNode):
|
||||||
async for stream_type, line in async_run_cmd(**prep_res):
|
async for stream_type, line in async_run_cmd(**prep_res):
|
||||||
if stream_type == "stderr":
|
if stream_type == "stderr":
|
||||||
err.append(str(line))
|
err.append(str(line))
|
||||||
|
logger.debug(f'err: {line}')
|
||||||
else:
|
else:
|
||||||
out.append(str(line))
|
out.append(str(line))
|
||||||
|
logger.debug(f"out: {line}")
|
||||||
|
|
||||||
err_str = "\n".join(err)
|
err_str = "\n".join(err)
|
||||||
out_str = "\n".join(out)
|
out_str = "\n".join(out)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React</title>
|
<title>工作流</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,14 @@
|
||||||
|
html,
|
||||||
|
body,
|
||||||
#root {
|
#root {
|
||||||
max-width: 1280px;
|
height: 100%;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
padding: 2rem;
|
padding: 0;
|
||||||
text-align: center;
|
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 {
|
div.no-select {
|
||||||
height: 6em;
|
user-select: none;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { login as loginToServer, currentUser } from "../api/user";
|
import { login as loginToServer, currentUser } from "../api/user";
|
||||||
const AuthContext = createContext(null);
|
const AuthContext = createContext(null);
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const token = localStorage.getItem('_token')
|
const token = localStorage.getItem('_token')
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(token?.length > 0);
|
const [loading, setLoading] = useState(token?.length > 0);
|
||||||
|
|
||||||
const getUserInfoFromToken = async () => {
|
const getUserInfoFromToken = async () => {
|
||||||
const res = await currentUser();
|
try {
|
||||||
if (res?.success) {
|
const res = await currentUser();
|
||||||
setUser({
|
if (res?.success) {
|
||||||
token: token,
|
setUser({
|
||||||
...res?.data
|
token: token,
|
||||||
})
|
...res?.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
navigate("/login")
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,6 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -49,10 +41,6 @@ button {
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,12 @@ import { ProConfigProvider } from "@ant-design/pro-components";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ConfigProvider locale={zhCN} theme={{
|
<ConfigProvider locale={zhCN}>
|
||||||
components: {
|
<ProConfigProvider>
|
||||||
Button: {
|
<App />
|
||||||
algorithm: true
|
</ProConfigProvider>
|
||||||
}
|
</ConfigProvider>
|
||||||
}
|
</AuthProvider>
|
||||||
}}>
|
|
||||||
<ProConfigProvider>
|
|
||||||
<App />
|
|
||||||
</ProConfigProvider>
|
|
||||||
</ConfigProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 <RunningStatus record={record} />
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'user',
|
||||||
|
title: '最后执行用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'time',
|
||||||
|
title: '最后执行时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'result',
|
||||||
|
title: '最后执行结果',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
key: 'option',
|
||||||
|
width: user.level === 0 ? 120 : 60,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (_, record) => [
|
||||||
|
<Popconfirm key='1' title="运行工作" onConfirm={() => run(record)}>
|
||||||
|
<Button type="link" style={{ padding: '0 0', border: 'none' }}>运行</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
user?.level === 0 && <Button key='2' type="link" style={{ padding: '0 0', border: 'none' }}>编辑</Button>,
|
||||||
|
user?.level === 0 && <Button key='3' type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Card title="工作台">
|
||||||
|
<ProTable
|
||||||
|
rowKey='id'
|
||||||
|
search={false}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
options={false}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={[{ id: 1 }]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunningStatus = ({record}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return <>
|
||||||
|
<a onClick={() => setOpen(true)}>
|
||||||
|
<Space>
|
||||||
|
<LoadingOutlined />
|
||||||
|
执行中
|
||||||
|
</Space>
|
||||||
|
</a>
|
||||||
|
<Modal
|
||||||
|
title={record?.title || `#${record?.id} 执行状态`}
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
open={open}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
footer={[<Button key="cancel" onClick={() => setOpen(false)}>关闭</Button>]}
|
||||||
|
>
|
||||||
|
<Steps
|
||||||
|
progressDot
|
||||||
|
current={1}
|
||||||
|
direction="vertical"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: 'Finished',
|
||||||
|
description: 'This is a description.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <Space>In Progress<LoadingOutlined /></Space>,
|
||||||
|
description: 'This is a description.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Waiting',
|
||||||
|
description: 'This is a description.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { Button, Card } from "antd"
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return <>
|
return <Card>
|
||||||
<h1>Home</h1>
|
<Button type='text'>ABC</Button>
|
||||||
</>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Outlet, useNavigate, useLocation } from "react-router-dom";
|
import { Outlet, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { Layout, Menu, Button, theme, Flex } from "antd";
|
import { Layout, Menu, Button, Flex, Dropdown, Steps } from "antd";
|
||||||
import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, VideoCameraOutlined, UploadOutlined, LoginOutlined } from "@ant-design/icons";
|
import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LoginOutlined, ProfileOutlined, NodeExpandOutlined } from "@ant-design/icons";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import './Layout.css';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
export default function LayoutPage() {
|
export default function LayoutPage() {
|
||||||
const { logout } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: "1", icon: <UserOutlined />, label: "首页", path: "/welcome" },
|
{ key: "1", icon: <NodeExpandOutlined />, label: "工作台", path: "/flow" },
|
||||||
{ key: "2", icon: <VideoCameraOutlined />, label: "用户", path: "/user" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectedKey = menuItems.find(item => location.pathname.startsWith(item.path))?.key || "1";
|
const selectedKey = menuItems.find(item => location.pathname.startsWith(item.path))?.key || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: '100vh', width: '100vw' }}>
|
<Layout className="page-layout">
|
||||||
<Sider trigger={null} collapsible collapsed={collapsed} width={200}>
|
<Sider trigger={null} collapsible collapsed={collapsed} width={200}>
|
||||||
<div style={{ height: 40, width: 'calc(100% - 16px)', backgroundColor: 'lightgray', margin: '8px 8px' }} />
|
<Logo collapsed={collapsed} />
|
||||||
<Menu
|
<Menu
|
||||||
theme="dark"
|
theme="dark"
|
||||||
mode="inline"
|
mode="inline"
|
||||||
|
|
@ -34,7 +34,7 @@ export default function LayoutPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout style={{ backgroundColor: '#f2f3f5' }}>
|
<Layout className="page-content">
|
||||||
<Header style={{ padding: 0, backgroundColor: 'white', width: "100%" }}>
|
<Header style={{ padding: 0, backgroundColor: 'white', width: "100%" }}>
|
||||||
<Flex align="center" justify="space-between" style={{
|
<Flex align="center" justify="space-between" style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
@ -42,15 +42,12 @@ export default function LayoutPage() {
|
||||||
boxSizing: 'border-box'
|
boxSizing: 'border-box'
|
||||||
}}>
|
}}>
|
||||||
<Button
|
<Button
|
||||||
|
style={{ border: 'none'}}
|
||||||
type='text'
|
type='text'
|
||||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
/>
|
/>
|
||||||
<Button type="text"
|
<HeaderRight logout={logout} navigate={navigate} user={user} />
|
||||||
icon={<LoginOutlined />}
|
|
||||||
onClick={() => logout()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Header>
|
</Header>
|
||||||
<Content
|
<Content
|
||||||
|
|
@ -65,3 +62,47 @@ export default function LayoutPage() {
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Logo = ({ collapsed }) => {
|
||||||
|
return <div className="logo">
|
||||||
|
{collapsed ? "F" : "FLOW"}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderRight = ({ logout, navigate, user }) => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: '用户信息',
|
||||||
|
key: '2',
|
||||||
|
icon: <ProfileOutlined />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: '1',
|
||||||
|
icon: <LoginOutlined/>
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const onClick = ({ key }) => {
|
||||||
|
if (key === '1') {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
if (key === '2') {
|
||||||
|
navigate('/user/detail')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items, onClick }} arrow>
|
||||||
|
<a onClick={e => e.preventDefault()}>
|
||||||
|
<Button type="text"
|
||||||
|
style={{ border: 'none' }}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
user && navigate("/welcome")
|
user && navigate("/flow")
|
||||||
!user && !loading && navigate("/login")
|
!user && !loading && navigate("/login")
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default () => {
|
||||||
|
return <>
|
||||||
|
<h1>Profile</h1>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import Loading from './pages/Loading';
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
import User from "./pages/User";
|
import User from "./pages/User";
|
||||||
|
import Profile from "./pages/User/Profile";
|
||||||
|
import Flow from "./pages/Flow";
|
||||||
import LayoutPage from "./pages/Layout";
|
import LayoutPage from "./pages/Layout";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,7 +17,7 @@ function PrivateRoute() {
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
// 登录页公开访问
|
// 登录页公开访问
|
||||||
{ path: "/", element: <Loading />},
|
{ path: "/", element: <Loading /> },
|
||||||
{ path: "/login", element: <Login /> },
|
{ path: "/login", element: <Login /> },
|
||||||
|
|
||||||
// 需要登录才能访问的路由
|
// 需要登录才能访问的路由
|
||||||
|
|
@ -25,8 +27,9 @@ const routes = [
|
||||||
{
|
{
|
||||||
element: <LayoutPage />,
|
element: <LayoutPage />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "/welcome", element: <Home /> },
|
{ path: "/flow", element: <Flow /> },
|
||||||
{ path: "/User", element: <User /> },
|
{ path: "/user", element: <User /> },
|
||||||
|
{ path: "/user/detail", element: <Profile /> }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue