From 884b9f47a2ce936a49f6c9f7010eb8bce626d32b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 27 Jan 2026 17:30:17 +0800 Subject: [PATCH] save code --- README.md | 9 +- .../app/api/__pycache__/auth.cpython-310.pyc | Bin 1386 -> 1423 bytes .../api/__pycache__/project.cpython-310.pyc | Bin 3644 -> 4386 bytes backend/app/api/auth.py | 7 +- backend/app/api/project.py | 39 ++- .../__pycache__/database.cpython-310.pyc | Bin 737 -> 735 bytes .../__pycache__/project.cpython-310.pyc | Bin 1053 -> 1474 bytes backend/app/models/project.py | 34 ++- .../__pycache__/project.cpython-310.pyc | Bin 1735 -> 2576 bytes backend/app/schemas/project.py | 40 ++- backend/create_initial_users.py | 59 ++++ backend/import_projects_from_excel.py | 89 ++++++ backend/init_db.py | 13 + backend/init_db_with_data.py | 168 +++++++++++ frontend/src/components/ProjectList.css | 271 ++++++++++++++++++ frontend/src/components/ProjectList.jsx | 200 ++++++++++++- frontend/src/services/api.js | 25 +- 17 files changed, 910 insertions(+), 44 deletions(-) create mode 100644 backend/create_initial_users.py create mode 100644 backend/import_projects_from_excel.py create mode 100644 backend/init_db.py create mode 100644 backend/init_db_with_data.py create mode 100644 frontend/src/components/ProjectList.css 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 b4dd7ca403157ef0d1457ae6600227458c8e8f06..076fa097b6ed8b90f7549545696f0b61aefdf201 100644 GIT binary patch delta 472 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 vr$i^8!{{?&kjKO= 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 ce902c4d4dbe0ef5090e6d70f126cacfaf06cf21..961041e0c8e5e8026ca1cdd977b553bf7d9ce94b 100644 GIT binary patch delta 30 kcmaFJdY_d$pO=@50SJT&D>icPWMs6Me3VgP@^i*t0C)-s0RR91 delta 32 ncmcc5`jC}7pO=@50SHd~E8EDulabMC@=-Z@wAM3}+}+qfwt==;e7q5<>p;!uE(D zJi^;N#w3WKB~dh^D#!vAW}%9*NcFOwinCZHSwg){P^M}p+v#vv_SGO8Q1Ueqk?8$E zL{APb!fYtwCuEvj!z&gSe_55S(k}T%Te*xRJEbmq+Une|U+_le-sSe_L3o6>IiZ9|l2JjjK+r4{K^DoLj75kix(u@f zQYv~Ev}0m0I~|jNNyW}(ko9jYE@(CY9%3C~jpbPM*H&RRxv^UB+J(Fa%l(drH{d&4 z_+Sg)1wKUl!KJsl2Pww-+Aa3=Thjeo(i@QW#88Zo?VDTptu6dEu+RJf)>Mp<_oTCb z2l6_$eD|04u87!$m-qk2?ty(M_R+vQE_rOK?__Sl_SAYHdqf6_R zra7GkZcv*-akFI4ed7u(f)@GX6zjZOvh+ts@t;U^HRP{+eVt^s1TmD zxC1XMEf+BB2FOInr@Srg^#K@{)KUxF94N-v_(B@C*8BaHZfk+NbTa+GrHpB=BxA16 zn5sludOT$8tmWlelQ1T#9Q24W>(g2BCO{|?`glPhjQ>d7aD7~sS38M_s0UY=$G#_C~ zNkk($4)*CT9nzHgH3jm|7--x*D>;Skklv@Ahm+|D^*SqgnLm?ixw!&e`@80LweFg_ zYTkHbegwEW?Oh~!go^Vyepau5@xS3N&gQ%;^eGfB{cLqXKgTbOH2!8hBtFN3@INMc Bq4)p* delta 524 zcmY*VO>5gg5ZzhH(pr+`kGM%_a&XDTa*0U`B{_sbPq~y5$W51`cDAXBWTlcEXfL5S zkW&s~&w=(AU(OcW<1>aXC-x$oE>PJH*icumzrDHkk5>eTwa*U(# zF)236chMv+RKMIx%*(E9=b!k7bcry2;6lC@WrLRLDwD|01)9Ehc#; z)MsSwcD-+&)M{T z#2T9R7lP%d2H*g`;sgZKm>z41?&&(Vjo3&G&!Dy$n@P>9>Ds#*t1;^ufxB#4?FRks^22q zsBnEg&Zd4B`kQ3$5N=iD8id;wb_s7(xJkGJxJ&je!kd7%2)7AuSNaXYy$W{-?^L)8 zc+=|xR{m@e9#nXn@U;r}$bJ{F+W!vVzIVOC1JuLWUz5$dBKw4gC7%6c3y$*9*&h#2 zf1Ukw{_yO}i=WSCKYnv4Te%wKhY%fEOM`@8_Lb9|%O>MRC?blJbQ#YBQ6@YsWhczi zQUqb?2T3-;j>TL_kOpHeb{`$@(oF}7R2{~0(l*Vw+#ve5TM9UKx^ zbJ8qIAv?Ygo5)})Sg-)SgLHEAz4k?zN_-^j^Gdv zO4r%fr?WrLRnpGWbyySE!Cvg4xPjs(ifXT_^&5ri^51ehKIzW{MjUo#G)_~kwL_wFcOjB?SWqTr-TkC=aDOp?m-`E>|31(1Umh93Prr^dDRB)}1Upj>aD!t}uY?9E&W0IZF{8 zO-f#@J7rl{_6IO~fxPT!jxOH7wcNF7LSd!kKzb0bqd+^dS+|0C6|`@|S0HyITRQZ1 z^(}*Rhj5HeK4n2#MxiipE}UA{;gW@$M$L|tODsjmmykZs4UFk``a!y(0=79{QH)W{3Q)btFyDo4&vg?7>G3vzX(CVG77O0~<-R=`p z{Uuu@CV2tvTd8k5hoSMB@ubtV0V7abVQkl3@C8(k*l{7*&RnNjpq;<100hZOe zTa5J5J$Yu_PJtO0McFhh7V5U?iULCP1|C6#VBES>oPs+0~D^UitSQQA%M& zsX6(M^kr%_rRL 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 (

项目列表

+ + {/* 筛选表单 */} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
{projects.length === 0 ? (
暂无项目
) : ( projects.map((project) => (
-

{project.name}

-

{project.description}

-
- 部门: {project.department} - 预算: {project.budget} - 状态: {project.status} +
+
+

{project.name}

+
项目编号: {project.project_code || '-'}
+ {project.description && ( +
{project.description}
+ )} +
+ +
+ {/* 基本信息 */} +
+
基本信息
+
+
工程类别
+
{project.project_type || '-'}
+
+
+
所属部门
+
{project.department}
+
+
+
状态
+
{project.status || '-'}
+
+
+ + {/* 财务信息 */} +
+
财务信息
+
+
合同金额
+
{project.contract_amount || 0} 万元
+
+
+
实际收款
+
{project.actual_received_amount || 0} 万元
+
+
+
回款完成率
+
{project.payment_completion_rate || 0}%
+
+
+ + {/* 人员信息 */} +
+
人员信息
+
+
项目负责人
+
{project.project_manager || '-'}
+
+
+
建设单位
+
{project.client_company || '-'}
+
+
+
联系人
+
{project.client_contact || '-'}
+
+
+ + {/* 时间信息 */} +
+
时间信息
+
+
合同签订
+
{project.contract_date ? new Date(project.contract_date).toLocaleDateString() : '-'}
+
+
+
开始日期
+
{project.start_date ? new Date(project.start_date).toLocaleDateString() : '-'}
+
+
+
计划结束
+
{project.planned_end_date ? new Date(project.planned_end_date).toLocaleDateString() : '-'}
+
+
+
实际结束
+
{project.actual_end_date ? new Date(project.actual_end_date).toLocaleDateString() : '-'}
+
+
+ + {/* 进度信息 */} +
+
进度信息
+
+
累计进度
+
{project.progress || 0}%
+
+
+ + {/* 付款方式 */} +
+
付款方式
+
+
{project.payment_terms || '-'}
+
+
+
+