2 Commits 108d1a60ed ... 8026050971

Author SHA1 Message Date
  yue 8026050971 Update 代码优化,定额输入章节可以修改 2 months ago
  yue e03675d12f update 优化代码 2 months ago
25 changed files with 835 additions and 198 deletions
  1. 1 1
      SourceCode/IntelligentRailwayCosting/Docker/Dockerfile
  2. 1 1
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose.yml
  3. 1 1
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose_mysql.yml
  4. 1 1
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose_sqlserver.yml
  5. 6 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py
  6. 6 4
      SourceCode/IntelligentRailwayCosting/app/core/dtos/chapter.py
  7. 40 36
      SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py
  8. 29 22
      SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py
  9. 2 1
      SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py
  10. 1 1
      SourceCode/IntelligentRailwayCosting/app/main.py
  11. 52 21
      SourceCode/IntelligentRailwayCosting/app/routes/project.py
  12. 16 1
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  13. 42 4
      SourceCode/IntelligentRailwayCosting/app/services/project.py
  14. 132 19
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  15. 37 1
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  16. 62 7
      SourceCode/IntelligentRailwayCosting/app/stores/chapter.py
  17. 8 8
      SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py
  18. 33 19
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py
  19. 77 26
      SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css
  20. 131 0
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/select2tree.js
  21. 14 8
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js
  22. 1 1
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  23. 139 14
      SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js
  24. 2 1
      SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html
  25. 1 0
      SourceCode/IntelligentRailwayCosting/app/views/templates/project/quota_info.html

+ 1 - 1
SourceCode/IntelligentRailwayCosting/Docker/Dockerfile

@@ -28,7 +28,7 @@ COPY app/ /app
 
 
 # 暴露端口
-EXPOSE 5123
+EXPOSE 8080
 
 
 # 启动命令

+ 1 - 1
SourceCode/IntelligentRailwayCosting/Docker/docker-compose.yml

@@ -51,7 +51,7 @@ services:
     networks:
       - railway_costing_v1
     ports:
-      - "7010:5123"
+      - "7010:8080"
     restart: always
 
 networks:

+ 1 - 1
SourceCode/IntelligentRailwayCosting/Docker/docker-compose_mysql.yml

@@ -33,7 +33,7 @@ services:
     networks:
       - railway_costing_mysql_v1
     ports:
-      - "7012:5123"
+      - "7011:8080"
     restart: always
 
 networks:

+ 1 - 1
SourceCode/IntelligentRailwayCosting/Docker/docker-compose_sqlserver.yml

@@ -15,5 +15,5 @@ services:
       - /home/docker/iwb_railway_costing_v1/app/logs:/app/logs
       - /home/docker/iwb_railway_costing_v1/app/temp_files:/app/temp_files
     ports:
-      - "7010:5123"
+      - "7010:8080"
     restart: always

+ 6 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py

@@ -8,6 +8,7 @@ class AppConfig:
     _task_api_url = ""
     _task_max_projects_count = 10
     _task_interval = 300  # 任务执行间隔时间,单位秒
+    _ai_flag = "_AI"
 
     @property
     def name(self):
@@ -37,6 +38,10 @@ class AppConfig:
     def task_interval(self) -> int:
         return self._task_interval
 
