YueYunyun 7 月之前
父節點
當前提交
b718161832
共有 35 個文件被更改,包括 1703 次插入98 次删除
  1. 73 0
      SourceCode/IntelligentRailwayCosting/.script/init.sql
  2. 12 0
      SourceCode/IntelligentRailwayCosting/app/__init__.py
  3. 18 4
      SourceCode/IntelligentRailwayCosting/app/core/api/response.py
  4. 4 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py
  5. 4 1
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project.py
  6. 78 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py
  7. 77 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py
  8. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/tree.py
  9. 2 2
      SourceCode/IntelligentRailwayCosting/app/core/enum/log.py
  10. 10 3
      SourceCode/IntelligentRailwayCosting/app/core/log/log_record.py
  11. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/models/__init__.py
  12. 41 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py
  13. 35 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py
  14. 0 3
      SourceCode/IntelligentRailwayCosting/app/core/models/quota_input.py
  15. 4 0
      SourceCode/IntelligentRailwayCosting/app/routes/__init__.py
  16. 25 0
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  17. 46 0
      SourceCode/IntelligentRailwayCosting/app/routes/project_task.py
  18. 2 0
      SourceCode/IntelligentRailwayCosting/app/services/__init__.py
  19. 1 1
      SourceCode/IntelligentRailwayCosting/app/services/project.py
  20. 147 0
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  21. 263 0
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  22. 1 0
      SourceCode/IntelligentRailwayCosting/app/stores/budget.py
  23. 244 0
      SourceCode/IntelligentRailwayCosting/app/stores/project_quota.py
  24. 220 0
      SourceCode/IntelligentRailwayCosting/app/stores/project_task.py
  25. 9 6
      SourceCode/IntelligentRailwayCosting/app/tools/db_helper/sqlserver_helper.py
  26. 5 1
      SourceCode/IntelligentRailwayCosting/app/tools/utils/string_helper.py
  27. 12 2
      SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css
  28. 7 5
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js
  29. 二進制
      SourceCode/IntelligentRailwayCosting/app/views/static/media/favicon.ico
  30. 12 0
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget.css
  31. 329 31
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  32. 1 1
      SourceCode/IntelligentRailwayCosting/app/views/templates/account/login.html
  33. 1 3
      SourceCode/IntelligentRailwayCosting/app/views/templates/base/base.html
  34. 1 3
      SourceCode/IntelligentRailwayCosting/app/views/templates/base/layout.html
  35. 15 32
      SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

+ 73 - 0
SourceCode/IntelligentRailwayCosting/.script/init.sql

@@ -1,6 +1,79 @@
 CREATE DATABASE IF NOT EXISTS iwb_railway_costing_v1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
 USE iwb_railway_costing_v1;
 
