fc
This commit is contained in:
commit
6b6bbab434
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.idea
|
||||
dist*
|
||||
node_modules
|
||||
.DS_Store
|
||||
offline_packages
|
||||
__pycache__
|
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/config/settings.py
Normal file
0
backend/app/config/settings.py
Normal file
11
backend/app/core/config.py
Normal file
11
backend/app/core/config.py
Normal file
@ -0,0 +1,11 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str = "mysql+pymysql://root:root@localhost:3306/db_wisdom_cube?charset=utf8mb4"
|
||||
DB_POOL_SIZE: int = 5
|
||||
DB_MAX_OVERFLOW: int = 10
|
||||
DEBUG: bool = False
|
||||
|
||||
|
||||
settings = Settings()
|
26
backend/app/core/database.py
Normal file
26
backend/app/core/database.py
Normal file
@ -0,0 +1,26 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(settings.DATABASE_URL,
|
||||
pool_size=settings.DB_POOL_SIZE,
|
||||
max_overflow=settings.DB_MAX_OVERFLOW,
|
||||
pool_pre_ping=True)
|
||||
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
0
backend/app/core/depedencies.py
Normal file
0
backend/app/core/depedencies.py
Normal file
46
backend/app/core/security.py
Normal file
46
backend/app/core/security.py
Normal file
@ -0,0 +1,46 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
# 密钥,用于签名JWT
|
||||
SECRET_KEY = "_CM__CMCMCM__CM_"
|
||||
ALGORITHM = "HS256"
|
||||
user_db = {
|
||||
"admin": {
|
||||
"user_id": "1",
|
||||
"username": "admin",
|
||||
"password": bcrypt.hashpw("admin123".encode("utf-8"), bcrypt.gensalt())
|
||||
}
|
||||
}
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=120)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
if payload["exp"] < int(datetime.utcnow().timestamp()):
|
||||
raise HTTPException(status_code=401, detail="Token已过期")
|
||||
username = payload["sub"]
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token已过期")
|
||||
except jwt.InvalidSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token无效")
|
||||
return username
|
52
backend/app/main.py
Normal file
52
backend/app/main.py
Normal file
@ -0,0 +1,52 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.sql.functions import user
|
||||
|
||||
from app.routers import developer, project, requirement, chat, auth
|
||||
from app.core.database import engine, Base
|
||||
|
||||
import logging
|
||||
logging.basicConfig()
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
||||
|
||||
app = FastAPI(title="HNMFBackend", version="0.1.0")
|
||||
|
||||
# 跨域
|
||||
origins = [
|
||||
"http://localhost",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(developer.router)
|
||||
app.include_router(project.router)
|
||||
app.include_router(requirement.router)
|
||||
app.include_router(chat.router)
|
||||
app.include_router(auth.router)
|
||||
|
||||
|
||||
# 创建生命周期处理器
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时执行(原on_event("startup"))
|
||||
Base.metadata.create_all(bind=engine)
|
||||
yield
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status", "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
12
backend/app/models/developer.py
Normal file
12
backend/app/models/developer.py
Normal file
@ -0,0 +1,12 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, types
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Developer(Base):
|
||||
__tablename__ = "sys_developer"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=True)
|
||||
profile = Column(types.Text().with_variant(types.Text(length=4294967295), "mysql"), nullable=True)
|
||||
post = Column(String, nullable=True)
|
||||
gmt_create = Column(String(20), nullable=True)
|
14
backend/app/models/project.py
Normal file
14
backend/app/models/project.py
Normal file
@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, types
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = 'sys_project'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(types.Text().with_variant(types.Text(length=4294967295), "mysql"), nullable=False)
|
||||
manager = Column(String, nullable=False)
|
||||
demander = Column(String, nullable=False)
|
||||
developers = Column(String, nullable=False)
|
||||
gmt_create = Column(String(20), nullable=False)
|
12
backend/app/models/requirement.py
Normal file
12
backend/app/models/requirement.py
Normal file
@ -0,0 +1,12 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, types
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Requirement(Base):
|
||||
__tablename__ = "sys_requirement"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, index=True)
|
||||
description = Column(types.Text().with_variant(types.Text(length=4294967295), "mysql"), nullable=True)
|
||||
project_id = Column(Integer, nullable=False)
|
||||
gmt_create = Column(String(20), nullable=True)
|
20
backend/app/routers/auth.py
Normal file
20
backend/app/routers/auth.py
Normal file
@ -0,0 +1,20 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from app.core.security import verify_password, create_access_token, user_db
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", description="登录")
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = user_db.get(form_data.username)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="用户名不存在", headers={"WWW-Authenticate": "Bearer"})
|
||||
elif not verify_password(form_data.password, user["password"]):
|
||||
raise HTTPException(status_code=401, detail="密码错误", headers={"WWW-Authenticate": "Bearer"})
|
||||
access_token = create_access_token(data={"sub": user["username"]})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer"
|
||||
}
|
88
backend/app/routers/chat.py
Normal file
88
backend/app/routers/chat.py
Normal file
@ -0,0 +1,88 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from openai import OpenAI
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.requirement import Requirement
|
||||
from app.utils.common_util import SuccessData
|
||||
from app.core.security import get_current_user
|
||||
|
||||
client = OpenAI(
|
||||
api_key="sk-P7bm1Ioe7eovIWkbabAbY8yEON2VFu4RcHqumAFCfoxDVDdi",
|
||||
base_url="https://api.lkeap.cloud.tencent.com/v1"
|
||||
)
|
||||
|
||||
messages = [
|
||||
# {'role': 'system', 'content': '您现在是一个代码质量审核专家,您的任务是结合业务需求代码进行审核,判断代码是否符合需求'},
|
||||
]
|
||||
|
||||
|
||||
class WorkScore(BaseModel):
|
||||
prompt: str
|
||||
requirement_id: int
|
||||
|
||||
|
||||
router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
|
||||
@router.post("/work-score", description="工作内容审核")
|
||||
async def work_chat(work_score: WorkScore,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
requirement = db.query(Requirement).filter(Requirement.id == work_score.requirement_id).first()
|
||||
if not requirement:
|
||||
raise HTTPException(status_code=400, detail="需求不存在")
|
||||
|
||||
prompt = f"""
|
||||
接下来,需要您根据提供的需求,对以下代码进行审核,有如下要求:
|
||||
1.判断代码是否符合需求,尽量详细地给出判断依据
|
||||
2.对代码的整体质量进行打分,满分为 100 分,打分时可以从代码的结构、性能等方面综合考量
|
||||
3..对代码的完成度进行打分,满分为 100 分,主要看代码是否实现了需求中的主要功能
|
||||
4.对代码的可读性进行打分,满分为 100 分,依据代码的注释、变量命名等是否规范易懂来评判
|
||||
5.对代码的可维护性进行打分,满分为 100 分,可从代码是否模块化、是否易于修改等方面考虑
|
||||
6.对代码的可扩展性进行打分,满分为 100 分,评估代码是否方便增加新功能或修改现有功能
|
||||
7.对代码是否符合需求业务进行打分,满分为 100 分,判断代码是否契合业务逻辑和目标
|
||||
8.综合结合 2-7 的分数,整体打分,满分为 100 分,给出一个综合的评判分数
|
||||
9.最终以 markdown 表格格式输出,尽量让表格呈现清晰、易读
|
||||
需求:```
|
||||
{requirement.description}
|
||||
```
|
||||
代码:```
|
||||
{work_score.prompt}
|
||||
```
|
||||
"""
|
||||
|
||||
print(">>> prompt: \n%s" % prompt)
|
||||
messages.append({'role': 'system', 'content': prompt})
|
||||
stream = client.chat.completions.create(
|
||||
model="deepseek-r1",
|
||||
messages=messages,
|
||||
stream=True
|
||||
)
|
||||
|
||||
reasoning_content = "" # 定义完整思考过程
|
||||
answer_content = "" # 定义完整回复
|
||||
is_answering = False # 判断是否结束思考过程并开始回复
|
||||
for chunk in stream:
|
||||
if not getattr(chunk, 'choices', None):
|
||||
print("\n" + "=" * 20 + "Token 使用情况" + "=" * 20 + "\n")
|
||||
print(chunk.usage)
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
# 处理空内容情况
|
||||
if not getattr(delta, 'reasoning_content', None) and not getattr(delta, 'content', None):
|
||||
continue
|
||||
# 处理开始回答的情况
|
||||
if not getattr(delta, 'reasoning_content', None) and not is_answering:
|
||||
print("\n" + "=" * 20 + "完整回复" + "=" * 20 + "\n")
|
||||
is_answering = True
|
||||
# 处理思考过程
|
||||
if getattr(delta, 'reasoning_content', None):
|
||||
print(delta.reasoning_content, end='', flush=True)
|
||||
reasoning_content += delta.reasoning_content
|
||||
# 处理回复内容
|
||||
elif getattr(delta, 'content', None):
|
||||
print(delta.content, end='', flush=True)
|
||||
answer_content += delta.content
|
||||
return SuccessData(answer_content)
|
102
backend/app/routers/developer.py
Normal file
102
backend/app/routers/developer.py
Normal file
@ -0,0 +1,102 @@
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.developer import Developer
|
||||
from app.utils.common_util import get_total_page, get_offset, SuccessListPage
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/developer", tags=["developer"])
|
||||
|
||||
|
||||
class DeveloperSave(BaseModel):
|
||||
id: int = 0
|
||||
name: str
|
||||
profile: str
|
||||
post: str
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
async def save_developer(developer: DeveloperSave,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
params = Developer(
|
||||
name=developer.name,
|
||||
profile=developer.profile,
|
||||
post=developer.post,
|
||||
gmt_create=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
try:
|
||||
db.add(params)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.delete("/delete")
|
||||
async def delete_developer(ids: str = Query(..., description="删除ID列表", min_length=1),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
try:
|
||||
id_list = ids.split(",")
|
||||
db.query(Developer).filter(Developer.id.in_(id_list)).delete()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.put("/update/{developer_id}")
|
||||
async def update_developer(developer_id: int,
|
||||
developer: DeveloperSave,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
params = db.query(Developer).filter(Developer.id == developer_id).first()
|
||||
if not params:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
params.name = developer.name
|
||||
params.profile = developer.profile
|
||||
params.post = developer.post
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.get("/get/{developer_id}")
|
||||
async def get_developer(developer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
developer = db.query(Developer).filter(Developer.id == developer_id).first()
|
||||
if not developer:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
return developer
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_developer(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
return db.query(Developer).all()
|
||||
|
||||
|
||||
@router.get("/listpage")
|
||||
async def get_developer_listpage(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user),
|
||||
page: int = Query(1, gt=0, description="当前页面"),
|
||||
page_size: int = Query(10, gt=0, le=100, description="每页记录数"),
|
||||
name: str = Query(None, description="姓名")):
|
||||
query = db.query(Developer)
|
||||
if name:
|
||||
query = query.filter(Developer.name.ilike(f"%{name}%"))
|
||||
total = query.count()
|
||||
total_page = get_total_page(total, page_size)
|
||||
offset = get_offset(page, page_size)
|
||||
data_list = query.offset(offset).limit(page_size).all()
|
||||
return SuccessListPage(page, total, total_page, data_list)
|
130
backend/app/routers/project.py
Normal file
130
backend/app/routers/project.py
Normal file
@ -0,0 +1,130 @@
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from app.core.database import get_db
|
||||
from app.utils.common_util import get_total_page, get_offset, Success, SuccessListPage
|
||||
from app.models.project import Project
|
||||
from app.models.developer import Developer
|
||||
from app.core.security import get_current_user
|
||||
|
||||
# 创建路由
|
||||
router = APIRouter(prefix="/project", tags=["project"])
|
||||
|
||||
|
||||
# 创建编辑类
|
||||
class ProjectEdit(BaseModel):
|
||||
id: int = 0
|
||||
title: str
|
||||
description: str
|
||||
manager: str
|
||||
demander: str
|
||||
developers: str
|
||||
|
||||
|
||||
@router.post("/save", description="保存项目")
|
||||
async def save_project(project: ProjectEdit,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
params = Project(
|
||||
title=project.title,
|
||||
description=project.description,
|
||||
manager=project.manager,
|
||||
demander=project.demander,
|
||||
developers=project.developers,
|
||||
gmt_create=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
try:
|
||||
db.add(params)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("保存成功")
|
||||
|
||||
|
||||
@router.delete("/delete", description="删除项目")
|
||||
async def delete_project(ids: str = Query(..., description="删除项目ID列表", min_length=1),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
try:
|
||||
id_list = ids.split(",")
|
||||
db.query(Project).filter(Project.id.in_(id_list)).delete()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("删除成功")
|
||||
|
||||
|
||||
@router.put("/update/{project_id}", description="更新项目")
|
||||
async def update_project(project_id: int,
|
||||
project: ProjectEdit,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
try:
|
||||
params = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not params:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
params.title = project.title
|
||||
params.description = project.description
|
||||
params.manager = project.manager
|
||||
params.demander = project.demander
|
||||
params.developers = project.developers
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("修改成功")
|
||||
|
||||
|
||||
@router.get("/get/{project_id}", description="获取项目详情")
|
||||
async def get_project(project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
params = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not params:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
return params
|
||||
|
||||
|
||||
@router.get("/list", description="获取项目列表")
|
||||
async def list_project(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user)):
|
||||
data_list = db.query(Project).all()
|
||||
set_developer_name(data_list, db)
|
||||
return data_list
|
||||
|
||||
|
||||
@router.get("/listpage", description="获取项目分页列表")
|
||||
async def listpage_project(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user),
|
||||
title: str = Query(None, description="项目名称"),
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="分页大小")):
|
||||
query = db.query(Project)
|
||||
if title:
|
||||
query = query.filter(Project.title.ilike(f"%{title}%"))
|
||||
total = query.count()
|
||||
total_page = get_total_page(total, page_size)
|
||||
offset = get_offset(page, page_size)
|
||||
data_list = query.offset(offset).limit(page_size).all()
|
||||
# 查询开发者
|
||||
set_developer_name(data_list, db)
|
||||
return SuccessListPage(page, total, total_page, data_list)
|
||||
|
||||
|
||||
# 设置开发人
|
||||
def set_developer_name(data_list: list, db: Session):
|
||||
developer_ids = []
|
||||
for data in data_list:
|
||||
developer_id_list = data.developers.split(",")
|
||||
developer_ids.extend(developer_id_list)
|
||||
developers = db.query(Developer).filter(Developer.id.in_(developer_ids)).all()
|
||||
if len(developers) != 0:
|
||||
for data in data_list:
|
||||
developer_name_array = list([])
|
||||
for developer in developers:
|
||||
if str(developer.id) in data.developers.split(","):
|
||||
developer_name_array.append(developer.name)
|
||||
data.developer_names = ",".join(developer_name_array)
|
115
backend/app/routers/requirement.py
Normal file
115
backend/app/routers/requirement.py
Normal file
@ -0,0 +1,115 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from app.core.database import get_db
|
||||
from app.models.requirement import Requirement
|
||||
from app.models.project import Project
|
||||
from app.utils.common_util import Success, SuccessListPage, get_total_page, get_offset
|
||||
from app.core.security import get_current_user
|
||||
|
||||
|
||||
class RequirementEdit(BaseModel):
|
||||
id: int = 0
|
||||
title: str
|
||||
description: str
|
||||
project_id: int
|
||||
|
||||
|
||||
router = APIRouter(prefix="/requirement", tags=["需求管理"])
|
||||
|
||||
|
||||
@router.post("/save", description="新增需求")
|
||||
async def save_requirement(requirement: RequirementEdit,
|
||||
db: Session = Depends(get_db)):
|
||||
try:
|
||||
params = Requirement(
|
||||
title=requirement.title,
|
||||
description=requirement.description,
|
||||
project_id=requirement.project_id,
|
||||
gmt_create=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
db.add(params)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("新增成功")
|
||||
|
||||
|
||||
@router.delete("/delete", description="删除需求")
|
||||
async def delete_requirement(ids: str = Query(..., description="删除的ID列表", min_length=1),
|
||||
current_user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
try:
|
||||
id_list = ids.split(",")
|
||||
db.query(Requirement).filter(Requirement.id.in_(id_list)).delete()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("删除成功")
|
||||
|
||||
|
||||
@router.put("/update/{requirement_id}", description="修改需求")
|
||||
async def update_requirement(requirement_id: int,
|
||||
requirement: RequirementEdit,
|
||||
current_user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
try:
|
||||
params = db.query(Requirement).filter(Requirement.id == requirement_id).first()
|
||||
if not params:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
params.title = requirement.title
|
||||
params.description = requirement.description
|
||||
params.project_id = requirement.project_id
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return Success("修改成功")
|
||||
|
||||
|
||||
@router.get("/get/{requirement_id}", description="获取需求详情")
|
||||
async def get_requirement(requirement_id: int,
|
||||
current_user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
data = db.query(Requirement).filter(Requirement.id == requirement_id).first()
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="数据不存在")
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/list", description="需求列表")
|
||||
async def list_requirement(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user), ):
|
||||
return db.query(Requirement).order_by(desc(Requirement.gmt_create)).all()
|
||||
|
||||
|
||||
@router.get("/listpage", description="需求分页列表")
|
||||
async def listpage_requirement(db: Session = Depends(get_db),
|
||||
current_user: str = Depends(get_current_user),
|
||||
page: int = Query(1, ge=1, description="当前页面"),
|
||||
page_size: int = Query(10, ge=10, description="每页条数"),
|
||||
title: str = Query(None, description="标题"),
|
||||
proj_id: str = Query(None, description="项目ID")):
|
||||
query = db.query(Requirement)
|
||||
if title:
|
||||
query = query.filter(Requirement.title.ilike(f"%{title}%"))
|
||||
if proj_id:
|
||||
query = query.filter(Requirement.project_id == proj_id)
|
||||
total = query.count()
|
||||
offset = get_offset(page, page_size)
|
||||
total_page = get_total_page(page, page_size)
|
||||
data_list = query.offset(offset).limit(page_size).all()
|
||||
project_ids = list([])
|
||||
for data in data_list:
|
||||
project_ids.append(data.project_id)
|
||||
if len(project_ids) > 0:
|
||||
project_list = db.query(Project).filter(Project.id.in_(project_ids)).all()
|
||||
for project in project_list:
|
||||
for data in data_list:
|
||||
if project.id == data.project_id:
|
||||
data.project_title = project.title
|
||||
return SuccessListPage(page, total, total_page, data_list)
|
31
backend/app/utils/common_util.py
Normal file
31
backend/app/utils/common_util.py
Normal file
@ -0,0 +1,31 @@
|
||||
class Success:
|
||||
def __init__(self, msg: str):
|
||||
self.code = 0
|
||||
self.status = "success"
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class SuccessData:
|
||||
def __init__(self, data: any):
|
||||
self.code = 0
|
||||
self.status = "success"
|
||||
self.data = data
|
||||
|
||||
|
||||
class SuccessListPage:
|
||||
def __init__(self, page: int, total: int, total_page: int, data_list: list):
|
||||
self.code = 0
|
||||
self.status = "success"
|
||||
self.page = page
|
||||
self.total = total
|
||||
self.total_page = total_page
|
||||
self.data_list = data_list
|
||||
|
||||
|
||||
# 获取页数
|
||||
def get_total_page(total: int, page_size: int):
|
||||
return total / page_size if total % page_size == 0 else int(total / page_size) + 1
|
||||
|
||||
|
||||
def get_offset(page: int, page_size: int):
|
||||
return (page - 1) * page_size
|
39
backend/requirements.txt
Normal file
39
backend/requirements.txt
Normal file
@ -0,0 +1,39 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
bcrypt==3.2.0
|
||||
certifi==2025.1.31
|
||||
cffi==1.17.1
|
||||
click==8.1.8
|
||||
distro==1.9.0
|
||||
exceptiongroup==1.2.2
|
||||
fastapi==0.115.11
|
||||
greenlet==3.1.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
jiter==0.9.0
|
||||
openai==1.66.2
|
||||
pip==25.0
|
||||
pycparser==2.21
|
||||
pydantic==2.10.6
|
||||
pydantic_core==2.27.2
|
||||
pydantic-settings==2.8.1
|
||||
PyJWT==2.10.1
|
||||
PyMySQL==1.1.1
|
||||
python-dotenv==1.0.1
|
||||
python-multipart==0.0.20
|
||||
PyYAML==6.0.2
|
||||
setuptools==75.8.0
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
SQLAlchemy==2.0.38
|
||||
starlette==0.46.1
|
||||
tqdm==4.67.1
|
||||
typing_extensions==4.12.2
|
||||
uvicorn==0.34.0
|
||||
uvloop==0.21.0
|
||||
watchfiles==1.0.4
|
||||
websockets==15.0.1
|
||||
wheel==0.45.1
|
3
frontend/.env.dev
Normal file
3
frontend/.env.dev
Normal file
@ -0,0 +1,3 @@
|
||||
VITE_BACKEND_BASE_URL=http://127.0.0.1:8000
|
||||
VITE_SYSTEM_TITLE=慧脑魔方
|
||||
VITE_SYSTEM_COPYRIGHT=© 2025 Sucstep. All rights reserved.
|
3
frontend/.env.prod
Normal file
3
frontend/.env.prod
Normal file
@ -0,0 +1,3 @@
|
||||
VITE_BACKEND_BASE_URL=https://www.wgink.ink/wcb
|
||||
VITE_SYSTEM_TITLE=慧脑魔方
|
||||
VITE_SYSTEM_COPYRIGHT=© 2025 Sucstep. All rights reserved.
|
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
5
frontend/README.md
Normal file
5
frontend/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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 + Vue</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
1571
frontend/package-lock.json
generated
Normal file
1571
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"axios": "^1.8.2",
|
||||
"marked": "^15.0.7",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
27
frontend/src/App.vue
Normal file
27
frontend/src/App.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import {NMessageProvider} from 'naive-ui'
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-message-provider>
|
||||
<router-view/>
|
||||
</n-message-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
134
frontend/src/components/developer/DeveloperEdit.vue
Normal file
134
frontend/src/components/developer/DeveloperEdit.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<script setup>
|
||||
import {NForm, NFormItem, NInput, NSelect, NButton, NGrid, NGridItem, useMessage, NSpin} from 'naive-ui'
|
||||
import {ref, defineProps, defineEmits, onMounted} from "vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from '../../utils/request.js'
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
developerId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['onDataEdited'])
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
const formRef = ref(null)
|
||||
const formModel = ref({
|
||||
name: '',
|
||||
profile: '',
|
||||
post: '',
|
||||
})
|
||||
const rules = ref({
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入研发姓名',
|
||||
trigger: 'blur'
|
||||
},
|
||||
profile: {
|
||||
required: true,
|
||||
message: '请输入研发简介',
|
||||
trigger: 'blur'
|
||||
},
|
||||
post: {
|
||||
required: true,
|
||||
message: '请输入研发职位',
|
||||
trigger: 'blur'
|
||||
},
|
||||
})
|
||||
/**
|
||||
* 提交表单
|
||||
* @param e
|
||||
*/
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value.validate(error => {
|
||||
if (!error) {
|
||||
isLoading.value = true
|
||||
if(!props.developerId) {
|
||||
request.post('/developer/save', formModel.value).then(res => {
|
||||
message.success('保存成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
request.put(`/developer/update/${props.developerId}`, formModel.value).then(res => {
|
||||
message.success('修改成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const get = () => {
|
||||
isLoading.value = true;
|
||||
request.get(`/developer/get/${props.developerId}`).then(res => {
|
||||
formModel.value.name = res.data.name
|
||||
formModel.value.profile = res.data.profile
|
||||
formModel.value.post = res.data.post
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if(props.developerId) {
|
||||
get()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-spin :show="isLoading">
|
||||
<div class="form-container">
|
||||
<n-form ref="formRef"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:show-feedback="true"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
:show-require-mark="true"
|
||||
require-mark-placement="right">
|
||||
<n-form-item label="研发姓名" path="name">
|
||||
<n-input v-model:value="formModel.name"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入研发姓名"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="研发简介" path="profile">
|
||||
<n-input v-model:value="formModel.profile"
|
||||
:clearable="true"
|
||||
type="textarea"
|
||||
rows="6"
|
||||
placeholder="请输入研发简介"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="岗位" path="post">
|
||||
<n-input v-model:value="formModel.post"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入研发岗位"/>
|
||||
</n-form-item>
|
||||
<n-button type="primary" @click="handleSubmit">提交</n-button>
|
||||
</n-form>
|
||||
</div>
|
||||
</n-spin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
238
frontend/src/components/developer/DeveloperList.vue
Normal file
238
frontend/src/components/developer/DeveloperList.vue
Normal file
@ -0,0 +1,238 @@
|
||||
<script setup>
|
||||
import {NInput, NSpace, NButton, NPagination, NModal, NSelect, NSpin, NPopconfirm, NEmpty, useMessage} from 'naive-ui'
|
||||
import {onMounted, ref} from "vue";
|
||||
import DeveloperEdit from "./DeveloperEdit.vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from '../../utils/request.js'
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const isLoadingData = ref(false);
|
||||
const showDeveloperEditModal = ref(false)
|
||||
|
||||
const developerListRef = ref(null)
|
||||
const filterSectionRef = ref(null);
|
||||
const paginationRef = ref(null)
|
||||
|
||||
const cardContainerRef = ref(null)
|
||||
|
||||
const filterName = ref(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(18)
|
||||
const developerList = ref([])
|
||||
const editId = ref(null)
|
||||
|
||||
/**
|
||||
* 计算高度
|
||||
*/
|
||||
const calculateCardHeight = () => {
|
||||
const requirementListHeight = parseInt(developerListRef.value.getBoundingClientRect().height);
|
||||
const filterSectionHeight = parseInt(filterSectionRef.value.getBoundingClientRect().height);
|
||||
const paginationHeight = parseInt(paginationRef.value.getBoundingClientRect().height);
|
||||
cardContainerRef.value.style.maxHeight = `${requirementListHeight - filterSectionHeight - paginationHeight - 55}px`;
|
||||
}
|
||||
|
||||
|
||||
const listpage = () => {
|
||||
isLoadingData.value = true;
|
||||
request.get('/developer/listpage', {
|
||||
params: {
|
||||
name: filterName.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
}).then(res => {
|
||||
developerList.value = res.data.data_list
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const del = (id) => {
|
||||
isLoadingData.value = true;
|
||||
request.delete(`/developer/delete`, {
|
||||
params: {
|
||||
ids: id
|
||||
}
|
||||
}).then(res => {
|
||||
message.success('删除成功')
|
||||
listpage()
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
calculateCardHeight();
|
||||
listpage()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="developer-list" ref="developerListRef">
|
||||
<n-spin :show="isLoadingData">
|
||||
<div class="filter-section" ref="filterSectionRef">
|
||||
<div class="filter-form">
|
||||
<n-space>
|
||||
<n-space class="filter-item" align="center">
|
||||
<label>姓名</label>
|
||||
<n-input v-model:value="filterName"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
style="width: 120px"
|
||||
placeholder="输入姓名"/>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<n-space justify="space-between">
|
||||
<n-button type="primary" @click="() => {
|
||||
listpage()
|
||||
}">搜索
|
||||
</n-button>
|
||||
<n-button type="info" @click="() => {
|
||||
editId = null
|
||||
showDeveloperEditModal = true
|
||||
}">
|
||||
<i class="fas fa-plus"></i>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 项目列表 -->
|
||||
<div class="card-container" ref="cardContainerRef" v-show="developerList.length > 0">
|
||||
<div class="card-item" v-for="developer in developerList">
|
||||
<div class="name">{{ developer.name }}</div>
|
||||
<div class="profile" :title="developer.profile">{{ developer.profile }}</div>
|
||||
<div class="actions">
|
||||
<n-space justify="space-between">
|
||||
<n-space>{{ developer.post }}</n-space>
|
||||
<n-space justify="end" :size="5">
|
||||
<i class="fas fa-edit" @click="() => {
|
||||
editId = developer.id
|
||||
showDeveloperEditModal = true
|
||||
}"></i>
|
||||
<n-popconfirm
|
||||
@positive-click="() => {
|
||||
del(developer.id)
|
||||
}"
|
||||
positive-text="确定"
|
||||
negative-text="取消"
|
||||
>
|
||||
<template #trigger>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
确定删除吗?删除后数据无法恢复!
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" ref="paginationRef" v-show="developerList.length > 0">
|
||||
<n-pagination v-model:page="page"
|
||||
:page-size="pageSize"
|
||||
:on-update-page="(currentPage) => {
|
||||
page = currentPage
|
||||
listpage()
|
||||
}"/>
|
||||
</div>
|
||||
<div class="empty-container" v-show="developerList.length === 0">
|
||||
<n-empty description="暂无数据"/>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-modal v-model:show="showDeveloperEditModal"
|
||||
preset="card"
|
||||
:style="{width: '600px'}"
|
||||
title="需求编辑"
|
||||
:bordered="false">
|
||||
<DeveloperEdit :developer-id="editId"
|
||||
@on-data-edited="() => {
|
||||
showDeveloperEditModal = false
|
||||
listpage()
|
||||
}"/>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.developer-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-actions {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 10px;
|
||||
row-gap: 10px;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
|
||||
.card-item {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
height: 140px;
|
||||
min-width: 0; /** 防止grid子元素扩展 **/
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 24px;
|
||||
height: 72px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 5px;
|
||||
|
||||
.fas {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fa-edit:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.fa-trash:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
187
frontend/src/components/project/ProjectEdit.vue
Normal file
187
frontend/src/components/project/ProjectEdit.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import {NForm, NFormItem, NInput, NSelect, NButton, NGrid, NGridItem, useMessage} from 'naive-ui'
|
||||
import {defineEmits, onMounted, ref} from "vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from "../../utils/request.js";
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['onDataEdited'])
|
||||
|
||||
const isLoading = ref(false);
|
||||
const formRef = ref(null)
|
||||
const formModel = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
manager: '',
|
||||
demander: '',
|
||||
developers: [],
|
||||
})
|
||||
const rules = ref({
|
||||
title: {
|
||||
required: true,
|
||||
message: '请输入项目标题',
|
||||
trigger: 'blur'
|
||||
},
|
||||
description: {
|
||||
required: true,
|
||||
message: '请输入项目描述',
|
||||
trigger: 'blur'
|
||||
},
|
||||
manager: {
|
||||
required: true,
|
||||
message: '请输入项目经理',
|
||||
trigger: 'blur'
|
||||
},
|
||||
demander: {
|
||||
required: true,
|
||||
message: '请输入项目需求',
|
||||
trigger: 'blur'
|
||||
},
|
||||
developers: {
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: '请选择开发人员',
|
||||
trigger: 'blur'
|
||||
},
|
||||
})
|
||||
|
||||
const developerOptions = ref([])
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const listDeveloperOptions = async () => {
|
||||
request.get('/developer/list').then(res => {
|
||||
developerOptions.value = res.data.map(developer => {
|
||||
return {
|
||||
label: developer.name,
|
||||
value: developer.id + '',
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* @param e
|
||||
*/
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value.validate(error => {
|
||||
if (!error) {
|
||||
isLoading.value = true
|
||||
formModel.value.developers = formModel.value.developers.join(',')
|
||||
if (!props.projectId) {
|
||||
request.post('/project/save', formModel.value).then(res => {
|
||||
message.success('保存成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
request.put(`/project/update/${props.projectId}`, formModel.value).then(res => {
|
||||
message.success('修改成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const get = () => {
|
||||
isLoading.value = true;
|
||||
request.get(`/project/get/${props.projectId}`).then(res => {
|
||||
formModel.value.title = res.data.title
|
||||
formModel.value.description = res.data.description
|
||||
formModel.value.manager = res.data.manager
|
||||
formModel.value.demander = res.data.demander
|
||||
formModel.value.developers = res.data.developers.split(',')
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
listDeveloperOptions()
|
||||
if (props.projectId) {
|
||||
get()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<n-form ref="formRef"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:show-feedback="true"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
:show-require-mark="true"
|
||||
require-mark-placement="right">
|
||||
<n-form-item label="标题" path="title">
|
||||
<n-input v-model:value="formModel.title"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入项目标题"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="项目描述" path="description">
|
||||
<n-input v-model:value="formModel.description"
|
||||
:clearable="true"
|
||||
type="textarea"
|
||||
rows="6"
|
||||
placeholder="请输入项目描述"/>
|
||||
</n-form-item>
|
||||
<n-grid x-gap="10">
|
||||
<n-grid-item span="12">
|
||||
<n-form-item label="经理" path="manager">
|
||||
<n-input v-model:value="formModel.manager"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入项目经理"/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="12">
|
||||
<n-form-item label="需求" path="demander">
|
||||
<n-input v-model:value="formModel.demander"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入项目需求"/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-form-item label="研发" path="developers">
|
||||
<n-select v-model:value="formModel.developers"
|
||||
placeholder="请选择项目研发人员"
|
||||
multiple
|
||||
:options="developerOptions"/>
|
||||
</n-form-item>
|
||||
<n-button type="primary" @click="handleSubmit">提交</n-button>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
249
frontend/src/components/project/ProjectList.vue
Normal file
249
frontend/src/components/project/ProjectList.vue
Normal file
@ -0,0 +1,249 @@
|
||||
<script setup>
|
||||
import {NInput, NSpace, NButton, NPagination, NModal, useMessage, NSpin, NEmpty, NPopconfirm} from 'naive-ui'
|
||||
import {onMounted, ref} from "vue";
|
||||
import ProjectEdit from "./ProjectEdit.vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from "../../utils/request.js";
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const isLoadingData = ref(false);
|
||||
const showProjectEditModal = ref(false)
|
||||
|
||||
const projectListRef = ref(null)
|
||||
const filterSectionRef = ref(null);
|
||||
const paginationRef = ref(null)
|
||||
|
||||
const filterProjectName = ref('')
|
||||
const projectList = ref([])
|
||||
const cardContainerRef = ref(null)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(18)
|
||||
const editId = ref(null)
|
||||
|
||||
/**
|
||||
* 计算高度
|
||||
*/
|
||||
const calculateCardHeight = () => {
|
||||
const projectListHeight = parseInt(projectListRef.value.getBoundingClientRect().height);
|
||||
const filterSectionHeight = parseInt(filterSectionRef.value.getBoundingClientRect().height);
|
||||
const paginationHeight = parseInt(paginationRef.value.getBoundingClientRect().height);
|
||||
cardContainerRef.value.style.maxHeight = `${projectListHeight - filterSectionHeight - paginationHeight - 55}px`;
|
||||
}
|
||||
|
||||
const listpage = () => {
|
||||
isLoadingData.value = true;
|
||||
request.get('/project/listpage', {
|
||||
params: {
|
||||
title: filterProjectName.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
}).then(res => {
|
||||
projectList.value = res.data.data_list
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const del = (id) => {
|
||||
isLoadingData.value = true;
|
||||
request.delete(`/project/delete`, {
|
||||
params: {
|
||||
ids: id
|
||||
}
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).then(res => {
|
||||
message.success('删除成功')
|
||||
listpage()
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
calculateCardHeight();
|
||||
listpage()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-list" ref="projectListRef">
|
||||
<n-spin :show="isLoadingData">
|
||||
<div class="filter-section" ref="filterSectionRef">
|
||||
<div class="filter-form">
|
||||
<n-space>
|
||||
<n-space class="filter-item" align="center">
|
||||
<label>项目名称</label>
|
||||
<n-input v-model:value="filterProjectName"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="输入项目名称"/>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<n-space justify="space-between">
|
||||
<n-button type="primary" @click="() => {
|
||||
listpage()
|
||||
}">搜索</n-button>
|
||||
<n-button type="info" @click="() => {
|
||||
editId = null
|
||||
showProjectEditModal = true
|
||||
}">
|
||||
<i class="fas fa-plus"></i>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 项目列表 -->
|
||||
<div class="card-container" ref="cardContainerRef" v-show="projectList.length > 0">
|
||||
<div class="card-item" v-for="project in projectList">
|
||||
<div class="title">{{ project.title }}</div>
|
||||
<div class="description" :title="project.description">{{ project.description }}</div>
|
||||
<div class="users">
|
||||
<div class="user manager" :title="project.manager">经理:{{ project.manager }}</div>
|
||||
<div class="user demander" :title="project.demander">需求:{{ project.demander }}</div>
|
||||
<div class="user developers" :title="project.developer_names">研发:{{ project.developer_names }}</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<n-space justify="end" :size="5">
|
||||
<i class="fas fa-edit" @click="() => {
|
||||
editId = project.id
|
||||
showProjectEditModal = true
|
||||
}"></i>
|
||||
<n-popconfirm
|
||||
@positive-click="() => {
|
||||
del(project.id)
|
||||
}"
|
||||
positive-text="确定"
|
||||
negative-text="取消"
|
||||
>
|
||||
<template #trigger>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
确定删除吗?删除后数据无法恢复!
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" ref="paginationRef" v-show="projectList.length > 0">
|
||||
<n-pagination v-model:page="page"
|
||||
:page-size="pageSize"
|
||||
:on-update-page="(currentPage) => {
|
||||
page = currentPage
|
||||
listpage()
|
||||
}"/>
|
||||
</div>
|
||||
<div class="empty-container" v-show="projectList.length === 0">
|
||||
<n-empty description="暂无数据"/>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-modal v-model:show="showProjectEditModal"
|
||||
preset="card"
|
||||
:style="{width: '600px'}"
|
||||
title="项目编辑"
|
||||
:bordered="false">
|
||||
<ProjectEdit :project-id="editId"
|
||||
@on-data-edited="() => {
|
||||
showProjectEditModal = false
|
||||
listpage()
|
||||
}"/>
|
||||
</n-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-actions {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 10px;
|
||||
row-gap: 10px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.card-item {
|
||||
border: 1px solid var(--border-color);
|
||||
height: 180px;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
min-width: 0; /** 防止grid子元素扩展 **/
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 24px;
|
||||
height: 48px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.users {
|
||||
width: 100%;
|
||||
|
||||
.user {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 5px;
|
||||
|
||||
.fas {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fa-edit:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.fa-trash:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
146
frontend/src/components/requirement/RequirementEdit.vue
Normal file
146
frontend/src/components/requirement/RequirementEdit.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
import {NForm, NFormItem, NInput, NSelect, NButton, NGrid, NGridItem, useMessage} from 'naive-ui'
|
||||
import {defineEmits, onMounted, ref} from "vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from "../../utils/request.js";
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
requirementId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['onDataEdited'])
|
||||
|
||||
const isLoading = ref(false);
|
||||
const formRef = ref(null)
|
||||
const formModel = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
project_id: null,
|
||||
})
|
||||
const rules = ref({
|
||||
title: {
|
||||
required: true,
|
||||
message: '请输入需求标题',
|
||||
trigger: 'blur'
|
||||
},
|
||||
description: {
|
||||
required: true,
|
||||
message: '请输入需求描述',
|
||||
trigger: 'blur'
|
||||
},
|
||||
project_id: {
|
||||
required: true,
|
||||
message: '请选择关联项目',
|
||||
trigger: 'blur'
|
||||
},
|
||||
})
|
||||
|
||||
const projectOptions = ref([])
|
||||
|
||||
const listProjectOptions = () => {
|
||||
request.get('/project/list').then(res => {
|
||||
projectOptions.value = res.data.map(item => {
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.id + '',
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
})
|
||||
}
|
||||
|
||||
const get = () => {
|
||||
isLoading.value = true
|
||||
request.get(`/requirement/get/${props.requirementId}`).then(res => {
|
||||
formModel.value.title = res.data.title
|
||||
formModel.value.description = res.data.description
|
||||
formModel.value.project_id = res.data.project_id.toString()
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* @param e
|
||||
*/
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value.validate(error => {
|
||||
if(!error) {
|
||||
isLoading.value = true
|
||||
if (!props.requirementId) {
|
||||
request.post('/requirement/save', formModel.value).then(res => {
|
||||
message.success('保存成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
request.put(`/requirement/update/${props.requirementId}`, formModel.value).then(res => {
|
||||
message.success('修改成功')
|
||||
emits('onDataEdited')
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
listProjectOptions()
|
||||
if(props.requirementId) {
|
||||
get()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<n-form ref="formRef"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
:show-feedback="true"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
:show-require-mark="true"
|
||||
require-mark-placement="right">
|
||||
<n-form-item label="需求标题" path="title">
|
||||
<n-input v-model:value="formModel.title"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
placeholder="请输入需求标题"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="需求描述" path="description">
|
||||
<n-input v-model:value="formModel.description"
|
||||
:clearable="true"
|
||||
type="textarea"
|
||||
rows="6"
|
||||
placeholder="请输入需求描述"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="关联项目" path="project_id">
|
||||
<n-select v-model:value="formModel.project_id"
|
||||
placeholder="请选择关联项目"
|
||||
:options="projectOptions" />
|
||||
</n-form-item>
|
||||
<n-button type="primary" @click="handleSubmit">提交</n-button>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
262
frontend/src/components/requirement/RequirementList.vue
Normal file
262
frontend/src/components/requirement/RequirementList.vue
Normal file
@ -0,0 +1,262 @@
|
||||
<script setup>
|
||||
import {NInput, NSpace, NButton, NPagination, NModal, NSelect, useMessage, NPopconfirm, NEmpty, NSpin} from 'naive-ui'
|
||||
import {onMounted, ref} from "vue";
|
||||
import RequirementEdit from "./RequirementEdit.vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from "../../utils/request.js";
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter();
|
||||
const showRequirementEditModal = ref(false)
|
||||
const isLoadingData = ref(false)
|
||||
const editId = ref(null)
|
||||
const requirementListRef = ref(null)
|
||||
const filterSectionRef = ref(null);
|
||||
const paginationRef = ref(null)
|
||||
|
||||
const filterRequirement = ref(null)
|
||||
const filterProjectId = ref(null)
|
||||
const filterProjectOptions = ref([])
|
||||
const requirementList = ref([])
|
||||
const cardContainerRef = ref(null)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(18)
|
||||
|
||||
/**
|
||||
* 计算高度
|
||||
*/
|
||||
const calculateCardHeight = () => {
|
||||
const requirementListHeight = parseInt(requirementListRef.value.getBoundingClientRect().height);
|
||||
const filterSectionHeight = parseInt(filterSectionRef.value.getBoundingClientRect().height);
|
||||
const paginationHeight = parseInt(paginationRef.value.getBoundingClientRect().height);
|
||||
cardContainerRef.value.style.maxHeight = `${requirementListHeight - filterSectionHeight - paginationHeight - 55}px`;
|
||||
}
|
||||
|
||||
const listpage = () => {
|
||||
isLoadingData.value = true;
|
||||
request.get('/requirement/listpage', {
|
||||
params: {
|
||||
title: filterRequirement.value,
|
||||
project_id: filterProjectId.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
}).then(res => {
|
||||
requirementList.value = res.data.data_list
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const del = (id) => {
|
||||
isLoadingData.value = true;
|
||||
request.delete(`/requirement/delete`, {
|
||||
params: {
|
||||
ids: id
|
||||
}
|
||||
}).then(res => {
|
||||
message.success('删除成功')
|
||||
listpage()
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isLoadingData.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const listProjectOptions = () => {
|
||||
request.get('/project/list').then(res => {
|
||||
filterProjectOptions.value = res.data.map(item => {
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.id + '',
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
calculateCardHeight();
|
||||
listProjectOptions()
|
||||
listpage()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="requirement-list" ref="requirementListRef">
|
||||
<n-spin :show="isLoadingData">
|
||||
<div class="filter-section" ref="filterSectionRef">
|
||||
<div class="filter-form">
|
||||
<n-space>
|
||||
<n-space class="filter-item" align="center">
|
||||
<label>需求</label>
|
||||
<n-input v-model:value="filterRequirement"
|
||||
:clearable="true"
|
||||
type="text"
|
||||
style="width: 120px"
|
||||
placeholder="输入需求"/>
|
||||
</n-space>
|
||||
<n-space class="filter-item" align="center">
|
||||
<label>项目</label>
|
||||
<n-select v-model:value="filterProjectId"
|
||||
:options="filterProjectOptions"
|
||||
:clearable="true"
|
||||
style="width: 120px"
|
||||
placeholder="请选择项目"/>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<n-space justify="space-between">
|
||||
<n-button type="primary" @click="() => {
|
||||
listpage()
|
||||
}">搜索
|
||||
</n-button>
|
||||
<n-button type="info" @click="() => {
|
||||
editId = null
|
||||
showRequirementEditModal = true
|
||||
}">
|
||||
<i class="fas fa-plus"></i>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 项目列表 -->
|
||||
<div class="card-container" ref="cardContainerRef" v-show="requirementList.length > 0">
|
||||
<div class="card-item" v-for="requirement in requirementList">
|
||||
<div class="title">{{ requirement.title }}</div>
|
||||
<div class="description" :title="requirement.description">{{ requirement.description }}</div>
|
||||
<div class="actions">
|
||||
<n-space justify="space-between">
|
||||
<n-space>
|
||||
项目:{{ requirement.project_title }}
|
||||
</n-space>
|
||||
<n-space justify="end" :size="5">
|
||||
<i class="fas fa-edit" @click="() => {
|
||||
editId = requirement.id
|
||||
showRequirementEditModal = true
|
||||
}"></i>
|
||||
<n-popconfirm
|
||||
@positive-click="() => {
|
||||
del(requirement.id)
|
||||
}"
|
||||
positive-text="确定"
|
||||
negative-text="取消"
|
||||
>
|
||||
<template #trigger>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
确定删除吗?删除后数据无法恢复!
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" ref="paginationRef" v-show="requirementList.length > 0">
|
||||
<n-pagination v-model:page="page"
|
||||
:page-size="pageSize"
|
||||
:on-update-page="(currentPage) => {
|
||||
page = currentPage
|
||||
listpage()
|
||||
}"/>
|
||||
</div>
|
||||
<div class="empty-container" v-show="requirementList.length === 0">
|
||||
<n-empty description="暂无数据"/>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-modal v-model:show="showRequirementEditModal"
|
||||
preset="card"
|
||||
:style="{width: '600px'}"
|
||||
title="需求编辑"
|
||||
:bordered="false">
|
||||
<RequirementEdit :requirement-id="editId"
|
||||
@on-data-edited="() => {
|
||||
showRequirementEditModal = false
|
||||
listpage()
|
||||
}"/>
|
||||
</n-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.requirement-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
|
||||
.filter-actions {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
column-gap: 10px;
|
||||
row-gap: 10px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.card-item {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
height: 140px;
|
||||
min-width: 0; /** 防止grid子元素扩展 **/
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 24px;
|
||||
height: 72px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 5px;
|
||||
|
||||
.fas {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fa-edit:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.fa-trash:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import '@fortawesome/fontawesome-free/css/all.css'
|
||||
|
||||
import router from './router/router.js'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router).mount('#app')
|
25
frontend/src/router/router.js
Normal file
25
frontend/src/router/router.js
Normal file
@ -0,0 +1,25 @@
|
||||
import {createRouter, createWebHashHistory, createWebHistory} from "vue-router";
|
||||
import MainPage from "../views/MainPage.vue";
|
||||
import LoginPage from "../views/LoginPage.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
name: 'main',
|
||||
path: '/main',
|
||||
component: MainPage
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
64
frontend/src/style.css
Normal file
64
frontend/src/style.css
Normal file
@ -0,0 +1,64 @@
|
||||
:root {
|
||||
--primary-color: #2080f0;
|
||||
--danger-color: #d03050;
|
||||
--success-color: #18a058;
|
||||
--waiting-color: rgba(46, 51, 56, .05);
|
||||
--border-color: rgb(239, 239, 245);
|
||||
--table-header-bg: #3f51b5;
|
||||
--table-header-color: white;
|
||||
--table-zebra-bg: #f9f9f9;
|
||||
--table-hover-bg: #e8eaf6;
|
||||
}
|
||||
|
||||
html, body, div, span, header, main, footer, p, a, img, ul, li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
p {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ol li, ul li {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
ol li:last-child, ul li:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
ol ul {
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--table-header-bg);
|
||||
color: var(--table-header-color);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: var(--table-zebra-bg);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--table-hover-bg);
|
||||
}
|
||||
}
|
54
frontend/src/utils/request.js
Normal file
54
frontend/src/utils/request.js
Normal file
@ -0,0 +1,54 @@
|
||||
import axios from 'axios'
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
export const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_BACKEND_BASE_URL,
|
||||
timeout: 3 * 60 * 1000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
|
||||
// // 请求拦截器
|
||||
request.interceptors.request.use(request => {
|
||||
const access_token = sessionStorage.getItem('access_token');
|
||||
if (access_token) {
|
||||
request.headers.Authorization = `Bearer ${access_token}`;
|
||||
}
|
||||
return Promise.resolve(request);
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(response => {
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
return Promise.reject(response)
|
||||
}
|
||||
return Promise.resolve(response)
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
})
|
||||
|
||||
export const errorHandler = (error, message, router) => {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
message.error(error)
|
||||
break;
|
||||
case 401:
|
||||
message.error('处理未授权')
|
||||
if (router) {
|
||||
router.replace('/login')
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
message.error('处理未找到')
|
||||
break;
|
||||
case 422:
|
||||
message.error(error.statusText)
|
||||
break;
|
||||
case 500:
|
||||
message.error('系统错误')
|
||||
break;
|
||||
}
|
||||
}
|
113
frontend/src/views/LoginPage.vue
Normal file
113
frontend/src/views/LoginPage.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {NInput, NButton, useMessage, NSpin} from 'naive-ui';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {request, errorHandler} from '../utils/request.js';
|
||||
|
||||
const systemTitle = ref(import.meta.env.VITE_SYSTEM_TITLE)
|
||||
const systemCopyright = ref(import.meta.env.VITE_SYSTEM_COPYRIGHT)
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const isSubmitting = ref(false);
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!username.value || !password.value) {
|
||||
message.error('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('username', username.value);
|
||||
formData.append('password', password.value);
|
||||
|
||||
isSubmitting.value = true;
|
||||
request.post('/auth/login', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(res => {
|
||||
const access_token = res.data.access_token;
|
||||
sessionStorage.setItem('access_token', access_token);
|
||||
router.push('/main')
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
errorHandler(err, message);
|
||||
}).finally(() => {
|
||||
isSubmitting.value = true;
|
||||
})
|
||||
// 这里添加登录逻辑
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<n-spin :show="isSubmitting">
|
||||
<h1 class="system-title">{{ systemTitle }}</h1>
|
||||
<div class="form-item">
|
||||
<n-input
|
||||
v-model:value="username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
clearable
|
||||
show-password-on="click"
|
||||
/>
|
||||
</div>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
<div class="copyright">{{ systemCopyright }}</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 400px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.system-title {
|
||||
text-align: center;
|
||||
margin: 0 15px 15px 10px;
|
||||
color: #2c3e50;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
286
frontend/src/views/MainPage.vue
Normal file
286
frontend/src/views/MainPage.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<script setup>
|
||||
import {NSpace, NButton, NModal, NInput, NSelect, useMessage, NSpin, NPopconfirm} from 'naive-ui'
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import DeveloperList from "../components/developer/DeveloperList.vue";
|
||||
import ProjectList from "../components/project/ProjectList.vue";
|
||||
import RequirementList from "../components/requirement/RequirementList.vue";
|
||||
import {errorHandler, request} from "../utils/request.js";
|
||||
import {marked} from "marked";
|
||||
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const systemTitle = ref(import.meta.env.VITE_SYSTEM_TITLE)
|
||||
const systemCopyright = ref(import.meta.env.VITE_SYSTEM_COPYRIGHT)
|
||||
const showDeveloperListModal = ref(false)
|
||||
const showProjectListModal = ref(false)
|
||||
const showRequirementListModal = ref(false)
|
||||
|
||||
const mainContentRef = ref(null)
|
||||
const promptTextareaHeight = ref('100px')
|
||||
const responseChatContentHeight = ref('100px')
|
||||
|
||||
const prompt = ref(null)
|
||||
|
||||
const requirement_id = ref(null)
|
||||
const requirementOptions = ref([])
|
||||
|
||||
// 是否对话中
|
||||
const isChatting = ref(false)
|
||||
// 是否对话结束
|
||||
const isChatDone = ref(true)
|
||||
const chatResponse = ref(``)
|
||||
|
||||
const listRequirement = () => {
|
||||
request.get('/requirement/list').then(res => {
|
||||
requirementOptions.value = res.data.map(requirement => {
|
||||
return {
|
||||
label: requirement.title,
|
||||
value: requirement.id.toString()
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
const mainContentHeight = parseInt(mainContentRef.value.getBoundingClientRect().height)
|
||||
promptTextareaHeight.value = (mainContentHeight - 150) + 'px'
|
||||
responseChatContentHeight.value = (mainContentHeight - 100) + 'px'
|
||||
}, 500)
|
||||
listRequirement();
|
||||
})
|
||||
|
||||
/**
|
||||
* 提交内容
|
||||
*/
|
||||
const onChatSubmit = () => {
|
||||
isChatting.value = true
|
||||
isChatDone.value = false
|
||||
request.post('/chat/work-score', {
|
||||
prompt: prompt.value,
|
||||
requirement_id: requirement_id.value
|
||||
}).then(res => {
|
||||
chatResponse.value = res.data.data
|
||||
}).catch(error => {
|
||||
errorHandler(error, message, router)
|
||||
}).finally(() => {
|
||||
isChatDone.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
sessionStorage.removeItem('access_token');
|
||||
router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<header class="main-header">
|
||||
<div class="logo">{{systemTitle}}</div>
|
||||
<div class="header-actions">
|
||||
<n-space>
|
||||
<n-button style="background-color: #FFF" @click="showDeveloperListModal = true">
|
||||
<span>研发管理</span>
|
||||
</n-button>
|
||||
<n-button type="primary" @click="showProjectListModal = true">
|
||||
<span>项目管理</span>
|
||||
</n-button>
|
||||
<n-button type="info" @click="showRequirementListModal = true">
|
||||
<span>需求管理</span>
|
||||
</n-button>
|
||||
<n-popconfirm
|
||||
positive-text="确认"
|
||||
negative-text="取消"
|
||||
@positive-click="logout"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button strong secondary type="success" round>退出系统</n-button>
|
||||
</template>
|
||||
确认退出系统吗?
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main-content" ref="mainContentRef">
|
||||
<div class="chat-container" v-show="!isChatting">
|
||||
<div class="title">请输入需求相关代码</div>
|
||||
<div class="chat">
|
||||
<div class="prompt">
|
||||
<n-input
|
||||
v-model:value="prompt"
|
||||
:show-count="true"
|
||||
ref="promptTextareaRef"
|
||||
type="textarea"
|
||||
placeholder="请输入需求描述"
|
||||
:style="{
|
||||
height: promptTextareaHeight
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<n-space align="center" style="width: 750px;">
|
||||
选择需求
|
||||
<n-select v-model:value="requirement_id"
|
||||
:options="requirementOptions"
|
||||
placeholder="请选择需求"
|
||||
clearable
|
||||
style="width: 200px"/>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<n-button type="primary"
|
||||
circle
|
||||
:disabled="!prompt && !requirement_id"
|
||||
@click="onChatSubmit"
|
||||
>
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-response" v-show="isChatting">
|
||||
<div class="content markdown-content"
|
||||
:style="{height: responseChatContentHeight}">
|
||||
<div v-html="marked(chatResponse)"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-space align="center" v-if="!isChatDone">
|
||||
<n-spin size="small"/>
|
||||
<span style="color: var(--success-color)">AI正在分析</span>
|
||||
</n-space>
|
||||
<n-space v-if="isChatDone"
|
||||
style="color: var(--primary-color)">分析结束
|
||||
</n-space>
|
||||
<n-button type="primary"
|
||||
circle
|
||||
:disabled="!isChatDone"
|
||||
@click="() => {
|
||||
isChatting = false
|
||||
chatResponse = ''
|
||||
}"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="main-footer">
|
||||
<p>{{systemCopyright}}</p>
|
||||
</footer>
|
||||
<n-modal v-model:show="showDeveloperListModal"
|
||||
preset="card"
|
||||
:style="{width: '1000px', height: responseChatContentHeight}"
|
||||
title="研发管理"
|
||||
size="small"
|
||||
:bordered="false">
|
||||
<DeveloperList/>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showProjectListModal"
|
||||
preset="card"
|
||||
:style="{width: '1000px', height: responseChatContentHeight}"
|
||||
title="项目管理"
|
||||
size="small"
|
||||
:bordered="false">
|
||||
<ProjectList/>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showRequirementListModal"
|
||||
preset="card"
|
||||
:style="{width: '1000px', height: responseChatContentHeight}"
|
||||
title="需求管理"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
@close="() => {
|
||||
listRequirement()
|
||||
}">
|
||||
<RequirementList/>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
.main-header {
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color)
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.chat-container {
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat {
|
||||
margin-top: 10px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-response {
|
||||
width: 800px;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
|
||||
table {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-footer {
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
background-color: #f5f5f5;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
mode: 'prod',
|
||||
base: './',
|
||||
})
|
Loading…
Reference in New Issue
Block a user