save code
This commit is contained in:
@@ -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.
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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()) # 更新时间
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
<h3>{project.name}</h3>
|
<div className="project-header">
|
||||||
<p>{project.description}</p>
|
<div className="project-header-left">
|
||||||
<div className="project-details">
|
<h3>{project.name}</h3>
|
||||||
<span>部门: {project.department}</span>
|
<div className="project-code">项目编号: {project.project_code || '-'}</div>
|
||||||
<span>预算: {project.budget}</span>
|
{project.description && (
|
||||||
<span>状态: {project.status}</span>
|
<div className="project-description">{project.description}</div>
|
||||||
|
)}
|
||||||
|
</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)}>
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ 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');
|
||||||
window.location.href = '/login';
|
// 如果不是在登录页,才跳转到登录页
|
||||||
|
if (!window.location.pathname.includes('/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}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user