+-- 创建项目任务表
+CREATE TABLE IF NOT EXISTS project_task (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_name VARCHAR(255) NOT NULL COMMENT '任务名称',
+    task_desc VARCHAR(1000) COMMENT '任务描述',
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    build_id int NOT NULL COMMENT '概算序号',
+    item_id int NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    file_path text COMMENT '文件路径',
+    collect_status TINYINT NOT NULL DEFAULT 0 COMMENT '采集状态(0:未开始, 1:进行中, 2:已完成, 3:采集失败)',
+    collect_time DATETIME COMMENT '采集时间',
+    collect_error VARCHAR(1000) COMMENT '采集错误信息',
+    process_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)',
+    process_time DATETIME COMMENT '处理时间',
+    process_error VARCHAR(1000) COMMENT '处理错误信息',
+    send_status TINYINT NOT NULL DEFAULT 0 COMMENT '发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)',
+    send_time DATETIME COMMENT '发送时间',
+    send_error VARCHAR(1000) COMMENT '发送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_build_id (build_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
+
+-- 创建项目任务表
+CREATE TABLE IF NOT EXISTS project_quota (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    build_id int NOT NULL COMMENT '概算序号',
+    item_id int NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    quota_code VARCHAR(50) COMMENT '定额编号',
+    project_name VARCHAR(255) COMMENT '工程或费用项目名称',
+    unit VARCHAR(20) COMMENT '单位',
+    project_quantity FLOAT COMMENT '工程数量',
+    project_quantity_input VARCHAR(1000) COMMENT '工程数量输入',
+    quota_adjustment VARCHAR(1000) COMMENT '定额调整',
+    unit_price FLOAT COMMENT '单价',
+    total_price FLOAT COMMENT '合价',
+    unit_weight FLOAT COMMENT '单重',
+    total_weight FLOAT COMMENT '合重',
+    labor_cost FLOAT COMMENT '人工费',
+    process_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)',
+    process_time DATETIME COMMENT '处理时间',
+    process_error VARCHAR(1000) COMMENT '处理错误信息',
+    send_status TINYINT NOT NULL DEFAULT 0 COMMENT '发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)',
+    send_time DATETIME COMMENT '发送时间',
+    send_error VARCHAR(1000) COMMENT '发送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_build_id (build_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
+
+
+
 -- 创建日志表
 CREATE TABLE IF NOT EXISTS sys_log (
     id INT AUTO_INCREMENT PRIMARY KEY,

+ 12 - 0
SourceCode/IntelligentRailwayCosting/app/__init__.py

@@ -1,13 +1,25 @@
 from flask import Flask
+from flask.json.provider import JSONProvider
 from routes.auth import login_manager
 from routes import register_api
 from views import register_views
 
 
+class CustomJSONProvider(JSONProvider):
+    def default(self, obj):
+        try:
+            return super().default(obj)
+        except TypeError:
+            return str(obj)
+            
+    def encode(self, obj):
+        return super().encode(obj).encode('utf-8').decode('unicode_escape')
+
 def create_app():
     app = Flask(__name__, static_folder='views/static')
     app.secret_key = "1qwe2iwb3vber"
     app.config['JSON_AS_ASCII'] = False
+    app.json_provider_class = CustomJSONProvider
 
     login_manager.init_app(app)
 

+ 18 - 4
SourceCode/IntelligentRailwayCosting/app/core/api/response.py

@@ -1,10 +1,21 @@
 from typing import Dict, Any, Optional, List
-from flask import jsonify, Response
+from flask import jsonify, Response, make_response
 
 
 class ResponseBase:
     """统一的API响应结构"""
 
+    @staticmethod
+    def _set_response_headers(response: Response) -> Response:
+        """设置响应头
+        Args:
+            response: Flask响应对象
+        Returns:
+            Response: 设置好响应头的Flask响应对象
+        """
+        response.headers['Content-Type'] = 'application/json; charset=utf-8'
+        return response
+
     @staticmethod
     def success(data: Optional[Any] = None, message: str = "操作成功") -> Response:
         """成功响应
@@ -21,7 +32,8 @@ class ResponseBase:
         }
         if data is not None:
             response["data"] = data
-        return jsonify(response)
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)
 
     @staticmethod
     def error(message: str = "操作失败", code: int = 400, data: Optional[Any] = None) -> Response:
@@ -40,7 +52,8 @@ class ResponseBase:
         }
         if data is not None:
             response["data"] = data
-        return jsonify(response)
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)
 
     @staticmethod
     def json_response(success: bool = True, code: int = 200, message: str = "", data: Optional[Any] = None) -> Response:
@@ -60,6 +73,7 @@ class ResponseBase:
         }
         if data is not None:
             response["data"] = data
-        return jsonify(response)
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)
 
 

+ 4 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py

@@ -1,4 +1,6 @@
 from .log import LogDto
+from .project_quota import ProjectQuotaDto
+from .project_task import ProjectTaskDto
 
 from .user import UserDto
 from .project import ProjectDto
@@ -6,3 +8,5 @@ from .total_budget_info import TotalBudgetInfoDto
 from .total_budget_item import TotalBudgetItemDto
 from .chapter import ChapterDto
 from .quota_input import QuotaInputDto
+
+

+ 4 - 1
SourceCode/IntelligentRailwayCosting/app/core/dtos/project.py

@@ -78,7 +78,10 @@ class ProjectDto(BaseModel):
 
     def to_dict(self) -> dict:
         """转换为字典格式"""
-        return self.model_dump()
+        data = self.model_dump()
+        if self.create_time:
+            data['create_time'] = self.create_time.strftime('%Y-%m-%d %H:%M:%S')
+        return data
 
     class Config:
         from_attributes = True

+ 78 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py

@@ -0,0 +1,78 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from core.models import ProjectQuotaModel
+
+class ProjectQuotaDto(BaseModel):
+    """项目定额DTO"""
+    id: Optional[int] = None
+    project_id: str
+    build_id: int
+    item_id: int
+    item_code: str
+    quota_code: Optional[str] = None
+    project_name: Optional[str] = None
+    unit: Optional[str] = None
+    project_quantity: Optional[float] = None
+    project_quantity_input: Optional[str] = None
+    quota_adjustment: Optional[str] = None
+    unit_price: Optional[float] = None
+    total_price: Optional[float] = None
+    unit_weight: Optional[float] = None
+    total_weight: Optional[float] = None
+    labor_cost: Optional[float] = None
+    process_status: Optional[int] = 0
+    process_time: Optional[datetime] = None
+    process_error: Optional[str] = None
+    send_status: Optional[int] = 0
+    send_time: Optional[datetime] = None
+    send_error: Optional[str] = None
+    is_del: Optional[int] = 0
+    deleted_by: Optional[str] = None
+    deleted_at: Optional[datetime] = None
+    created_by: Optional[str] = None
+    created_at: Optional[datetime] = None
+    updated_by: Optional[str] = None
+    updated_at: Optional[datetime] = None
+
+    @classmethod
+    def from_model(cls, model: ProjectQuotaModel) -> 'ProjectQuotaDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            project_id=model.project_id,
+            build_id=model.build_id,
+            item_id=model.item_id,
+            item_code=model.item_code,
+            quota_code=model.quota_code,
+            project_name=model.project_name,
+            unit=model.unit,
+            project_quantity=model.project_quantity,
+            project_quantity_input=model.project_quantity_input,
+            quota_adjustment=model.quota_adjustment,
+            unit_price=model.unit_price,
+            total_price=model.total_price,
+            unit_weight=model.unit_weight,
+            total_weight=model.total_weight,
+            labor_cost=model.labor_cost,
+            process_status=model.process_status,
+            process_time=model.process_time,
+            process_error=model.process_error,
+            send_status=model.send_status,
+            send_time=model.send_time,
+            send_error=model.send_error,
+            is_del=model.is_del,
+            deleted_by=model.deleted_by,
+            deleted_at=model.deleted_at,
+            created_by=model.created_by,
+            created_at=model.created_at,
+            updated_by=model.updated_by,
+            updated_at=model.updated_at
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    class Config:
+        from_attributes = True

+ 77 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py

@@ -0,0 +1,77 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from ..models import ProjectTaskModel
+
+class ProjectTaskDto(BaseModel):
+    """项目任务DTO"""
+    id: Optional[int] = None
+    task_name: str
+    task_desc: Optional[str] = None
+    project_id: str
+    build_id: int
+    item_id: int
+    item_code: Optional[str] = None
+    file_path: Optional[str] = None
+    collect_status: Optional[int] = 0
+    collect_time: Optional[datetime] = None
+    collect_error: Optional[str] = None
+    process_status: Optional[int] = 0
+    process_time: Optional[datetime] = None
+    process_error: Optional[str] = None
+    send_status: Optional[int] = 0
+    send_time: Optional[datetime] = None
+    send_error: Optional[str] = None
+    is_del: Optional[int] = 0
+    deleted_by: Optional[str] = None
+    deleted_at: Optional[datetime] = None
+    created_by: Optional[str] = None
+    created_at: Optional[datetime] = None
+    updated_by: Optional[str] = None
+    updated_at: Optional[datetime] = None
+
+    @classmethod
+    def from_model(cls, model: ProjectTaskModel) -> 'ProjectTaskDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            task_name=model.task_name,
+            task_desc=model.task_desc,
+            project_id=model.project_id,
+            build_id=model.build_id,
+            item_id=model.item_id,
+            item_code=model.item_code,
+            file_path=model.file_path,
+            collect_status=model.collect_status,
+            collect_time=model.collect_time,
+            collect_error=model.collect_error,
+            process_status=model.process_status,
+            process_time=model.process_time,
+            process_error=model.process_error,
+            send_status=model.send_status,
+            send_time=model.send_time,
+            send_error=model.send_error,
+            is_del=model.is_del,
+            deleted_by=model.deleted_by,
+            deleted_at=model.deleted_at,
+            created_by=model.created_by,
+            created_at=model.created_at,
+            updated_by=model.updated_by,
+            updated_at=model.updated_at
+        )
+
+    @classmethod
+    def from_dict(cls, data: dict) -> 'ProjectTaskDto':
+        """从字典创建DTO对象"""
+        if 'budget_id' in data and 'build_id' not in data:
+            data['build_id'] = data.pop('budget_id')
+        return cls(**data)
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    def get_path(self):
+        return f"{self.project_id}_{self.build_id}_{self.item_id}_{self.id}"
+    class Config:
+        from_attributes = True

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/tree.py

@@ -10,6 +10,7 @@ class TreeDto:
         self.parent = parent
         self.text = text
         self.children = children
+        self.icon = ('fa fa-folder text-primary' if children else 'fa fa-file text-warning') + ' fs-3'
         self.data = data
 
 
@@ -19,6 +20,7 @@ class TreeDto:
             "parent": self.parent,
             "text": self.text,
             "children": self.children,
+            "icon": self.icon,
             "data": self.data._asdict() if hasattr(self.data, '_asdict') else self.data
         }
         return data

+ 2 - 2
SourceCode/IntelligentRailwayCosting/app/core/enum/log.py

@@ -32,5 +32,5 @@ class OperationModule(Enum):
     """操作模块枚举"""
     ACCOUNT = "账户"
     PROJECT = "项目"
-    SUB_PROJECT = "工程"
-    SUB_PROJECT_DETAIL = "工程明细"
+    TASK = "任务"
+    QUOTA = "定额输入"

+ 10 - 3
SourceCode/IntelligentRailwayCosting/app/core/log/log_record.py

@@ -1,10 +1,16 @@
 from flask import request, session
 from typing import Optional
-from services import LogService
 from core.enum import OperationType,OperationModule,OperationResult
 
 class LogRecordHelper:
-    _log_service = LogService()
+    _log_service = None
+
+    @staticmethod
+    def get_log_service():
+        if LogRecordHelper._log_service is None:
+            from services import LogService
+            LogRecordHelper._log_service = LogService()
+        return LogRecordHelper._log_service
 
     @staticmethod
     def get_client_ip() -> str:
@@ -68,7 +74,8 @@ class LogRecordHelper:
             data_changes: 数据变更记录(可选)
             username: 用户名(可选,默认从session获取)
         """
-        LogRecordHelper._log_service.add_operation_log(
+        log_service = LogRecordHelper.get_log_service()
+        log_service.add_operation_log(
             username=username or session.get('username'),
             operation_type=operation_type.value,
             operation_desc=operation_desc,

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/__init__.py

@@ -1,4 +1,6 @@
 from .log import LogModel
+from .project_quota import ProjectQuotaModel
+from .project_task import ProjectTaskModel
 
 from .user import UserModel
 from .team import TeamModel

+ 41 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py

@@ -0,0 +1,41 @@
+from sqlalchemy import Column, Integer, String, DateTime, Float, Text
+from sqlalchemy.sql import func
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class ProjectQuotaModel(Base):
+    __tablename__ = 'project_quota'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    project_id = Column(String(50), nullable=False, comment='项目编号')
+    build_id = Column(Integer, nullable=False, comment='概算序号')
+    item_id = Column(Integer, nullable=False, comment='条目序号')
+    item_code = Column(String(255), nullable=False, comment='条目编号')
+    quota_code = Column(String(50), comment='定额编号')
+    project_name = Column(String(255), comment='工程或费用项目名称')
+    unit = Column(String(20), comment='单位')
+    project_quantity = Column(Float, comment='工程数量')
+    project_quantity_input = Column(String(1000), comment='工程数量输入')
+    quota_adjustment = Column(String(1000), comment='定额调整')
+    unit_price = Column(Float, comment='单价')
+    total_price = Column(Float, comment='合价')
+    unit_weight = Column(Float, comment='单重')
+    total_weight = Column(Float, comment='合重')
+    labor_cost = Column(Float, comment='人工费')
+    process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
+    process_time = Column(DateTime, comment='处理时间')
+    process_error = Column(String(1000), comment='处理错误信息')
+    send_status = Column(Integer, nullable=False, default=0, comment='发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)')
+    send_time = Column(DateTime, comment='发送时间')
+    send_error = Column(String(1000), comment='发送错误信息')
+    is_del = Column(Integer, nullable=False, default=0, comment='是否删除(0:否, 1:是)')
+    deleted_by = Column(String(50), comment='删除人')
+    deleted_at = Column(DateTime, comment='删除时间')
+    created_by = Column(String(50), comment='创建人')
+    created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), comment='创建时间')
+    updated_by = Column(String(50), comment='更新人')
+    updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), server_onupdate=func.current_timestamp(), comment='更新时间')
+
+    def __repr__(self):
+        return f"<ProjectQuota(id='{self.id}', project_id='{self.project_id}')>"

+ 35 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py

@@ -0,0 +1,35 @@
+from sqlalchemy import Column, Integer, String, DateTime,  Text
+from sqlalchemy.sql import func
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+class ProjectTaskModel(Base):
+    __tablename__ = 'project_task'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    task_name = Column(String(255), nullable=False, comment='任务名称')
+    task_desc = Column(String(1000), comment='任务描述')
+    project_id = Column(String(50), nullable=False, comment='项目编号')
+    build_id = Column(Integer, nullable=False, comment='概算序号')
+    item_id = Column(Integer, nullable=False, comment='条目序号')
+    item_code = Column(String(255), nullable=False, comment='条目编号')
+    file_path = Column(Text, comment='文件路径')
+    collect_status = Column(Integer, nullable=False, default=0, comment='采集状态(0:未开始, 1:进行中, 2:已完成, 3:采集失败)')
+    collect_time = Column(DateTime, comment='采集时间')
+    collect_error = Column(String(1000), comment='采集错误信息')
+    process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
+    process_time = Column(DateTime, comment='处理时间')
+    process_error = Column(String(1000), comment='处理错误信息')
+    send_status = Column(Integer, nullable=False, default=0, comment='发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)')
+    send_time = Column(DateTime, comment='发送时间')
+    send_error = Column(String(1000), comment='发送错误信息')
+    is_del = Column(Integer, nullable=False, default=0, comment='是否删除(0:否, 1:是)')
+    deleted_by = Column(String(50), comment='删除人')
+    deleted_at = Column(DateTime, comment='删除时间')
+    created_by = Column(String(50), comment='创建人')
+    created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), comment='创建时间')
+    updated_by = Column(String(50), comment='更新人')
+    updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), server_onupdate=func.current_timestamp(), comment='更新时间')
+
+    def __repr__(self):
+        return f"<ProjectTask(id='{self.id}', task_name='{self.task_name}')>"

+ 0 - 3
SourceCode/IntelligentRailwayCosting/app/core/models/quota_input.py

@@ -1,8 +1,5 @@
 from sqlalchemy import Column, String, Integer, Float, Text, ForeignKey
 from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import relationship
-from .total_budget_info import TotalBudgetInfoModel
-from .chapter import ChapterModel
 
 Base = declarative_base()
 

+ 4 - 0
SourceCode/IntelligentRailwayCosting/app/routes/__init__.py

@@ -1,6 +1,8 @@
 from routes.auth import auth_api
 from routes.log import log_api
 from routes.project import project_api
+from routes.project_task import project_task_api
+from routes.project_quota import project_quota_api
 
 def register_api(app):
     url_prefix = '/api'