+    @property
+    def ai_flag(self) -> str:
+        return self._ai_flag
+
     def update_config(self, config):
         """更新应用配置
 
@@ -50,3 +55,4 @@ class AppConfig:
         self._task_api_url = config.get("task_api_url", "")
         self._task_max_projects_count = int(config.get("task_max_projects", 10))
         self._task_interval = int(config.get("task_interval", 300))
+        self._ai_flag = config.get("ai_flag", "_AI")

+ 6 - 4
SourceCode/IntelligentRailwayCosting/app/core/dtos/chapter.py

@@ -8,6 +8,7 @@ class ChapterDto(BaseModel):
     # 章节表字段
     item_id: int
     item_code: Optional[str] = None
+    parent_code: Optional[str] = None
     default_first_page: Optional[str] = None
     chapter: Optional[str] = None
     section: Optional[str] = None
@@ -66,16 +67,15 @@ class ChapterDto(BaseModel):
 
     @classmethod
     def from_model(
-        cls,
-        chapter_model: ChapterModel,
-        budget_item_model: Optional[TotalBudgetItemModel] = None,
+            cls,
+            chapter_model: ChapterModel,
+            budget_item_model: Optional[TotalBudgetItemModel] = None,
     ) -> "ChapterDto":
         """从数据库模型创建DTO对象"""
         dto = cls(
             # 章节表字段
             item_id=chapter_model.item_id,
             item_code=chapter_model.item_code,
-            default_first_page=chapter_model.default_first_page,
             chapter=chapter_model.chapter,
             section=chapter_model.section,
             project_name=chapter_model.project_name,
@@ -109,6 +109,8 @@ class ChapterDto(BaseModel):
             # professional_name=chapter_model.professional_name,
             # tax_category=chapter_model.tax_category
         )
+        if "default_first_page" in chapter_model:
+            dto.default_first_page = chapter_model.default_first_page
 
         # 如果提供了总概算条目模型,则添加相关字段
         if budget_item_model:

+ 40 - 36
SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py

@@ -52,17 +52,18 @@ class ExcelParseFileDto:
 
 class ExcelParseDto:
     def __init__(
-        self,
-        task_id: int,
-        version: str,
-        project_id: str,
-        project_name: str,
-        project_stage: str,
-        selected_zgs_id: int,
-        zgs_list: list[ExcelParseZgsDto],
-        hierarchy: list[ExcelParseItemDto],
-        components: list[ExcelParseItemDto],
-        files: list[ExcelParseFileDto],
+            self,
+            task_id: int,
+            version: str,
+            project_id: str,
+            project_name: str,
+            project_stage: str,
+            selected_zgs_id: int,
+            zgs_list: list[ExcelParseZgsDto],
+            selected_chapter: list[ExcelParseItemDto],
+            # hierarchy: list[ExcelParseItemDto],
+            # components: list[ExcelParseItemDto],
+            files: list[ExcelParseFileDto],
     ):
         self.task_id = task_id
         self.version = version
@@ -71,12 +72,13 @@ class ExcelParseDto:
         self.project_stage = project_stage
         self.selected_zgs_id = -1 if selected_zgs_id == 0 else selected_zgs_id
         self.zgs_list = zgs_list
-        self.hierarchy = hierarchy
-        self.components = components
+        self.selected_chapter = selected_chapter
+        # self.hierarchy = hierarchy
+        # self.components = components
         self.files = files
 
     def to_dict(self):
-        return {
+        data = {
             "task_id": self.task_id,
             "version": self.version,
             "project_id": self.project_id,
@@ -85,9 +87,11 @@ class ExcelParseDto:
             "selected_zgs_id": self.selected_zgs_id,
             "files": [file.to_dict() for file in self.files],
             "zgs_list": [zgs.to_dict() for zgs in self.zgs_list],
-            "hierarchy": [item.to_dict() for item in self.hierarchy],
-            "components": [item.to_dict() for item in self.components],
+            "selected_chapter": [item.to_dict() for item in self.selected_chapter],
+            # "hierarchy": [item.to_dict() for item in self.hierarchy],
+            # "components": [item.to_dict() for item in self.components],
         }
+        return data
 
 
 class ExcelParseResultDataDto:
@@ -108,21 +112,21 @@ class ExcelParseResultDataDto:
     #     "ex_amount": excel中给出的数量, // number
     # }
     def __init__(
-        self,
-        zgs_id: int,  # 总概算id
-        zgs_code: str,  # 总概算编号
-        item_id: int,  # 条⽬序号
-        item_code: str,  # 条⽬编码
-        entry_name: str,  # ⼯程或费⽤项⽬名称,来⾃于定额表,
-        dinge_code: str,  # 定额编号,
-        units: str,  # 单位,
-        amount: float,  # 数量,
-        target_id: int,  # ⽤户数据库中条⽬的id,-1表示没有,
-        ex_file_id: str,  # excel⽂件id,
-        ex_cell: str,  # 数量单元格位置,例如 "C17",
-        ex_row: str,  # 该⾏内容,由逗号连接多个单元格得到,
-        ex_unit: str,  # excel中给出的单位,
-        ex_amount: float,  # excel中给出的数量,
+            self,
+            zgs_id: int,  # 总概算id
+            zgs_code: str,  # 总概算编号
+            item_id: int,  # 条⽬序号
+            item_code: str,  # 条⽬编码
+            entry_name: str,  # ⼯程或费⽤项⽬名称,来⾃于定额表,
+            dinge_code: str,  # 定额编号,
+            units: str,  # 单位,
+            amount: float,  # 数量,
+            target_id: int,  # ⽤户数据库中条⽬的id,-1表示没有,
+            ex_file_id: str,  # excel⽂件id,
+            ex_cell: str,  # 数量单元格位置,例如 "C17",
+            ex_row: str,  # 该⾏内容,由逗号连接多个单元格得到,
+            ex_unit: str,  # excel中给出的单位,
+            ex_amount: float,  # excel中给出的数量,
     ):
         self.zgs_id = zgs_id
         self.zgs_code = zgs_code
@@ -188,11 +192,11 @@ class ExcelParseResultDataDto:
 
 class ExcelParseResultDto:
     def __init__(
-        self,
-        task_id: int,
-        result: int = -1,
-        reason: str = "",
-        data: list[ExcelParseResultDataDto] = None,
+            self,
+            task_id: int,
+            result: int = -1,
+            reason: str = "",
+            data: list[ExcelParseResultDataDto] = None,
     ):
         self.task_id = task_id
         self.result = result  # -1-失败;0-运行中;1-成功

+ 29 - 22
SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py

@@ -54,15 +54,19 @@ class TaskProcessor:
             budgets = [
                 TotalBudgetInfoDto.from_model(budget) for budget in budget_models
             ]
-            parents = self._chapter_store.get_all_parents_chapter_items(
+            chapter = self._chapter_store.get_chapter_item_by_item_code(
                 task.project_id, task.item_code
             )
-            children = self._chapter_store.get_all_children_chapter_items(
-                task.project_id, task.item_code
-            )
-            data, msg = self._build_api_body(
-                task, project, budgets, parents, children, files
-            )
+            # parents = self._chapter_store.get_all_parents_chapter_items(
+            #     task.project_id, task.item_code
+            # )
+            # children = self._chapter_store.get_all_children_chapter_items(
+            #     task.project_id, task.item_code
+            # )
+            # data, msg = self._build_api_body(
+            #     task, project, budgets, parents, children, files
+            # )
+            data, msg = self._build_api_body(task, project, budgets, chapter, files)
             if not data:
                 raise Exception(msg)
             res = self._call_api(self._task_submit_url, data)
@@ -172,18 +176,20 @@ class TaskProcessor:
             return None, msg
 
     def _build_api_body(
-        self,
-        task: ProjectTaskDto,
-        project: ProjectDto,
-        budgets: list[TotalBudgetInfoDto],
-        parents: list[ChapterDto],
-        children: list[ChapterDto],
-        files: list[ExcelParseFileDto],
+            self,
+            task: ProjectTaskDto,
+            project: ProjectDto,
+            budgets: list[TotalBudgetInfoDto],
+            chapter: list[ChapterDto],
+            # parents: list[ChapterDto],
+            # children: list[ChapterDto],
+            files: list[ExcelParseFileDto],
     ):
         try:
             budgets_data = [ExcelParseZgsDto.from_dto(budget) for budget in budgets]
-            parents_data = [ExcelParseItemDto.from_dto(parent) for parent in parents]
-            children_data = [ExcelParseItemDto.from_dto(child) for child in children]
+            chapter_data = [ExcelParseItemDto.from_dto(chapter) for chapter in chapter]
+            # parents_data = [ExcelParseItemDto.from_dto(parent) for parent in parents]
+            # children_data = [ExcelParseItemDto.from_dto(child) for child in children]
             data = ExcelParseDto(
                 task_id=task.id or 0,
                 version=configs.app.version or "2020",
@@ -192,8 +198,9 @@ class TaskProcessor:
                 project_stage=project.design_stage,
                 selected_zgs_id=task.budget_id or 0,
                 zgs_list=budgets_data,
-                hierarchy=parents_data,
-                components=children_data,
+                selected_chapter=chapter_data,
+                # hierarchy=parents_data,
+                # components=children_data,
                 files=files,
             )
             return data, ""
@@ -226,10 +233,10 @@ class TaskProcessor:
             raise Exception(msg)
 
     def _insert_data(
-        self,
-        task: ProjectTaskDto,
-        project: ProjectDto,
-        data: list[ExcelParseResultDataDto],
+            self,
+            task: ProjectTaskDto,
+            project: ProjectDto,
+            data: list[ExcelParseResultDataDto],
     ):
         try:
             self._logger.debug(f"开始插入数据:{task.task_name}")

+ 2 - 1
SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py

@@ -1,4 +1,4 @@
-import tools.utils as utils
+import tools.utils as utils, core.configs as configs
 from core.dtos import ProjectTaskDto, ProjectQuotaDto, QuotaInputDto
 from core.enum import SendStatusEnum
 from stores import ProjectQuotaStore, ProjectTaskStore, QuotaInputStore
@@ -63,6 +63,7 @@ class TaskSender:
     def _save_quota(self, quota: QuotaInputDto, project_id: str):
         try:
             # data = self._quota_input_store.get_quota(project_id, quota.budget_id,quota.item_id,quota.quota_code)
+            quota.project_name = f"{quota.project_name}{configs.app.ai_flag}"
             if quota.quota_id and quota.quota_id > 0:
                 self._logger.debug(
                     f"修改定额输入[{quota.quota_id}]:{quota.project_name} {quota.quota_code} {quota.quota_id}"

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/main.py

@@ -13,7 +13,7 @@ def main():
     thread.daemon = True
     thread.start()
 
-    app.run(host="0.0.0.0", port=5123)  # 指定HTTP端口为5123
+    app.run(host="0.0.0.0", port=8080)  # 指定HTTP端口为8080
 
 
 if __name__ == "__main__":

+ 52 - 21
SourceCode/IntelligentRailwayCosting/app/routes/project.py

@@ -1,57 +1,88 @@
 from flask import Blueprint, request
 from core.user_session import Permission
-from core.api import  ResponseBase,TableResponse
-from services import  ProjectService
+from core.api import ResponseBase, TableResponse
+from services import ProjectService, ProjectTaskService
 
-project_api = Blueprint('project_api', __name__)
+project_api = Blueprint("project_api", __name__)
 project_srvice = ProjectService()
+task_service = ProjectTaskService()
 
-@project_api.route('/list', methods=['POST'])
+
+@project_api.route("/list", methods=["POST"])
 @Permission.authorize
 def get_page_list():
     try:
         data = request.get_json()
-        page = int(data.get('pageNum', 1))
-        per_page = int(data.get('pageSize', 10))
-        keyword = data.get('keyword')
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
         start_date = None
         end_date = None
-        date = data.get('date')
+        date = data.get("date")
         if date:
-            date = date.split(' - ')
+            date = date.split(" - ")
             start_date = date[0]
             end_date = date[1]
 
-        projects, total_count = project_srvice.get_projects_paginated(page, per_page, keyword, start_date, end_date)
+        projects, total_count = project_srvice.get_projects_paginated(
+            page, per_page, keyword, start_date, end_date
+        )
         return TableResponse.success(projects, total_count)
     except Exception as e:
         from routes.error import handle_api_error
-        return handle_api_error(e, '项目列表')
 
-@project_api.route('/budget/<project_id>', methods=['POST'])
+        return handle_api_error(e, "项目列表")
+
+
+@project_api.route("/budget/<project_id>", methods=["POST"])
+@Permission.authorize
+def get_budget_info(project_id: str):
+    try:
+        data, msg = project_srvice.get_budget_info(project_id)
+        if not data:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(data)
+    except Exception as e:
+        from routes.error import handle_api_error
+
+        return handle_api_error(e, "项目概算信息")
+
+
+@project_api.route("/chapter/<project_id>", methods=["POST"])
 @Permission.authorize
-def get_budget_info(project_id:str):
+def get_chapter_items(project_id: str):
+    code = request.args.get("c", None)
+    task_id = request.args.get("t", 0)
+    item_code = code if code else None
+    if not item_code and task_id != 0:
+        task = task_service.get_task(task_id)
+        item_code = task.item_code if task else None
     try:
-        data,msg = project_srvice.get_budget_info(project_id)
+        # 任务页面第一次请求
+        task_first = True if task_id != 0 and not code else False
+        data, msg = project_srvice.get_chapter_items(project_id, item_code, task_first)
         if not data:
             return ResponseBase.error(msg)
         return ResponseBase.success(data)
     except Exception as e:
         from routes.error import handle_api_error
-        return handle_api_error(e, '项目概算信息')
 
-@project_api.route('/chapter/<project_id>', methods=['POST'])
+        return handle_api_error(e, "项目章节条目")
+
+
+@project_api.route("/task_chapters/<int:task_id>", methods=["POST"])
 @Permission.authorize
-def get_chapter_items(project_id:str):
-    item_code = request.args.get('c', None)
+def get_all_chapters(task_id: int):
     try:
-        data,msg = project_srvice.get_chapter_items(project_id, item_code)
+        task = task_service.get_task(task_id)
+        data, msg = project_srvice.get_task_chapters(task.project_id, task.item_code)
         if not data:
             return ResponseBase.error(msg)
         return ResponseBase.success(data)
     except Exception as e:
         from routes.error import handle_api_error
-        return handle_api_error(e, '项目章节条目')
+
+        return handle_api_error(e, "项目章节条目")
 
 # @project_api.route('/budget-item/<budget_id>/<project_id>', methods=['POST'])
 # @Permission.authorize
@@ -63,4 +94,4 @@ def get_chapter_items(project_id:str):
 #             return ResponseBase.error(msg)
 #         return ResponseBase.success(data)
 #     except Exception as e:
-#         return ResponseBase.error(f'获取项目概算子条目失败:{str(e)}')
+#         return ResponseBase.error(f'获取项目概算子条目失败:{str(e)}')

+ 16 - 1
SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py

@@ -34,7 +34,7 @@ def get_page_list(budget_id: int, project_id: str, item_code: str):
 )
 @Permission.authorize
 def get_quotas_by_task_paginated(
-    task_id: int, budget_id: int, project_id: str, item_code: str
+        task_id: int, budget_id: int, project_id: str, item_code: str
 ):
     try:
         data = request.get_json()
@@ -106,3 +106,18 @@ def start_send():
         return ResponseBase.success()
     except Exception as e:
         return ResponseBase.error(f"启动定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/save_change_chapter/<project_id>", methods=["POST"])
+@Permission.authorize
+def save_change_chapter(project_id: str):
+    try:
+        data = request.get_json()
+        ids = data.get("ids", "")
+        new_id = data.get("new_id", "")
+        msg = quota_service.save_change_chapter(project_id, ids, new_id)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"变更章节失败:{str(e)}")

+ 42 - 4
SourceCode/IntelligentRailwayCosting/app/services/project.py

@@ -1,7 +1,7 @@
 from typing import Optional
 from datetime import datetime, timedelta
 
-from core.dtos import TotalBudgetInfoDto, TotalBudgetItemDto
+from core.dtos import TotalBudgetInfoDto, TotalBudgetItemDto, ChapterDto
 from core.dtos.project import ProjectDto
 from core.dtos.tree import TreeDto
 from core.user_session import UserSession
@@ -63,7 +63,43 @@ class ProjectService:
         items = self._budget_store.get_top_budget_items(project_id, budget_id)
         return [TotalBudgetItemDto.from_model(item).to_dict() for item in items], ""
 
-    def get_chapter_items(self, project_id: str, item_code: str):
+    def get_task_chapters(self, project_id: str, item_code: str):
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        data = self._chapter_store.get_task_chapter_items(project_id, item_code)
+        data_list = []
+        for item in data:
+            text = (
+                f"第{item.chapter}章、{item.project_name}"
+                if item.chapter
+                else (
+                    f"{item.section}  {item.project_name}"
+                    if item.section
+                    else item.project_name
+                )
+            )
+            parent = (
+                None
+                if item.item_code == item_code
+                else self.get_parent_code(item.item_code)
+            )
+            data_list.append(
+                TreeDto(item.item_code, parent, text, False, item).to_dict()
+            )
+
+        return data_list, ""
+
+    @staticmethod
+    def get_parent_code(item_code: str):
+        if not item_code or len(item_code) == 2:
+            return None
+        if len(item_code) == 4:
+            return item_code[:2]
+        else:
+            return item_code[: len(item_code) - 3]
+
+    def get_chapter_items(self, project_id: str, item_code: str, task_first: bool):
         msg = self._check_project_db_exit(project_id)
         if msg:
             return None, msg
@@ -87,9 +123,11 @@ class ProjectService:
                 project_id, team_item_code
             )
         else:
-            items = self._chapter_store.get_child_chapter_items(project_id, item_code)
+            items = self._chapter_store.get_child_chapter_items(
+                project_id, item_code, task_first
+            )
         parent = "#"
-        if item_code:
+        if item_code and not task_first:
             item = self._chapter_store.get_chapter_item_by_item_code(
                 project_id, item_code
             )

+ 132 - 19
SourceCode/IntelligentRailwayCosting/app/services/project_quota.py

@@ -2,15 +2,18 @@ from typing import Optional
 
 import tools.utils as utils, threading
 from core.dtos import ProjectQuotaDto
-from core.enum import SendStatusEnum
 from core.models import ProjectQuotaModel
-from stores import ProjectQuotaStore
+from stores import ProjectQuotaStore, ChapterStore
+from core.log.log_record import LogRecordHelper
+from core.enum import OperationModule, OperationType, SendStatusEnum
+
 import executor
 
 
 class ProjectQuotaService:
     def __init__(self):
         self.store = ProjectQuotaStore()
+        self.chapter_store = ChapterStore()
         self._logger = utils.get_logger()
 
     def get_quotas_paginated(
@@ -115,28 +118,16 @@ class ProjectQuotaService:
             # 业务验证
             if quota_dto.id == 0:
                 quota_dto = self.create_quota(quota_dto)
-            else:
-                quota = self.get_quota_dto(quota_dto.id)
-                if quota:
-                    quota_dto.id = quota.id
-                    quota_dto.project_id = quota.project_id
-                    quota_dto.budget_id = quota.budget_id
-                    quota_dto.item_id = quota.item_id
-                    quota_dto.item_code = quota.item_code
-                    quota_dto.quota_id = quota.quota_id
-                    quota_dto.ex_file = quota.ex_file
-                    quota_dto.ex_row = quota.ex_row
-                    quota_dto.ex_cell = quota.ex_cell
-                    quota_dto.ex_unit = quota.ex_unit
-                    quota_dto.ex_amount = quota.ex_amount
 
+            else:
                 quota_dto = self.update_quota(quota_dto)
-                # self.update_process_status(quota_dto.id,4)
+
                 if quota_dto.send_status != SendStatusEnum.NEW.value:
                     self.update_send_status(quota_dto.id, SendStatusEnum.CHANGE.value)
             return quota_dto
         except Exception as e:
             self._logger.error(f"保存定额条目失败: {str(e)}")
+
             raise
 
     def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
@@ -152,10 +143,21 @@ class ProjectQuotaService:
             # 业务验证
             if not quota_dto.project_id or not quota_dto.budget_id:
                 raise ValueError("项目编号和概算序号不能为空")
-
+            LogRecordHelper.log_success(
+                OperationType.CREATE,
+                OperationModule.QUOTA,
+                f"新增定额条目: {quota_dto.entry_name}",
+                utils.to_str(quota_dto.to_dict()),
+            )
             return self.store.create_quota(quota_dto)
         except Exception as e:
             self._logger.error(f"创建项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.CREATE,
+                OperationModule.QUOTA,
+                f"新增定额条目失败: {str(e)}",
+                utils.to_str(quota_dto.to_dict()),
+            )
             raise
 
     def update_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
@@ -171,9 +173,38 @@ class ProjectQuotaService:
             # 业务验证
             if not quota_dto.id:
                 raise ValueError("定额ID不能为空")
+            quota = self.get_quota_dto(quota_dto.id)
+            log_data = utils.to_str(quota.to_dict())
+            if quota:
+                quota_dto.id = quota.id
+                quota_dto.project_id = quota.project_id
+                quota_dto.budget_id = quota.budget_id
+                quota_dto.item_id = quota.item_id
+                quota_dto.item_code = quota.item_code
+                quota_dto.quota_id = quota.quota_id
+                quota_dto.ex_file = quota.ex_file
+                quota_dto.ex_row = quota.ex_row
+                quota_dto.ex_cell = quota.ex_cell
+                quota_dto.ex_unit = quota.ex_unit
+                quota_dto.ex_amount = quota.ex_amount
+
+            # self.update_process_status(quota_dto.id,4)
+            LogRecordHelper.log_success(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目: {quota_dto.entry_name}",
+                log_data,
+                utils.to_str(quota_dto.to_dict()),
+            )
             return self.store.update_quota(quota_dto)
         except Exception as e:
             self._logger.error(f"更新项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目失败: {str(e)}",
+                utils.to_str(quota_dto.to_dict()),
+            )
             raise
 
     def delete_quota(self, quota_id: int) -> bool:
@@ -186,11 +217,68 @@ class ProjectQuotaService:
             bool: 删除是否成功
         """
         try:
