diff --git a/README.md b/README.md index 3244ba3..969a4ec 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,14 @@ 6. 访问: `http://localhost:8000` 7. API文档: `http://localhost:8000/docs` -## 数据库配置 +##### 数据库配置 1. 创建数据库: `CREATE DATABASE project_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` -2. 执行数据库初始化脚本: `docs/init.sql` -3. 配置数据库连接信息 +2. 创建用户并授权: `CREATE USER 'project_user'@'localhost' IDENTIFIED BY 'project_password'; GRANT ALL PRIVILEGES ON project_management.* TO 'project_user'@'localhost'; FLUSH PRIVILEGES;` +3. 执行数据库初始化脚本: `docs/init.sql` +4. 配置数据库连接信息 + +**注意**: 系统默认使用MySQL数据库,不再支持SQLite。 ## 系统初始化 diff --git a/backend/app/api/__pycache__/auth.cpython-310.pyc b/backend/app/api/__pycache__/auth.cpython-310.pyc index b4dd7ca..076fa09 100644 Binary files a/backend/app/api/__pycache__/auth.cpython-310.pyc and b/backend/app/api/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/project.cpython-310.pyc b/backend/app/api/__pycache__/project.cpython-310.pyc index 21fe95d..5996ea5 100644 Binary files a/backend/app/api/__pycache__/project.cpython-310.pyc and b/backend/app/api/__pycache__/project.cpython-310.pyc differ diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 06a389b..5656a66 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -3,11 +3,12 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session import os +import bcrypt from app.config import settings from app.database.database import get_db from app.models.user import User 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=["认证"]) @@ -17,8 +18,8 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend """用户登录""" user = db.query(User).filter(User.username == form_data.username).first() - # 密码验证:比较用户存储的密码 - if not user or form_data.password != user.password: + # 密码验证:使用bcrypt直接验证密码 + if not user or not bcrypt.checkpw(form_data.password.encode('utf-8'), user.password.encode('utf-8')): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", diff --git a/backend/app/api/project.py b/backend/app/api/project.py index f772b17..11c754a 100644 --- a/backend/app/api/project.py +++ b/backend/app/api/project.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.database.database import get_db @@ -16,11 +16,32 @@ router = APIRouter(prefix="/projects", tags=["项目管理"]) def get_projects( skip: int = 0, 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), 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 @@ -38,12 +59,24 @@ def create_project( ) # 创建新项目 db_project = Project( + project_code=project.project_code, name=project.name, + project_type=project.project_type, description=project.description, 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, 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, created_by=current_user.id ) diff --git a/backend/app/database/__pycache__/database.cpython-310.pyc b/backend/app/database/__pycache__/database.cpython-310.pyc index ce902c4..961041e 100644 Binary files a/backend/app/database/__pycache__/database.cpython-310.pyc and b/backend/app/database/__pycache__/database.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/project.cpython-310.pyc b/backend/app/models/__pycache__/project.cpython-310.pyc index ef9ac43..5355ace 100644 Binary files a/backend/app/models/__pycache__/project.cpython-310.pyc and b/backend/app/models/__pycache__/project.cpython-310.pyc differ diff --git a/backend/app/models/project.py b/backend/app/models/project.py index fbbba9b..b2fc9c3 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -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 app.database.database import Base @@ -8,13 +8,25 @@ class Project(Base): __tablename__ = "projects" id = Column(Integer, primary_key=True, index=True) - name = Column(String(100), nullable=False) - description = Column(Text) - created_by = Column(Integer, ForeignKey("users.id"), nullable=False) - department = Column(String(50), nullable=False) - budget = Column(Integer) - start_date = Column(DateTime(timezone=True)) - end_date = Column(DateTime(timezone=True)) - 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()) + project_code = Column(String(50), unique=True, index=True) # 项目编号 + name = Column(String(100), nullable=False) # 项目名称 + project_type = Column(String(50)) # 工程类别 + description = Column(Text) # 项目描述 + created_by = Column(Integer, ForeignKey("users.id"), nullable=False) # 创建人 + department = Column(String(50), nullable=False) # 所属部门 + contract_amount = Column(Float) # 合同金额(万元) + project_manager = Column(String(100)) # 项目负责人 + client_company = Column(String(100)) # 建设单位 + 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()) # 更新时间 diff --git a/backend/app/schemas/__pycache__/project.cpython-310.pyc b/backend/app/schemas/__pycache__/project.cpython-310.pyc index 869b4c8..9fec6aa 100644 Binary files a/backend/app/schemas/__pycache__/project.cpython-310.pyc and b/backend/app/schemas/__pycache__/project.cpython-310.pyc differ diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 8868f2b..3208f75 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -5,13 +5,25 @@ from typing import Optional class ProjectBase(BaseModel): """项目基础模型""" - name: str - description: Optional[str] = None - department: str - budget: Optional[int] = None - start_date: Optional[datetime] = None - end_date: Optional[datetime] = None - status: Optional[str] = "pending" + project_code: Optional[str] = None # 项目编号 + name: str # 项目名称 + project_type: Optional[str] = None # 工程类别 + description: Optional[str] = None # 项目描述 + department: str # 所属部门 + 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 # 预算 + 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): @@ -21,12 +33,24 @@ class ProjectCreate(ProjectBase): class ProjectUpdate(BaseModel): """更新项目模型""" + project_code: Optional[str] = None name: Optional[str] = None + project_type: Optional[str] = None description: 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 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 diff --git a/backend/create_initial_users.py b/backend/create_initial_users.py new file mode 100644 index 0000000..e64ddc0 --- /dev/null +++ b/backend/create_initial_users.py @@ -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() diff --git a/backend/import_projects_from_excel.py b/backend/import_projects_from_excel.py new file mode 100644 index 0000000..1981c84 --- /dev/null +++ b/backend/import_projects_from_excel.py @@ -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() diff --git a/backend/init_db.py b/backend/init_db.py new file mode 100644 index 0000000..fbc2bd7 --- /dev/null +++ b/backend/init_db.py @@ -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() diff --git a/backend/init_db_with_data.py b/backend/init_db_with_data.py new file mode 100644 index 0000000..113f2fc --- /dev/null +++ b/backend/init_db_with_data.py @@ -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() diff --git a/frontend/src/components/ProjectList.css b/frontend/src/components/ProjectList.css new file mode 100644 index 0000000..4dccf85 --- /dev/null +++ b/frontend/src/components/ProjectList.css @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/components/ProjectList.jsx b/frontend/src/components/ProjectList.jsx index 81b946e..9a41330 100644 --- a/frontend/src/components/ProjectList.jsx +++ b/frontend/src/components/ProjectList.jsx @@ -1,20 +1,27 @@ import React, { useState, useEffect } from 'react'; import { projectApi } from '../services/api'; +import './ProjectList.css'; function ProjectList() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [filters, setFilters] = useState({ + start_date: '', + end_date: '', + min_budget: '', + max_budget: '' + }); useEffect(() => { fetchProjects(); }, []); - const fetchProjects = async () => { + const fetchProjects = async (filterParams = {}) => { setLoading(true); setError(''); try { - const data = await projectApi.getProjects(); + const data = await projectApi.getProjects(filterParams); setProjects(data); } catch (err) { 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) => { if (window.confirm('确定要删除这个项目吗?')) { try { @@ -47,19 +82,168 @@ function ProjectList() { return (
{project.description}
-