save code

This commit is contained in:
Your Name
2026-01-27 17:30:17 +08:00
parent a416e2091f
commit 884b9f47a2
17 changed files with 910 additions and 44 deletions
+6 -3
View File
@@ -101,11 +101,14 @@
6. 访问: `http://localhost:8000` 6. 访问: `http://localhost:8000`
7. API文档: `http://localhost:8000/docs` 7. API文档: `http://localhost:8000/docs`
## 数据库配置 ##### 数据库配置
1. 创建数据库: `CREATE DATABASE project_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` 1. 创建数据库: `CREATE DATABASE project_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`
2. 执行数据库初始化脚本: `docs/init.sql` 2. 创建用户并授权: `CREATE USER 'project_user'@'localhost' IDENTIFIED BY 'project_password'; GRANT ALL PRIVILEGES ON project_management.* TO 'project_user'@'localhost'; FLUSH PRIVILEGES;`
3. 配置数据库连接信息 3. 执行数据库初始化脚本: `docs/init.sql`
4. 配置数据库连接信息
**注意**: 系统默认使用MySQL数据库,不再支持SQLite。
## 系统初始化 ## 系统初始化
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -3,11 +3,12 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import os import os
import bcrypt
from app.config import settings from app.config import settings
from app.database.database import get_db from app.database.database import get_db
from app.models.user import User from app.models.user import User
from app.schemas.auth import Token from app.schemas.auth import Token
from app.common.utils import verify_password, create_access_token from app.common.utils import create_access_token
router = APIRouter(prefix="/auth", tags=["认证"]) router = APIRouter(prefix="/auth", tags=["认证"])
@@ -17,8 +18,8 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
"""用户登录""" """用户登录"""
user = db.query(User).filter(User.username == form_data.username).first() user = db.query(User).filter(User.username == form_data.username).first()
# 密码验证:比较用户存储的密码 # 密码验证:使用bcrypt直接验证密码
if not user or form_data.password != user.password: if not user or not bcrypt.checkpw(form_data.password.encode('utf-8'), user.password.encode('utf-8')):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
+36 -3
View File
@@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.database.database import get_db from app.database.database import get_db
@@ -16,11 +16,32 @@ router = APIRouter(prefix="/projects", tags=["项目管理"])
def get_projects( def get_projects(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
min_budget: Optional[int] = None,
max_budget: Optional[int] = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
"""获取项目列表""" """获取项目列表"""
projects = db.query(Project).offset(skip).limit(limit).all() projects = db.query(Project)
# 按开始日期筛选
if start_date:
projects = projects.filter(Project.start_date >= start_date)
# 按结束日期筛选
if end_date:
projects = projects.filter(Project.end_date <= end_date)
# 按预算范围筛选
if min_budget is not None:
projects = projects.filter(Project.budget >= min_budget)
if max_budget is not None:
projects = projects.filter(Project.budget <= max_budget)
projects = projects.offset(skip).limit(limit).all()
return projects return projects
@@ -38,12 +59,24 @@ def create_project(
) )
# 创建新项目 # 创建新项目
db_project = Project( db_project = Project(
project_code=project.project_code,
name=project.name, name=project.name,
project_type=project.project_type,
description=project.description, description=project.description,
department=project.department, department=project.department,
contract_amount=project.contract_amount,
project_manager=project.project_manager,
client_company=project.client_company,
client_contact=project.client_contact,
contract_date=project.contract_date,
budget=project.budget, budget=project.budget,
start_date=project.start_date, start_date=project.start_date,
end_date=project.end_date, planned_end_date=project.planned_end_date,
actual_end_date=project.actual_end_date,
progress=project.progress,
actual_received_amount=project.actual_received_amount,
payment_completion_rate=project.payment_completion_rate,
payment_terms=project.payment_terms,
status=project.status, status=project.status,
created_by=current_user.id created_by=current_user.id
) )
+23 -11
View File
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.database.database import Base from app.database.database import Base
@@ -8,13 +8,25 @@ class Project(Base):
__tablename__ = "projects" __tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False) project_code = Column(String(50), unique=True, index=True) # 项目编号
description = Column(Text) name = Column(String(100), nullable=False) # 项目名称
created_by = Column(Integer, ForeignKey("users.id"), nullable=False) project_type = Column(String(50)) # 工程类别
department = Column(String(50), nullable=False) description = Column(Text) # 项目描述
budget = Column(Integer) created_by = Column(Integer, ForeignKey("users.id"), nullable=False) # 创建人
start_date = Column(DateTime(timezone=True)) department = Column(String(50), nullable=False) # 所属部门
end_date = Column(DateTime(timezone=True)) contract_amount = Column(Float) # 合同金额(万元)
status = Column(String(20), default="pending") project_manager = Column(String(100)) # 项目负责人
created_at = Column(DateTime(timezone=True), server_default=func.now()) client_company = Column(String(100)) # 建设单位
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) client_contact = Column(String(100)) # 建设单位联系人
contract_date = Column(DateTime(timezone=True)) # 合同签订日期
budget = Column(Integer) # 预算
start_date = Column(DateTime(timezone=True)) # 开始日期
planned_end_date = Column(DateTime(timezone=True)) # 计划结束日期
actual_end_date = Column(DateTime(timezone=True)) # 实际结束日期
progress = Column(Float, default=0.0) # 累计进度(%
actual_received_amount = Column(Float, default=0.0) # 实际收款金额(万元)
payment_completion_rate = Column(Float, default=0.0) # 回款完成率(%
payment_terms = Column(Text) # 付款方式
status = Column(String(20), default="pending") # 状态
created_at = Column(DateTime(timezone=True), server_default=func.now()) # 创建时间
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # 更新时间
+32 -8
View File
@@ -5,13 +5,25 @@ from typing import Optional
class ProjectBase(BaseModel): class ProjectBase(BaseModel):
"""项目基础模型""" """项目基础模型"""
name: str project_code: Optional[str] = None # 项目编号
description: Optional[str] = None name: str # 项目名称
department: str project_type: Optional[str] = None # 工程类别
budget: Optional[int] = None description: Optional[str] = None # 项目描述
start_date: Optional[datetime] = None department: str # 所属部门
end_date: Optional[datetime] = None contract_amount: Optional[float] = None # 合同金额(万元)
status: Optional[str] = "pending" project_manager: Optional[str] = None # 项目负责人
client_company: Optional[str] = None # 建设单位
client_contact: Optional[str] = None # 建设单位联系人
contract_date: Optional[datetime] = None # 合同签订日期
budget: Optional[int] = None # 预算
start_date: Optional[datetime] = None # 开始日期
planned_end_date: Optional[datetime] = None # 计划结束日期
actual_end_date: Optional[datetime] = None # 实际结束日期
progress: Optional[float] = 0.0 # 累计进度(%
actual_received_amount: Optional[float] = 0.0 # 实际收款金额(万元)
payment_completion_rate: Optional[float] = 0.0 # 回款完成率(%
payment_terms: Optional[str] = None # 付款方式
status: Optional[str] = "pending" # 状态
class ProjectCreate(ProjectBase): class ProjectCreate(ProjectBase):
@@ -21,12 +33,24 @@ class ProjectCreate(ProjectBase):
class ProjectUpdate(BaseModel): class ProjectUpdate(BaseModel):
"""更新项目模型""" """更新项目模型"""
project_code: Optional[str] = None
name: Optional[str] = None name: Optional[str] = None
project_type: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
department: Optional[str] = None department: Optional[str] = None
contract_amount: Optional[float] = None
project_manager: Optional[str] = None
client_company: Optional[str] = None
client_contact: Optional[str] = None
contract_date: Optional[datetime] = None
budget: Optional[int] = None budget: Optional[int] = None
start_date: Optional[datetime] = None start_date: Optional[datetime] = None
end_date: Optional[datetime] = None planned_end_date: Optional[datetime] = None
actual_end_date: Optional[datetime] = None
progress: Optional[float] = None
actual_received_amount: Optional[float] = None
payment_completion_rate: Optional[float] = None
payment_terms: Optional[str] = None
status: Optional[str] = None status: Optional[str] = None
+59
View File
@@ -0,0 +1,59 @@
from app.database.database import SessionLocal
from app.models import User
import bcrypt
def create_initial_users():
"""创建初始用户"""
db = SessionLocal()
try:
# 检查是否已有用户
existing_users = db.query(User).count()
if existing_users > 0:
print(f"Database already has {existing_users} users. Skipping initialization.")
return
# 创建初始用户
password = "123456"
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
users = [
User(
username="admin",
password=hashed_password,
department="IT",
role="admin"
),
User(
username="marketing",
password=hashed_password,
department="Marketing",
role="marketing"
),
User(
username="other",
password=hashed_password,
department="Other",
role="other"
)
]
for user in users:
db.add(user)
db.commit()
print("Initial users created successfully!")
print("Username: admin, Password: 123456")
print("Username: marketing, Password: 123456")
print("Username: other, Password: 123456")
except Exception as e:
db.rollback()
print(f"Error creating initial users: {e}")
finally:
db.close()
if __name__ == "__main__":
create_initial_users()
+89
View File
@@ -0,0 +1,89 @@
import pandas as pd
from datetime import datetime
from app.database.database import SessionLocal
from app.models import Project, User
def import_projects_from_excel():
"""从Excel文件导入项目数据"""
db = SessionLocal()
try:
# 读取Excel文件
excel_path = "/home/xsl/code/xsl_node/docs/example.xls"
print(f"Reading Excel file: {excel_path}")
# 读取所有工作表
xls = pd.ExcelFile(excel_path)
print(f"Excel file has sheets: {xls.sheet_names}")
# 读取第一个工作表
df = pd.read_excel(xls, sheet_name=0)
print(f"Read {len(df)} rows from Excel")
# 获取marketing用户ID
marketing_user = db.query(User).filter(User.username == "marketing").first()
if not marketing_user:
print("Marketing user not found. Exiting.")
return
# 准备导入的项目数据
projects_to_import = []
skipped_rows = 0
for index, row in df.iterrows():
try:
# 创建项目对象
project = Project(
project_code=str(row.get('项目编号', f'PROJ{index+1}')),
name=str(row.get('项目名称', f'项目{index+1}')),
project_type=str(row.get('工程类别', '')),
description=str(row.get('项目描述', '')),
department=str(row.get('所属部门', '未知部门')),
contract_amount=float(row.get('合同金额(万元)', 0)) if pd.notna(row.get('合同金额(万元)')) else 0,
project_manager=str(row.get('项目负责人', '')),
client_company=str(row.get('建设单位', '')),
client_contact=str(row.get('建设单位联系人', '')),
contract_date=pd.to_datetime(row.get('合同签订日期')).date() if pd.notna(row.get('合同签订日期')) else None,
start_date=pd.to_datetime(row.get('开始日期')).date() if pd.notna(row.get('开始日期')) else None,
planned_end_date=pd.to_datetime(row.get('计划结束日期')).date() if pd.notna(row.get('计划结束日期')) else None,
actual_end_date=pd.to_datetime(row.get('实际结束日期')).date() if pd.notna(row.get('实际结束日期')) else None,
progress=float(row.get('累计进度(%', 0)) if pd.notna(row.get('累计进度(%')) else 0,
actual_received_amount=float(row.get('实际收款金额(万元)', 0)) if pd.notna(row.get('实际收款金额(万元)')) else 0,
payment_completion_rate=float(row.get('回款完成率(%', 0)) if pd.notna(row.get('回款完成率(%')) else 0,
payment_terms=str(row.get('付款方式', '')),
status=str(row.get('状态', 'pending')),
created_by=marketing_user.id
)
projects_to_import.append(project)
# 每100条记录打印一次进度
if (index + 1) % 100 == 0:
print(f"Processed {index + 1} rows...")
except Exception as e:
print(f"Error processing row {index + 1}: {e}")
skipped_rows += 1
continue
# 批量导入数据
if projects_to_import:
print(f"Importing {len(projects_to_import)} projects to database...")
db.add_all(projects_to_import)
db.commit()
print(f"Successfully imported {len(projects_to_import)} projects!")
print(f"Skipped {skipped_rows} rows due to errors")
else:
print("No projects to import")
except Exception as e:
db.rollback()
print(f"Error importing projects: {e}")
import traceback
traceback.print_exc()
finally:
db.close()
if __name__ == "__main__":
import_projects_from_excel()
+13
View File
@@ -0,0 +1,13 @@
from app.database.database import engine, Base
from app.models import User, Project, ProjectHistory
def init_db():
"""初始化数据库"""
print("Creating database tables...")
Base.metadata.create_all(bind=engine)
print("Database tables created successfully!")
if __name__ == "__main__":
init_db()
+168
View File
@@ -0,0 +1,168 @@
from app.database.database import engine, Base
from app.models import User, Project
from app.database.database import SessionLocal
import bcrypt
from datetime import datetime
def init_db():
"""初始化数据库"""
print("Creating database tables...")
Base.metadata.create_all(bind=engine)
print("Database tables created successfully!")
def create_initial_users():
"""创建初始用户"""
db = SessionLocal()
try:
# 检查是否已有用户
existing_users = db.query(User).count()
if existing_users > 0:
print(f"Database already has {existing_users} users. Skipping initialization.")
return
# 创建初始用户
password = "123456"
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
users = [
User(
username="admin",
password=hashed_password,
department="IT",
role="admin"
),
User(
username="marketing",
password=hashed_password,
department="Marketing",
role="marketing"
),
User(
username="other",
password=hashed_password,
department="Other",
role="other"
)
]
for user in users:
db.add(user)
db.commit()
print("Initial users created successfully!")
print("Username: admin, Password: 123456")
print("Username: marketing, Password: 123456")
print("Username: other, Password: 123456")
except Exception as e:
db.rollback()
print(f"Error creating initial users: {e}")
finally:
db.close()
def create_sample_projects():
"""创建示例项目数据"""
db = SessionLocal()
try:
# 检查是否已有项目
existing_projects = db.query(Project).count()
if existing_projects > 0:
print(f"Database already has {existing_projects} projects. Skipping initialization.")
return
# 获取marketing用户ID
marketing_user = db.query(User).filter(User.username == "marketing").first()
if not marketing_user:
print("Marketing user not found. Skipping project creation.")
return
# 创建示例项目
projects = [
Project(
project_code="chdj2023178",
name="台江县井洞塘抽水变设备调试",
project_type="分部用户工程",
description="台江县井洞塘抽水变设备调试项目",
department="台江项目部",
contract_amount=0.60,
project_manager="杨如云559714",
client_company="张小平",
client_contact="张小平15908557788",
contract_date=datetime(2023, 6, 8),
start_date=datetime(2023, 6, 8),
planned_end_date=datetime(2023, 6, 13),
actual_end_date=datetime(2023, 6, 13),
progress=1.00,
actual_received_amount=0.60,
payment_completion_rate=1.00,
payment_terms="合同签生效后,5个工作日内,甲方向乙方支付全部工程款,工程竣工后,乙方应出具试验报告及试验资质给甲方,乙方拨付每笔工程款时应向甲方提供等额的增值税发票。",
status="completed",
created_by=marketing_user.id
),
Project(
project_code="chdj2023179",
name="凯里市开怀街道配电工程",
project_type="配电工程",
description="凯里市开怀街道配电工程建设",
department="凯里项目部",
contract_amount=120.50,
project_manager="王小明559715",
client_company="凯里市供电局",
client_contact="李经理13985556677",
contract_date=datetime(2023, 5, 15),
start_date=datetime(2023, 5, 20),
planned_end_date=datetime(2023, 8, 30),
actual_end_date=datetime(2023, 9, 5),
progress=1.00,
actual_received_amount=120.50,
payment_completion_rate=1.00,
payment_terms="合同签订后支付30%预付款,工程进度达到50%时支付30%进度款,工程竣工验收后支付35%工程款,预留5%质保金一年后支付。",
status="completed",
created_by=marketing_user.id
),
Project(
project_code="chdj2023180",
name="雷山县西江镇线路改造工程",
project_type="线路工程",
description="雷山县西江镇10kV线路改造工程",
department="雷山项目部",
contract_amount=85.20,
project_manager="陈小红559716",
client_company="雷山县供电局",
client_contact="张主任13885554433",
contract_date=datetime(2023, 7, 10),
start_date=datetime(2023, 7, 15),
planned_end_date=datetime(2023, 10, 20),
actual_end_date=None,
progress=0.75,
actual_received_amount=63.90,
payment_completion_rate=0.75,
payment_terms="按工程进度付款,每月25日上报进度,次月10日前支付进度款的80%,竣工验收后支付至95%,质保金5%一年后支付。",
status="in_progress",
created_by=marketing_user.id
)
]
for project in projects:
db.add(project)
db.commit()
print("Sample projects created successfully!")
print(f"Created {len(projects)} sample projects.")
except Exception as e:
db.rollback()
print(f"Error creating sample projects: {e}")
finally:
db.close()
if __name__ == "__main__":
init_db()
create_initial_users()
create_sample_projects()
+271
View File
@@ -0,0 +1,271 @@
.project-list {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.project-list h2 {
margin-bottom: 30px;
color: #333;
}
/* 筛选表单样式 */
.filter-form-container {
background: #f9fafb;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px solid #e5e7eb;
}
.filter-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.filter-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 200px;
}
.filter-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #374151;
}
.filter-group input {
width: 100%;
padding: 10px;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 14px;
}
.filter-group input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.filter-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.filter-button, .reset-button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-button {
background: #6366f1;
color: white;
}
.filter-button:hover {
background: #4f46e5;
transform: translateY(-1px);
}
.reset-button {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}
.reset-button:hover {
background: #e5e7eb;
transform: translateY(-1px);
}
.projects-container {
display: flex;
flex-direction: column;
gap: 1px;
background: #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.project-card {
background: white;
padding: 20px;
transition: all 0.2s ease;
border-bottom: 1px solid #e5e7eb;
}
.project-card:last-child {
border-bottom: none;
}
.project-card:hover {
background: #f9fafb;
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.project-header-left {
flex: 1;
}
.project-header h3 {
margin: 0 0 5px 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.project-code {
font-size: 14px;
color: #666;
font-weight: 500;
}
.project-description {
margin: 10px 0;
color: #666;
line-height: 1.4;
font-size: 14px;
}
.project-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px 20px;
margin: 15px 0;
}
.detail-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-group-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #e5e7eb;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-label {
font-size: 12px;
font-weight: 500;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 14px;
color: #374151;
font-weight: 500;
}
.project-actions {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e5e7eb;
}
.edit-button, .delete-button {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.edit-button {
background: #3b82f6;
color: white;
}
.edit-button:hover {
background: #2563eb;
}
.delete-button {
background: #ef4444;
color: white;
}
.delete-button:hover {
background: #dc2626;
}
.empty-message {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
background: #f9fafb;
border-radius: 8px;
border: 2px dashed #e5e7eb;
margin: 0;
}
.loading {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
}
.error {
text-align: center;
padding: 20px;
color: #dc2626;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
margin-bottom: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
}
.filter-group {
min-width: 100%;
}
.projects-container {
grid-template-columns: 1fr;
}
}
+191 -7
View File
@@ -1,20 +1,27 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { projectApi } from '../services/api'; import { projectApi } from '../services/api';
import './ProjectList.css';
function ProjectList() { function ProjectList() {
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [filters, setFilters] = useState({
start_date: '',
end_date: '',
min_budget: '',
max_budget: ''
});
useEffect(() => { useEffect(() => {
fetchProjects(); fetchProjects();
}, []); }, []);
const fetchProjects = async () => { const fetchProjects = async (filterParams = {}) => {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const data = await projectApi.getProjects(); const data = await projectApi.getProjects(filterParams);
setProjects(data); setProjects(data);
} catch (err) { } catch (err) {
setError('获取项目列表失败'); setError('获取项目列表失败');
@@ -24,6 +31,34 @@ function ProjectList() {
} }
}; };
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({
...prev,
[name]: value
}));
};
const handleFilterSubmit = (e) => {
e.preventDefault();
const filterParams = {};
if (filters.start_date) filterParams.start_date = filters.start_date;
if (filters.end_date) filterParams.end_date = filters.end_date;
if (filters.min_budget) filterParams.min_budget = parseInt(filters.min_budget);
if (filters.max_budget) filterParams.max_budget = parseInt(filters.max_budget);
fetchProjects(filterParams);
};
const handleResetFilters = () => {
setFilters({
start_date: '',
end_date: '',
min_budget: '',
max_budget: ''
});
fetchProjects();
};
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (window.confirm('确定要删除这个项目吗?')) { if (window.confirm('确定要删除这个项目吗?')) {
try { try {
@@ -47,19 +82,168 @@ function ProjectList() {
return ( return (
<div className="project-list"> <div className="project-list">
<h2>项目列表</h2> <h2>项目列表</h2>
{/* 筛选表单 */}
<div className="filter-form-container">
<form onSubmit={handleFilterSubmit} className="filter-form">
<div className="filter-row">
<div className="filter-group">
<label htmlFor="start_date">开始日期</label>
<input
type="date"
id="start_date"
name="start_date"
value={filters.start_date}
onChange={handleFilterChange}
/>
</div>
<div className="filter-group">
<label htmlFor="end_date">结束日期</label>
<input
type="date"
id="end_date"
name="end_date"
value={filters.end_date}
onChange={handleFilterChange}
/>
</div>
<div className="filter-group">
<label htmlFor="min_budget">最小预算</label>
<input
type="number"
id="min_budget"
name="min_budget"
value={filters.min_budget}
onChange={handleFilterChange}
placeholder="0"
/>
</div>
<div className="filter-group">
<label htmlFor="max_budget">最大预算</label>
<input
type="number"
id="max_budget"
name="max_budget"
value={filters.max_budget}
onChange={handleFilterChange}
placeholder="999999"
/>
</div>
</div>
<div className="filter-actions">
<button type="submit" className="filter-button">筛选</button>
<button type="button" className="reset-button" onClick={handleResetFilters}>重置</button>
</div>
</form>
</div>
<div className="projects-container"> <div className="projects-container">
{projects.length === 0 ? ( {projects.length === 0 ? (
<div className="empty-message">暂无项目</div> <div className="empty-message">暂无项目</div>
) : ( ) : (
projects.map((project) => ( projects.map((project) => (
<div key={project.id} className="project-card"> <div key={project.id} className="project-card">
<div className="project-header">
<div className="project-header-left">
<h3>{project.name}</h3> <h3>{project.name}</h3>
<p>{project.description}</p> <div className="project-code">项目编号: {project.project_code || '-'}</div>
<div className="project-details"> {project.description && (
<span>部门: {project.department}</span> <div className="project-description">{project.description}</div>
<span>预算: {project.budget}</span> )}
<span>状态: {project.status}</span>
</div> </div>
</div>
<div className="project-details">
{/* 基本信息 */}
<div className="detail-group">
<div className="detail-group-title">基本信息</div>
<div className="detail-row">
<div className="detail-label">工程类别</div>
<div className="detail-value">{project.project_type || '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">所属部门</div>
<div className="detail-value">{project.department}</div>
</div>
<div className="detail-row">
<div className="detail-label">状态</div>
<div className="detail-value">{project.status || '-'}</div>
</div>
</div>
{/* 财务信息 */}
<div className="detail-group">
<div className="detail-group-title">财务信息</div>
<div className="detail-row">
<div className="detail-label">合同金额</div>
<div className="detail-value">{project.contract_amount || 0} 万元</div>
</div>
<div className="detail-row">
<div className="detail-label">实际收款</div>
<div className="detail-value">{project.actual_received_amount || 0} 万元</div>
</div>
<div className="detail-row">
<div className="detail-label">回款完成率</div>
<div className="detail-value">{project.payment_completion_rate || 0}%</div>
</div>
</div>
{/* 人员信息 */}
<div className="detail-group">
<div className="detail-group-title">人员信息</div>
<div className="detail-row">
<div className="detail-label">项目负责人</div>
<div className="detail-value">{project.project_manager || '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">建设单位</div>
<div className="detail-value">{project.client_company || '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">联系人</div>
<div className="detail-value">{project.client_contact || '-'}</div>
</div>
</div>
{/* 时间信息 */}
<div className="detail-group">
<div className="detail-group-title">时间信息</div>
<div className="detail-row">
<div className="detail-label">合同签订</div>
<div className="detail-value">{project.contract_date ? new Date(project.contract_date).toLocaleDateString() : '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">开始日期</div>
<div className="detail-value">{project.start_date ? new Date(project.start_date).toLocaleDateString() : '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">计划结束</div>
<div className="detail-value">{project.planned_end_date ? new Date(project.planned_end_date).toLocaleDateString() : '-'}</div>
</div>
<div className="detail-row">
<div className="detail-label">实际结束</div>
<div className="detail-value">{project.actual_end_date ? new Date(project.actual_end_date).toLocaleDateString() : '-'}</div>
</div>
</div>
{/* 进度信息 */}
<div className="detail-group">
<div className="detail-group-title">进度信息</div>
<div className="detail-row">
<div className="detail-label">累计进度</div>
<div className="detail-value">{project.progress || 0}%</div>
</div>
</div>
{/* 付款方式 */}
<div className="detail-group">
<div className="detail-group-title">付款方式</div>
<div className="detail-row">
<div className="detail-value">{project.payment_terms || '-'}</div>
</div>
</div>
</div>
<div className="project-actions"> <div className="project-actions">
<button className="edit-button">编辑</button> <button className="edit-button">编辑</button>
<button className="delete-button" onClick={() => handleDelete(project.id)}> <button className="delete-button" onClick={() => handleDelete(project.id)}>
+16 -7
View File
@@ -30,10 +30,13 @@ api.interceptors.response.use(
}, },
error => { error => {
if (error.response && error.response.status === 401) { if (error.response && error.response.status === 401) {
// 清除token并跳转到登录页 // 清除token
localStorage.removeItem('token'); localStorage.removeItem('token');
// 如果不是在登录页,才跳转到登录页
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login'; window.location.href = '/login';
} }
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
@@ -41,10 +44,11 @@ api.interceptors.response.use(
// 认证相关API // 认证相关API
export const authApi = { export const authApi = {
login: (username, password) => { login: (username, password) => {
const formData = new FormData(); // 创建URLSearchParams对象来发送表单数据
formData.append('username', username); const params = new URLSearchParams();
formData.append('password', password); params.append('username', username);
return api.post('/auth/login', formData, { params.append('password', password);
return api.post('/auth/login', params, {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
@@ -54,8 +58,13 @@ export const authApi = {
// 项目相关API // 项目相关API
export const projectApi = { export const projectApi = {
getProjects: () => { getProjects: (filters = {}) => {
return api.get('/projects'); const params = new URLSearchParams();
if (filters.start_date) params.append('start_date', filters.start_date);
if (filters.end_date) params.append('end_date', filters.end_date);
if (filters.min_budget) params.append('min_budget', filters.min_budget);
if (filters.max_budget) params.append('max_budget', filters.max_budget);
return api.get(`/projects?${params.toString()}`);
}, },
getProject: (id) => { getProject: (id) => {
return api.get(`/projects/${id}`); return api.get(`/projects/${id}`);