-            return self.store.delete_quota(quota_id)
+            flag = self.store.delete_quota(quota_id)
+            LogRecordHelper.log_success(
+                OperationType.DELETE,
+                OperationModule.QUOTA,
+                f"删除定额条目: {quota_id}",
+                f"{quota_id}",
+            )
+            return flag
         except Exception as e:
             self._logger.error(f"删除项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.DELETE,
+                OperationModule.QUOTA,
+                f"删除定额条目失败: {quota_id}",
+                f"{quota_id}",
+            )
             raise
 
+    def save_change_chapter(self, project_id, ids, new_id):
+        try:
+            if not ids:
+                return "请选择要变更的章节"
+            chapter = self.chapter_store.get_chapter_dto(project_id, new_id)
+            if not chapter:
+                return "章节不存在"
+            ids_arr = ids.split(",")
+
+            err = ""
+            for id_str in ids_arr:
+                try:
+                    self.store.update_quota_chapter(
+                        int(id_str), chapter.item_id, chapter.item_code
+                    )
+                except Exception as e:
+                    err += str(e) + ";"
+                    continue
+            if err:
+                LogRecordHelper.log_fail(
+                    OperationType.UPDATE,
+                    OperationModule.QUOTA,
+                    f"修改定额条目章节失败: {err}",
+                    ids,
+                )
+                return err
+            LogRecordHelper.log_success(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目章节",
+                ids,
+            )
+            return None
+        except Exception as e:
+            msg = f"章节变更失败: {str(e)}"
+            self._logger.error(msg)
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目章节失败: {str(e)}",
+                ids,
+            )
+            return msg
+
     def update_send_status(self, quota_id: int, status: int, err: str = None) -> bool:
         """更新发送状态
 
@@ -236,8 +324,27 @@ class ProjectQuotaService:
                 msg = self.start_send(int(_id), is_cover)
                 if msg:
                     err += f"{msg}[{_id}],"
+            if err:
+                LogRecordHelper.log_fail(
+                    OperationType.SEND,
+                    OperationModule.QUOTA,
+                    f"批量推送定额条目,部分失败: {err}",
+                    ids,
+                )
+            LogRecordHelper.log_success(
+                OperationType.SEND,
+                OperationModule.QUOTA,
+                f"批量推送定额条目",
+                ids,
+            )
             return err
         except Exception as e:
+            LogRecordHelper.log_fail(
+                OperationType.SEND,
+                OperationModule.QUOTA,
+                f"批量推送定额条目失败: {str(e)}",
+                ids,
+            )
             self._logger.error(f"批量启动定额条目发送失败: {str(e)}")
             raise
 
@@ -250,6 +357,12 @@ class ProjectQuotaService:
                 quota.quota_id = 0
             thread = threading.Thread(target=self._send_quota, args=(quota,))
             thread.start()
+            LogRecordHelper.log_success(
+                OperationType.SEND,
+                OperationModule.QUOTA,
+                f"推送定额条目",
+                f"ID:{_id},是否覆盖:{is_cover}",
+            )
             return None
         else:
             return "定额条目没有查询到"

+ 37 - 1
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -37,6 +37,12 @@ class ProjectTaskService:
                 thread = threading.Thread(target=self._run_task, args=(task,))
                 thread.daemon = True
                 thread.start()
+                LogRecordHelper.log_success(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"开始运行:{task.task_name}",
+                    task.id,
+                )
                 if executor.project_is_running(task.project_id):
                     self.update_process_status(task_id, TaskStatusEnum.WAIT.value, "")
                     return "0"
@@ -50,6 +56,12 @@ class ProjectTaskService:
                 self.update_process_status(
                     task_id, TaskStatusEnum.FAILURE.value, str(e)
                 )
+                LogRecordHelper.log_fail(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"开始运行失败:{task.task_name}",
+                    task.id,
+                )
                 raise
 
     def _run_task(self, task: ProjectTaskDto):
@@ -60,7 +72,7 @@ class ProjectTaskService:
                 # if not msg:
                 #     self.start_send_task(task.id)
             except Exception as e:
-                self._logger.error(f"采集项目任务失败: {str(e)}")
+                self._logger.error(f"运行项目任务失败: {str(e)}")
                 self.update_process_status(
                     task.id, TaskStatusEnum.FAILURE.value, str(e)
                 )
@@ -81,10 +93,22 @@ class ProjectTaskService:
                 thread = threading.Thread(target=self._send_task, args=(task,))
                 thread.daemon = True
                 thread.start()
+                LogRecordHelper.log_success(
+                    OperationType.SEND,
+                    OperationModule.TASK,
+                    f"开始发送:{task.task_name}",
+                    task.id,
+                )
                 return None
             except Exception as e:
                 self._logger.error(f"启动发送任务失败: {str(e)}")
                 self.update_send_status(task_id, TaskStatusEnum.FAILURE.value, str(e))
+                LogRecordHelper.log_fail(
+                    OperationType.SEND,
+                    OperationModule.TASK,
+                    f"开始发送失败:{task.task_name}",
+                    task.id,
+                )
                 raise
 
     def _send_task(self, task: ProjectTaskDto):
@@ -389,7 +413,19 @@ class ProjectTaskService:
             msg = executor.cancel_task(task)
             if not msg:
                 self.store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
+                LogRecordHelper.log_success(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"取消运行:{task.task_name}",
+                    task.id,
+                )
             else:
+                LogRecordHelper.log_fail(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"取消运行失败:{task.task_name}。 {msg}",
+                    task.id,
+                )
                 return msg
         else:
             return "没有查询到任务"

+ 62 - 7
SourceCode/IntelligentRailwayCosting/app/stores/chapter.py

@@ -1,6 +1,7 @@
 from sqlalchemy.orm import aliased
-from sqlalchemy import or_, func
+from sqlalchemy import or_, func, and_
 
+from core.dtos import ChapterDto
 from core.models import ChapterModel
 from tools import db_helper
 
@@ -10,6 +11,32 @@ class ChapterStore:
         self._database = None
         self._db_session = None
 
+    def get_chapter(self, project_id: str, item_id: int):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            chapter = (
+                db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .filter(ChapterModel.item_id == item_id)
+                .first()
+            )
+            if chapter is None:
+                return None
+            return chapter
+
+    def get_chapter_dto(self, project_id: str, item_id: int):
+        chapter = self.get_chapter(project_id, item_id)
+        if chapter is None:
+            return None
+        return ChapterDto.from_model(chapter)
+
     def get_chapter_item_by_item_code(self, project_id: str, item_code: str):
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
@@ -75,6 +102,30 @@ class ChapterStore:
             .distinct()
         )
 
+    def get_task_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = (
+                self.db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .filter(
+                    and_(
+                        ChapterModel.item_code != "0",
+                        ChapterModel.item_code.like(f"{item_code}%"),
+                    ),
+                )
+                .order_by(ChapterModel.item_code)
+            )
+            items = query.all()
+            return items
+
     def get_top_chapter_items(self, project_id: str, item_code: list[str] = None):
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as self.db_session:
@@ -90,7 +141,9 @@ class ChapterStore:
             items = query.all()
             return items
 
-    def get_child_chapter_items(self, project_id: str, parent_item_code: str):
+    def get_child_chapter_items(
+            self, project_id: str, parent_item_code: str, task_first: bool
+    ):
         # 构建子节点的模式:支持两种格式
         # 1. 父级编号后跟-和两位数字(如:01-01)
         # 2. 父级编号直接跟两位数字(如:0101)
@@ -98,16 +151,18 @@ class ChapterStore:
         pattern_without_dash = f"{parent_item_code}__"
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = self._build_chapter_items_query()
             query = (
-                self._build_chapter_items_query()
-                .filter(
+                query.filter(ChapterModel.item_code == parent_item_code)
+                if task_first
+                else query.filter(
                     or_(
                         ChapterModel.item_code.like(pattern_with_dash),
                         ChapterModel.item_code.like(pattern_without_dash),
                     )
                 )
-                .order_by(ChapterModel.item_code)
             )
+            query = query.order_by(ChapterModel.item_code)
             items = query.all()
             return items
 
@@ -131,12 +186,12 @@ class ChapterStore:
 
             # 按'-'分割item_code
             parts = current_code.split("-")
-            
+
             # 处理第一个部分(如'0101')
             first_part = parts[0]
             for i in range(2, len(first_part) + 1, 2):
                 parent_codes.append(first_part[:i])
-            
+
             # 逐步拼接其他部分构建完整父节点列表
             current_parent = first_part
             for i in range(1, len(parts)):

+ 8 - 8
SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py

@@ -20,13 +20,13 @@ class QuotaInputStore:
         return self._current_user
 
     def get_quotas_paginated(
-            self,
-            project_id: str,
-            budget_id: int,
-            item_id: int,
-            page: int = 1,
-            page_size: int = 10,
-            keyword: Optional[str] = None,
+        self,
+        project_id: str,
+        budget_id: int,
+        item_id: int,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
     ):
         """分页查询定额输入列表
 