@@ -8,3 +10,5 @@ def register_api(app):
     app.register_blueprint(auth_api, url_prefix=f"{url_prefix}/auth")
     app.register_blueprint(log_api, url_prefix=f"{url_prefix}/log")
     app.register_blueprint(project_api, url_prefix=f"{url_prefix}/project")
+    app.register_blueprint(project_task_api, url_prefix=f"{url_prefix}/task")
+    app.register_blueprint(project_quota_api, url_prefix=f"{url_prefix}/quota")

+ 25 - 0
SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py

@@ -0,0 +1,25 @@
+from flask import Blueprint, request
+
+from core.user_session import Permission
+from core.api import  ResponseBase,TableResponse
+from services import  ProjectQuotaService
+
+project_quota_api = Blueprint('project_quota_api', __name__)
+quota_service = ProjectQuotaService()
+
+@project_quota_api.route('/list/<int:budget_id>/<project_id>/<int:item_id>', methods=['GET'])
+@Permission.authorize
+def get_page_list(budget_id:int,project_id:str,item_id:int):
+    try:
+        data = request.get_json()
+        page = int(data.get('pageNum', 1))
+        per_page = int(data.get('pageSize', 10))
+        keyword = data.get('keyword')
+        process_status = int(data.get('process_status')) if data.get('process_status') else None
+        send_status = int(data.get('send_status')) if data.get('send_status') else None
+        data,count = quota_service.get_quotas(budget_id, project_id, item_id, page, per_page, keyword, process_status, send_status)
+        return TableResponse.success(data,count)
+    except Exception as e:
+        return ResponseBase.error(f'获取项目失败:{str(e)}')
+
+

+ 46 - 0
SourceCode/IntelligentRailwayCosting/app/routes/project_task.py

@@ -0,0 +1,46 @@
+from flask import Blueprint, request
+
+from core.dtos import ProjectTaskDto
+from core.user_session import Permission
+from core.api import  ResponseBase,TableResponse
+from services import  ProjectTaskService
+
+project_task_api = Blueprint('project_task_api', __name__)
+task_service = ProjectTaskService()
+
+@project_task_api.route('/list/<int:budget_id>/<project_id>/<int:item_id>', methods=['POST'])
+@Permission.authorize
+def get_page_list(budget_id:int,project_id:str,item_id:int):
+    try:
+        data = request.get_json()
+        page = int(data.get('pageNum', 1))
+        per_page = int(data.get('pageSize', 10))
+        keyword = data.get('keyword')
+        collect_status = int(data.get('collect_status')) if data.get('collect_status') else None
+        process_status = int(data.get('process_status')) if data.get('process_status') else None
+        send_status = int(data.get('send_status')) if data.get('send_status') else None
+        task, total_count = task_service.get_tasks_paginated(budget_id, project_id, item_id, page, per_page, keyword, collect_status, process_status, send_status)
+        return TableResponse.success(task, total_count)
+    except Exception as e:
+        return ResponseBase.error(f'获取项目失败:{str(e)}')
+
+@project_task_api.route('/get/<int:task_id>', methods=['POST'])
+def get(task_id:int):
+    try:
+        task = task_service.get_task_dto(task_id)
+        return ResponseBase.success(task.to_dict())
+    except Exception as e:
+        return ResponseBase.error(f'获取项目失败:{str(e)}')
+
+@project_task_api.route('/save/<int:task_id>', methods=['POST'])
+def save_task(task_id:int):
+    try:
+        data = request.get_json()
+        task_dto = ProjectTaskDto.from_dict(data)
+        delete_old = data.get('delete_old', False)
+        files= request.files.getlist('files')
+        files = files if files else []
+        task = task_service.save_task(task_id, task_dto, files, delete_old)
+        return ResponseBase.success(task.to_dict())
+    except Exception as e:
+        return ResponseBase.error(f'保存项目失败:{str(e)}')

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/services/__init__.py

@@ -1,4 +1,6 @@
 from .user import UserService
 from .log import LogService
 from .project import ProjectService
+from .project_quota import ProjectQuotaService
+from .project_task import ProjectTaskService
 

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/services/project.py

@@ -69,7 +69,7 @@ class ProjectService:
             item = budget_store.get_budget_item_by_item_code(budget_id,item_code)
             parent = item.item_id
         for item in items:
-            text = f"{item.chapter}  {item.project_name}" if item.chapter else ( f"{item.section}  {item.project_name}" if item.section else item.project_name)
+            text = f"{item.chapter}{item.project_name}" if item.chapter else ( f"{item.section}  {item.project_name}" if item.section else item.project_name)
             data_list.append(TreeDto(item.item_id,parent,text,item.children_count>0,item).to_dict())
         return data_list,""
 

+ 147 - 0
SourceCode/IntelligentRailwayCosting/app/services/project_quota.py

@@ -0,0 +1,147 @@
+from typing import Optional
+from datetime import datetime
+
+from core.dtos import ProjectQuotaDto
+from stores.project_quota import ProjectQuotaStore
+
+class ProjectQuotaService:
+    def __init__(self):
+        self.store = ProjectQuotaStore()
+
+    def get_quotas(self, build_id: int, project_id: str, item_id: int, page: int = 1, page_size: int = 10,
+                   keyword: Optional[str] = None, process_status: Optional[int] = None,
+                   send_status: Optional[int] = None):
+        """获取项目定额列表
+
+        Args:
+            build_id: 概算序号
+            project_id: 项目编号
+            item_id: 条目序号
+            page: 页码
+            page_size: 每页数量
+            keyword: 关键字
+            process_status: 处理状态
+            send_status: 发送状态
+
+        Returns:
+            dict: 包含总数和定额列表的字典
+        """
+        try:
+            data = self.store.get_quotas(
+                build_id=build_id,
+                project_id=project_id,
+                item_id=item_id,
+                page=page,
+                page_size=page_size,
+                keyword=keyword,
+                process_status=process_status,
+                send_status=send_status
+            )
+            return [ProjectQuotaDto.from_model(quota).to_dict() for quota in data.get('data',[])],data.get('total',0)
+
+        except Exception as e:
+            # 记录错误日志
+            print(f"获取项目定额列表失败: {str(e)}")
+            raise
+
+    def get_quota(self, quota_id: int) -> Optional[ProjectQuotaDto]:
+        """获取单个项目定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            Optional[ProjectQuotaDto]: 项目定额DTO对象
+        """
+        try:
+            return self.store.get_quota(quota_id)
+        except Exception as e:
+            print(f"获取项目定额失败: {str(e)}")
+            raise
+
+    def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
+        """创建项目定额
+
+        Args:
+            quota_dto: 定额DTO对象
+
+        Returns:
+            ProjectQuotaDto: 创建后的定额DTO对象
+        """
+        try:
+            # 业务验证
+            if not quota_dto.project_id or not quota_dto.build_id:
+                raise ValueError("项目编号和概算序号不能为空")
+
+            return self.store.create_quota(quota_dto)
+        except Exception as e:
+            print(f"创建项目定额失败: {str(e)}")
+            raise
+
+    def update_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
+        """更新项目定额
+
+        Args:
+            quota_dto: 定额DTO对象
+
+        Returns:
+            Optional[ProjectQuotaDto]: 更新后的定额DTO对象
+        """
+        try:
+            # 业务验证
+            if not quota_dto.id:
+                raise ValueError("定额ID不能为空")
+
+            return self.store.update_quota(quota_dto)
+        except Exception as e:
+            print(f"更新项目定额失败: {str(e)}")
+            raise
+
+    def delete_quota(self, quota_id: int) -> bool:
+        """删除项目定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            bool: 删除是否成功
+        """
+        try:
+            return self.store.delete_quota(quota_id)
+        except Exception as e:
+            print(f"删除项目定额失败: {str(e)}")
+            raise
+
+    def update_process_status(self, quota_id: int, status: int, err: str = None) -> bool:
+        """更新处理状态
+
+        Args:
+            quota_id: 定额ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_process_status(quota_id, status, err)
+        except Exception as e:
+            print(f"更新项目定额处理状态失败: {str(e)}")
+            raise
+
+    def update_send_status(self, quota_id: int, status: int, err: str = None) -> bool:
+        """更新发送状态
+
+        Args:
+            quota_id: 定额ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_send_status(quota_id, status, err)
+        except Exception as e:
+            print(f"更新项目定额发送状态失败: {str(e)}")
+            raise

