This commit is contained in:
wanggeng 2025-03-14 16:57:26 +08:00
commit 6b6bbab434
43 changed files with 4154 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea
dist*
node_modules
.DS_Store
offline_packages
__pycache__

0
backend/app/__init__.py Normal file
View File

View File

View 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()

View 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()

View File

View 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
View 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)

View File

View 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)

View 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)

View 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)

View 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"
}

View 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)

View 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)

View 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)

View 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)

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
frontend/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

23
frontend/package.json Normal file
View 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
View 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
View 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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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')

View 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
View 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);
}
}

View 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;
}
}

View 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>

View 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
View 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: './',
})