@@ -147,7 +147,7 @@ class QuotaInputStore:
             return QuotaInputDto.from_model(model)
 
     def update_quota(
-            self, project_id: str, dto: QuotaInputDto
+        self, project_id: str, dto: QuotaInputDto
     ) -> Optional[QuotaInputDto]:
         """更新定额输入
 

+ 33 - 19
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py

@@ -22,14 +22,14 @@ class ProjectQuotaStore:
         return self._current_user
 
     def get_quotas_paginated(
-        self,
-        budget_id: int,
-        project_id: str,
-        item_code: str,
-        page: int = 1,
-        page_size: int = 10,
-        keyword: Optional[str] = None,
-        send_status: Optional[int] = None,
+            self,
+            budget_id: int,
+            project_id: str,
+            item_code: str,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            send_status: Optional[int] = None,
     ):
         """分页查询定额列表
 
@@ -81,15 +81,15 @@ class ProjectQuotaStore:
             return {"total": total_count, "data": quotas}
 
     def get_quotas_by_task_paginated(
-        self,
-        task_id: int,
-        budget_id,
-        project_id,
-        item_code,
-        page: int = 1,
-        page_size: int = 10,
-        keyword: Optional[str] = None,
-        send_status: Optional[int] = None,
+            self,
+            task_id: int,
+            budget_id,
+            project_id,
+            item_code,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            send_status: Optional[int] = None,
     ):
         with db_helper.mysql_query_session(self._database) as db_session:
             query = db_session.query(ProjectQuotaModel).filter(
@@ -176,7 +176,7 @@ class ProjectQuotaStore:
         return ProjectQuotaDto.from_model(quota) if quota else None
 
     def get_quota_by_quota_input(
-        self, project_id: str, budget_id: int, quota_input_id: int
+            self, project_id: str, budget_id: int, quota_input_id: int
     ):
         with db_helper.mysql_query_session(self._database) as db_session:
             quota = (
@@ -254,7 +254,7 @@ class ProjectQuotaStore:
             quota.ex_row = quota_dto.ex_row
             quota.ex_unit = quota_dto.ex_unit
             quota.ex_amount = quota_dto.ex_amount
-            quota.send_status = SendStatusEnum.CHANGE.value
+            quota.send_status = quota_dto.send_status
             quota.send_error = None
             quota.updated_by = self.current_user.username
             quota.updated_at = datetime.now()
@@ -262,6 +262,20 @@ class ProjectQuotaStore:
             quota = db_session.merge(quota)
             return ProjectQuotaDto.from_model(quota)
 
+    def update_quota_chapter(
+            self, quota_id: int, item_id: int, item_code: str
+    ) -> Optional[ProjectQuotaDto]:
+        quota = self.get_quota(quota_id)
+        if not quota:
+            raise Exception(f"定额条目[{quota_id}]不存在")
+        with db_helper.mysql_session(self._database) as db_session:
+            quota.item_id = item_id
+            quota.item_code = item_code
+            quota.updated_by = self.current_user.username
+            quota.updated_at = datetime.now()
+            quota = db_session.merge(quota)
+            return ProjectQuotaDto.from_model(quota)
+
     def delete_quota(self, quota_id: int) -> bool:
         """删除定额
 

+ 77 - 26
SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css

@@ -2,82 +2,96 @@
     --bs-app-header-height: 64px;
     --bs-app-header-height-actual: 64px;
 }
-*{
+
+* {
     margin: 0;
     padding: 0;
     box-sizing: border-box;
 }
+
 .app-container {
-    padding: 0!important;
+    padding: 0 !important;
 }
-.app-body-header{
+
+.app-body-header {
     display: flex;
     align-items: center;
     padding: 0 30px;
     border-bottom: 2px solid var(--bs-border-color);
 }
-.app-body-header h3{
+
+.app-body-header h3 {
     margin-bottom: 0;
     padding: 3px 15px;
     border-right: 2px solid #666;
 }
-.app-footer{
+
+.app-footer {
     border-top: 2px solid var(--bs-border-color);
     background: #f9f9f9;
 }
+
 .nav-tabs.nav-line-tabs .nav-link.active {
     color: var(--bs-primary);
 }
 
-body > .container, body > .container-fluid{
+body > .container, body > .container-fluid {
     padding: 0;
     margin: 0;
 }
 
-.table-box{
+.table-box {
     position: relative;
     width: 100%;
     height: 100%;
     min-height: 300px;
 }
+
 .table-box, .table-box .table {
     width: 100%;
 }
-.table-box th,.table-box td{
+
+.table-box th, .table-box td {
     text-align: center;
     vertical-align: middle;
     height: 40px;
     padding: 3px 5px;
 }
-.table-box th{
+
+.table-box th {
     font-weight: 600;
     font-size: 16px;
     height: 45px;
 }
-.table-box td > .link:hover{
+
+.table-box td > .link:hover {
     border-bottom: 2px solid;
 }
-.table-box td > span{
-   font-size: 13px;
+
+.table-box td > span {
+    font-size: 13px;
 }
-.table-box td > .btn{
-    padding: calc(.2rem + 1px) calc(.4rem + 1px)!important;
+
+.table-box td > .btn {
+    padding: calc(.2rem + 1px) calc(.4rem + 1px) !important;
     margin: 0 5px;
     --bs-btn-border-radius: .3rem;
 }
-.table-box td > .btn-icon{
-    width: 25px!important;
-    height: 25px!important;
+
+.table-box td > .btn-icon {
+    width: 25px !important;
+    height: 25px !important;
 }
-.table-box td > .link:hover{
+
+.table-box td > .link:hover {
     border-bottom: 2px solid;
 }
 
-.table-box .table-loading{
+.table-box .table-loading {
     position: absolute;
-    top:0;
+    top: 0;
     bottom: 0;
-    left:0;
+    left: 0;
     right: 0;
     display: flex;
     align-items: center;
@@ -85,24 +99,61 @@ body > .container, body > .container-fluid{
     /*background: rgba(0,0,0,.15);*/
     min-height: 300px;
 }
-.table-box .table-loading span{
+
+.table-box .table-loading span {
     font-size: 16px;
     color: #666;
 }
-.table-box td .one-line{
+
+.table-box td .one-line {
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
     display: inline-block;
 }
-.pagination-row{
+
+.pagination-row {
     width: 100%;
     display: flex;
     justify-content: space-between;
 }
-.form-check-input,.form-check-label{
+
+.form-check-input:disabled {
+    cursor: not-allowed;
+    opacity: .5;
+}
+
+.form-check-input, .form-check-label {
     cursor: pointer;
 }
-.form-check-input:disabled, .form-check-input:disabled + .form-check-label{
+
+.form-check-input:disabled, .form-check-input:disabled + .form-check-label {
     cursor: not-allowed;
+}
+
+.fa-file {
+    content: "\f15b";
+    margin-left: 2px;
+}
+
+.fa-folder {
+    content: "\f07b";
+}
+
+.fa-folder-open:before {
+    content: "\f07c";
+}
+
+.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--selected {
+    color: var(--bs-component-hover-bg);
+    background-color: var(--bs-component-hover-color);
+
+}
+
+.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--selected:after {
+    background-color: var(--bs-component-hover-bg);
+}
+
+.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--selected span.text-primary {
+    color: var(--bs-component-hover-bg) !important;
 }

+ 131 - 0
SourceCode/IntelligentRailwayCosting/app/views/static/base/js/select2tree.js

@@ -0,0 +1,131 @@
+(function ($) {
+    $.fn.select2tree = function (options) {
+        console.log("select2tree")
+        const defaults = {
+            language: "zh-CN",
+            minimumResultsForSearch: -1
+            /*theme: "bootstrap"*/
+        };
+
+        const opts = $.extend(defaults, options);
+        opts.templateResult = function (data, container) {
+            if (data.element) {
+
+                //insert span element and add 'parent' property
+                var $wrapper = $("<span style=\"margin-right:6px;\"></span><span>" + data.text + "</span>");
+                var $element = $(data.element);
+                $(container).attr("val", $element.val());
+                if ($element.attr("parent")) {
+                    $(container).attr("parent", $element.attr("parent"));
+                }
+                return $wrapper;
+            } else {
+                return data.text;
+            }
+        };
+        $(this).select2(opts).on("select2:open", function () {
+            open($(this))
+        });
+    };
+
+    const icons = {
+        "folderClose": "fa-folder",
+        "folderOpen": "fa-folder-open",
+        'file': "fa-file"
+    }
+
+    function moveOption(id, index) {
+        index = index || 0;
+        if (id) {
+            const $children = $(".select2-results__options li[parent=" + id + "]")
+            $children.insertAfter(".select2-results__options li[val=" + id + "]");
+            $children.each(function () {
+                $(this).attr('level', index).css({
+                        "display": "none",
+                        "padding": `3px 6px 3px ${index * 6 + 10}px`
+                    }
+                );
+                moveOption($(this).attr("val"), index + 1);
+            });
+        } else {
+            const $root = $(".select2-results__options li:not([parent])")
+            $root.appendTo(".select2-results__options ul");
+            $root.each(function () {
+                $(this).attr('level', index).css({"padding": "3px 10px"});
+                moveOption($(this).attr("val"), index + 1);
+            });
+        }
+    }
+
+    //deal switch action
+    function switchAction(id, open, level) {
+        const $children = $(".select2-results__options li[parent='" + id + "']");
+        if (!id || $children.length <= 0) {
+            return;
+        }
+        if (level && $(".select2-results__options li[val=" + id + "][level='" + level + "']").length > 0) {
+            return;
+        }
+        if (open) {
+            $(".select2-results__options li[val=" + id + "] span[class]:eq(0)").removeClass(icons.folderClose).addClass(icons.folderOpen);
+        } else {
+            $(".select2-results__options li[val=" + id + "] span[class]:eq(0)").addClass(icons.folderClose).removeClass(icons.folderOpen);
+        }
+        $children.each(function () {
+            const $that = $(this);
+            open ? $that.slideDown() : $that.slideUp();
+            if (level || !open) {
+                switchAction($that.attr("val"), open, level);
+            }
+        });
+    }
+
+    function open(that) {
+        setTimeout(function () {
+            moveOption();
+            $(".select2-results__options li").each(function () {
+                var $this = $(this);
+                $this.find("span:eq(0)").addClass('fa text-warning fs-4 ').addClass(icons.file);
+                if ($this.attr("parent")) {
+                    $(this).siblings("li[val=" + $this.attr("parent") + "]").find("span:eq(0)")
+                        .removeClass(icons.file).removeClass('text-warning').addClass(icons.folderClose + " text-primary switch").css({"cursor": "pointer"});
+                    // $(this).siblings("li[val=" + $this.attr("parent") + "]").find("span:eq(1)").css("font-weight", "bold");
+                }
+            });
+            $(".select2-results__options li[level='0']").each(function () {
+                var $this = $(this);
+                var id = $this.attr("val");
+                switchAction(id, true, 1);
+            });
+
+            //override mousedown for collapse/expand
+            $(".switch").off("mousedown").on("mousedown", function (event) {
+                switchAction($(this).parent().attr("val"), $(this).hasClass(icons.folderClose));
+                event.stopPropagation();
+                event.preventDefault();
+            });
+
+            //override mouseup to nothing
+            $(".switch").off("mouseup").on("mouseup", function () {
+                return false;
+            });
+
+            const val = $(that).val(), parent = [];
+            getParent(parent, val)
+            if (parent.length) {
+                parent.forEach(function (id) {
+                    switchAction(id, true);
+                })
+            }
+        }, 0);
+    }
+
+    function getParent(parent, id) {
+        const $that = $(".select2-results__options li[val=" + id + "]")
+        parent.push($that.attr('val'))
+        if ($that.attr("parent")) {
+            getParent(parent, $that.attr("parent"))
+        }
+    }
+
+})(jQuery);

+ 14 - 8
SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js

@@ -72,6 +72,7 @@ function IwbTable(table, opts, isReload) {
         selected_rows: new Set(),
         rows: new Set(),
         checkBoxWidth: 50,
+        checkBoxDisable: undefined,
         pageSize: 15,
         pageNum: 1,
         search: {
@@ -153,11 +154,12 @@ function IwbTable(table, opts, isReload) {
                 const row = rows[i]
                 body_str += '<tr>'
                 if (opt.checkBox) {
+                    const disabled_checkbox = opt.checkBoxDisable && opt.checkBoxDisable(row) ? 'disabled' : ''
                     if (opt.selected_ids.size && opt.selected_ids.has(row[opt.idFiled] + "")) {
-                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, 'checked')}</td>`
+                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, `checked ${disabled_checkbox}`)}</td>`
                     } else {
                         check_all = false
-                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, '')}</td>`
+                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, `${disabled_checkbox}`)}</td>`
                     }
                 }
                 for (let j = 0; j < opt.columns.length; j++) {
@@ -180,12 +182,16 @@ function IwbTable(table, opts, isReload) {
             $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).on('change', function () {
                 const checked = $(this).is(':checked')
                 if (checked) {
-                    $tableBox.find(`input[name=${opt.tableCheckboxName}]`).prop('checked', true)
-                    // 页面上所有的的checkbox都选中,值添加到selected_ids中,不是赋值
-                    rows.forEach(row => {
-                        opt.selected_ids.add(row[opt.idFiled] + "")
-                        opt.selected_rows.add(row)
+                    $tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:disabled)`).prop('checked', true)
+                    $tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:disabled)`).each(function () {
+                        opt.selected_ids.add($(this).val())
+                        opt.selected_rows.add(rows.find(row => row[opt.idFiled] + "" === $(this).val()))
                     })
+                    // 页面上所有的的checkbox都选中,值添加到selected_ids中,不是赋值
+                    // rows.forEach(row => {
+                    //     opt.selected_ids.add(row[opt.idFiled] + "")
+                    //     opt.selected_rows.add(row)
+                    // })
                 } else {
                     $tableBox.find(`input[name=${opt.tableCheckboxName}]`).prop('checked', false)
                     rows.forEach(row => {
@@ -195,7 +201,7 @@ function IwbTable(table, opts, isReload) {
                 }
             })
             $tableBox.find(`input[name=${opt.tableCheckboxName}]`).on('change', function () {
-                if ($tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:checked)`).length) {
+                if ($tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:disabled):not(:checked)`).length || $tableBox.find(`input[name=${opt.tableCheckboxName}]`).length === 0) {
                     $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', false)
                 } else {
                     $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', true)

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

@@ -305,7 +305,7 @@ function RenderRightBox_Custom(data) {
                         }
                         str += `<!--<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>-->`
                         if (row.process_status === 2 || row.process_status === 200) {
-                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary"  data-bs-toggle="tooltip" data-bs-placement="top" title="定额输入列表" onclick="GoTo('/quota_info/${project_id}/${row.id}')"><i class="ki-duotone ki-eye fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary"  data-bs-toggle="tooltip" data-bs-placement="top" title="定额输入列表" onclick="GoTo('/quota_info/${project_id}/${row.id}',0)"><i class="ki-duotone ki-eye fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
                         }
                         if (row.process_status !== 2 && row.process_status !== 1) {
                             str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete(${row.id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`

+ 139 - 14
SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js

@@ -78,16 +78,54 @@ const quota_modal_template = `
       </div>
     </div>
   </div>
-</div>`
+</div>`,
+    chapter_modal_template = `
+<div class="modal fade" id="modal_chapter" tabindex="-1" aria-hidden="true">
+ <div class="modal-dialog modal-lg modal-dialog-centered">
+    <div class="modal-content rounded">
+      <div class="modal-header">
+        <h3 class="modal-title"><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>
+            <span class="path2"></span>
+          </i>
+        </div>
+      </div>
+      <div class="modal-body">
+        <form>
+          <div class="form-group">
+            <input type="hidden" name="id" value="">
+            <input type="hidden" name="budget_id" value="">
+            <input type="hidden" name="project_id" value="">
+            <input type="hidden" name="item_id" value="">
+            <input type="hidden" name="item_code" value="">
+            <input type="hidden" name="quota_id" value="">
+            <input type="hidden" name="task_id" value="">
+            <div class="fv-row form-group mb-3">
+              <label for="quota_code" class="form-label">章节条目</label>
+              <select type="text" class="form-select" name="chapter"  onchange="OnChapterChange()"></select>
+            </div>
+          </div>
+        </form>
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
+        <button type="button" class="btn btn-light-success" onclick="SaveChapterChange()">保存</button>
+      </div>
+    </div>
+  </div>
+</div>
+`
 $('.app-main .app-container').append(quota_modal_template)
+$('.app-main .app-container').append(chapter_modal_template)
 const nav_template = `<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>`,
     nav_tab_template = `
 		<li class="nav-item" data-id="{0}">
 			<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 h-100" id="iwb_tab_{0}" role="tabpanel">{1}</div>`,
-    table_add_quota_btn_template = `<button type="button" class="quota_add_btn btn btn-primary btn-sm" onclick="Send_Quota_Batch('{0}')">批量推送</button>` //`<button type="button" class="quota_add_btn btn btn-primary btn-sm" onclick="Add_Quota('{0}')">添加定额</button>`,
-table_run_select_template = `<select class="form-select form-select-sm me-5" name="process_status">
+    table_run_select_template = `<select class="form-select form-select-sm me-5" name="process_status" data-placeholder="请选择运行状态">
 												<option value="">全部运行状态</option>
 												<option value="0">草稿</option>
 												<option value="1">等待运行</option>
@@ -97,13 +135,13 @@ table_run_select_template = `<select class="form-select form-select-sm me-5" nam
 												<option value="5">运行失败</option>
 												<!--<option value="4">已修改</option>-->
 											</select>`,
-    table_send_select_template = `<select class="form-select form-select-sm me-5" name="send_status">
+    table_send_select_template = `<select class="form-select form-select-sm me-5" name="send_status" data-placeholder="请选择推送状态">
 												<option value="">全部推送状态</option>
 												<option value="0">未推送</option>
-												<option value="1">推送中</option>
+<!--												<option value="1">推送中</option>-->
 												<option value="200">推送成功</option>
 												<option value="2">推送失败</option>
-												<option value="3">数据变更</option>
+<!--												<option value="3">数据变更</option>-->
 											</select>`,
     table_template = `<div class="table-box table-responsive" data-id="{0}" id="table_box_{0}">
 								<div class="d-flex justify-content-between my-5">
@@ -123,8 +161,11 @@ table_run_select_template = `<select class="form-select form-select-sm me-5" nam
 								</table>
 								<div class="pagination-row"></div>
 							</div>`
+let table_add_quota_btn_template = `<button type="button" class="quota_add_btn btn btn-primary btn-sm" onclick="Send_Quota_Batch('{0}')">批量推送</button>`;
+// table_add_quota_btn_template += `<button type="button" class="ms-5 btn btn-warning btn-sm" onclick="Edit_QuotaChapter(null,'{0}')">批量修改章节</button>`
+//`<button type="button" class="quota_add_btn btn btn-primary btn-sm" onclick="Add_Quota('{0}')">添加定额</button>`
 
-const $modalQuota = $('#modal_quota')
+const $modalQuota = $('#modal_quota'), $modalChapter = $('#modal_chapter')
 let $rightBox, $rightBoxHeader, $rightBoxBody, $taskBox, $quotaBox, budget_id, item_code
 
 console.log(`加载项目:${project_id}`)
@@ -132,6 +173,7 @@ InitBody()
 $(function () {
     BuildChapterInfo()
     $(window).on('resize', AdjustBoxHeight)
+    RenderChapterSelect()
 })
 
 function InitBody() {
@@ -183,8 +225,9 @@ function AdjustBoxHeight() {
     $('#body_box .project-box').height(h)
 }
 
+
 function BuildChapterInfo() {
-    const $tree = $(`#js-tree`)
+    const $tree = $(`#js-tree`), taskId = task_id || ""
     const opt = {
         core: {
             themes: {
@@ -197,7 +240,7 @@ function BuildChapterInfo() {
             data: function (node, callback) {
                 // console.log('TREE_NODE', node)
                 IwbAjax_1({
-                    url: `/api/project/chapter/${project_id}?c=${node?.data?.item_code || ''}`,
+                    url: `/api/project/chapter/${project_id}?c=${node?.data?.item_code || ''}&&t=${taskId}`,
                     success: res => {
                         if (res.success) {
                             console.log('TREE', res.data)
@@ -233,6 +276,7 @@ function BuildChapterInfo() {
     $tree.jstree(opt)
 }
 
+
 function RenderRightBox(data) {
     console.log('RenderRightBox', arguments)
     $rightBoxBody.data('data', data)
@@ -320,6 +364,9 @@ function LoadQuotaTable(table) {
     IwbTable(table, {
         url,
         checkBox: true,
+        checkBoxDisable: (row) => {
+            return row.send_status === 200
+        },
         columns: [
             {
                 title: '序号',
@@ -428,7 +475,7 @@ function LoadQuotaTable(table) {
             {
                 title: '操作',
                 data: 'id',
-                width: '120px',
+                width: '160px',
                 render: (row) => {
                     let str = ''
                     // if (row.process_status === 0) {
@@ -444,14 +491,18 @@ function LoadQuotaTable(table) {
                     if (row.send_status === 0) {
                         str += `<button type="button" class="btn btn-icon btn-sm ${row.quota_id ? 'btn-primary' : 'btn-light-primary'}" data-bs-toggle="tooltip" data-bs-placement="top" title="开始推送" onclick="StartSendQuota(${row.id}, ${row.budget_id}, ${row.quota_id})"><i class="ki-duotone ki-send fs-3"><span class="path1"></span><span class="path2"></span></i></button>`
                     } else if (row.send_status === 200) {
-                        str += `<button type="button" class="btn btn-icon btn-sm  ${row.quota_id ? 'btn-warning' : 'btn-light-warning'}" data-bs-toggle="tooltip" data-bs-placement="top" title="重新推送" onclick="ReStartSendQuota(${row.id}, ${row.budget_id}, ${row.quota_id})"><i class="ki-duotone ki-send fs-3"><span class="path1"></span><span class="path2"></span></i></button>`
+                        str = `<span class="text-gray-500">暂无操作</span>`
+                        // str += `<button type="button" class="btn btn-icon btn-sm  ${row.quota_id ? 'btn-warning' : 'btn-light-warning'}" data-bs-toggle="tooltip" data-bs-placement="top" title="重新推送" onclick="ReStartSendQuota(${row.id}, ${row.budget_id}, ${row.quota_id})"><i class="ki-duotone ki-send fs-3"><span class="path1"></span><span class="path2"></span></i></button>`
                     } else if (row.send_status === 2) {
                         str += `<button type="button" class="btn btn-icon btn-sm  ${row.quota_id ? 'btn-danger' : 'btn-light-danger'}" data-bs-toggle="tooltip" data-bs-placement="top" title="重新推送" onclick="ReStartSendQuota(${row.id}, ${row.budget_id}, ${row.quota_id})"><i class="ki-duotone ki-send fs-3"><span class="path1"></span><span class="path2"></span></i></button>`
                     } else if (row.send_status === 3) {
                         str += `<button type="button" class="btn btn-icon btn-sm  ${row.quota_id ? 'btn-info' : 'btn-light-info'}" data-bs-toggle="tooltip" data-bs-placement="top" title="重新推送" onclick="ReStartSendQuota(${row.id}, ${row.budget_id}, ${row.quota_id})"><i class="ki-duotone ki-send fs-3"><span class="path1"></span><span class="path2"></span></i></button>`
                     }
-                    str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-                    str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete_Quota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                    if (row.send_status !== 200) {
+                        str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+                        str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="修改条目" onclick="Edit_QuotaChapter(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-burger-menu-1 fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                        str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete_Quota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                    }
                     return str
                 }
             },
@@ -474,6 +525,7 @@ function Add_Quota(budget_id,) {
         $modalQuota.find('[name="task_id"]').val('0');
         $modalQuota.find('[name="budget_id"]').val(budget_id);
         $modalQuota.find('[name="is_cover"]').val('0')
+        $modalQuota.find('[name="chapter"]').val('').data('id', "").data('code', "").select2tree();
         $modalQuota.find('.is_cover_box').hide()
     })
 }
@@ -502,6 +554,7 @@ function Edit_Quota(id) {
                 $modalQuota.find('[name="amount"]').val(data.amount);
                 $modalQuota.find('[name="units"]').val(data.units);
                 $modalQuota.find('[name="quota_code"]').val(data.quota_code);
+                $modalQuota.find('[name="chapter"]').data('id', data.item_id).data('code', data.item_code).val(data.item_code).select2tree();
                 $modalQuota.find('#ex_row').html(data.ex_row);
                 $modalQuota.find('#ex_amount').html(data.ex_amount);
                 $modalQuota.find('#ex_cell').html(data.ex_cell);
@@ -527,7 +580,8 @@ function SaveQuota(isSubmit) {
         amount = $modalQuota.find('[name="amount"]').val(),
         units = $modalQuota.find('[name="units"]').val(),
         quota_code = $modalQuota.find('[name="quota_code"]').val(),
-        is_cover = $modalQuota.find('[name="is_cover"]:checked').val()
+        is_cover = $modalQuota.find('[name="is_cover"]:checked').val(),
+        chapter = $modalQuota.find('[name="chapter"]').val()
     IwbAjax({
         url: `/api/quota/save`,
         data: {
@@ -542,6 +596,7 @@ function SaveQuota(isSubmit) {
             amount,
             units,
             quota_code,
+            chapter,
             run_now: isSubmit ? 'true' : 'false',
             is_cover: is_cover === '1' || is_cover === 1 ? 'true' : 'false',
         },
@@ -634,4 +689,74 @@ function SendQuota(title, ids, budget_id, is_cover, is_html) {
             table: `#table_${budget_id}`
         })
     }
+}
+
+function Edit_QuotaChapter(id, budget_id) {
+    let ids = id
+    if (!ids) {
+        ids = IwbTableGetSelectedIds('#table_' + budget_id).join(",")
+    }
+    console.log("Edit_QuotaChapter", ids)
+    EditModal($modalChapter, () => {
+        $modalChapter.find('[name="budget_id"]').val(budget_id);
+        $modalChapter.find('[name="id"]').val(ids);
+        RenderChapterSelect()
+    })
+}
+
+function RenderChapterSelect() {
+    const $chapter = $modalChapter.find('[name="chapter"]')
+    if ($chapter.data('init')) {
+        return
+    }
+    IwbAjax_1({
+        url: `/api/project/task_chapters/${task_id}`,
+        success: function (res) {
+            if (res.success) {
+                let str = FormatChapterOption(res.data)
+                $chapter.html(str).data('init', true).select2tree()
+            } else {
+                console.error('加载章节出错:', res.message)
+            }
+        }
+    })
+}
+
+function FormatChapterOption(data) {
+    console.log('FormatChapterOption', data)
+    let str = ''
+    if (data && data.length) {
+        data.forEach(item => {
+            // let parent_code = get_parent_code(item.item_code)
+            // console.log('item_code', item.id, item.parent, item.text)
+            if (item.parent) {
+                str += `<option data-id="${item.data.item_id}" value="${item.id}" parent="${item.parent}">${item.text}</option>`
+            } else {
+                str += `<option data-id="${item.data.item_id}" value="${item.id}">${item.text}</option>`
+            }
+        })
+    }
+    return str
+}
+
+function OnChapterChange() {
+    // const $this = $modalChapter.find('[name="chapter"]')
+    // let code = $this.val(), id = $this.find('option[value="' + code + '"]').data('id')
+    // console.log('OnChapterChange', code, id)
+}
+
+function SaveChapterChange() {
+    const $this = $modalChapter.find('[name="chapter"]'), budget_id = $modalChapter.find('[name="budget_id"]').val()
+    let code = $this.val(), newId = $this.find('option[value="' + code + '"]').data('id')
+    if (!newId) {
+        MsgWarning('请选择章节编号!')
+    }
+    IwbAjax_2({
+        url: `/api/quota/save_change_chapter/${project_id}`,
+        data: {
+            ids: $modalChapter.find('[name="id"]').val(),
+            new_id: newId
+        }
+    }, $modalChapter, $('#table_' + budget_id))
+
 }

+ 2 - 1
SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

@@ -23,9 +23,10 @@
 
 {% endblock %} {% block page_scripts %}
 <script src="{{ url_for('static', filename='base/plugins/jstree/jstree.bundle.js') }}"></script>
+<script src="{{ url_for('static', filename='base/js/select2tree.js') }}"></script>
 <script>
     ChangeHeadMenu('#{{page_active}}_menu')
-    const project_id = '{{project.project_id}}'
+    const project_id = '{{project.project_id}}', task_id = ''
 </script>
 <script src="{{ url_for('static', filename='project/quota_info.js') }}"></script>
 <script src="{{ url_for('static', filename='project/budget_info.js') }}"></script>

+ 1 - 0
SourceCode/IntelligentRailwayCosting/app/views/templates/project/quota_info.html

@@ -19,6 +19,7 @@
 {% endblock %}
 {% block page_scripts %}
 <script src="{{ url_for('static', filename='base/plugins/jstree/jstree.bundle.js') }}"></script>
+<script src="{{ url_for('static', filename='base/js/select2tree.js') }}"></script>
 <script>
     ChangeHeadMenu("#{{page_active}}_menu")
     const project_id = '{{project.project_id}}', task_id = '{{task.id}}'