+ 263 - 0
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -0,0 +1,263 @@
+from typing import Optional
+
+import tools.utils as utils, os
+from core.log.log_record import LogRecordHelper
+from core.enum import OperationModule,OperationType
+from core.dtos import ProjectTaskDto
+from stores.project_task import ProjectTaskStore
+
+class ProjectTaskService:
+    def __init__(self):
+        self.store = ProjectTaskStore()
+        self._logger = utils.get_logger()
+
+    def get_tasks_paginated(self, build_id: int, project_id: str, item_id: int, page: int = 1, page_size: int = 10,
+                            keyword: Optional[str] = None, collect_status: Optional[int] = None,
+                            process_status: Optional[int] = None, send_status: Optional[int] = None):
+        """获取项目任务列表
+
+        Args:
+            project_id: 项目编号
+            build_id: 概算序号
+            item_id: 条目序号
+            page: 页码
+            page_size: 每页数量
+            keyword: 关键字
+            collect_status: 采集状态
+            process_status: 处理状态
+            send_status: 发送状态
+
+        Returns:
+            dict: 包含总数和任务列表的字典
+        """
+        try:
+            data =  self.store.get_tasks(
+                build_id=build_id,
+                project_id=project_id,
+                item_id=item_id,
+                page=page,
+                page_size=page_size,
+                keyword=keyword,
+                collect_status=collect_status,
+                process_status=process_status,
+                send_status=send_status
+            )
+            return [ProjectTaskDto.from_model(task).to_dict() for task in data.get('data',[])],data.get('total',0)
+        except Exception as e:
+            print(f"获取项目任务列表失败: {str(e)}")
+            raise
+
+    def get_task(self, task_id: int) -> Optional[ProjectTaskDto]:
+        """获取单个项目任务
+
+        Args:
+            task_id: 任务ID
+
+        Returns:
+            Optional[ProjectTaskDto]: 项目任务DTO对象
+        """
+        try:
+            task = self.store.get_task(task_id)
+            return task
+        except Exception as e:
+            print(f"获取项目任务失败: {str(e)}")
+            raise
+    def get_task_dto(self, task_id: int):
+        try:
+            task = self.store.get_task_dto(task_id)
+            return task
+        except Exception as e:
+            print(f"获取项目任务失败: {str(e)}")
+            raise
+
+    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files:list, delete_old: bool = False):
+        log_data=""
+        if task_id == 0:
+            task = self.store.create_task(task_dto)
+        else:
+            task = self.get_task_dto(task_id)
+            log_data = utils.to_str(task.to_dict())
+            if task is None:
+                LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK,
+                                         f"修改任务失败 错误:任务[{task_id}]不存在")
+                raise Exception("项目任务不存在")
+            task_dto.id = task_id
+            task = self.store.update_task(task_dto)
+
+        try:
+            if task:
+                paths = self._process_file_upload(task_dto, files, delete_old)
+                self.store.update_task_files(task.id,paths)
+                if task_id == 0:
+                    LogRecordHelper.log_success(OperationType.CREATE, OperationModule.TASK,
+                                                f"创建任务成功 任务:{task.task_name}", utils.to_str(task.to_dict()))
+                else:
+                    LogRecordHelper.log_success(OperationType.UPDATE, OperationModule.TASK, f"修改任务成功", log_data,
+                                                utils.to_str(task.to_dict()))
+            else:
+                if task_id == 0:
+                    LogRecordHelper.log_fail(OperationType.CREATE, OperationModule.TASK, f"创建任务失败",
+                                             utils.to_str(task_dto.to_dict()))
+                else:
+                    LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK, f"修改任务失败", log_data)
+            return task
+        except ValueError as ve:
+            LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK,
+                               f"{'修改' if task_id == 0 else '添加'}任务失败 错误:{task_dto.task_name} {str(ve)}",
+                               utils.to_str(task_dto.to_dict()))
+            raise ve
+        except Exception as e:
+            LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK,
+                               f"{'修改' if task_id == 0 else '添加'}任务失败 错误:{task_dto.task_name} {str(e)}",
+                               utils.to_str(task_dto.to_dict()))
+            raise e
+
+    def _process_file_upload(self, task: ProjectTaskDto, files: list, delete_old: bool) -> str:
+        """处理文件上传流程"""
+        base_path = utils.get_config_value('file.source_path', './temp_files')
+        task_dir = os.path.join(base_path, f'{task.get_path()}')
+        os.makedirs(task_dir, exist_ok=True)
+        self._logger.info(f"保存处理文件,项目ID:{task.project_id},任务ID:{task.id}")
+        if delete_old:
+            if os.path.exists(task_dir):
+                delete_paths = []
+                for filename in os.listdir(task_dir):
+                    file_path = os.path.join(task_dir, filename)
+                    if os.path.isfile(file_path):
+                        delete_dir = os.path.join(task_dir, 'delete')
+                        os.makedirs(delete_dir, exist_ok=True)
+                        # 处理文件名冲突
+                        base_name = os.path.basename(file_path)
+                        target_path = os.path.join(delete_dir, base_name)
+                        counter = 1
+                        while os.path.exists(target_path):
+                            name, ext = os.path.splitext(base_name)
+                            target_path = os.path.join(delete_dir, f"{name}_{counter}{ext}")
+                            counter += 1
+                        os.rename(file_path, target_path)
+                        delete_paths.append(target_path)
+                if len(delete_paths) > 0:
+                    LogRecordHelper.log_success(OperationType.DELETE, OperationModule.TASK,
+                                          f"删除任务文件:{task.sub_project_name}", utils.to_str(delete_paths))
+        file_paths = [] if delete_old or not task.file_path else task.file_path.split(',')
+        if files and len(files) > 0:
+            for file in files:
+                if not file.filename:
+                    continue
+                allowed_ext = {'xlsx', 'xls', 'csv'}
+                ext = file.filename.rsplit('.', 1)[-1].lower()
+                if ext not in allowed_ext:
+                    continue
+                file_path = os.path.join(task_dir, file.filename)
+                file_path = file_path.replace('\\', '/')
+                file.save(file_path)
+                file_paths.append(file_path)
+        return ','.join(file_paths)
+
+    def _create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
+        """创建项目任务
+
+        Args:
+            task_dto: 任务DTO对象
+
+        Returns:
+            ProjectTaskDto: 创建后的任务DTO对象
+        """
+        try:
+            # 业务验证
+            if not task_dto.project_id or not task_dto.build_id:
+                raise ValueError("项目编号和概算序号不能为空")
+            if not task_dto.task_name:
+                raise ValueError("任务名称不能为空")
+
+            return self.store.create_task(task_dto)
+        except Exception as e:
+            print(f"创建项目任务失败: {str(e)}")
+            raise
+
+    def _update_task(self, task_dto: ProjectTaskDto) -> Optional[ProjectTaskDto]:
+        """更新项目任务
+
+        Args:
+            task_dto: 任务DTO对象
+
+        Returns:
+            Optional[ProjectTaskDto]: 更新后的任务DTO对象
+        """
+        try:
+            # 业务验证
+            if not task_dto.id:
+                raise ValueError("任务ID不能为空")
+            if not task_dto.task_name:
+                raise ValueError("任务名称不能为空")
+
+            return self.store.update_task(task_dto)
+        except Exception as e:
+            print(f"更新项目任务失败: {str(e)}")
+            raise
+
+    def delete_task(self, task_id: int) -> bool:
+        """删除项目任务
+
+        Args:
+            task_id: 任务ID
+
+        Returns:
+            bool: 删除是否成功
+        """
+        try:
+            return self.store.delete_task(task_id)
+        except Exception as e:
+            print(f"删除项目任务失败: {str(e)}")
+            raise
+
+    def update_collect_status(self, task_id: int, status: int, err: str = None) -> bool:
+        """更新采集状态
+
+        Args:
+            task_id: 任务ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_collect_status(task_id, status, err)
+        except Exception as e:
+            print(f"更新项目任务采集状态失败: {str(e)}")
+            raise
+
+    def update_process_status(self, task_id: int, status: int, err: str = None) -> bool:
+        """更新处理状态
+
+        Args:
+            task_id: 任务ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_process_status(task_id, status, err)
+        except Exception as e:
+            print(f"更新项目任务处理状态失败: {str(e)}")
+            raise
+
+    def update_send_status(self, task_id: int, status: int, err: str = None) -> bool:
+        """更新发送状态
+
+        Args:
+            task_id: 任务ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_send_status(task_id, status, err)
+        except Exception as e:
+            print(f"更新项目任务发送状态失败: {str(e)}")
+            raise

+ 1 - 0
SourceCode/IntelligentRailwayCosting/app/stores/budget.py

@@ -72,6 +72,7 @@ class BudgetStore:
     def get_top_budget_items(self, budget_id: str):
         query = self._build_budget_items_query(budget_id)
         query = query.filter(ChapterModel.item_code.like('__'))\
+            .filter(ChapterModel.chapter.is_not(None))\
             .order_by(ChapterModel.item_code)
         items = query.all()
         return items

+ 244 - 0
SourceCode/IntelligentRailwayCosting/app/stores/project_quota.py

@@ -0,0 +1,244 @@
+from sqlalchemy import and_, or_
+from sqlalchemy.orm import Session
+from datetime import datetime
+from typing import Optional, List, Tuple
+
+import tools.db_helper as db_helper
+from core.dtos import ProjectQuotaDto
+from core.models import ProjectQuotaModel
+from core.user_session import UserSession
+
+class ProjectQuotaStore:
+    def __init__(self, db_session: Session = None):
+        self.db_session = db_session or db_helper.create_mysql_session()
+        self._current_user = None
+
+    @property
+    def current_user(self):
+        if self._current_user is None:
+            self._current_user = UserSession.get_current_user()
+        return self._current_user
+
+    def get_quotas(
+        self,
+        build_id: int,
+        project_id: str,
+        item_id: int,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+        process_status: Optional[int] = None,
+        send_status: Optional[int] = None,
+    ):
+        """分页查询定额列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            project_id: 项目编号
+            build_id: 概算序号
+            item_id: 条目序号
+            keyword: 关键字
+            process_status: 处理状态
+            send_status: 发送状态
+
+        Returns:
+            Tuple[total_count, quotas]
+        """
+        query = self.db_session.query(ProjectQuotaModel)
+
+        # 构建查询条件
+        conditions = [
+            ProjectQuotaModel.is_del == 0,
+            ProjectQuotaModel.project_id == project_id,
+            ProjectQuotaModel.build_id == build_id,
+            ProjectQuotaModel.item_id == item_id
+        ]
+        if keyword:
+            conditions.append(or_(
+                ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
+                ProjectQuotaModel.project_name.like(f"%{keyword}%"),
+            ))
+        if process_status is not None:
+            conditions.append(ProjectQuotaModel.process_status == process_status)
+        if send_status is not None:
+            conditions.append(ProjectQuotaModel.send_status == send_status)
+
+        query = query.filter(and_(*conditions))
+
+        # 计算总数
+        total_count = query.count()
+
+        # 分页
+        query = query.offset((page - 1) * page_size).limit(page_size)
+
+        quotas = query.all()
+
+        return {
+            'total': total_count,
+            'data': quotas
+        }
+
+    def get_quota(self, quota_id: int) -> Optional[ProjectQuotaModel]:
+        """根据ID查询定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            Optional[ProjectQuotaDto]
+        """
+        quota = self.db_session.query(ProjectQuotaModel).filter(
+            and_(
+                ProjectQuotaModel.id == quota_id,
+                ProjectQuotaModel.is_del == 0
+            )
+        ).first()
+        return quota
+    def get_quota_dto(self, quota_id: int) -> Optional[ProjectQuotaDto]:
+        """根据ID查询定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            Optional[ProjectQuotaDto]
+        """
+        quota = self.get_quota(quota_id)
+
+        return ProjectQuotaDto.from_model(quota) if quota else None
+
+    def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
+        """创建定额
+
+        Args:
+            quota_dto: 定额DTO
+
+        Returns:
+            ProjectQuotaDto
+        """
+        quota = ProjectQuotaModel(
+            project_id=quota_dto.project_id,
+            build_id=quota_dto.build_id,
+            item_id=quota_dto.item_id,
+            item_code=quota_dto.item_code,
+            quota_code=quota_dto.quota_code,
+            project_name=quota_dto.project_name,
+            unit=quota_dto.unit,
+            project_quantity=quota_dto.project_quantity,
+            project_quantity_input=quota_dto.project_quantity_input,
+            quota_adjustment=quota_dto.quota_adjustment,
+            unit_price=quota_dto.unit_price,
+            total_price=quota_dto.total_price,
+            unit_weight=quota_dto.unit_weight,
+            total_weight=quota_dto.total_weight,
+            labor_cost=quota_dto.labor_cost,
+            process_status=quota_dto.process_status,
+            process_time=quota_dto.process_time,
+            process_error=quota_dto.process_error,
+            send_status=quota_dto.send_status,
+            send_time=quota_dto.send_time,
+            send_error=quota_dto.send_error,
+            created_by=self.current_user.username,
+            created_at=datetime.now(),
+        )
+
+        self.db_session.add(quota)
+        self.db_session.commit()
+
+        return ProjectQuotaDto.from_model(quota)
+
+    def update_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
+        """更新定额
+
+        Args:
+            quota_dto: 定额DTO
+
+        Returns:
+            Optional[ProjectQuotaDto]
+        """
+        quota = self.get_quota(quota_dto.id)
+
+        if not quota:
+            return None
+        quota.quota_code = quota_dto.quota_code
+        quota.project_name = quota_dto.project_name
+        quota.unit = quota_dto.unit
+        quota.project_quantity = quota_dto.project_quantity
+        quota.project_quantity_input = quota_dto.project_quantity_input
+        quota.quota_adjustment = quota_dto.quota_adjustment
+        quota.unit_price = quota_dto.unit_price
+        quota.total_price = quota_dto.total_price
+        quota.unit_weight = quota_dto.unit_weight
+        quota.total_weight = quota_dto.total_weight
+        quota.labor_cost = quota_dto.labor_cost
+        quota.updated_by = self.current_user.username
+        quota.updated_at = datetime.now()
+
+        self.db_session.commit()
+
+        return ProjectQuotaDto.from_model(quota)
+
+    def delete_quota(self, quota_id: int) -> bool:
+        """删除定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            bool
+        """
+        quota = self.db_session.query(ProjectQuotaModel).filter(
+            and_(
+                ProjectQuotaModel.id == quota_id,
+                ProjectQuotaModel.is_del == 0
+            )
+        ).first()
+
+        if not quota:
+            return False
+
+        quota.is_del = 1
+        quota.deleted_by = self.current_user.username
+        quota.deleted_at = datetime.now()
+        self.db_session.commit()
+        return True
+
+    def update_process_status(self,quota_id:int, status:int, err:str = None):
+        """
+        更新项目定额的流程状态
+        Args:
+            quota_id: 定额ID
+            status: 流程状态
+            err:
+        Returns:
+            bool
+        """
+        quota = self.get_quota(quota_id)
+        if not quota:
+            return False
+        quota.process_status = status
+        if err:
+            quota.process_error = err
+        quota.process_time = datetime.now()
+        self.db_session.commit()
+
+    def update_send_status(self,quota_id:int, status:int, err:str = None) -> bool:
+        """
+        更新发送状态
+        Args:
+            quota_id: int
+            status: int
+            err: str
+        Returns:
+            bool
+        """
+        quota = self.get_quota(quota_id)
+        if not quota:
+            return False
+        quota.send_status = status
+        if err:
+            quota.send_error = err
+        quota.send_time = datetime.now()
+        self.db_session.commit()
+

+ 220 - 0
SourceCode/IntelligentRailwayCosting/app/stores/project_task.py

@@ -0,0 +1,220 @@
+from sqlalchemy import and_
+from sqlalchemy.orm import Session
+from datetime import datetime
+from typing import Optional
+
+import tools.db_helper as db_helper
+from core.dtos import ProjectTaskDto
+from core.models import ProjectTaskModel
+from core.user_session import UserSession
+
+
+class ProjectTaskStore:
+    def __init__(self, db_session: Session = None):
+        self.db_session = db_session or db_helper.create_mysql_session()
+        self._current_user = None
+
+    @property
+    def current_user(self):
+        if self._current_user is None:
+            self._current_user = UserSession.get_current_user()
+        return self._current_user
+
+    def get_tasks(
+        self,
+        build_id: int,
+        project_id: str,
+        item_id: int,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+        collect_status: Optional[int] = None,
+        process_status: Optional[int] = None,
+        send_status: Optional[int] = None,
+    ) :
+        """分页查询任务列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            project_id: 项目编号
+            build_id: 概算序号
+            item_id: 条目序号
+            keyword: 关键字
+            collect_status: 采集状态
+            process_status: 处理状态
+            send_status: 发送状态
+
+        Returns:
+
+        """
+        query = self.db_session.query(ProjectTaskModel)
+
+        # 构建查询条件
+        conditions = [
+            ProjectTaskModel.is_del == 0,
+            ProjectTaskModel.project_id == project_id,
+            ProjectTaskModel.build_id == build_id,
+            ProjectTaskModel.item_id == item_id
+        ]
+        if keyword:
+            conditions.append(ProjectTaskModel.task_name.like(f'%{keyword}%'))
+        if collect_status is not None:
+            conditions.append(ProjectTaskModel.collect_status == collect_status)
+        if process_status is not None:
+            conditions.append(ProjectTaskModel.process_status == process_status)
+        if send_status is not None:
+            conditions.append(ProjectTaskModel.send_status == send_status)
+
+        query = query.filter(and_(*conditions))
+
+        # 计算总数
+        total_count = query.count()
+
+        # 分页
+        query = query.offset((page - 1) * page_size).limit(page_size)
+
+        tasks = query.all()
+
+        return {
+            'total': total_count,
+            'data': tasks
+        }
+    def get_task(self, task_id: int) -> Optional[ProjectTaskModel]:
+        task = self.db_session.query(ProjectTaskModel).filter(
+            and_(
+                ProjectTaskModel.id == task_id,
+                ProjectTaskModel.is_del == 0
+            )).first()
+        return task
+
+
+    def get_task_dto(self, task_id: int) -> Optional[ProjectTaskDto]:
+        """根据ID查询任务
+
+        Args:
+            task_id: 任务ID
+
+        Returns:
+            Optional[ProjectTaskDto]
+        """
+        task_dto = self.get_task(task_id)
+        return ProjectTaskDto.from_model(task_dto) if task_dto else None
+
+    def create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
+        """创建任务
+
+        Args:
+            task_dto: 任务DTO
+
+        Returns:
+            ProjectTaskDto
+        """
+        task = ProjectTaskModel(
+            task_name=task_dto.task_name,
+            task_desc=task_dto.task_desc,
+            project_id=task_dto.project_id,
+            build_id=task_dto.build_id,
+            item_id=task_dto.item_id,
+            item_code=task_dto.item_code,
+            file_path=task_dto.file_path,
+            created_by=self.current_user.username,
+            created_at=datetime.now(),
+        )
+
+        self.db_session.add(task)
+        self.db_session.commit()
+
+        return ProjectTaskDto.from_model(task)
+
+    def update_task(self, task_dto: ProjectTaskDto) -> Optional[ProjectTaskDto]:
+        """更新任务
+
+        Args:
+            task_dto: 任务DTO
+
+        Returns:
+            Optional[ProjectTaskDto]
+        """
+        task = self.get_task(task_dto.id)
+
+        if not task:
+            return None
+
+        task.task_name = task_dto.task_name
+        task.task_desc = task_dto.task_desc
+        # task.project_id = task_dto.project_id
+        # task.build_id = task_dto.build_id
+        # task.item_id = task_dto.item_id
+        # task.item_code = task_dto.item_code
+        task.file_path = task_dto.file_path
+        task.updated_by=self.current_user.username
+        task.updated_at=datetime.now()
+
+        self.db_session.commit()
+
+        return ProjectTaskDto.from_model(task)
+
+
+    def update_task_files(self, task_id: int,files: str):
+        task = self.get_task(task_id)
+        if not task:
+            return None
+        task.file_path = files
+        task.updated_by=self.current_user.username
+        task.updated_at=datetime.now()
+        self.db_session.commit()
+    def delete_task(self, task_id: int) -> bool:
+        """删除任务
+
+        Args:
+            task_id: 任务ID
+        Returns:
+            bool
+        """
+        task = self.get_task(task_id)
+
+        if not task:
+            return False
+
+        task.is_del = 1
+        task.deleted_by = self.current_user.username
+        task.deleted_at = datetime.now()
+
+        self.db_session.commit()
+
+        return True
+
+    def update_collect_status(self,task_id:int, status:int, err:str = None):
+        task = self.get_task(task_id)
+        if not task:
+            return False
+        task.collect_status = status
+        if err:
+            task.collect_error = err
+        task.collect_time = datetime.now()
+        self.db_session.commit()
+        return True
+
+    def update_process_status(self,task_id:int, status:int, err:str = None):
+        task = self.get_task(task_id)
+        if not task:
+            return False
+        task.process_status = status
+        if err:
+            task.process_error = err
+        task.process_time = datetime.now()
+        self.db_session.commit()
+        return True
+
+    def update_send_status(self,task_id:int, status:int, err:str = None):
+        task = self.get_task(task_id)
+        if not task:
+            return False
+        task.send_status = status
+        if err:
+            task.send_error = err
+        task.send_time = datetime.now()
+        self.db_session.commit()
+        return True
+

+ 9 - 6
SourceCode/IntelligentRailwayCosting/app/tools/db_helper/sqlserver_helper.py

@@ -20,15 +20,18 @@ class SQLServerHelper(DBHelper):
             'trusted_connection': 'yes'
         }
         self._pool_config = {
-            'pool_size': 10,  # 增加初始连接数
-            'max_overflow': 20,  # 增加最大溢出连接数
-            'pool_timeout': 30,
+            'pool_size': 5,  # 减少初始连接数以降低资源占用
+            'max_overflow': 10,  # 适当减少最大溢出连接数
+            'pool_timeout': 60,  # 增加池等待超时时间
             'pool_recycle': 1800,  # 每30分钟回收连接
             'pool_pre_ping': True,  # 启用连接健康检查
             'connect_args': {
-                'connect_timeout': 60,
-                'connect_retries': 5,  # 增加重试次数
-                'connect_retry_interval': 3  # 减少重试间隔
+                'timeout': 60,  # 连接超时时间
+                'driver_connects_timeout': 60,  # 驱动连接超时
+                'connect_timeout': 60,  # ODBC连接超时
+                'connect_retries': 3,  # 连接重试次数
+                'connect_retry_interval': 10,  # 重试间隔增加到10秒
+                'connection_timeout': 60  # 额外的连接超时设置
             }
         }
 

+ 5 - 1
SourceCode/IntelligentRailwayCosting/app/tools/utils/string_helper.py

@@ -77,7 +77,11 @@ class StringHelper:
 
     @staticmethod
     def to_str(data:dict|list|tuple):
-        return json.dumps(data, ensure_ascii=False)
+        def datetime_handler(obj):
+            if hasattr(obj, 'isoformat'):
+                return obj.isoformat()
+            return str(obj)
+        return json.dumps(data, ensure_ascii=False, default=datetime_handler)
     @staticmethod
     def is_email(s: str) -> bool:
         """

+ 12 - 2
SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css

@@ -7,8 +7,15 @@
     padding: 0;
     box-sizing: border-box;
 }
-
-
+.app-container{
+    padding: 0 10px;
+}
+.app-footer{
+    background: #f9f9f9;
+}
+.nav-tabs.nav-line-tabs .nav-link.active {
+    color: var(--bs-primary);
+}
 
 body > .container, body > .container-fluid{
     padding: 0;
@@ -43,6 +50,9 @@ body > .container, body > .container-fluid{
     font-size: 16px;
     height: 45px;
 }
+.table-box td > .link:hover{
+    border-bottom: 2px solid;
+}
 .table-box td > .btn{
     padding: calc(.2rem + 1px) calc(.6rem + 1px)!important;
     margin: 0 5px;

+ 7 - 5
SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js

@@ -63,11 +63,10 @@ function IwbAjax_2(opt, modal, table) {
 	opt.table = table || '#table'
 	IwbAjax(opt)
 }
-function IwbTable(table, opts) {
+function IwbTable(table, opts,isReload) {
 	const $table = $(table)
 	const $tableBox = $table.closest('.table-box')
 	if (table.length === 0) return
-	const options = $table.data('options')
 	const defaultOptions = {
 		pageSize: 15,
 		pageNum: 1,
@@ -75,7 +74,8 @@ function IwbTable(table, opts) {
 			keyword: '',
 		},
 	}
-	const tableOptions = $.extend({}, options || defaultOptions, opts || {})
+	const options = isReload ? defaultOptions : ($table.data('options') || defaultOptions)
+	const tableOptions = $.extend({}, options, opts || {})
 	let isSearch = false
 	function ajaxTable(opt) {
 		if (isSearch) {
@@ -94,7 +94,9 @@ function IwbTable(table, opts) {
 			.then((response) => response.json())
 			.then((res) => {
 				if (res.success) {
+					opt.callBeforeRender && opt.callBeforeRender($table,res.data.rows)
 					renderTable(res.data.rows, res.data.total)
+					setTimeout(() => {opt.callAfterRender && opt.callAfterRender($table,res.data.rows)},1000)
 				} else {
 					renderTable([], 0)
 					console.error('加载表格出错:', res.message, opt.url)
@@ -109,11 +111,11 @@ function IwbTable(table, opts) {
 	}
 	function loading() {
 		isSearch = true
-		$tableBox.append(`<div class="table-loading"><span>正在加载中...</span></div>`)
+		$tableBox.addClass('table-loading').append(`<div class="table-loading-message"><span>正在加载中...</span></div>`)
 	}
 	function clearLoading() {
 		isSearch = false
-		$tableBox.find('.table-loading').remove()
+		$tableBox.removeClass('table-loading').find('.table-loading-message').remove()
 	}
 	function renderTable(rows, total) {
 		const opt = $table.data('options')

二進制
SourceCode/IntelligentRailwayCosting/app/views/static/media/favicon.ico


+ 12 - 0
SourceCode/IntelligentRailwayCosting/app/views/static/project/budget.css

@@ -0,0 +1,12 @@
+.project-box{
+    width: 100%;
+}
+.project-box .left-box{
+    border-right: 2px solid var(--bs-border-color);
+    padding: 10px 15px 10px 5px;
+    max-width: 300px;
+}
+.project-box .right-box{
+    padding: 10px 15px;
+    overflow: auto;
+}

+ 329 - 31
SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js

@@ -3,20 +3,43 @@ const table = '#table',
 console.log(project_id)
 const nav_tab_template = `
 		<li class="nav-item" data-id="{0}">
-			<a class="nav-link btn btn-active-light btn-color-gray-600 btn-active-color-primary {2} rounded-bottom-0" data-bs-toggle="tab" href="#iwb_tab_{0}">{1}</a>
+			<button type="button" class="nav-link {2} btn-light-primary btn-active-color-primary" data-id="{0}"  data-bs-toggle="tab" data-bs-target="#iwb_tab_{0}">{1}</button>
 		</li>`,
-	tab_content_template = `<div class="tab-pane fade show active" id="iwb_tab_{0}" role="tabpanel">{1}</div>`,
+	tab_content_template = `<div class="tab-pane h-100 fade" id="iwb_tab_{0}" role="tabpanel">{1}</div>`,
 	table_template = `
-<div class="d-flex flex-row h-100">
-	<div class="w-200px flex-row-auto left-box border-left-1 border-gray-500">
-		<div class="tree-dom"></div>
+<div class="d-flex flex-row h-100 project-box">
+	<div class="flex-row-auto h-100 left-box">
+		<div class="tree-dom h-100 overflow-auto" id="js-tree_{0}"></div>
 	</div>
 	<div class="flex-row-fluid right-box"> 
-		<div class="table-box table-responsive" id="table_{0}">
-			<div class="d-flex justify-content-between mb-5 mx-10">
-				<div></div>
+		<div class="table-box table-responsive" id="table_box_{0}" style="display: none">
+			<section class="d-none">
+				<input type="hidden" name="budget_id" value="{0}">
+				<input type="hidden" name="project_id" value="">
+				<input type="hidden" name="item_id" value="">
+				<input type="hidden" name="item_code" value="">
+			</section>
+			<span class="my-3 fs-3 table-title mt-5"><span class="fw-bolder me-5 title"></span><span class="badge badge-primary">定额任务列表</span></span>
+			<div class="d-flex justify-content-between my-5">
+				<div>
+					<button type="button" class="btn btn-primary btn-sm" onclick="Add('{0}')">添加任务</button>
+				</div>
 				<form class="search-box d-flex">
 					<div class="d-flex">
+						<select class="form-select form-select-sm me-5" name="process_status">
+							<option value="">全部处理状态</option>
+							<option value="0">未处理</option>
+							<option value="1">处理中</option>
+							<option value="2">已处理</option>
+							<option value="3">处理失败</option>
+						</select>
+						<select class="form-select form-select-sm me-5" name="send_status">
+							<option value="">全部发送状态</option>
+							<option value="0">未发送</option>
+							<option value="1">发送中</option>
+							<option value="2">已发送</option>
+							<option value="3">发送失败</option>
+						</select>
 						<input type="text" class="form-control form-control-sm w-200px" placeholder="请输入关键字" name="keyword" />
 					</div>
 					<div class="btn-group ms-5">
@@ -25,47 +48,58 @@ const nav_tab_template = `
 					</div>
 				</form>
 			</div>
-			<table class="table table-striped table-bordered table-hover  table-rounded" id="table">
+			<table class="table table-striped table-bordered table-hover  table-rounded" id="table_{0}">
 			</table>
 			<div class="pagination-row"></div>
-		</div>
+		</section>
 	</div>
 </div>`
 
 $(function () {
 	GetBudgetInfo()
+	// setTimeout(function () {GetBudgetItems(1)},1000)
 })
 
 function GetBudgetInfo() {
 	IwbAjax_1({
 		url: `/api/project/budget/${project_id}`,
 		success: function (res) {
-			console.log(res)
 			if (res.success) {
-				let str1 = '',
-					str2 = '',
-					first_id = ''
-				for (let i = 0; i < res.data.length; i++) {
-					const item = res.data[i]
-					let active = ''
-					if (i === 0) {
-						first_id = item.budget_id
-						active = 'active'
-					}
-					str1 += nav_tab_template.format(item.budget_id, item.budget_code, active)
-					str2 += tab_content_template.format(item.budget_id, table_template.format(item.budget_id))
-				}
-				$('#nav_tab').html(str1)
-				$('#tab_content').html(str2)
-				GetBudgetItems(first_id)
+				RenderTabs(res.data)
+			}else{
+				console.error(res.message)
 			}
 		},
 	})
 }
 
+function RenderTabs(data){
+	console.log('RenderTabs', data)
+	let str1 = '',
+		str2 = ''
+	if(data && data.length){
+		for (let i = 0; i < data.length; i++) {
+			const item = data[i]
+			str1 += nav_tab_template.format(item.budget_id, item.budget_code)
+			str2 += tab_content_template.format(item.budget_id, table_template.format(item.budget_id))
+		}
+	}
+	$('#nav_tab').html(str1)
+	const h= $('.app-main .app-container').height() - $('#nav_tab').height() - $('#breadcrumb_header').height()
+	$('#tab_content').html(str2).height(h)
+	const $tab = $('#nav_tab li button[data-bs-toggle="tab"]')
+	$tab.on('shown.bs.tab',(e)=>{
+		console.log('TAB', e)
+		const tab_id = $(e.target).data('id')
+		GetBudgetItems(tab_id)
+	})
+ 	const firstTab = new bootstrap.Tab($tab.eq(0))
+	firstTab.show()
+}
+
 function GetBudgetItems(id) {
-	console.log('GetBudgetItems', id)
-	const $tree = $(`#iwb_tab_${id} .tree-dom`)
+	const $tree = $(`#js-tree_${id}`)
+	// console.log('GetBudgetItems', id,$tree)
 	const opt = {
 		core: {
 			themes: {
@@ -73,7 +107,7 @@ function GetBudgetItems(id) {
 			},
 			check_callback: true,
 			data: function (node, callback) {
-				console.log('TREE_NODE', node)
+				// console.log('TREE_NODE', node)
 				IwbAjax_1({
 					url: `/api/project/budget-item/${id}/${project_id}?c=${node?.data?.item_code || ''}`,
 					success: res => {
@@ -97,6 +131,270 @@ function GetBudgetItems(id) {
 		},
 		plugins: ['dnd', 'types'],
 	}
-	$tree.jstree('destroy')
+	// $tree.jstree('destroy')
+	$tree.on('loaded.jstree', function(e, data){
+		// console.log('TREE_LOADED', e, data)
+		const inst = data.instance;
+		const obj = inst.get_node(e.target.firstChild.firstChild.firstChild);
+		inst.select_node(obj);
+	})
+	$tree.on('select_node.jstree', function (e, data) {
+		console.log('TREE_SELECTED', e, data)
+		RenderTabCondent(data.node?.data)
+	})
 	$tree.jstree(opt)
 }
+
+function RenderTabCondent(data) {
+	// console.log('RenderTabCondent', data)
+	if(data.chapter){
+		const $tableBox = $(`#table_box_${data.budget_id}`),
+			$table = $(`#table_${data.budget_id}`)
+		$tableBox.find('.table-title .title').text(`${data.chapter}、${data.project_name}`)
+		$tableBox.show()
+		$tableBox.find('input[name="budget_id"]').val(data.budget_id);
+		$tableBox.find('input[name="project_id"]').val(project_id);
+		$tableBox.find('input[name="item_id"]').val(data.item_id);
+		$tableBox.find('input[name="item_code"]').val(data.item_code);
+
+		IwbTable($table, {
+			url: `/api/task/list/${data.budget_id}/${project_id}/${data.item_id}`,
+			columns: [
+				{
+					title: '任务编号',
+					data: 'id',
+					width: '100px',
+				},
+				{
+					title: '任务名称',
+					data: 'task_name',
+					width: '240px',
+				},
+				{
+					title: '文件数据',
+					data: 'file_data',
+					width: 'auto',
+					render: (row) => {
+						let str = ``
+						const file_paths = row.file_path ? row.file_path.split(',') : []
+						if(file_paths.length){
+							for (let i = 0; i < file_paths.length; i++) {
+								const path = file_paths[i]
+								const names = path.split('/')
+								const file_name = names[names.length - 1]
+								str += `<a href="#" onclick="DownloadFile('/api/task/download?filename=${encodeURIComponent(path)}','${file_name}')"  class="link link-info px-2">${file_name}</a>`
+							}
+						}else{
+							str ="-"
+						}
+						return str
+					}
+				},
+				{
+					title: '任务状态',
+					data: 'task_status',
+					width: '220px',
+					render: (row) => {
+						let str = ``
+						if (row.collect_status === 0) {
+							str += `<span class="badge badge-light-primary">未采集</span>`
+						} else if (row.collect_status === 1){
+							str += `<span class="badge badge-light-warning">采集中</span>`
+						} else if (row.collect_status === 2){
+							str += `<span class="badge badge-light-success">采集完成</span>`
+							if (row.process_status === 0) {
+								str += `<span class="badge badge-light-primary">未处理</span>`
+							} else if (row.process_status === 1){
+								str += `<span class="badge badge-light-warning">处理中</span>`
+							} else if (row.process_status === 2){
+								str += `<span class="badge badge-light-success">已处理</span>`
+								if (row.send_status === 0) {
+									str += `<span class="badge badge-light-primary">未发送</span>`
+								} else if (row.send_status === 1){
+									str += `<span class="badge badge-light-warning">发送中</span>`
+								} else if (row.send_status === 2){
+									str += `<span class="badge badge-light-success">已发送</span>`
+								} else if (row.send_status === 3){
+									str += `<span class="badge badge-light-danger">发送失败</span>`
+								}
+							} else if (row.process_status === 3){
+								str += `<span class="badge badge-light-danger">处理失败</span>`
+							}
+						} else if (row.collect_status === 3){
+							str += `<span class="badge badge-light-danger">采集失败</span>`
+						}
+
+						return str
+					}
+				},
+				{
+					title: '操作',
+					data: 'id',
+					width: '160px',
+					render: (row) => {
+						let str = ``
+						str += `<button type="button" class="btn btn-primary btn-sm" data-kt-menu="true" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end" data-kt-menu-flip="top-end">
+                                操作
+                                <span class="svg-icon fs-5 m-0">
+                                    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1">
+                                        <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+                                            <polygon points="0 0 24 0 24 24 0 24"></polygon>
+                                            <path d="M6.70710678,15.7071068 C6.31658249,16.0976311 5.68341751,16.0976311 5.29289322,15.7071068 C4.90236893,15.3165825 4.90236893,14.6834175 5.29289322,14.2928932 L11.2928932,8.29289322 C11.6714722,7.91431428 12.2810586,7.90106866 12.6757246,8.26284586 L18.6757246,13.7628459 C19.0828436,14.1360383 19.1103465,14.7686056 18.7371541,15.1757246 C18.3639617,15.5828436 17.7313944,15.6103465 17.3242754,15.2371541 L12.0300757,10.3841378 L6.70710678,15.7071068 Z" fill="currentColor" fill-rule="nonzero" transform="translate(12.000003, 11.999999) rotate(-180.000000) translate(-12.000003, -11.999999)"></path>
+                                        </g>
+                                    </svg>
+                                </span>
+                            </button>
+						  <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-bold fs-7 w-125px px-3 py-1" data-kt-menu="true">
+							`
+						const template = `<div class="menu-item py-1 px-1"><button type="button" class="btn btn-sm btn-light-{0} w-100" onclick="{1}(${row.id},${data.budget_id})">{2}</button></div>`
+						if (row.collect_status === 0) {
+							str += template.format('primary', 'StartCollect', '开始采集')
+						} else if (row.collect_status === 2) {
+							str += template.format('warning', 'ReStartCollect', '重新采集')
+							if (row.process_status === 0) {
+								str += template.format('primary', 'StartProcess', '开始处理')
+							} else if (row.process_status === 2) {
+								str += template.format('warning', 'ReStartProcess', '重新处理')
+								if (row.send_status === 0) {
+									str += template.format('primary', 'StartSend', '开始发送')
+								} else if (row.send_status === 2) {
+									str += template.format('warning', 'ReStartSend', '重新发送')
+								} else if (row.send_status === 3) {
+									str += template.format('danger', 'ReStartSend', '重新发送')
+								}
+							} else if (row.process_status === 3) {
+								str += template.format('danger', 'ReStartProcess', '重新处理')
+							}
+						} else if (row.collect_status === 3) {
+							str += template.format('danger', 'ReStartCollect', '重新采集')
+						}
+						str += template.format('primary', 'Edit', '编辑')
+						str += template.format('danger', 'Delete', '删除')
+						// if (row.collect_status === 0) {
+						// 	str += `<button type="button" class="btn btn-sm btn-light-primary" onclick="StartCollect(${row.id}, ${data.budget_id})">开始任务</button>`
+						// } else if (row.collect_status === 2) {
+						// 	str += `<button type="button" class="btn btn-sm btn-light-warning" onclick="ReStartCollect(${row.id}, ${data.budget_id})">重新采集</button>`
+						// 	if (row.process_status === 0) {
+						// 		str += `<button type="button" class="btn btn-sm btn-light-primary" onclick="StartProcess(${row.id}, ${data.budget_id})">开始处理</button>`
+						// 	} else if (row.process_status === 2) {
+						// 		str += `<button type="button" class="btn btn-sm btn-light-warning" onclick="ReStartProcess(${row.id}, ${data.budget_id})">重新处理</button>`
+						// 		if (row.send_status === 0) {
+						// 			str += `<button type="button" class="btn btn-sm btn-light-primary" onclick="StartSend(${row.id}, ${data.budget_id})">开始发送</button>`
+						// 		} else if (row.send_status === 2) {
+						// 			str += `<button type="button" class="btn btn-sm btn-light-warning" onclick="ReStartSend(${row.id}, ${data.budget_id})">重新发送</button>`
+						// 		} else if (row.send_status === 3) {
+						// 			str += `<button type="button" class="btn btn-sm btn-light-danger" onclick="ReStartSend(${row.id}, ${data.budget_id})">重新发送</button>`
+						// 		}
+						// 	} else if (row.process_status === 3) {
+						// 		str += `<button type="button" class="btn btn-sm btn-light-danger" onclick="ReStartProcess(${row.id}, ${data.budget_id})">重新处理</button>`
+						// 	}
+						// } else if (row.collect_status === 3) {
+						// 	str += `<button type="button" class="btn btn-sm btn-light-danger" onclick="ReStartCollect(${row.id}, ${data.budget_id})">重新采集</button>`
+						// }
+						// str += `<button type="button" class="btn btn-sm btn-light-info" onclick="GoTo('')">详情</button>`
+						// str += `<button type="button" class="btn btn-sm btn-light-primary" onclick="Edit(${row.id}, ${data.budget_id})">编辑</button>`
+						// str += `<button type="button" class="btn btn-sm btn-light-danger" onclick="Delete(${row.id}, ${data.budget_id})">删除</button>`
+						str+= `</div>`
+						str += `<button type="button" class="btn btn-sm btn-light-primary ms-5" onclick="GoTo('')">详情</button></div>`
+
+						return str
+					}
+				},
+			],
+			callAfterRender:()=>{
+				KTMenu.createInstances(`#table_${data.budget_id} [data-kt-menu="true"]`)
+			}
+		}, true)
+	} else {
+
+	}
+
+}
+
+function Add(budget_id) {
+	AddModal($modal, () => {
+		$modal.find('#task_id').val('0');
+		SetBudgetData(budget_id)
+	})
+}
+
+function Edit(id) {
+    EditModal($modal,()=>{
+        IwbAjax_1({
+            url:`/api/task/get/${id}`,
+            success:res=>{
+				if(!res.success){
+					console.error(res.message)
+					return
+				}
+				const data = res.data
+				// SetBudgetData(budget_id)
+        		$modal.find('#task_id').val(data.id);
+				$modal.find('#budget_id').val(data.budget_id);
+				$modal.find('#project_id').val(data.project_id);
+				$modal.find('#item_id').val(data.item_id);
+				$modal.find('#item_code').val(data.item_code);
+                $modal.find('#task_name').val(data.task_name);
+                $modal.find('#task_desc').val(data.task_desc);
+
+            }
+        })
+    })
+}
+
+function SetBudgetData(budget_id){
+	const $tableBox = $(`#table_box_${budget_id}`)
+	$modal.find('#budget_id').val(budget_id);
+	$modal.find('#project_id').val($tableBox.find('input[name="project_id"]').val());
+	$modal.find('#item_id').val($tableBox.find('input[name="item_id"]').val());
+	$modal.find('#item_code').val($tableBox.find('input[name="item_code"]').val());
+}
+function SaveProject() {
+    const
+		budget_id = $modal.find('#budget_id').val(),
+		item_id = $modal.find('#item_id').val(),
+		item_code = $modal.find('#item_code').val(),
+		project_id = $modal.find('#project_id').val(),
+		task_id=  $modal.find('#task_id').val(),
+		task_name = $modal.find('#task_name').val(),
+		task_desc = $modal.find('#task_desc').val()
+    IwbAjax({
+        url:`/api/task/save/${task_id}`,
+        data:{
+			budget_id,
+			project_id,
+			item_id,
+			item_code,
+            task_name,
+            task_desc,
+        },
+		modal:"#modal",
+		table:`#table_${budget_id}`
+    })
+}
+
+function Delete(id,budget_id){
+	ConfirmUrl('确定删除吗?',`/api/task/delete/${id}`,`#table_${budget_id}`)
+}
+
+function StartCollect(id,budget_id){
+	ConfirmUrl('确定开始采集吗?',`/api/task/start_collect/${id}`,`#table_${budget_id}`)
+}
+function ReStartCollect(id,budget_id){
+	ConfirmUrl('确定重新开始采集吗?',`/api/task/start_collect/${id}`,`#table_${budget_id}`)
+}
+
+function StartProcess(id,budget_id){
+	ConfirmUrl('确定开始处理吗?',`/api/task/start_process/${id}`,`#table_${budget_id}`)
+}
+
+function ReStartProcess(id,budget_id){
+	ConfirmUrl('确定重新开始处理吗?',`/api/task/start_process/${id}`,`#table_${budget_id}`)
+}
+
+function StartSend(id,budget_id){
+	ConfirmUrl('确定开始发送吗?',`/api/task/start_send/${id}`,`#table_${budget_id}`)
+}
+function ReStartSend(id,budget_id){
+	ConfirmUrl('确定重新开始发送吗?',`/api/task/start_send/${id}`,`#table_${budget_id}`)
+}

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/views/templates/account/login.html

@@ -25,5 +25,5 @@
 </div>
 {% endblock %}
 {% block scripts %}
-<script src="{{ url_for('', filename='/account/login.js') }}"></script>
+<script src="{{ url_for('static', filename='/account/login.js') }}"></script>
 {% endblock %}

+ 1 - 3
SourceCode/IntelligentRailwayCosting/app/views/templates/base/base.html

@@ -4,17 +4,15 @@
 		<meta charset="UTF-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 		<title>{% block title %}{% endblock %} - IWB</title>
-<!--		<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/bootstrap.css') }}" />-->
 		<link rel="stylesheet" href="{{ url_for('static', filename='base/plugins/plugins.bundle.css') }}" />
 		<link rel="stylesheet" href="{{ url_for('static', filename='base/css/style.bundle.css') }}" />
+		<link rel="icon" href="{{ url_for('static', filename='media/favicon.ico') }}" />
 		{% block head %}{% endblock %}
 	</head>
 	<body data-kt-app-layout="dark-header"
 		  data-kt-app-header-fixed="true"
 		  class="app-default">
 		{% block content %}{% endblock %}
-<!--		<script src="{{ url_for('static', filename='jquery/jquery.js') }}"></script>
-		<script src="{{ url_for('static', filename='bootstrap/bootstrap.js') }}"></script>-->
 		<script src="{{ url_for('static', filename='base/plugins/plugins.bundle.js') }}"></script>
 		<script src="{{ url_for('static', filename='base/js/scripts.bundle.js') }}"></script>
 		{% block scripts %}{% endblock %}

+ 1 - 3
SourceCode/IntelligentRailwayCosting/app/views/templates/base/layout.html

@@ -47,9 +47,7 @@
             <div class="app-main flex-column flex-row-fluid">
                 <div class="d-flex flex-column flex-column-fluid">
                     <div class="app-container flex-column-fluid">
-                        <div class="app-container container-fluid">
-                            {% block page_content %}{% endblock %}
-                        </div>
+                       {% block page_content %}{% endblock %}
                     </div>
                 </div>
                 <div class="app-footer">

+ 15 - 32
SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

@@ -1,44 +1,23 @@
 {% extends "base/layout.html" %} {% block title %}概算信息{% endblock %} {% block page_head_plugins %}
 <link rel="stylesheet" href="{{ url_for('static', filename='base/plugins/jstree/jstree.bundle.css') }}" />
+<link rel="stylesheet" href="{{ url_for('static', filename='project/budget.css') }}" />
 {% endblock %} {% block page_content %}
 
-<div class="header my-5">
+<div class="header my-5" id="breadcrumb_header">
 	<h3>概算信息</h3>
 	<ol class="breadcrumb breadcrumb-dot text-muted fs-6 fw-semibold ms-5">
 		<li class="breadcrumb-item"><a href="{{ url_for('project.index') }}" class="">项目管理</a></li>
 		<li class="breadcrumb-item text-muted">概算信息</li>
 	</ol>
-	<button type="button" class="btn btn-primary btn-sm ms-5" onclick="GetBudgetItems(1)">TEST</button>
-</div>
-<div class="mb-5 hover-scroll-x">
-	<div class="d-grid">
-		<ul id="nav_tab" class="nav nav-tabs flex-nowrap text-nowrap"></ul>
-	</div>
-</div>
-<div class="tab-content" id="tab_content"></div>
-<div class="table-box table-responsive" style="display: none">
-	<div class="d-flex justify-content-between mb-5 mx-10">
-		<div>
-			<button class="btn btn-primary btn-sm" onclick="Add()">添加</button>
-		</div>
-		<form class="search-box d-flex">
-			<div class="d-flex">
-				<input type="text" class="form-control form-control-sm w-200px" placeholder="请输入关键字" name="keyword" />
-			</div>
-			<div class="btn-group ms-5">
-				<button type="button" class="btn btn-primary btn-sm" onclick="IwbTableSearch(this)">查询</button>
-				<button type="button" class="btn btn-danger btn-sm" onclick="IwbTableResetSearch(this)">重置</button>
-			</div>
-		</form>
-	</div>
-	<table class="table table-striped table-bordered table-hover table-rounded" id="table"></table>
-	<div class="pagination-row"></div>
 </div>
+<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-line-tabs-2x fs-6"></ul>
+
+<div class="tab-content" id="tab_content" style="height: calc(100% - 80px);"></div>
 <div class="modal fade" id="modal" tabindex="-1" aria-hidden="true">
 	<div class="modal-dialog modal-dialog-centered">
 		<div class="modal-content rounded">
 			<div class="modal-header">
-				<h3 class="modal-title" id="changePasswordModalLabel"><span class="prefix"></span>项目</h3>
+				<h3 class="modal-title" id="changePasswordModalLabel"><span class="prefix"></span>任务</h3>
 				<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
 					<i class="ki-duotone ki-cross fs-1">
 						<span class="path1"></span>
@@ -49,14 +28,18 @@
 			<div class="modal-body">
 				<form>
 					<div class="form-group">
-						<input type="hidden" id="project_id" />
+						<input type="hidden" id="budget_id" value="">
+						<input type="hidden" id="project_id" value="">
+						<input type="hidden" id="item_id" value="">
+						<input type="hidden" id="item_code" value="">
+						<input type="hidden" id="task_id" value="">
 						<div class="fv-row form-group mb-3">
-							<label for="project_name" class="form-label required">项目名称</label>
-							<input type="text" class="form-control" name="project_name" id="project_name" placeholder="请输入" required />
+							<label for="task_name" class="form-label required">任务名称</label>
+							<input type="text" class="form-control" name="task_name" id="task_name" placeholder="请输入" required />
 						</div>
 						<div class="fv-row form-group mb-3">
-							<label for="project_desc" class="form-label">项目名称</label>
-							<textarea type="text" class="form-control" name="project_desc" id="project_desc" placeholder="请输入"></textarea>
+							<label for="task_desc" class="form-label">任务详情</label>
+							<textarea type="text" class="form-control" name="task_desc" id="task_desc" placeholder="请输入"></textarea>
 						</div>
 					</div>
 				</form>