yue 7 月之前
父节点
当前提交
821e43da3e
共有 25 个文件被更改,包括 2393 次插入1841 次删除
  1. 二进制
      Doc/05.Excel解析.pdf
  2. 0 1
      SourceCode/IntelligentRailwayCosting/app/core/api/__init__.py
  3. 13 20
      SourceCode/IntelligentRailwayCosting/app/core/api/response.py
  4. 20 15
      SourceCode/IntelligentRailwayCosting/app/executor/__init__.py
  5. 144 72
      SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py
  6. 185 92
      SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py
  7. 41 24
      SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py
  8. 8 7
      SourceCode/IntelligentRailwayCosting/app/main.py
  9. 69 67
      SourceCode/IntelligentRailwayCosting/app/routes/excel_test.py
  10. 61 41
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  11. 54 15
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  12. 101 51
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  13. 109 67
      SourceCode/IntelligentRailwayCosting/app/stores/budget.py
  14. 92 66
      SourceCode/IntelligentRailwayCosting/app/stores/chapter.py
  15. 65 38
      SourceCode/IntelligentRailwayCosting/app/stores/project.py
  16. 41 34
      SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py
  17. 14 0
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_task.py
  18. 82 54
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py
  19. 14 0
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py
  20. 14 5
      SourceCode/IntelligentRailwayCosting/app/stores/user.py
  21. 27 21
      SourceCode/IntelligentRailwayCosting/app/tools/db_helper/__init__.py
  22. 72 54
      SourceCode/IntelligentRailwayCosting/app/tools/db_helper/base.py
  23. 444 406
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js
  24. 302 299
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  25. 421 392
      SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js

二进制
Doc/05.Excel解析.pdf


+ 0 - 1
SourceCode/IntelligentRailwayCosting/app/core/api/__init__.py

@@ -1,3 +1,2 @@
 from .response import ResponseBase
 from .table_response import TableResponse
-

+ 13 - 20
SourceCode/IntelligentRailwayCosting/app/core/api/response.py

@@ -13,7 +13,7 @@ class ResponseBase:
         Returns:
             Response: 设置好响应头的Flask响应对象
         """
-        response.headers['Content-Type'] = 'application/json; charset=utf-8'
+        response.headers["Content-Type"] = "application/json; charset=utf-8"
         return response
 
     @staticmethod
@@ -25,18 +25,16 @@ class ResponseBase:
         Returns:
             Dict: 统一的成功响应格式
         """
-        response = {
-            "success": True,
-            "code": 200,
-            "message": message
-        }
+        response = {"success": True, "code": 200, "message": message}
         if data is not None:
             response["data"] = data
         response = make_response(jsonify(response))
         return ResponseBase._set_response_headers(response)
 
     @staticmethod
-    def error(message: str = "操作失败", code: int = 400, data: Optional[Any] = None) -> Response:
+    def error(
+            message: str = "操作失败", code: int = 400, data: Optional[Any] = None
+    ) -> Response:
         """错误响应
         Args:
             message: 错误消息
@@ -45,18 +43,19 @@ class ResponseBase:
         Returns:
             Dict: 统一的错误响应格式
         """
-        response = {
-            "success": False,
-            "code": code,
-            "message": message
-        }
+        response = {"success": False, "code": code, "message": message}
         if data is not None:
             response["data"] = data
         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:
+    def json_response(
+            success: bool = True,
+            code: int = 200,
+            message: str = "",
+            data: Optional[Any] = None,
+    ) -> Response:
         """自定义响应
         Args:
             success: 是否成功
@@ -66,14 +65,8 @@ class ResponseBase:
         Returns:
             Dict: 统一的响应格式
         """
-        response = {
-            "success": success,
-            "code": code,
-            "message": message
-        }
+        response = {"success": success, "code": code, "message": message}
         if data is not None:
             response["data"] = data
         response = make_response(jsonify(response))
         return ResponseBase._set_response_headers(response)
-
-

+ 20 - 15
SourceCode/IntelligentRailwayCosting/app/executor/__init__.py

@@ -2,31 +2,36 @@ from .task_runner import TaskRunner
 from .task_sender import TaskSender
 from core.dtos import ProjectTaskDto, ProjectQuotaDto
 
+
 def init():
-    TaskRunner.run()
+    TaskRunner.init()
+
+
+def run_task(task: ProjectTaskDto):
+    return TaskRunner.run_task(task)
 
-def run_task(task:ProjectTaskDto):
-    return TaskRunner.run(task)
 
-def cancel_task(task:ProjectTaskDto):
+def cancel_task(task: ProjectTaskDto):
     return TaskRunner.cancel(task)
 
-def project_is_running(project_id:str):
+
+def project_is_running(project_id: str):
     return TaskRunner.get_project_running_state(project_id)
 
-def send_task(task:ProjectTaskDto):
+
+def send_task(task: ProjectTaskDto):
     return TaskSender().send_task(task)
 
 
-def send_quota(quota:ProjectQuotaDto):
+def send_quota(quota: ProjectQuotaDto):
     return TaskSender().send_quota(quota)
 
 
-__all__=[
-    'init',
-    'run_task',
-    'cancel_task',
-    'send_task',
-    'send_quota',
-    'project_is_running'
- ]
+__all__ = [
+    "init",
+    "run_task",
+    "cancel_task",
+    "send_task",
+    "send_quota",
+    "project_is_running",
+]

+ 144 - 72
SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py

@@ -1,8 +1,28 @@
-import requests,tools.utils as utils, core.configs as configs
-from core.dtos import ProjectDto,ProjectTaskDto, ProjectQuotaDto, ChapterDto, TotalBudgetInfoDto, ExcelParseResultDataDto
+import requests, tools.utils as utils, core.configs as configs
+from core.dtos import (
+    ProjectDto,
+    ProjectTaskDto,
+    ProjectQuotaDto,
+    ChapterDto,
+    TotalBudgetInfoDto,
+    ExcelParseResultDataDto,
+)
 from core.enum import TaskStatusEnum
-from stores import ProjectStore,ProjectTaskStore, ProjectQuotaStore, ChapterStore, BudgetStore
-from core.dtos import ExcelParseDto,ExcelParseZgsDto,ExcelParseItemDto,ExcelParseFileDto,ExcelParseResultDto
+from stores import (
+    ProjectStore,
+    ProjectTaskStore,
+    ProjectQuotaStore,
+    ChapterStore,
+    BudgetStore,
+)
+from core.dtos import (
+    ExcelParseDto,
+    ExcelParseZgsDto,
+    ExcelParseItemDto,
+    ExcelParseFileDto,
+    ExcelParseResultDto,
+)
+
 
 class TaskProcessor:
     def __init__(self):
@@ -15,116 +35,151 @@ class TaskProcessor:
         self._task_submit_url = "/task_submit"
         self._task_status_url = "/task_status"
         self._task_cancel_url = "/cancel_task"
-    
-    def submit_task(self, task:ProjectTaskDto):
+
+    def submit_task(self, task: ProjectTaskDto):
         try:
             self._logger.info(f"开始运行任务:{task.task_name}")
-            self._task_store.update_task_status(task.id, TaskStatusEnum.PROCESSING.value)
+            self._task_store.update_task_status(
+                task.id, TaskStatusEnum.PROCESSING.value
+            )
             if not task.file_path:
                 raise Exception("任务文件不存在")
             files, msg = self._read_files(task.file_path)
-            if not files or len(files)==0:
+            if not files or len(files) == 0:
                 raise Exception(msg)
             project = self._project_store.get(task.project_id)
             if not project:
                 raise Exception("项目不存在")
             budget_models = self._budget_store.get_budget_info(task.project_id)
-            budgets = [TotalBudgetInfoDto.from_model(budget) for budget in budget_models]
-            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)
+            budgets = [
+                TotalBudgetInfoDto.from_model(budget) for budget in budget_models
+            ]
+            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
+            )
             if not data:
                 raise Exception(msg)
-            res = self._call_api(self._task_submit_url,data)
+            res = self._call_api(self._task_submit_url, data)
 
-            if res.result==-1:
-                self._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+            if res.result == -1:
+                self._task_store.update_task_status(
+                    task.id, TaskStatusEnum.FAILURE.value, res.reason
+                )
                 return res.reason
 
-            if res.data and len(res.data)>0:
-                self._insert_data(task,project,res.data)
-            if res.result == 1 :
-                self._logger.info(f"运行任务:{task.task_name}完成")
-                self._task_store.update_task_status(task.id,TaskStatusEnum.SUCCESS.value)
+            if res.data and len(res.data) > 0:
+                self._insert_data(task, project, res.data)
+            if res.result == 1:
+                self._logger.debug(f"运行任务:{task.task_name}完成")
+                self._task_store.update_task_status(
+                    task.id, TaskStatusEnum.SUCCESS.value
+                )
             else:
-                self._logger.info(f"运行任务:{task.task_name}请求中,等待结果")
-                self._query_task(task,project)
+                self._logger.debug(f"运行任务:{task.task_name}请求中,等待结果")
+                self.query_task(task, project)
             return None
         except Exception as e:
             msg = f"任务运行失败,原因:{e}"
             self._logger.error(f"运行任务:{task.task_name}, {msg}")
-            self._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, msg)
+            self._task_store.update_task_status(
+                task.id, TaskStatusEnum.FAILURE.value, msg
+            )
             return msg
 
-    def cancel_task(self, task:ProjectTaskDto):
+    def cancel_task(self, task: ProjectTaskDto):
         try:
             self._logger.info(f"开始取消运行任务:{task.id}")
             if task.process_status == TaskStatusEnum.PROCESSING.value:
-                res = self._call_api(self._task_cancel_url,{"task_id":task.id})
-                if res.result==-1:
-                    self._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                res = self._call_api(self._task_cancel_url, {"task_id": task.id})
+                if res.result == -1:
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.FAILURE.value, res.reason
+                    )
                     return res.reason
                 project = self._project_store.get(task.project_id)
                 if not project:
                     self._logger.error(f"取消运行任务:{task.id}失败,原因:项目不存在")
                     return "项目不存在"
-                if res.data and len(res.data)>0:
-                    self._insert_data(task,project,res.data)
-                if res.result == 0 :
+                if res.data and len(res.data) > 0:
+                    self._insert_data(task, project, res.data)
+                if res.result == 0:
                     self._logger.info(f"取消运行任务:{task.id}成功")
                     # self._task_store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
                     return None
                 elif res.result == 1:
-                    self._logger.info(f"取消运行任务失败:{task.id}已运行完成")
-                    self._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
+                    self._logger.error(f"取消运行任务失败:{task.id}已运行完成")
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.SUCCESS.value
+                    )
                     return "取消失败,任务已运行完成"
             else:
-                self._logger.info(f"取消运行任务:{task.id}失败,原因:任务状态错误")
+                self._logger.error(f"取消运行任务:{task.id}失败,原因:任务状态错误")
                 return "任务状态错误"
         except Exception as e:
             msg = f"取消运行任务失败,原因:{e}"
             self._logger.error(msg)
             return msg
 
-    def _query_task(self, task:ProjectTaskDto,project:ProjectDto):
+    def query_task(self, task: ProjectTaskDto, project: ProjectDto = None):
         try:
             import time
+
             while True:
                 time.sleep(30)
-                res = self._call_api(self._task_status_url,{"task_id":task.id})
-                if res.result==-1:
-                    self._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                res = self._call_api(self._task_status_url, {"task_id": task.id})
+                if res.result == -1:
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.FAILURE.value, res.reason
+                    )
                     return res.reason
                 if res.data and len(res.data) > 0:
+                    if not project:
+                        project = self._project_store.get(task.project_id)
                     self._insert_data(task, project, res.data)
                 if res.result == 1:
-                    self._logger.info(f"运行任务:{task.task_name}完成")
-                    self._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
+                    self._logger.debug(f"运行任务:{task.task_name}完成")
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.SUCCESS.value
+                    )
                     break
                 else:
-                    self._logger.info(f"运行任务:{task.task_name}请求中,等待结果")
-                    self._query_task(task, project)
+                    self._logger.debug(f"运行任务:{task.task_name}请求中,等待结果")
+                    self.query_task(task, project)
         except Exception as e:
             msg = f"任务状态查询失败,原因:{e}"
             self._logger.error(msg)
             raise Exception(msg)
 
-    def _read_files(self, paths:str)->(list[ExcelParseFileDto],str):
+    def _read_files(self, paths: str) -> (list[ExcelParseFileDto], str):
         try:
-            files=[]
+            files = []
             self._logger.debug(f"开始读取文件:{paths}")
-            path_list= paths.split(",")
+            path_list = paths.split(",")
             for path in path_list:
                 file = utils.encode_file(path)
                 files.append(ExcelParseFileDto(file_id=path, content=file))
             self._logger.debug(f"读取文件完成:{paths}")
-            return files, ''
+            return files, ""
         except Exception as e:
             msg = f"读取文件失败,原因:{e}"
             self._logger.error(msg)
-            return None,msg
+            return None, msg
 
-    def _build_api_body(self, task:ProjectTaskDto,project:ProjectDto,budgets:list[TotalBudgetInfoDto],parents:list[ChapterDto],children:list[ChapterDto],files:list[ExcelParseFileDto]):
+    def _build_api_body(
+            self,
+            task: ProjectTaskDto,
+            project: ProjectDto,
+            budgets: list[TotalBudgetInfoDto],
+            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]
@@ -139,21 +194,23 @@ class TaskProcessor:
                 zgs_list=budgets_data,
                 hierarchy=parents_data,
                 components=children_data,
-                files=files
+                files=files,
             )
             return data, ""
         except Exception as e:
             msg = f"构建API BODY,原因:{e}"
             self._logger.error(msg)
-            return None,msg
+            return None, msg
 
-    def _call_api(self, api_url,data)->ExcelParseResultDto:
-       try:
+    def _call_api(self, api_url, data) -> ExcelParseResultDto:
+        try:
             url = f"{configs.app.task_api_url}{api_url}"
             if isinstance(data, ExcelParseDto):
                 data = data.to_dict()
             self._logger.debug(f"调用接口:{url},data:{data}")
-            response = requests.post(url, headers={"Content-Type": "application/json"}, json=data)
+            response = requests.post(
+                url, headers={"Content-Type": "application/json"}, json=data
+            )
             self._logger.debug(f"调用接口返回[{response.status_code}]:{response.text}")
             if response.status_code == 200:
                 result = response.json()
@@ -163,35 +220,50 @@ class TaskProcessor:
             else:
                 self._logger.error("调用接口失败")
                 raise Exception(response.text)
-       except Exception as e:
+        except Exception as e:
             msg = f"调用接口失败,原因:{e}"
             self._logger.error(msg)
             raise Exception(msg)
 
-    def _insert_data(self, task:ProjectTaskDto,project:ProjectDto,data:list[ExcelParseResultDataDto]):
+    def _insert_data(
+            self,
+            task: ProjectTaskDto,
+            project: ProjectDto,
+            data: list[ExcelParseResultDataDto],
+    ):
         try:
             self._logger.debug(f"开始插入数据:{task.task_name}")
             for item in data:
                 self._logger.debug(f"数据:{item.to_dict()}")
-                quota_dto = self._quota_store.get_quota_by_quota_input(project.project_id, task.budget_id, item.target_id)
+                quota_dto = (
+                    self._quota_store.get_quota_by_quota_input(
+                        project.project_id, task.budget_id, item.target_id
+                    )
+                    if item.target_id > 0
+                    else None
+                )
                 if quota_dto:
-                    self._logger.debug(f"新增数据:{item.item_id}/{item.item_code} {item.dinge_code}")
-                    quota_dto.item_code=item.item_code
-                    quota_dto.item_id=item.item_id
-                    quota_dto.quota_id=item.target_id and item.target_id>0  or 0
-                    quota_dto.quota_code=item.dinge_code
-                    quota_dto.entry_name=item.entry_name
-                    quota_dto.units=item.units
-                    quota_dto.amount=item.amount
-                    quota_dto.ex_file=item.ex_file_id
-                    quota_dto.ex_cell=item.ex_cell
-                    quota_dto.ex_row=item.ex_row
-                    quota_dto.ex_unit=item.ex_unit
-                    quota_dto.ex_amount=item.ex_amount
-                    quota_dto.send_error=None
+                    self._logger.debug(
+                        f"更新数据[{item.target_id}]:{item.item_id}/{item.item_code} {item.dinge_code}"
+                    )
+                    quota_dto.item_code = item.item_code
+                    quota_dto.item_id = item.item_id
+                    quota_dto.quota_id = item.target_id if item.target_id > 0 else 0
+                    quota_dto.quota_code = item.dinge_code
+                    quota_dto.entry_name = item.entry_name
+                    quota_dto.units = item.units
+                    quota_dto.amount = item.amount
+                    quota_dto.ex_file = item.ex_file_id
+                    quota_dto.ex_cell = item.ex_cell
+                    quota_dto.ex_row = item.ex_row
+                    quota_dto.ex_unit = item.ex_unit
+                    quota_dto.ex_amount = item.ex_amount
+                    quota_dto.send_error = None
                     self._quota_store.update_quota(quota_dto)
                 else:
-                    self._logger.debug(f"更新数据[{item.target_id}]:{item.item_id}/{item.item_code} {item.dinge_code}")
+                    self._logger.debug(
+                        f"新增数据[{item.target_id}]:{item.item_id}/{item.item_code} {item.dinge_code}"
+                    )
                     quota_dto = ProjectQuotaDto(
                         task_id=task.id,
                         budget_id=item.zgs_id,
@@ -199,7 +271,7 @@ class TaskProcessor:
                         project_id=project.project_id,
                         item_code=item.item_code,
                         item_id=item.item_id,
-                        quota_id=item.target_id,
+                        quota_id=item.target_id if item.target_id > 0 else 0,
                         quota_code=item.dinge_code,
                         entry_name=item.entry_name,
                         units=item.units,
@@ -217,4 +289,4 @@ class TaskProcessor:
         except Exception as e:
             msg = f"插入数据失败,原因:{e}"
             self._logger.error(msg)
-            return False,msg
+            return False, msg

+ 185 - 92
SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py

@@ -1,16 +1,17 @@
 from time import sleep
 import threading
-from typing import  Optional
 
 import tools.utils as utils, core.configs as configs
 from core.dtos import ProjectTaskDto
 from core.enum import TaskStatusEnum
 from executor.task_processor import TaskProcessor
-from stores import ProjectStore,ProjectTaskStore
+from stores import ProjectStore, ProjectTaskStore
+
 
 class TaskRunner:
     _is_running = {}
     _task_wait_list = {}
+    _process_task_wait_list = {}
     _running_projects = set()
     _max_concurrent_projects = configs.app.task_max_projects_count
     _task_sleep_interval = configs.app.task_interval
@@ -19,7 +20,94 @@ class TaskRunner:
     _task_store = ProjectTaskStore()
     _lock = threading.Lock()
     _task_processor = None
-    
+
+    @classmethod
+    def run_task(cls, task: ProjectTaskDto):
+        project_id = task.project_id
+        # 检查项目是否已有任务在运行
+        if project_id in cls._is_running and cls._is_running[project_id]:
+            cls._add_task_to_wait_list(
+                task,
+                f"项目已有任务在运行[{task.project_id}],任务[{task.task_name}]将等待",
+            )
+            return
+
+        # 如果项目没有任务在运行,但并发项目数已满且该项目不在运行集合中
+        if (
+                len(cls._running_projects) >= cls._max_concurrent_projects
+                and project_id not in cls._running_projects
+        ):
+            cls._add_task_to_wait_list(
+                task,
+                f"项目运行队列[{cls._max_concurrent_projects}]已满,任务[{task.task_name}]将等待",
+            )
+            return
+
+        # 可以执行的新任务
+        cls._add_task_to_wait_list(
+            task, f"添加到待运行队列[{task.project_id}]:{task.task_name}"
+        )
+        cls._execute_project_tasks(project_id)
+        return
+
+    @classmethod
+    def cancel(cls, task: ProjectTaskDto):
+        try:
+            cls._logger.info(f"开始取消运行任务[{task.project_id}]:{task.id}")
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                task_processor = cls._get_task_processor()
+                result = task_processor.cancel_task(task)
+                return result
+            elif task.process_status == TaskStatusEnum.WAIT.value:
+                cls._task_wait_list[task.project_id].remove(task)
+                cls._logger.info(f"取消运行任务[{task.project_id}]:{task.id}成功")
+            else:
+                cls._logger.info(
+                    f"取消运行任务[{task.project_id}]:{task.id}失败,原因:任务状态错误"
+                )
+                return "任务状态错误"
+            return None
+        except Exception as e:
+            msg = f"取消运行任务失败[{task.project_id}],原因:{e}"
+            cls._logger.error(msg)
+            return msg
+
+    @classmethod
+    def get_project_running_state(cls, project_id: str) -> bool:
+        return project_id in cls._is_running and cls._is_running[project_id]
+
+    @classmethod
+    def init(cls):
+        running_tasks = cls._task_store.get_tasks_by_status(
+            TaskStatusEnum.PROCESSING.value
+        )
+        cls._logger.info(
+            f"初始化正在运行的任务。正在运行中的任务数量:[{len(running_tasks)}]"
+        )
+        if len(running_tasks) > 0:
+            for task in running_tasks:
+                cls._execute_processing_task(task)
+        cls._logger.info(f"初始化正在运行的任务完成。")
+        cls._sync_wait_list()
+
+    @classmethod
+    def _run(cls):
+        if len(cls._running_projects) >= cls._max_concurrent_projects:
+            cls._logger.info(f"【运行等待队列中的任务】: 项目运行队列已满。")
+            return
+        cls._logger.info(f"【运行等待队列中的任务】")
+        if cls._task_wait_list and len(cls._task_wait_list) > 0:
+            # 遍历等待队列中的项目
+            for project_id in list(cls._task_wait_list.keys()):
+                # 只有当项目没有任务在运行且并发项目数未满时才执行
+                if (
+                        project_id not in cls._is_running or not cls._is_running[project_id]
+                ) and len(cls._running_projects) < cls._max_concurrent_projects:
+                    cls._execute_project_tasks(project_id)
+        else:
+            cls._logger.info(f"暂无任务在等待队列中,等待同步")
+            cls._sync_wait_list()
+
     @classmethod
     def _get_task_processor(cls):
         if cls._task_processor is None:
@@ -31,9 +119,11 @@ class TaskRunner:
         """确保项目的等待队列已初始化"""
         if project_id not in cls._task_wait_list:
             cls._task_wait_list[project_id] = []
-    
+
     @classmethod
-    def _add_task_to_wait_list(cls, task: ProjectTaskDto, log_message: str = None) -> None:
+    def _add_task_to_wait_list(
+            cls, task: ProjectTaskDto, log_message: str = None
+    ) -> None:
         """将任务添加到等待队列并更新状态"""
         project_id = task.project_id
         cls._ensure_wait_list(project_id)
@@ -42,43 +132,9 @@ class TaskRunner:
         cls._task_wait_list[project_id].append(task)
 
     @classmethod
-    def run(cls, task: ProjectTaskDto = None):
-        if task:
-            project_id = task.project_id
-            # 检查项目是否已有任务在运行
-            if project_id in cls._is_running and cls._is_running[project_id]:
-                cls._add_task_to_wait_list(
-                    task, 
-                    f"项目[{project_id}]已有任务在运行,任务[{task.task_name}]将等待"
-                )
-                return
-            
-            # 如果项目没有任务在运行,但并发项目数已满且该项目不在运行集合中
-            if len(cls._running_projects) >= cls._max_concurrent_projects and project_id not in cls._running_projects:
-                cls._add_task_to_wait_list(
-                    task, 
-                    f"项目运行队列[{cls._max_concurrent_projects}]已满,任务[{task.task_name}]将等待"
-                )
-                return
-            
-            # 可以执行的新任务
-            cls._add_task_to_wait_list(task, f"添加到待运行队列:{task.task_name}")
-            cls._execute_project_tasks(project_id)
-            return
-        
-        # 无新任务时,尝试执行等待队列中的任务
-        if not task:
-            if cls._task_wait_list and len(cls._task_wait_list) > 0:
-                # 遍历等待队列中的项目
-                for project_id in list(cls._task_wait_list.keys()):
-                    # 只有当项目没有任务在运行且并发项目数未满时才执行
-                    if (project_id not in cls._is_running or not cls._is_running[project_id]) and len(cls._running_projects) < cls._max_concurrent_projects:
-                        cls._execute_project_tasks(project_id)
-            else:
-                cls._logger.info(f"等待队列为空,等待{cls._task_sleep_interval}秒后同步数据运行")
-                cls._sync_wait_list()
-    @classmethod
-    def _update_project_running_status(cls, project_id: str, is_running: bool) -> None:
+    def _update_project_running_status(
+            cls, project_id: str, is_running: bool = False
+    ) -> None:
         """更新项目运行状态"""
         with cls._lock:
             cls._is_running[project_id] = is_running
@@ -86,11 +142,15 @@ class TaskRunner:
                 cls._running_projects.add(project_id)
             elif project_id in cls._running_projects:
                 cls._running_projects.remove(project_id)
-            
+
             # 如果项目没有等待任务,从等待列表中移除
-            if not is_running and project_id in cls._task_wait_list and len(cls._task_wait_list[project_id]) == 0:
+            if (
+                    not is_running
+                    and project_id in cls._task_wait_list
+                    and len(cls._task_wait_list[project_id]) == 0
+            ):
                 del cls._task_wait_list[project_id]
-    
+
     @classmethod
     def _can_execute_project(cls, project_id: str) -> bool:
         """检查项目是否可以执行"""
@@ -98,10 +158,13 @@ class TaskRunner:
         if project_id in cls._is_running and cls._is_running[project_id]:
             return False
         # 如果项目不在运行集合中且并发数已满,直接返回False
-        if project_id not in cls._running_projects and len(cls._running_projects) >= cls._max_concurrent_projects:
+        if (
+                project_id not in cls._running_projects
+                and len(cls._running_projects) >= cls._max_concurrent_projects
+        ):
             return False
         return True
-    
+
     @classmethod
     def _execute_project_tasks(cls, project_id: str):
         try:
@@ -111,20 +174,29 @@ class TaskRunner:
                     return
                 cls._is_running[project_id] = True
                 cls._running_projects.add(project_id)
-            
+
             def execute_tasks():
                 try:
-                    while project_id in cls._task_wait_list and len(cls._task_wait_list[project_id]) > 0:
+                    cls._logger.debug(f"开始执行项目[{project_id}]")
+                    while (
+                            project_id in cls._task_wait_list
+                            and len(cls._task_wait_list[project_id]) > 0
+                    ):
                         current_task = cls._task_wait_list[project_id].pop(0)
                         try:
                             task_processor = cls._get_task_processor()
+                            cls._logger.debug(
+                                f"开始执行任务[{current_task.project_id}]:{current_task.id}/{current_task.task_name}"
+                            )
                             task_processor.submit_task(current_task)
                         except Exception as ex:
-                            cls._logger.error(f"运行任务失败:{current_task.task_name}, {str(ex)}")
+                            cls._logger.error(
+                                f"运行任务失败[{current_task.project_id}]:{current_task.id}/{current_task.task_name}, {str(ex)}"
+                            )
                 finally:
                     cls._update_project_running_status(project_id, False)
-                    cls._sync_wait_list()
-            
+                    cls._run()
+
             # 创建新线程执行任务
             thread = threading.Thread(target=execute_tasks)
             thread.daemon = True
@@ -132,58 +204,79 @@ class TaskRunner:
         except Exception as e:
             cls._logger.error(f"执行项目任务失败:{project_id}, {str(e)}")
             cls._update_project_running_status(project_id, False)
-            cls._sync_wait_list()
-        
+            cls._run()
+
     @classmethod
-    def _sync_wait_list(cls, project_id: str = None) -> Optional[str]:
+    def _execute_processing_task(cls, task: ProjectTaskDto):
+        # 检查项目是否可以执行
+        project_id = task.project_id
+        try:
+            with cls._lock:
+                if project_id not in cls._process_task_wait_list:
+                    cls._process_task_wait_list[project_id] = []
+                cls._is_running[project_id] = True
+                cls._running_projects.add(project_id)
+                cls._process_task_wait_list[project_id].append(task)
+                cls._logger.debug(f"任务[{task.id}]添加到等待队列")
+
+            def execute_process_tasks():
+                try:
+                    while (
+                            project_id in cls._process_task_wait_list
+                            and len(cls._process_task_wait_list[project_id]) > 0
+                    ):
+                        current_task = cls._process_task_wait_list[project_id].pop(0)
+                        try:
+                            cls._logger.debug(
+                                f"开始执行运行中任务[{current_task.project_id}]:{current_task.id}/{current_task.task_name}"
+                            )
+                            task_processor = cls._get_task_processor()
+                            task_processor.query_task(current_task)
+                        except Exception as ex:
+                            cls._logger.error(
+                                f"运行任务失败[{current_task.project_id}]:{current_task.task_name}, {str(ex)}"
+                            )
+                finally:
+                    cls._update_project_running_status(project_id, False)
+                    cls._run()
+
+            # 创建新线程执行任务
+            thread = threading.Thread(target=execute_process_tasks)
+            thread.daemon = True
+            thread.start()
+        except Exception as e:
+            cls._logger.error(
+                f"执行项目[{project_id}]任务失败:{task.id}/{task.task_name}, {str(e)}"
+            )
+            cls._update_project_running_status(project_id, False)
+            cls._run()
+
+    @classmethod
+    def _sync_wait_list(cls, project_id: str = None) -> int:
         try:
-            cls._logger.info(f"开始同步待运行队列")
             tasks = cls._task_store.get_wait_tasks(project_id)
             for task in tasks:
                 cls._ensure_wait_list(task.project_id)
                 if task not in cls._task_wait_list[task.project_id]:
                     cls._task_wait_list[task.project_id].append(task)
-            
             total_tasks = sum(len(tasks) for tasks in cls._task_wait_list.values())
             if total_tasks > 0:
-                cls._logger.info(f"同步待运行队列,同步{total_tasks}条数据")
-                cls.run()
+                if len(cls._running_projects) < cls._max_concurrent_projects:
+                    cls._logger.debug(f"同步待运行队列,同步{total_tasks}条数据,运行")
+                    cls._run()
+                else:
+                    cls._logger.debug(
+                        f"同步待运行队列,同步{total_tasks}条数据。暂无空闲线程,稍后运行"
+                    )
             else:
-                cls._logger.info(f"同步待运行队列,无新增数据,等待{cls._task_sleep_interval}秒后同步数据运行")
+                cls._logger.info(
+                    f"同步待运行队列,无新增数据,等待{cls._task_sleep_interval}秒后同步数据运行"
+                )
                 sleep(cls._task_sleep_interval)
                 cls._sync_wait_list()
-        except Exception as e:
-            msg = f"同步待运行队列失败,原因:{e}"
-            cls._logger.error(msg)
-
 
-    @classmethod
-    def cancel(cls, task:ProjectTaskDto):
-        try:
-            cls._logger.info(f"开始取消运行任务:{task.id}")
-            if task.process_status == TaskStatusEnum.PROCESSING.value:
-                task_processor = cls._get_task_processor()
-                result = task_processor.cancel_task(task)
-                return result
-            elif task.process_status == TaskStatusEnum.WAIT.value:
-                cls._task_wait_list[task.project_id].remove(task)
-                cls._logger.info(f"取消运行任务:{task.id}成功")
-            else:
-                cls._logger.info(f"取消运行任务:{task.id}失败,原因:任务状态错误")
-                return "任务状态错误"
-            return None
+            return total_tasks
         except Exception as e:
-            msg = f"取消运行任务失败,原因:{e}"
+            msg = f"同步待运行队列失败,原因:{e}"
             cls._logger.error(msg)
-            return msg
-
-
-
-    @classmethod
-    def get_project_running_state(cls, project_id: str) -> bool:
-        return project_id in cls._is_running and cls._is_running[project_id]
-
-
-
-
-
+            return 0

+ 41 - 24
SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py

@@ -1,7 +1,7 @@
 import tools.utils as utils
 from core.dtos import ProjectTaskDto, ProjectQuotaDto, QuotaInputDto
 from core.enum import SendStatusEnum
-from stores import ProjectQuotaStore, ProjectTaskStore,QuotaInputStore
+from stores import ProjectQuotaStore, ProjectTaskStore, QuotaInputStore
 
 
 class TaskSender:
@@ -11,52 +11,69 @@ class TaskSender:
         self._quota_store = ProjectQuotaStore()
         self._quota_input_store = QuotaInputStore()
 
-    def send_task(self, task:ProjectTaskDto):
+    def send_task(self, task: ProjectTaskDto):
         try:
-            self._logger.info(f"开始发送任务:{task.task_name}")
-            self._task_store.update_send_status(task.id, SendStatusEnum.PROCESSING.value)
+            self._logger.info(f"开始推送任务:{task.task_name}")
+            self._task_store.update_send_status(
+                task.id, SendStatusEnum.PROCESSING.value
+            )
 
             error_count = 0
-            data_list = self._quota_store.get_quotas_by_task_id(task.id,True)
+            data_list = self._quota_store.get_quotas_by_task_id(task.id, True)
             for data in data_list:
-                msg = self._quota_store.update_send_status(data.id,SendStatusEnum.PROCESSING.value)
+                msg = self._quota_store.update_send_status(
+                    data.id, SendStatusEnum.PROCESSING.value
+                )
                 if msg:
-                    error_count+=1
+                    error_count += 1
                     continue
 
-            self._task_store.update_send_status(task.id,SendStatusEnum.SUCCESS.value)
-            self._logger.info(f"发送任务:{task.task_name}完成,{error_count}项错误/共{len(data_list)}项")
+            self._task_store.update_send_status(task.id, SendStatusEnum.SUCCESS.value)
+            self._logger.info(
+                f"推送任务:{task.task_name}完成,{error_count}项错误/共{len(data_list)}项"
+            )
             return None
         except Exception as e:
-            msg = f"任务发送失败,原因:{e}"
-            self._logger.error(f"发送任务:{task.task_name},{msg}")
-            self._task_store.update_send_status(task.id, SendStatusEnum.FAILURE.value, msg)
+            msg = f"任务推送失败,原因:{e}"
+            self._logger.error(f"推送任务:{task.task_name},{msg}")
+            self._task_store.update_send_status(
+                task.id, SendStatusEnum.FAILURE.value, msg
+            )
             return msg
 
-    def send_quota(self,quota:ProjectQuotaDto):
+    def send_quota(self, quota: ProjectQuotaDto):
         try:
-            self._logger.info(f"开始发送定额:{quota.id}")
-            self._quota_store.update_send_status(quota.id, SendStatusEnum.PROCESSING.value)
+            self._logger.debug(f"开始推送定额输入[{quota.id}]")
+            self._quota_store.update_send_status(
+                quota.id, SendStatusEnum.PROCESSING.value
+            )
             quota_input = QuotaInputDto.from_quota_dto(quota)
-            self._save_quota(quota_input,quota.project_id)
+            self._save_quota(quota_input, quota.project_id)
             self._quota_store.update_send_status(quota.id, SendStatusEnum.SUCCESS.value)
-            self._logger.info(f"发送定额:{quota.id}完成")
+            self._logger.debug(f"推送定额输入[{quota.id}]完成")
             return None
         except Exception as e:
-            msg = f"定额发送失败,原因:{e}"
-            self._logger.error(f"发送定额:{quota.id},{msg}")
-            self._quota_store.update_send_status(quota.id, SendStatusEnum.FAILURE.value, msg)
+            msg = f"定额输入[{quota.id}]推送失败,原因:{e}"
+            self._logger.error(msg)
+            self._quota_store.update_send_status(
+                quota.id, SendStatusEnum.FAILURE.value, msg
+            )
             return msg
 
-    def _save_quota(self,quota:QuotaInputDto,project_id:str):
+    def _save_quota(self, quota: QuotaInputDto, project_id: str):
         try:
-            self._logger.info(f"开始保存定额:{quota.project_name} {quota.quota_code} {quota.quota_id}")
             # data = self._quota_input_store.get_quota(project_id, quota.budget_id,quota.item_id,quota.quota_code)
             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}"
+                )
                 self._quota_input_store.update_quota(project_id, quota)
             else:
                 self._quota_input_store.create_quota(project_id, quota)
+                self._logger.debug(
+                    f"新增定额输入[{quota.quota_id}]:{quota.project_name} {quota.quota_code} {quota.quota_id}"
+                )
         except Exception as e:
-            msg = f"保存定额失败,原因:{e}"
-            self._logger.error(f"保存定额:{quota.quota_id},{msg}")
+            msg = f"保存定额[{quota.quota_id}]输入失败,原因:{e}"
+            self._logger.error(msg)
             raise Exception(msg)

+ 8 - 7
SourceCode/IntelligentRailwayCosting/app/main.py

@@ -1,19 +1,20 @@
-import executor,threading
+import executor, threading
 from flask_app import create_app
 import tools.utils as utils
 
 logger = utils.get_logger()
 
+
 def main():
     logger.info("程序启动")
     app = create_app()
-    
+
     thread = threading.Thread(target=executor.init)
     thread.daemon = True
     thread.start()
-       
-    
-    app.run(host='0.0.0.0', port=5123)  # 指定HTTP端口为5123
 
-if __name__ == '__main__':
-    main()
+    app.run(host="0.0.0.0", port=5123)  # 指定HTTP端口为5123
+
+
+if __name__ == "__main__":
+    main()

+ 69 - 67
SourceCode/IntelligentRailwayCosting/app/routes/excel_test.py

@@ -4,108 +4,116 @@ import string
 
 from core.dtos import ExcelParseResultDto, ExcelParseResultDataDto
 
-test_api = Blueprint('test_api', __name__)
+test_api = Blueprint("test_api", __name__)
 
-test_data_dic={}
-task_count_dic={}
+test_data_dic = {}
+task_count_dic = {}
 
-@test_api.route('/task_submit', methods=['POST'])
+
+@test_api.route("/task_submit", methods=["POST"])
 def test_submit():
     data = request.get_json()
-    print("[task_submit] 接受数据:",end="")
-    print(data)
-    task_id = int(data.get('task_id'))
-    test_data_dic[task_id]=data
+    # print("[task_submit] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
+    test_data_dic[task_id] = data
     result = build_result(task_id)
     return jsonify(result)
 
-@test_api.route('/task_status', methods=['POST'])
+
+@test_api.route("/task_status", methods=["POST"])
 def test_query():
     data = request.get_json()
-    print("[task_status] 接受数据:",end="")
-    print(data)
-    task_id = int(data.get('task_id'))
+    # print("[task_status] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
     result = build_result(task_id)
     return jsonify(result)
 
-@test_api.route('/cancel_task', methods=['POST'])
+
+@test_api.route("/cancel_task", methods=["POST"])
 def test_cancel():
     data = request.get_json()
-    print("[cancel_task] 接受数据:",end="")
-    print(data)
-    task_id = int(data.get('task_id'))
+    # print("[cancel_task] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
     result = build_result(task_id)
     return jsonify(result)
 
+
 def build_test_data(task_data):
     # 从task_data获取selected_zgs_id,如果没有则从zgs_list随机选择
-    selected_zgs_id = task_data.get('selected_zgs_id', None) if task_data else 1
+    selected_zgs_id = task_data.get("selected_zgs_id", None) if task_data else 1
     zgs_code = ""
-    zgs_list = task_data.get('zgs_list', None)
-    if  selected_zgs_id == -1 and zgs_list:
+    zgs_list = task_data.get("zgs_list", None)
+    if selected_zgs_id == -1 and zgs_list:
         selected_zgs = random.choice(zgs_list)
-        selected_zgs_id = selected_zgs.get('zgs_id')
-        zgs_code = selected_zgs.get('zgs_name')
+        selected_zgs_id = selected_zgs.get("zgs_id")
+        zgs_code = selected_zgs.get("zgs_name")
     elif zgs_list:
         # 修改: 使用列表推导式进行过滤
-        selected_zgs = [x for x in zgs_list if x.get('zgs_id') == selected_zgs_id]
+        selected_zgs = [x for x in zgs_list if x.get("zgs_id") == selected_zgs_id]
         if not selected_zgs:
             selected_zgs = random.choice(zgs_list)
         else:
             selected_zgs = selected_zgs[0]  # 取第一个匹配项
-        zgs_code = selected_zgs.get('zgs_name')
-    
-    file_excel = task_data.get('file_excel', None)
+        zgs_code = selected_zgs.get("zgs_name")
+
+    file_excel = task_data.get("files", None)
     # 从file_excel随机选择文件
     ex_file_id = ""
     if file_excel:
         file = random.choice(file_excel)
-        ex_file_id = file.get('file_id')
+        ex_file_id = file.get("file_id")
 
-    items = task_data.get('components', [])
+    items = task_data.get("components", [])
     item = random.choice(items)
-    item_id = item.get('item_id',2)
-    item_code = item.get('item_code',"01")
-    entry_name = item.get('item_name',"001")
+    item_id = item.get("item_id", 2)
+    item_code = item.get("item_code", "01")
+    entry_name = item.get("item_name", "001")
     dinge_code = f"TY-{''.join(random.choices(string.digits, k=3))}"
     # 随机选择单位和数量
-    units_list = ['个', '米', '千米', '平方米', '立方米', '吨', '件']
+    units_list = ["个", "米", "千米", "平方米", "立方米", "吨", "件"]
     units = random.choice(units_list)
     amount = round(random.uniform(1, 1000), 2)
-    
+
     # 随机生成单元格位置
     col = random.choice(string.ascii_uppercase)
     row = random.randint(1, 100)
     ex_cell = f"{col}{row}"
-    
+
     # 生成更丰富的行数据
-    remarks = ['按图施工', '特殊工艺', '现场测量', '质量要求高', '工期紧张']
+    remarks = ["按图施工", "特殊工艺", "现场测量", "质量要求高", "工期紧张"]
     ex_row = f"{entry_name},{amount},{units},{random.choice(remarks)}"
-    
+    target_id = -1
+    if task_data.get("task_id") == 1:
+        target_id = random.randint(-1, 15)
     return ExcelParseResultDataDto(
-            zgs_id=selected_zgs_id if selected_zgs_id else 1,
-            zgs_code=zgs_code,
-            item_id=item_id,
-            item_code=item_code,
-            entry_name=entry_name,
-            dinge_code=dinge_code,
-            units=units,
-            amount=amount,
-            ex_file_id=ex_file_id,
-            ex_cell=ex_cell,
-            ex_row=ex_row,
-            ex_unit=units,
-            ex_amount=amount
-        )
-
-def build_result(task_id:int):
+        zgs_id=selected_zgs_id if selected_zgs_id else 1,
+        zgs_code=zgs_code,
+        item_id=item_id,
+        item_code=item_code,
+        entry_name=entry_name,
+        target_id=target_id,
+        dinge_code=dinge_code,
+        units=units,
+        amount=amount,
+        ex_file_id=ex_file_id,
+        ex_cell=ex_cell,
+        ex_row=ex_row,
+        ex_unit=units,
+        ex_amount=amount,
+    )
+
+
+def build_result(task_id: int):
     data = test_data_dic.get(task_id)
     test_data = []
-    
+
     # 更新任务运行次数
     count = task_count_dic.get(task_id, 0) + 1
     task_count_dic[task_id] = count
-    
+
     # 根据运行次数和概率分布确定状态
     if count >= 3:
         result = 1  # 运行超过5次必定成功
@@ -114,10 +122,10 @@ def build_result(task_id:int):
         if rand < 0.05:
             result = -1  # 5%概率失败
         elif rand < 0.8:
-            result = 0   # 85%概率处理中
+            result = 0  # 85%概率处理中
         else:
-            result = 1   # 10%概率成功
-    
+            result = 1  # 10%概率成功
+
     if data and result != -1:
         # 当result为1(成功)时,有80%概率生成数据
         if result == 1:
@@ -127,16 +135,10 @@ def build_result(task_id:int):
         else:  # result为0(处理中)时,一定生成数据
             for i in range(random.randint(3, 5)):
                 test_data.append(build_test_data(data))
-    
-    status_msg = {
-        -1: "EXCEL远程解析失败",
-        0: "EXCEL正在解析",
-        1: "EXCEL解析完成"
-    }.get(result, "未知状态")
-    
-    result = ExcelParseResultDto(task_id, result, status_msg, test_data)
-    return result.to_dict()
-
-
 
+    status_msg = {-1: "EXCEL远程解析失败", 0: "EXCEL正在解析", 1: "EXCEL解析完成"}.get(
+        result, "未知状态"
+    )
 
+    result = ExcelParseResultDto(task_id, result, status_msg, test_data)
+    return result.to_dict()

+ 61 - 41
SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py

@@ -2,87 +2,107 @@ from flask import Blueprint, request
 
 from core.dtos import ProjectQuotaDto
 from core.user_session import Permission
-from core.api import  ResponseBase,TableResponse
-from services import  ProjectQuotaService
+from core.api import ResponseBase, TableResponse
+from services import ProjectQuotaService
 
-project_quota_api = Blueprint('project_quota_api', __name__)
+project_quota_api = Blueprint("project_quota_api", __name__)
 quota_service = ProjectQuotaService()
 
-@project_quota_api.route('/list/<int:budget_id>/<project_id>/<item_code>', methods=['POST'])
+
+@project_quota_api.route(
+    "/list/<int:budget_id>/<project_id>/<item_code>", methods=["POST"]
+)
 @Permission.authorize
-def get_page_list(budget_id:int,project_id:str,item_code:str):
+def get_page_list(budget_id: int, project_id: str, item_code: str):
     try:
         data = request.get_json()
-        page = int(data.get('pageNum', 1))
-        per_page = int(data.get('pageSize', 10))
-        keyword = data.get('keyword')
-        send_status = int(data.get('send_status')) if data.get('send_status') else None
-        data,count = quota_service.get_quotas_paginated(budget_id, project_id, item_code, page, per_page, keyword, send_status)
-        return TableResponse.success(data,count)
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        send_status = int(data.get("send_status")) if data.get("send_status") else None
+        data, count = quota_service.get_quotas_paginated(
+            budget_id, project_id, item_code, page, per_page, keyword, send_status
+        )
+        return TableResponse.success(data, count)
     except Exception as e:
-        return ResponseBase.error(f'获取定额条目列表失败:{str(e)}')
-@project_quota_api.route('/list/task/<int:task_id>/<int:budget_id>/<project_id>/<item_code>', methods=['POST'])
+        return ResponseBase.error(f"获取定额条目列表失败:{str(e)}")
+
+
+@project_quota_api.route(
+    "/list/task/<int:task_id>/<int:budget_id>/<project_id>/<item_code>",
+    methods=["POST"],
+)
 @Permission.authorize
-def get_quotas_by_task_paginated(task_id:int,budget_id:int,project_id:str,item_code:str):
+def get_quotas_by_task_paginated(
+        task_id: int, budget_id: int, project_id: str, item_code: str
+):
     try:
         data = request.get_json()
-        page = int(data.get('pageNum', 1))
-        per_page = int(data.get('pageSize', 10))
-        keyword = data.get('keyword')
-        send_status = int(data.get('send_status')) if data.get('send_status') else None
-        data,count = quota_service.get_quotas_by_task_paginated(task_id, budget_id, project_id, item_code,page, per_page, keyword, send_status)
-        return TableResponse.success(data,count)
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        send_status = int(data.get("send_status")) if data.get("send_status") else None
+        data, count = quota_service.get_quotas_by_task_paginated(
+            task_id,
+            budget_id,
+            project_id,
+            item_code,
+            page,
+            per_page,
+            keyword,
+            send_status,
+        )
+        return TableResponse.success(data, count)
     except Exception as e:
-        return ResponseBase.error(f'获取任务定额条目列表失败:{str(e)}')
+        return ResponseBase.error(f"获取任务定额条目列表失败:{str(e)}")
 
 
-@project_quota_api.route('/get/<int:quota_id>', methods=['POST'])
+@project_quota_api.route("/get/<int:quota_id>", methods=["POST"])
 @Permission.authorize
-def get_quota(quota_id:int):
+def get_quota(quota_id: int):
     try:
         data = quota_service.get_quota_dto(quota_id)
         return ResponseBase.success(data.to_dict())
     except Exception as e:
-        return ResponseBase.error(f'获取定额条目失败:{str(e)}')
+        return ResponseBase.error(f"获取定额条目失败:{str(e)}")
+
 
-@project_quota_api.route('/save', methods=['POST'])
+@project_quota_api.route("/save", methods=["POST"])
 @Permission.authorize
 def save_quota():
     try:
         data = request.get_json()
         quota_dto = ProjectQuotaDto(**data)
-        run_now = data.get('run_now',False)
-        is_cover = data.get('is_cover',False)
         quota_dto = quota_service.save_quota(quota_dto)
-        if not is_cover:
-            quota_dto.quota_id = 0
+        run_now = data.get("run_now", "false") == "true"
         if run_now:
-            quota_service.start_send(quota_dto.id)
+            is_cover = data.get("is_cover", "false") == "true"
+            quota_service.start_send(quota_dto.id, is_cover)
         return ResponseBase.success(quota_dto.to_dict())
     except Exception as e:
-        return ResponseBase.error(f'保存定额条目失败:{str(e)}')
+        return ResponseBase.error(f"保存定额条目失败:{str(e)}")
 
-@project_quota_api.route('/delete/<int:quota_id>', methods=['POST'])
+
+@project_quota_api.route("/delete/<int:quota_id>", methods=["POST"])
 @Permission.authorize
-def delete_quota(quota_id:int):
+def delete_quota(quota_id: int):
     try:
         quota_service.delete_quota(quota_id)
         return ResponseBase.success()
     except Exception as e:
-        return ResponseBase.error(f'删除定额条目失败:{str(e)}')
+        return ResponseBase.error(f"删除定额条目失败:{str(e)}")
+
 
-@project_quota_api.route('/start_send', methods=['POST'])
+@project_quota_api.route("/start_send", methods=["POST"])
 @Permission.authorize
 def start_send():
     try:
         data = request.get_json()
-        ids = data.get('ids')
-        is_cover = data.get('is_cover','false') =='true'
-        msg = quota_service.start_send_by_ids(ids,is_cover)
+        ids = data.get("ids", "")
+        is_cover = data.get("is_cover", "false") == "true"
+        msg = quota_service.start_send_by_ids(ids, is_cover)
         if msg:
             return ResponseBase.error(msg)
         return ResponseBase.success()
     except Exception as e:
-        return ResponseBase.error(f'启动定额条目失败:{str(e)}')
-
-
+        return ResponseBase.error(f"启动定额条目失败:{str(e)}")

+ 54 - 15
SourceCode/IntelligentRailwayCosting/app/services/project_quota.py

@@ -1,19 +1,28 @@
 from typing import Optional
 
-import tools.utils as utils,threading
+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
 import executor
 
+
 class ProjectQuotaService:
     def __init__(self):
         self.store = ProjectQuotaStore()
         self._logger = utils.get_logger()
 
-    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):
+    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,
+    ):
         """获取项目定额列表
 
         Args:
@@ -36,16 +45,29 @@ class ProjectQuotaService:
                 page=page,
                 page_size=page_size,
                 keyword=keyword,
-                send_status=send_status
+                send_status=send_status,
             )
-            return [ProjectQuotaDto.from_model(quota).to_dict() for quota in data.get('data',[])],data.get('total',0)
+            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_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):
+    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,
+    ):
         try:
             data = self.store.get_quotas_by_task_paginated(
                 task_id=task_id,
@@ -55,9 +77,12 @@ class ProjectQuotaService:
                 page=page,
                 page_size=page_size,
                 send_status=send_status,
-                keyword=keyword
+                keyword=keyword,
             )
-            return [ProjectQuotaDto.from_model(quota).to_dict() for quota in data.get('data',[])],data.get('total',0)
+            return [
+                ProjectQuotaDto.from_model(quota).to_dict()
+                for quota in data.get("data", [])
+            ], data.get("total", 0)
         except Exception as e:
             self._logger.error(f"获取任务项目定额列表失败: {str(e)}")
             raise
@@ -68,6 +93,7 @@ class ProjectQuotaService:
         except Exception as e:
             self._logger.error(f"获取定额条目DTO失败: {str(e)}")
             raise
+
     def get_quota(self, quota_id: int) -> Optional[ProjectQuotaModel]:
         """获取单个项目定额
 
@@ -87,9 +113,23 @@ class ProjectQuotaService:
         """保存定额"""
         try:
             # 业务验证
-            if  quota_dto.id == 0:
+            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
+
                 quota_dto = self.update_quota(quota_dto)
                 # self.update_process_status(quota_dto.id,4)
                 if quota_dto.send_status != SendStatusEnum.NEW.value:
@@ -188,12 +228,12 @@ class ProjectQuotaService:
     #         self.update_process_status(quota.id, 3, str(e))
     #         raise
 
-    def start_send_by_ids(self, ids: str,is_cover: bool = False):
+    def start_send_by_ids(self, ids: str, is_cover: bool = False):
         try:
-            id_list= ids.split(',')
-            err =""
+            id_list = ids.split(",")
+            err = ""
             for _id in id_list:
-                msg = self.start_send(int(_id),is_cover)
+                msg = self.start_send(int(_id), is_cover)
                 if msg:
                     err += f"{msg}[{_id}],"
             return err
@@ -201,7 +241,6 @@ class ProjectQuotaService:
             self._logger.error(f"批量启动定额条目发送失败: {str(e)}")
             raise
 
-
     def start_send(self, _id: int, is_cover: bool = False) -> Optional[str]:
         """启动发送"""
         quota = self.get_quota_dto(_id)
@@ -221,4 +260,4 @@ class ProjectQuotaService:
         except Exception as e:
             self._logger.error(f"发送定额条目失败: {str(e)}")
             self.update_send_status(quota.id, 3, str(e))
-            raise
+            raise

+ 101 - 51
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -8,6 +8,7 @@ from core.models import ProjectTaskModel
 from stores import ProjectTaskStore
 import executor
 
+
 class ProjectTaskService:
     def __init__(self):
         self.store = ProjectTaskStore()
@@ -26,25 +27,29 @@ class ProjectTaskService:
         with task_lock:
             task = self.store.get_task_dto(task_id)
             if not task:
-                return '没有查询到任务'
-            if not task.file_path or task.file_path.strip() == '':
-                return '没有上传文件'
+                return "没有查询到任务"
+            if not task.file_path or task.file_path.strip() == "":
+                return "没有上传文件"
             if task.process_status == TaskStatusEnum.PROCESSING:
-                return '正在运行中'
+                return "正在运行中"
 
             try:
                 thread = threading.Thread(target=self._run_task, args=(task,))
                 thread.daemon = True
                 thread.start()
                 if executor.project_is_running(task.project_id):
-                    self.update_process_status(task_id, TaskStatusEnum.WAIT.value, '')
-                    return '0'
+                    self.update_process_status(task_id, TaskStatusEnum.WAIT.value, "")
+                    return "0"
                 else:
-                    self.update_process_status(task_id, TaskStatusEnum.PROCESSING.value, '')
+                    self.update_process_status(
+                        task_id, TaskStatusEnum.PROCESSING.value, ""
+                    )
                 return None
             except Exception as e:
                 self._logger.error(f"启动任务失败: {str(e)}")
-                self.update_process_status(task_id, TaskStatusEnum.FAILURE.value, str(e))
+                self.update_process_status(
+                    task_id, TaskStatusEnum.FAILURE.value, str(e)
+                )
                 raise
 
     def _run_task(self, task: ProjectTaskDto):
@@ -56,7 +61,9 @@ class ProjectTaskService:
                 #     self.start_send_task(task.id)
             except Exception as e:
                 self._logger.error(f"采集项目任务失败: {str(e)}")
-                self.update_process_status(task.id, TaskStatusEnum.FAILURE.value, str(e))
+                self.update_process_status(
+                    task.id, TaskStatusEnum.FAILURE.value, str(e)
+                )
                 raise
 
     def start_send_task(self, task_id: int):
@@ -64,11 +71,11 @@ class ProjectTaskService:
         with task_lock:
             task = self.store.get_task_dto(task_id)
             if not task:
-                return '没有查询到任务'
+                return "没有查询到任务"
             if task.process_status != TaskStatusEnum.SUCCESS.value:
-                return '还未处理完成'
+                return "还未处理完成"
             if task.send_status == SendStatusEnum.PROCESSING.value:
-                return '正在发送中'
+                return "正在发送中"
 
             try:
                 thread = threading.Thread(target=self._send_task, args=(task,))
@@ -90,9 +97,16 @@ class ProjectTaskService:
                 self.update_send_status(task.id, TaskStatusEnum.FAILURE.value, str(e))
                 raise
 
-    def get_tasks_paginated(self, project_id: str, item_code: str, page: int = 1, page_size: int = 10,
-                            keyword: Optional[str] = None,
-                            process_status: Optional[int] = None, send_status: Optional[int] = None):
+    def get_tasks_paginated(
+            self,
+            project_id: str,
+            item_code: str,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            process_status: Optional[int] = None,
+            send_status: Optional[int] = None,
+    ):
         """获取项目任务列表
 
         Args:
@@ -108,21 +122,23 @@ class ProjectTaskService:
             dict: 包含总数和任务列表的字典
         """
         try:
-            data =  self.store.get_tasks_paginated(
+            data = self.store.get_tasks_paginated(
                 project_id=project_id,
                 item_code=item_code,
                 page=page,
                 page_size=page_size,
                 keyword=keyword,
                 process_status=process_status,
-                send_status=send_status
+                send_status=send_status,
             )
-            return [ProjectTaskDto.from_model(task).to_dict() for task in data.get('data',[])],data.get('total',0)
+            return [
+                ProjectTaskDto.from_model(task).to_dict()
+                for task in data.get("data", [])
+            ], data.get("total", 0)
         except Exception as e:
             self._logger.error(f"获取项目任务列表失败: {str(e)}")
             raise
 
-
     def get_task(self, task_id: int) -> Optional[ProjectTaskModel]:
         """获取单个项目任务
 
@@ -138,6 +154,7 @@ class ProjectTaskService:
         except Exception as e:
             self._logger.error(f"获取项目任务失败: {str(e)}")
             raise
+
     def get_task_dto(self, task_id: int):
         try:
             task = self.store.get_task_dto(task_id)
@@ -146,8 +163,8 @@ class ProjectTaskService:
             self._logger.error(f"获取项目任务失败: {str(e)}")
             raise
 
-    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files:list):
-        log_data=""
+    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files: list):
+        log_data = ""
         if task_id == 0:
             task = self.store.create_task(task_dto)
         else:
@@ -156,8 +173,11 @@ class ProjectTaskService:
                 raise Exception("项目任务提交处理过,不能再修改。")
             log_data = utils.to_str(task.to_dict())
             if task is None:
-                LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK,
-                                         f"修改任务失败 错误:任务[{task_id}]不存在")
+                LogRecordHelper.log_fail(
+                    OperationType.UPDATE,
+                    OperationModule.TASK,
+                    f"修改任务失败 错误:任务[{task_id}]不存在",
+                )
                 raise Exception("项目任务不存在")
             task_dto.id = task_id
             task = self.store.update_task(task_dto)
@@ -165,29 +185,53 @@ class ProjectTaskService:
             if task:
                 paths = self._process_file_upload(task, files)
                 if paths != task.file_path:
-                    task = self.store.update_task_files(task.id,paths)
+                    task = 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()))
+                    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()))
+                    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()))
+                    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)
+                    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()))
+            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()))
+            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) -> str:
@@ -196,10 +240,12 @@ class ProjectTaskService:
         self._logger.info(f"保存处理文件,项目ID:{task.project_id},任务ID:{task.id}")
         delete_old = task.process_status == 0
         if delete_old and task.file_path:
-            delete_paths=[]
-            for file_path in task.file_path.split(','):
+            delete_paths = []
+            for file_path in task.file_path.split(","):
                 if os.path.isfile(file_path):
-                    delete_dir = os.path.dirname(file_path).replace('upload_files/', 'delete_files/')
+                    delete_dir = os.path.dirname(file_path).replace(
+                        "upload_files/", "delete_files/"
+                    )
                     os.makedirs(delete_dir, exist_ok=True)
                     # 处理文件名冲突
                     base_name = os.path.basename(file_path)
@@ -216,27 +262,33 @@ class ProjectTaskService:
                     if not os.listdir(original_dir):
                         os.rmdir(original_dir)
             if len(delete_paths) > 0:
-                LogRecordHelper.log_success(OperationType.DELETE, OperationModule.TASK,
-                                        f"删除任务文件:{task.task_name}", utils.to_str(delete_paths))
+                LogRecordHelper.log_success(
+                    OperationType.DELETE,
+                    OperationModule.TASK,
+                    f"删除任务文件:{task.task_name}",
+                    utils.to_str(delete_paths),
+                )
 
         # file_paths = [] if delete_old or not task.file_path else task.file_path.split(',')
 
         file_paths = []
         if files and len(files) > 0:
-            task_dir = os.path.join(configs.app.source_path, f'upload_files/{task.get_path()}')
+            task_dir = os.path.join(
+                configs.app.source_path, f"upload_files/{task.get_path()}"
+            )
             os.makedirs(task_dir, exist_ok=True)
             for file in files:
                 if not file.filename:
                     continue
-                allowed_ext = {'xlsx', 'xls', 'csv'}
-                ext = file.filename.rsplit('.', 1)[-1].lower()
+                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_path = file_path.replace("\\", "/")
                 file.save(file_path)
                 file_paths.append(file_path)
-        return ','.join(file_paths)
+        return ",".join(file_paths)
 
     def _create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
         """创建项目任务
@@ -297,7 +349,6 @@ class ProjectTaskService:
             self._logger.error(f"删除项目任务失败: {str(e)}")
             raise
 
-
     def update_process_status(self, task_id: int, status: int, err: str = None) -> bool:
         """更新处理状态
 
@@ -332,7 +383,7 @@ class ProjectTaskService:
             self._logger.error(f"更新项目任务发送状态失败: {str(e)}")
             raise
 
-    def cancel_run_task(self, task_id:int):
+    def cancel_run_task(self, task_id: int):
         task = self.store.get_task_dto(task_id)
         if task:
             msg = executor.cancel_task(task)
@@ -341,5 +392,4 @@ class ProjectTaskService:
             else:
                 return msg
         else:
-            return '没有查询到任务'
-
+            return "没有查询到任务"

+ 109 - 67
SourceCode/IntelligentRailwayCosting/app/stores/budget.py

@@ -1,7 +1,7 @@
-from sqlalchemy.orm import  aliased
-from sqlalchemy import and_, or_,  func
+from sqlalchemy.orm import aliased
+from sqlalchemy import and_, or_, func
 
-from core.models import TotalBudgetInfoModel,TotalBudgetItemModel,ChapterModel
+from core.models import TotalBudgetInfoModel, TotalBudgetItemModel, ChapterModel
 from tools import db_helper
 
 
@@ -11,9 +11,8 @@ class BudgetStore:
         self._db_session = None
         pass
 
-
     def get_budget_info(self, project_id: str):
-        self._database=project_id
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
             budgets = db_session.query(TotalBudgetInfoModel).all()
             if budgets is None:
@@ -21,110 +20,148 @@ class BudgetStore:
             return budgets
 
     def get_budget_items(self, project_id: str, budget_id: int):
-        self._database=project_id
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
             if budget_id != 0:
                 budget_items = db_session.query(TotalBudgetItemModel).all()
             else:
-                budget_items = db_session.query(TotalBudgetItemModel).filter(TotalBudgetItemModel.budget_id == budget_id).all()
+                budget_items = (
+                    db_session.query(TotalBudgetItemModel)
+                    .filter(TotalBudgetItemModel.budget_id == budget_id)
+                    .all()
+                )
             if budget_items is None:
                 return None
             return budget_items
 
-    def get_budget_item_by_item_code(self, project_id: str, budget_id: int,item_code: str):
-        self._database=project_id
+    def get_budget_item_by_item_code(
+            self, project_id: str, budget_id: int, item_code: str
+    ):
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            budget = db_session.query(
-                TotalBudgetItemModel.budget_id,
-                TotalBudgetItemModel.item_id,
-                ChapterModel.item_code,
-                ChapterModel.chapter,
-                ChapterModel.section,
-                ChapterModel.project_name,
-                ChapterModel.item_type,
-                ChapterModel.unit,)\
-                .join(ChapterModel,ChapterModel.item_id == TotalBudgetItemModel.item_id)\
-                .filter(and_(TotalBudgetItemModel.budget_id == budget_id,ChapterModel.item_code == item_code))\
+            budget = (
+                db_session.query(
+                    TotalBudgetItemModel.budget_id,
+                    TotalBudgetItemModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .join(
+                    ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id
+                )
+                .filter(
+                    and_(
+                        TotalBudgetItemModel.budget_id == budget_id,
+                        ChapterModel.item_code == item_code,
+                    )
+                )
                 .first()
+            )
             if budget is None:
                 return None
             return budget
 
     def _build_children_count_subquery(self, model_class):
         # 创建父节点和子节点的别名
-        parent = aliased(model_class, name='parent')
-        child = aliased(model_class, name='child')
+        parent = aliased(model_class, name="parent")
+        child = aliased(model_class, name="child")
 
         # 子查询:计算每个节点的直接子节点数量
-        return self.db_session.query(
-            parent.item_code.label('parent_code'),
-            func.count(child.item_code).label('child_count')
-        ).outerjoin(
-            child,
-            or_(
-                # 匹配形如01-01的格式
-                child.item_code.like(parent.item_code + '-__'),
-                # 匹配形如0101的格式
-                child.item_code.like(parent.item_code + '__')
+        return (
+            self.db_session.query(
+                parent.item_code.label("parent_code"),
+                func.count(child.item_code).label("child_count"),
+            )
+            .outerjoin(
+                child,
+                or_(
+                    # 匹配形如01-01的格式
+                    child.item_code.like(parent.item_code + "-__"),
+                    # 匹配形如0101的格式
+                    child.item_code.like(parent.item_code + "__"),
+                ),
             )
-        ).group_by(parent.item_code).subquery()
+            .group_by(parent.item_code)
+            .subquery()
+        )
 
     def _build_budget_items_query(self, budget_id: int):
         # 子查询:计算每个节点的直接子节点数量
         children_count = self._build_children_count_subquery(ChapterModel)
 
-        return (self.db_session.query(
-            TotalBudgetItemModel.budget_id,
-            TotalBudgetItemModel.item_id,
-            ChapterModel.item_code,
-            ChapterModel.chapter,
-            ChapterModel.section,
-            ChapterModel.project_name,
-            ChapterModel.item_type,
-            ChapterModel.unit,
-            func.coalesce(children_count.c.child_count, 0).label('children_count')
-        )
-        .join(ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id)
-        .outerjoin(children_count, children_count.c.parent_code == ChapterModel.item_code)
-        .filter(TotalBudgetItemModel.budget_id == budget_id)
-        .distinct()
+        return (
+            self.db_session.query(
+                TotalBudgetItemModel.budget_id,
+                TotalBudgetItemModel.item_id,
+                ChapterModel.item_code,
+                ChapterModel.chapter,
+                ChapterModel.section,
+                ChapterModel.project_name,
+                ChapterModel.item_type,
+                ChapterModel.unit,
+                func.coalesce(children_count.c.child_count, 0).label("children_count"),
+            )
+            .join(ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id)
+            .outerjoin(
+                children_count, children_count.c.parent_code == ChapterModel.item_code
+            )
+            .filter(TotalBudgetItemModel.budget_id == budget_id)
+            .distinct()
         )
 
-    def get_top_budget_items(self, project_id: str, budget_id: int, item_code: list[str]=None):
-        self._database=project_id
+    def get_top_budget_items(
+            self, project_id: str, budget_id: int, item_code: list[str] = None
+    ):
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as self.db_session:
             query = self._build_budget_items_query(budget_id)
 
             if item_code:
                 query = query.filter(ChapterModel.item_code.in_(item_code))
             else:
-                query = query.filter(ChapterModel.item_code.like('__'))\
-                    .filter(ChapterModel.chapter.is_not(None))
+                query = query.filter(ChapterModel.item_code.like("__")).filter(
+                    ChapterModel.chapter.is_not(None)
+                )
             query = query.order_by(ChapterModel.item_code)
             items = query.all()
             return items
 
-    def get_child_budget_items(self, project_id: str, budget_id: int, parent_item_code: str):
+    def get_child_budget_items(
+            self, project_id: str, budget_id: int, parent_item_code: str
+    ):
         # 构建子节点的模式:支持两种格式
         # 1. 父级编号后跟-和两位数字(如:01-01)
         # 2. 父级编号直接跟两位数字(如:0101)
-        pattern_with_dash = f'{parent_item_code}-__'
-        pattern_without_dash = f'{parent_item_code}__'
+        pattern_with_dash = f"{parent_item_code}-__"
+        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_budget_items_query(budget_id)\
-                .filter(or_(ChapterModel.item_code.like(pattern_with_dash),
-                           ChapterModel.item_code.like(pattern_without_dash)))\
+            query = (
+                self._build_budget_items_query(budget_id)
+                .filter(
+                    or_(
+                        ChapterModel.item_code.like(pattern_with_dash),
+                        ChapterModel.item_code.like(pattern_without_dash),
+                    )
+                )
                 .order_by(ChapterModel.item_code)
+            )
             items = query.all()
             return items
 
-    def get_all_budget_items_not_children(self, project_id: str, budget_id: int, item_code: str):
+    def get_all_budget_items_not_children(
+            self, project_id: str, budget_id: int, item_code: str
+    ):
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as self.db_session:
             # 添加叶子节点过滤条件,使用复用的子查询方法
             children_count = self._build_children_count_subquery(ChapterModel)
-            query =  (self.db_session.query(
+            query = (
+                self.db_session.query(
                     TotalBudgetItemModel.budget_id,
                     TotalBudgetItemModel.item_id,
                     ChapterModel.item_code,
@@ -133,21 +170,26 @@ class BudgetStore:
                     ChapterModel.project_name,
                     ChapterModel.item_type,
                     ChapterModel.unit,
-                    func.coalesce(children_count.c.child_count, 0).label('children_count')
+                    func.coalesce(children_count.c.child_count, 0).label(
+                        "children_count"
+                    ),
+                )
+                .join(
+                    ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id
+                )
+                .outerjoin(
+                    children_count,
+                    children_count.c.parent_code == ChapterModel.item_code,
                 )
-                .join(ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id)
-                .outerjoin(children_count, children_count.c.parent_code == ChapterModel.item_code)
                 .filter(TotalBudgetItemModel.budget_id == budget_id)
                 .distinct()
-                )
+            )
 
             query = query.filter(func.coalesce(children_count.c.child_count, 0) == 0)
             # 如果指定了item_code,添加前缀匹配条件
             if item_code:
-                pattern_with_dash = f'{item_code}-%'
+                pattern_with_dash = f"{item_code}-%"
                 query = query.filter(ChapterModel.item_code.like(pattern_with_dash))
             query = query.order_by(ChapterModel.item_code)
             items = query.all()
             return items
-
-

+ 92 - 66
SourceCode/IntelligentRailwayCosting/app/stores/chapter.py

@@ -1,80 +1,91 @@
-from sqlalchemy.orm import  aliased
-from sqlalchemy import or_,  func
+from sqlalchemy.orm import aliased
+from sqlalchemy import or_, func
 
 from core.models import ChapterModel
 from tools import db_helper
 
+
 class ChapterStore:
     def __init__(self):
         self._database = None
         self._db_session = None
 
-
     def get_chapter_item_by_item_code(self, project_id: str, item_code: str):
-        self._database=project_id
+        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_code == item_code)\
+            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_code == item_code)
                 .first()
+            )
             if chapter is None:
                 return None
             return chapter
 
     def _build_children_count_subquery(self, model_class):
         # 创建父节点和子节点的别名
-        parent = aliased(model_class, name='parent')
-        child = aliased(model_class, name='child')
+        parent = aliased(model_class, name="parent")
+        child = aliased(model_class, name="child")
 
         # 子查询:计算每个节点的直接子节点数量
-        return self.db_session.query(
-            parent.item_code.label('parent_code'),
-            func.count(child.item_code).label('child_count')
-        ).outerjoin(
-            child,
-            or_(
-                # 匹配形如01-01的格式
-                child.item_code.like(parent.item_code + '-__'),
-                # 匹配形如0101的格式
-                child.item_code.like(parent.item_code + '__')
+        return (
+            self.db_session.query(
+                parent.item_code.label("parent_code"),
+                func.count(child.item_code).label("child_count"),
             )
-        ).group_by(parent.item_code).subquery()
+            .outerjoin(
+                child,
+                or_(
+                    # 匹配形如01-01的格式
+                    child.item_code.like(parent.item_code + "-__"),
+                    # 匹配形如0101的格式
+                    child.item_code.like(parent.item_code + "__"),
+                ),
+            )
+            .group_by(parent.item_code)
+            .subquery()
+        )
 
     def _build_chapter_items_query(self):
         # 子查询:计算每个节点的直接子节点数量
         children_count = self._build_children_count_subquery(ChapterModel)
 
-        return (self.db_session.query(
-            ChapterModel.item_id,
-            ChapterModel.item_code,
-            ChapterModel.chapter,
-            ChapterModel.section,
-            ChapterModel.project_name,
-            ChapterModel.item_type,
-            ChapterModel.unit,
-            func.coalesce(children_count.c.child_count, 0).label('children_count')
-        )
-        .outerjoin(children_count, children_count.c.parent_code == ChapterModel.item_code)
-        .distinct()
+        return (
+            self.db_session.query(
+                ChapterModel.item_id,
+                ChapterModel.item_code,
+                ChapterModel.chapter,
+                ChapterModel.section,
+                ChapterModel.project_name,
+                ChapterModel.item_type,
+                ChapterModel.unit,
+                func.coalesce(children_count.c.child_count, 0).label("children_count"),
+            )
+            .outerjoin(
+                children_count, children_count.c.parent_code == ChapterModel.item_code
+            )
+            .distinct()
         )
 
-
-    def get_top_chapter_items(self, project_id: str, item_code: list[str]=None):
-        self._database=project_id
+    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:
             query = self._build_chapter_items_query()
 
             if item_code:
                 query = query.filter(ChapterModel.item_code.in_(item_code))
             else:
-                query = query.filter(ChapterModel.item_code.like('__'))\
-                    .filter(ChapterModel.chapter.is_not(None))
+                query = query.filter(ChapterModel.item_code.like("__")).filter(
+                    ChapterModel.chapter.is_not(None)
+                )
             query = query.order_by(ChapterModel.item_code)
             items = query.all()
             return items
@@ -83,52 +94,63 @@ class ChapterStore:
         # 构建子节点的模式:支持两种格式
         # 1. 父级编号后跟-和两位数字(如:01-01)
         # 2. 父级编号直接跟两位数字(如:0101)
-        pattern_with_dash = f'{parent_item_code}-__'
-        pattern_without_dash = f'{parent_item_code}__'
+        pattern_with_dash = f"{parent_item_code}-__"
+        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()\
-                .filter(or_(ChapterModel.item_code.like(pattern_with_dash),
-                           ChapterModel.item_code.like(pattern_without_dash)))\
+            query = (
+                self._build_chapter_items_query()
+                .filter(
+                    or_(
+                        ChapterModel.item_code.like(pattern_with_dash),
+                        ChapterModel.item_code.like(pattern_without_dash),
+                    )
+                )
                 .order_by(ChapterModel.item_code)
+            )
             items = query.all()
             return items
 
     def get_all_children_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._build_chapter_items_query() \
-                .filter(ChapterModel.item_code.like(f'{item_code}%')) \
+            query = (
+                self._build_chapter_items_query()
+                .filter(ChapterModel.item_code.like(f"{item_code}%"))
                 .order_by(ChapterModel.item_code)
+            )
             items = query.all()
             return items
+
     def get_all_parents_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:
             # 获取所有可能的父节点编码
             parent_codes = []
             current_code = item_code
-            
+
             # 将带连字符的编码转换为纯数字格式
-            pure_code = current_code.replace('-', '')
-            
+            pure_code = current_code.replace("-", "")
+
             # 每两位截取一次获得父节点编码
             for i in range(2, len(pure_code), 2):
                 code = pure_code[:i]
                 # 如果原始编码包含连字符,则将纯数字格式转回带连字符格式
-                if '-' in current_code:
-                    parts = [code[j:j+2] for j in range(0, len(code), 2)]
-                    parent_codes.append('-'.join(parts))
+                if "-" in current_code:
+                    parts = [code[j: j + 2] for j in range(0, len(code), 2)]
+                    parent_codes.append("-".join(parts))
                 else:
                     parent_codes.append(code)
-            
+
             if not parent_codes:
                 return []
             print(parent_codes)
             # 查询所有父节点
-            query = self._build_chapter_items_query() \
-                .filter(ChapterModel.item_code.in_(parent_codes)) \
+            query = (
+                self._build_chapter_items_query()
+                .filter(ChapterModel.item_code.in_(parent_codes))
                 .order_by(ChapterModel.item_code)
+            )
             items = query.all()
             return items
 
@@ -137,7 +159,8 @@ class ChapterStore:
         with db_helper.sqlserver_query_session(self._database) as self.db_session:
             # 添加叶子节点过滤条件,使用复用的子查询方法
             children_count = self._build_children_count_subquery(ChapterModel)
-            query =  (self.db_session.query(
+            query = (
+                self.db_session.query(
                     ChapterModel.item_id,
                     ChapterModel.item_code,
                     ChapterModel.chapter,
@@ -145,19 +168,22 @@ class ChapterStore:
                     ChapterModel.project_name,
                     ChapterModel.item_type,
                     ChapterModel.unit,
-                    func.coalesce(children_count.c.child_count, 0).label('children_count')
+                    func.coalesce(children_count.c.child_count, 0).label(
+                        "children_count"
+                    ),
                 )
-                .outerjoin(children_count, children_count.c.parent_code == ChapterModel.item_code)
-                .distinct()
+                .outerjoin(
+                    children_count,
+                    children_count.c.parent_code == ChapterModel.item_code,
                 )
+                .distinct()
+            )
 
             query = query.filter(func.coalesce(children_count.c.child_count, 0) == 0)
             # 如果指定了item_code,添加前缀匹配条件
             if item_code:
-                pattern_with_dash = f'{item_code}-%'
+                pattern_with_dash = f"{item_code}-%"
                 query = query.filter(ChapterModel.item_code.like(pattern_with_dash))
             query = query.order_by(ChapterModel.item_code)
             items = query.all()
             return items
-
-

+ 65 - 38
SourceCode/IntelligentRailwayCosting/app/stores/project.py

@@ -3,27 +3,33 @@ from datetime import datetime
 from typing import Optional
 
 import tools.db_helper as db_helper
+from core import configs
 from core.dtos import ProjectDto
 from core.models.project import ProjectModel
 from core.models.team import TeamModel
 from core.user_session import UserSession
 
+
 class ProjectStore:
     def __init__(self):
-        self._database= None
+        self._database = (
+            f"sqlserver_mian_{configs.app.version}"
+            if configs.app.use_version
+            else "sqlserver_mian"
+        )
 
     def get_user_projects_paginated(
-        self,
-        page: int = 1,
-        page_size: int = 10,
-        keyword: Optional[str] = None,
-        start_time: Optional[datetime] = None,
-        end_time: Optional[datetime] = None,
-        can_edit:Optional[int]=0,
+            self,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            start_time: Optional[datetime] = None,
+            end_time: Optional[datetime] = None,
+            can_edit: Optional[int] = 0,
     ):
         """
         分页查询用户有权限的项目列表
-        
+
         Args:
             page: 页码,从1开始
             page_size: 每页数量
@@ -31,14 +37,14 @@ class ProjectStore:
             start_time: 开始时间
             end_time: 结束时间
             can_edit: 是否只显示可编辑的项目
-            
+
         Returns:
             Tuple[total_count, projects]
         """
 
         # 构建基础查询
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            query = (db_session.query(
+            query = db_session.query(
                 ProjectModel.project_id,
                 ProjectModel.project_name,
                 ProjectModel.project_manager,
@@ -49,28 +55,41 @@ class ProjectStore:
                 ProjectModel.project_type,
                 ProjectModel.unit,
                 ProjectModel.create_time,
-            ) .distinct())
+            ).distinct()
             user = UserSession.get_current_user()
             if not user.is_admin:
-                query = query.outerjoin(TeamModel, ProjectModel.project_id == TeamModel.project_id)
+                query = query.outerjoin(
+                    TeamModel, ProjectModel.project_id == TeamModel.project_id
+                )
                 if can_edit:
                     query = query.filter(
-                        or_(ProjectModel.project_manager == user.username,
-                            and_(TeamModel.name == user.username, TeamModel.compilation_status == can_edit)
-                            )
+                        or_(
+                            ProjectModel.project_manager == user.username,
+                            and_(
+                                TeamModel.name == user.username,
+                                TeamModel.compilation_status == can_edit,
+                            ),
                         )
+                    )
                 else:
-                    query = query.filter(or_(ProjectModel.project_manager == user.username,TeamModel.name == user.username))
+                    query = query.filter(
+                        or_(
+                            ProjectModel.project_manager == user.username,
+                            TeamModel.name == user.username,
+                        )
+                    )
 
             # 添加过滤条件
             if keyword:
-                query = query.filter(or_(
-                    ProjectModel.project_id.like(f'%{keyword}%'),
-                    ProjectModel.project_name.like(f'%{keyword}%'),
-                    ProjectModel.project_manager.like(f'%{keyword}%'),
-                    # ProjectModel.project_description.like(f'%{keyword}%'),
-                    ProjectModel.short_name.like(f'%{keyword}%')
-                ))
+                query = query.filter(
+                    or_(
+                        ProjectModel.project_id.like(f"%{keyword}%"),
+                        ProjectModel.project_name.like(f"%{keyword}%"),
+                        ProjectModel.project_manager.like(f"%{keyword}%"),
+                        # ProjectModel.project_description.like(f'%{keyword}%'),
+                        ProjectModel.short_name.like(f"%{keyword}%"),
+                    )
+                )
 
             if start_time:
                 query = query.filter(ProjectModel.create_time >= start_time)
@@ -80,27 +99,35 @@ class ProjectStore:
 
             # 获取总记录数和数据
             total_count = query.count()
-            projects = query.order_by(ProjectModel.create_time.desc())\
-                .offset((page - 1) * page_size)\
-                .limit(page_size)\
+            projects = (
+                query.order_by(ProjectModel.create_time.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
                 .all()
+            )
 
-            return {
-                'total': total_count,
-                'data': projects
-            }
-
+            return {"total": total_count, "data": projects}
 
-    def get_team_project_item_code(self,project_id:str, user_name:str):
+    def get_team_project_item_code(self, project_id: str, user_name: str):
         with db_helper.sqlserver_query_session(self._database) as session:
             db_session = session
-            data = db_session.query(TeamModel.item_code).filter(and_(TeamModel.project_id == project_id,TeamModel.name == user_name)).first()
+            data = (
+                db_session.query(TeamModel.item_code)
+                .filter(
+                    and_(
+                        TeamModel.project_id == project_id, TeamModel.name == user_name
+                    )
+                )
+                .first()
+            )
             return data[0] if data else None
 
-
-    def get(self,project_id:str):
+    def get(self, project_id: str):
         with db_helper.sqlserver_query_session(self._database) as session:
             db_session = session
-            data = db_session.query(ProjectModel).filter(ProjectModel.project_id == project_id).first()
+            data = (
+                db_session.query(ProjectModel)
+                .filter(ProjectModel.project_id == project_id)
+                .first()
+            )
             return ProjectDto.from_model(data)
-

+ 41 - 34
SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py

@@ -7,6 +7,7 @@ from core.dtos import QuotaInputDto
 from core.models import QuotaInputModel
 from core.user_session import UserSession
 
+
 class QuotaInputStore:
     def __init__(self):
         self._database = None
@@ -19,13 +20,14 @@ 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,
+    ):
         """分页查询定额输入列表
 
         Args:
@@ -45,14 +47,14 @@ class QuotaInputStore:
             # 构建查询条件
             conditions = [
                 QuotaInputModel.budget_id == budget_id,
-                QuotaInputModel.item_id == item_id
+                QuotaInputModel.item_id == item_id,
             ]
 
             if keyword:
                 conditions.append(
                     or_(
-                        QuotaInputModel.quota_code.like(f'%{keyword}%'),
-                        QuotaInputModel.project_name.like(f'%{keyword}%')
+                        QuotaInputModel.quota_code.like(f"%{keyword}%"),
+                        QuotaInputModel.project_name.like(f"%{keyword}%"),
                     )
                 )
 
@@ -67,24 +69,25 @@ class QuotaInputStore:
             # 转换为DTO
             quotas = [QuotaInputDto.from_model(model) for model in query.all()]
 
-            return {
-                'total': total_count,
-                'data': quotas
-            }
+            return {"total": total_count, "data": quotas}
 
-    def get_quota(self, project_id:str,budget_id: int, item_id: int, quota_code: str):
+    def get_quota(self, project_id: str, budget_id: int, item_id: int, quota_code: str):
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            model = db_session.query(QuotaInputModel).filter(
-                QuotaInputModel.budget_id == budget_id,
-                QuotaInputModel.item_id == item_id,
-                QuotaInputModel.quota_code == quota_code
-            ).first()
+            model = (
+                db_session.query(QuotaInputModel)
+                .filter(
+                    QuotaInputModel.budget_id == budget_id,
+                    QuotaInputModel.item_id == item_id,
+                    QuotaInputModel.quota_code == quota_code,
+                )
+                .first()
+            )
             if model is None:
                 return None
             return QuotaInputDto.from_model(model)
 
-    def create_quota(self, project_id:str, dto: QuotaInputDto) -> QuotaInputDto:
+    def create_quota(self, project_id: str, dto: QuotaInputDto) -> QuotaInputDto:
         """创建定额输入
 
         Args:
@@ -106,9 +109,7 @@ class QuotaInputStore:
                 project_quantity=dto.project_quantity,
                 # project_quantity_input=dto.project_quantity_input,
                 # quota_adjustment=dto.quota_adjustment,
-                unit_price=dto.unit_price,
                 # compilation_unit_price=dto.compilation_unit_price,
-                total_price=dto.total_price,
                 # compilation_total_price=dto.compilation_total_price,
                 # unit_weight=dto.unit_weight,
                 # total_weight=dto.total_weight,
@@ -133,7 +134,7 @@ class QuotaInputStore:
                 # mechanical_workday_salary=dto.mechanical_workday_salary,
                 # compilation_mechanical_workday_salary=dto.compilation_mechanical_workday_salary,
                 compiler=dto.compiler,
-                modify_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+                modify_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                 # quota_consumption=dto.quota_consumption,
                 # basic_quota=dto.basic_quota,
                 # quota_comprehensive_unit_price=dto.quota_comprehensive_unit_price,
@@ -145,7 +146,9 @@ class QuotaInputStore:
 
             return QuotaInputDto.from_model(model)
 
-    def update_quota(self, project_id:str, dto: QuotaInputDto) -> Optional[QuotaInputDto]:
+    def update_quota(
+            self, project_id: str, dto: QuotaInputDto
+    ) -> Optional[QuotaInputDto]:
         """更新定额输入
 
         Args:
@@ -157,9 +160,11 @@ class QuotaInputStore:
         """
         self._database = project_id
         with db_helper.sqlserver_session(self._database) as db_session:
-            model = db_session.query(QuotaInputModel).filter(
-                QuotaInputModel.quota_id == dto.quota_id
-            ).first()
+            model = (
+                db_session.query(QuotaInputModel)
+                .filter(QuotaInputModel.quota_id == dto.quota_id)
+                .first()
+            )
 
             if model is None:
                 return None
@@ -167,7 +172,7 @@ class QuotaInputStore:
             model.item_id = dto.item_id
             model.quota_code = dto.quota_code
             # model.sequence_number = dto.sequence_number
-            model.entry_name = dto.project_name
+            model.project_name = dto.project_name
             model.unit = dto.unit
             model.project_quantity = dto.project_quantity
             # model.project_quantity_input = dto.project_quantity_input
@@ -205,7 +210,7 @@ class QuotaInputStore:
             # model.quota_comprehensive_unit_price = dto.quota_comprehensive_unit_price
             # model.quota_comprehensive_total_price = dto.quota_comprehensive_total_price
 
-            db_session.flush()
+            db_session.merge(model)
 
             return QuotaInputDto.from_model(model)
 
@@ -219,12 +224,14 @@ class QuotaInputStore:
             bool
         """
         with db_helper.sqlserver_session(self._database) as db_session:
-            model = db_session.query(QuotaInputModel).filter(
-                QuotaInputModel.quota_id == quota_id
-            ).first()
+            model = (
+                db_session.query(QuotaInputModel)
+                .filter(QuotaInputModel.quota_id == quota_id)
+                .first()
+            )
 
             if model is None:
                 return False
 
             db_session.delete(model)
-            return True
+            return True

+ 14 - 0
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_task.py

@@ -119,6 +119,20 @@ class ProjectTaskStore:
             query.order_by(ProjectTaskModel.task_sort.desc())
             tasks = query.all()
             return [ProjectTaskDto.from_model(task) for task in tasks]
+
+
+    def get_tasks_by_status(self, status: int):
+        """查询指定状态的任务"""
+        with db_helper.mysql_query_session(self._database) as db_session:
+            query = db_session.query(ProjectTaskModel)
+            query = query.filter(
+                and_(
+                    ProjectTaskModel.is_del == 0,
+                    ProjectTaskModel.process_status == status))
+            query.order_by(ProjectTaskModel.task_sort.desc())
+            tasks = query.all()
+            return [ProjectTaskDto.from_model(task) for task in tasks]
+
     def create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
         """创建任务
 

+ 82 - 54
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py

@@ -8,10 +8,11 @@ from core.enum import SendStatusEnum
 from core.models import ProjectQuotaModel
 from core.user_session import UserSession
 
+
 class ProjectQuotaStore:
     def __init__(self):
         # self._database= None
-        self._database = 'Iwb_RailwayCosting'
+        self._database = "Iwb_RailwayCosting"
         self._current_user = None
 
     @property
@@ -21,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,
     ):
         """分页查询定额列表
 
@@ -51,32 +52,45 @@ class ProjectQuotaStore:
                 ProjectQuotaModel.is_del == 0,
                 ProjectQuotaModel.project_id == project_id,
                 ProjectQuotaModel.budget_id == budget_id,
-                ProjectQuotaModel.item_code.like(f"{item_code}%")
+                ProjectQuotaModel.item_code.like(f"{item_code}%"),
             ]
 
             if send_status is not None:
                 conditions.append(ProjectQuotaModel.send_status == send_status)
             if keyword:
-                conditions.append(or_(
-                    ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
-                    ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
-                ))
+                conditions.append(
+                    or_(
+                        ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
+                        ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
+                    )
+                )
             query = query.filter(and_(*conditions))
 
             # 计算总数
             total_count = query.count()
 
             # 分页
-            query = query.order_by(ProjectQuotaModel.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
+            query = (
+                query.order_by(ProjectQuotaModel.created_at.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+            )
 
             quotas = query.all()
 
-            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):
+            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,
+    ):
         with db_helper.sqlserver_query_session(self._database) as db_session:
             query = db_session.query(ProjectQuotaModel).filter(
                 and_(
@@ -84,46 +98,49 @@ class ProjectQuotaStore:
                     ProjectQuotaModel.budget_id == budget_id,
                     ProjectQuotaModel.project_id == project_id,
                     ProjectQuotaModel.item_code.like(f"{item_code}%"),
-                    ProjectQuotaModel.is_del == 0
+                    ProjectQuotaModel.is_del == 0,
                 )
             )
             if keyword:
-                query = query.filter(or_(
-                    ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
-                    ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
-                ))
+                query = query.filter(
+                    or_(
+                        ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
+                        ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
+                    )
+                )
             if send_status is not None:
                 query = query.filter(ProjectQuotaModel.send_status == send_status)
             # 计算总数
             total_count = query.count()
 
             # 分页
-            query = query.order_by(ProjectQuotaModel.created_at.desc()).offset((page - 1) * page_size).limit(
-                page_size)
+            query = (
+                query.order_by(ProjectQuotaModel.created_at.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+            )
 
             quotas = query.all()
 
-            return {
-                'total': total_count,
-                'data': quotas
-            }
-
+            return {"total": total_count, "data": quotas}
 
-    def get_quotas_by_task_id(self,task_id:int, with_quota_code:bool=False):
+    def get_quotas_by_task_id(self, task_id: int, with_quota_code: bool = False):
         with db_helper.sqlserver_query_session(self._database) as db_session:
             query = db_session.query(ProjectQuotaModel).filter(
                 and_(
-                    ProjectQuotaModel.task_id == task_id,
-                    ProjectQuotaModel.is_del == 0
+                    ProjectQuotaModel.task_id == task_id, ProjectQuotaModel.is_del == 0
                 )
             )
             if with_quota_code:
-                query = query.filter(and_(ProjectQuotaModel.quota_code!=None,ProjectQuotaModel.quota_code!='') )
+                query = query.filter(
+                    and_(
+                        ProjectQuotaModel.quota_code != None,
+                        ProjectQuotaModel.quota_code != "",
+                    )
+                )
             quotas = query.all()
             return quotas
 
-
-
     def get_quota(self, quota_id: int) -> Optional[ProjectQuotaModel]:
         """根据ID查询定额
 
@@ -134,13 +151,17 @@ class ProjectQuotaStore:
             Optional[ProjectQuotaDto]
         """
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            quota = db_session.query(ProjectQuotaModel).filter(
-                and_(
-                    ProjectQuotaModel.id == quota_id,
-                    ProjectQuotaModel.is_del == 0
+            quota = (
+                db_session.query(ProjectQuotaModel)
+                .filter(
+                    and_(
+                        ProjectQuotaModel.id == quota_id, ProjectQuotaModel.is_del == 0
+                    )
                 )
-            ).first()
+                .first()
+            )
             return quota
+
     def get_quota_dto(self, quota_id: int) -> Optional[ProjectQuotaDto]:
         """根据ID查询定额
 
@@ -154,16 +175,22 @@ 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):
+    def get_quota_by_quota_input(
+            self, project_id: str, budget_id: int, quota_input_id: int
+    ):
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            quota = db_session.query(ProjectQuotaModel).filter(
-                and_(
-                    ProjectQuotaModel.project_id == project_id,
-                    ProjectQuotaModel.budget_id == budget_id,
-                    ProjectQuotaModel.quota_id == quota_input_id,
-                    ProjectQuotaModel.is_del == 0
+            quota = (
+                db_session.query(ProjectQuotaModel)
+                .filter(
+                    and_(
+                        ProjectQuotaModel.project_id == project_id,
+                        ProjectQuotaModel.budget_id == budget_id,
+                        ProjectQuotaModel.quota_id == quota_input_id,
+                        ProjectQuotaModel.is_del == 0,
+                    )
                 )
-            ).first()
+                .first()
+            )
             return ProjectQuotaDto.from_model(quota) if quota else None
 
     def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
@@ -182,6 +209,7 @@ class ProjectQuotaStore:
                 task_id=quota_dto.task_id,
                 item_id=quota_dto.item_id,
                 item_code=quota_dto.item_code,
+                quota_id=quota_dto.quota_id,
                 quota_code=quota_dto.quota_code,
                 entry_name=quota_dto.entry_name,
                 units=quota_dto.units,
@@ -255,7 +283,7 @@ class ProjectQuotaStore:
             db_session.merge(quota)
             return True
 
-    def update_send_status(self,quota_id:int, status:int, err:str = None) -> bool:
+    def update_send_status(self, quota_id: int, status: int, err: str = None) -> bool:
         """
         更新发送状态
         Args:
@@ -293,4 +321,4 @@ class ProjectQuotaStore:
             quota.updated_by = self.current_user.username
             quota.updated_at = datetime.now()
             db_session.merge(quota)
-            return True
+            return True

+ 14 - 0
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py

@@ -119,6 +119,20 @@ class ProjectTaskStore:
             query.order_by(ProjectTaskModel.task_sort.desc())
             tasks = query.all()
             return [ProjectTaskDto.from_model(task) for task in tasks]
+
+    def get_tasks_by_status(self, status: int):
+        """查询指定状态的任务"""
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            query = db_session.query(ProjectTaskModel)
+            query = query.filter(
+                and_(
+                    ProjectTaskModel.is_del == 0,
+                    ProjectTaskModel.process_status == status))
+            query.order_by(ProjectTaskModel.task_sort.desc())
+            tasks = query.all()
+            return [ProjectTaskDto.from_model(task) for task in tasks]
+
+
     def create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
         """创建任务
 

+ 14 - 5
SourceCode/IntelligentRailwayCosting/app/stores/user.py

@@ -1,11 +1,17 @@
 from typing import Optional, List
 
-import tools.db_helper  as db_helper
+import tools.db_helper as db_helper
+from core import configs
 from core.models import UserModel
 
+
 class UserStore:
     def __init__(self):
-        self._database = None
+        self._database = (
+            f"sqlserver_mian_{configs.app.version}"
+            if configs.app.use_version
+            else "sqlserver_mian"
+        )
 
     def get_user_by_id(self, user_id: int) -> Optional[UserModel]:
         """根据用户ID获取用户信息"""
@@ -16,7 +22,11 @@ class UserStore:
     def get_user_by_username(self, username: str) -> Optional[UserModel]:
         """根据用户名获取用户信息"""
         with db_helper.sqlserver_query_session(self._database) as db_session:
-            user = db_session.query(UserModel).filter(UserModel.username == username).first()
+            user = (
+                db_session.query(UserModel)
+                .filter(UserModel.username == username)
+                .first()
+            )
             return user
 
     def get_all_users(self) -> List[UserModel]:
@@ -24,10 +34,9 @@ class UserStore:
         with db_helper.sqlserver_query_session(self._database) as db_session:
             return db_session.query(UserModel)
 
-
     def authenticate_user(self, username: str, password: str) -> Optional[UserModel]:
         """用户认证"""
         user = self.get_user_by_username(username)
         if user and user.password == password:
             return user
-        return None
+        return None

+ 27 - 21
SourceCode/IntelligentRailwayCosting/app/tools/db_helper/__init__.py

@@ -5,6 +5,7 @@ from typing import Generator, Optional, Dict, Any
 from .mysql_helper import MySQLHelper
 from .sqlserver_helper import SQLServerHelper
 
+
 # def get_sqlServer_main_db():
 #     return SQLServerHelper().main_database_name
 
@@ -17,14 +18,15 @@ from .sqlserver_helper import SQLServerHelper
 # def create_mysql_session(database:str=None)->Session:
 #     return MySQLHelper().get_session_maker(database)()
 
+
 @contextmanager
-def sqlserver_session(database: str=None, config: Optional[Dict[str, Any]]=None):
+def sqlserver_session(database: str = None, config: Optional[Dict[str, Any]] = None):
     """SQLServer数据库会话的上下文管理器
-    
+
     Args:
         database: 数据库名称
         config: 数据库配置信息
-        
+
     Yields:
         数据库会话
     """
@@ -38,14 +40,15 @@ def sqlserver_session(database: str=None, config: Optional[Dict[str, Any]]=None)
     finally:
         session.close()
 
+
 @contextmanager
-def mysql_session(database: str=None, config: Optional[Dict[str, Any]]=None):
+def mysql_session(database: str = None, config: Optional[Dict[str, Any]] = None):
     """MySQL数据库会话的上下文管理器
-    
+
     Args:
         database: 数据库名称
         config: 数据库配置信息
-        
+
     Yields:
         数据库会话
     """
@@ -59,31 +62,35 @@ def mysql_session(database: str=None, config: Optional[Dict[str, Any]]=None):
     finally:
         session.close()
 
+
 @contextmanager
-def sqlserver_query_session(database: str=None, config: Optional[Dict[str, Any]]=None):
+def sqlserver_query_session(
+        database: str = None, config: Optional[Dict[str, Any]] = None
+):
     """SQLServer数据库会话的上下文管理器(只读查询专用)
-    
+
     Args:
         database: 数据库名称
         config: 数据库配置信息
-        
+
     Yields:
         数据库会话
     """
-    session = SQLServerHelper().get_session_maker(database, config)()
+    session = SQLServerHelper().get_session_maker(database, config, True)()
     try:
         yield session
     finally:
         session.close()
 
+
 @contextmanager
-def mysql_query_session(database: str=None, config: Optional[Dict[str, Any]]=None):
+def mysql_query_session(database: str = None, config: Optional[Dict[str, Any]] = None):
     """MySQL数据库会话的上下文管理器(只读查询专用)
-    
+
     Args:
         database: 数据库名称
         config: 数据库配置信息
-        
+
     Yields:
         数据库会话
     """
@@ -93,15 +100,14 @@ def mysql_query_session(database: str=None, config: Optional[Dict[str, Any]]=Non
     finally:
         session.close()
 
+
 __all__ = [
-    'MySQLHelper',
-    'SQLServerHelper',
+    "MySQLHelper",
+    "SQLServerHelper",
     # 'create_sqlServer_session',
     # 'create_mysql_session',
-    'sqlserver_session',
-    'mysql_session',
-    'sqlserver_query_session',
-    'mysql_query_session'
+    "sqlserver_session",
+    "mysql_session",
+    "sqlserver_query_session",
+    "mysql_query_session",
 ]
-
-

+ 72 - 54
SourceCode/IntelligentRailwayCosting/app/tools/db_helper/base.py

@@ -7,161 +7,179 @@ from sqlalchemy.orm import sessionmaker, declarative_base
 # 创建基础模型类
 Base = declarative_base()
 
+
 class DBHelper:
     _instance = None
     _lock = threading.Lock()
     main_database_name = ""
+
     def __new__(cls, *args, **kwargs):
         with cls._lock:
             if cls._instance is None:
                 cls._instance = super(DBHelper, cls).__new__(cls)
                 cls._instance._initialized = False
             return cls._instance
-    
+
     def __init__(self):
         if self._initialized:
             return
-            
+
         self._config_cache: Dict[str, Dict[str, str]] = {}
         self._engines: Dict[str, Any] = {}
         self._sessions: Dict[str, sessionmaker] = {}
         self._default_config = {
-            'pool_size': 5,
-            'max_overflow': 10,
-            'pool_timeout': 30,
-            'pool_recycle': 3600
+            "pool_size": 5,
+            "max_overflow": 10,
+            "pool_timeout": 30,
+            "pool_recycle": 3600,
         }
         self._initialized = True
-        
+
     def set_default_config(self, config: Dict[str, str]) -> None:
         """设置默认连接配置"""
         self._default_config.update(config)
-    
+
     def get_config_for_database(self, database: str) -> Dict[str, str]:
         """获取数据库的连接配置
-        
+
         按照以下顺序查找配置:
         1. 配置缓存
         2. 配置文件中特定数据库的配置
         3. 配置文件中的main_config配置
         4. 默认配置
-        
+
         Args:
             database: 数据库名称
-            
+
         Returns:
             数据库连接配置
-            
+
         Raises:
             Exception: 当找不到特定数据库配置且main_config配置也不存在时
         """
-        if database in self._config_cache:
-            return self._config_cache[database]
-        
         db_config = configs.database[database]
         if db_config:
             self._config_cache[database] = db_config
             return db_config
-        
+
         main_config = configs.database[self.main_database_name]
         if not main_config:
             raise Exception(f"未找到数据库 {database} 的配置,且main_config配置不存在")
-        main_config['database'] = database
-        main_config['db'] = database
-        self._config_cache[database] = main_config
-        return main_config
-    
-    def execute_query(self, database: str, query: str, params: Optional[Any] = None) -> List[Tuple]:
+        db_config = main_config.copy()
+        db_config["database"] = database
+        db_config["db"] = database
+        self._config_cache[database] = db_config
+        return db_config
+
+    def execute_query(
+            self, database: str, query: str, params: Optional[Any] = None
+    ) -> List[Tuple]:
         """执行查询并返回结果
-        
+
         Args:
             database: 数据库名称
             query: SQL查询语句
             params: 查询参数
-            
+
         Returns:
             查询结果列表
         """
         raise NotImplementedError("子类必须实现execute_query方法")
-    
-    def execute_non_query(self, database: str, query: str, params: Optional[Any] = None) -> int:
+
+    def execute_non_query(
+            self, database: str, query: str, params: Optional[Any] = None
+    ) -> int:
         """执行非查询操作(如INSERT, UPDATE, DELETE)
-        
+
         Args:
             database: 数据库名称
             query: SQL语句
             params: 查询参数
-            
+
         Returns:
             受影响的行数
         """
         raise NotImplementedError("子类必须实现execute_non_query方法")
-    
-    def execute_scalar(self, database: str, query: str, params: Optional[Any] = None) -> Any:
+
+    def execute_scalar(
+            self, database: str, query: str, params: Optional[Any] = None
+    ) -> Any:
         """执行查询并返回第一行第一列的值
-        
+
         Args:
             database: 数据库名称
             query: SQL查询语句
             params: 查询参数
-            
+
         Returns:
             查询结果的第一行第一列的值
         """
         raise NotImplementedError("子类必须实现execute_scalar方法")
-    
-    def execute_procedure(self, database: str, procedure_name: str, params: Optional[Dict[str, Any]] = None) -> List[Tuple]:
+
+    def execute_procedure(
+            self,
+            database: str,
+            procedure_name: str,
+            params: Optional[Dict[str, Any]] = None,
+    ) -> List[Tuple]:
         """执行存储过程
-        
+
         Args:
             database: 数据库名称
             procedure_name: 存储过程名称
             params: 存储过程参数
-            
+
         Returns:
             存储过程执行结果
         """
         raise NotImplementedError("子类必须实现execute_procedure方法")
-    
+
     def get_engine(self, database: str, config: Optional[Dict[str, Any]] = None) -> Any:
         """获取或创建数据库引擎
-        
+
         Args:
             database: 数据库名称
             config: 数据库配置信息
-            
+
         Returns:
             SQLAlchemy引擎实例
         """
         raise NotImplementedError("子类必须实现get_engine方法")
-    
-    def get_session_maker(self, database: str=None, config: Optional[Dict[str, Any]] = None) -> sessionmaker:
+
+    def get_session_maker(
+            self,
+            database: str = None,
+            config: Optional[Dict[str, Any]] = None,
+            is_query_only: bool = False,
+    ) -> sessionmaker:
         """获取或创建会话工厂
-        
+
         Args:
             database: 数据库名称
             config: 数据库配置信息
-            
+            is_query_only:
         Returns:
             会话工厂实例
         """
         database = database or self.main_database_name
-        if database in self._sessions:
+        if is_query_only and database in self._sessions:
             return self._sessions[database]
-        
+
         engine = self.get_engine(database, config)
         session = sessionmaker(bind=engine)
         self._sessions[database] = session
         return session
-    
+
     @contextmanager
-    def session_scope(self, database: str, config: Optional[Dict[str, Any]] = None) -> Generator:
+    def session_scope(
+            self, database: str, config: Optional[Dict[str, Any]] = None
+    ) -> Generator:
         """创建会话的上下文管理器
-        
+
         Args:
             database: 数据库名称
             config: 数据库配置信息
-            
+
         Yields:
             数据库会话
         """
@@ -175,10 +193,10 @@ class DBHelper:
             raise
         finally:
             session.close()
-    
+
     def dispose_engine(self, database: str) -> None:
         """释放指定数据库的引擎资源
-        
+
         Args:
             database: 数据库名称
         """
@@ -187,12 +205,12 @@ class DBHelper:
             del self._engines[database]
         if database in self._sessions:
             del self._sessions[database]
-    
+
     def dispose_all(self) -> None:
         """释放所有数据库资源"""
         for database in list(self._engines.keys()):
             self.dispose_engine(database)
-    
+
     def __del__(self):
         """析构函数,确保所有资源被释放"""
-        self.dispose_all()
+        self.dispose_all()

+ 444 - 406
SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js

@@ -1,473 +1,511 @@
 function GoTo(url, isNewWindow) {
-	if (isNewWindow || isNewWindow === undefined) {
-		window.open(url)
-	} else {
-		window.location.href = url
-	}
+    if (isNewWindow || isNewWindow === undefined) {
+        window.open(url)
+    } else {
+        window.location.href = url
+    }
 }
 
 function IwbAjax(opt) {
-	opt = opt || {}
-	if (!opt.url) {
-		MsgWarning('请传入url')
-	}
-	if (opt.method === undefined) {
-		opt.method = 'POST'
-	}
-	if (opt.data === undefined) {
-		opt.data = {}
-	}
-	opt = $.extend({},{isAlert: true,},opt)
-	fetch(opt.url, {
-		method: opt.method,
-		headers:opt.headers || {
-			'Content-Type': 'application/json',
-		},
-		body: opt.body || JSON.stringify(opt.data),
-	})
-		.then((response) => response.json())
-		.then((data) => {
-			console.log(`[${opt.url}]返回数据:`, data)
-			if (data.success) {
-				if (opt.isAlert) {
-					MsgSuccess('操作成功',data.message||"")
-				}
-				if (opt.table) {
-					IwbTable($(opt.table))
-				}
-				if (opt.modal) {
-					$(opt.modal).modal('hide')
-				}
-				opt.success && opt.success(data)
-			} else {
-				console.error(opt.url, data.message)
-				if ((opt.isAlert && opt.isAlertError === undefined) || opt.isAlertError) {
-					MsgError(data.message)
-				}
-			}
-		})
+    opt = opt || {}
+    if (!opt.url) {
+        MsgWarning('请传入url')
+    }
+    if (opt.method === undefined) {
+        opt.method = 'POST'
+    }
+    if (opt.data === undefined) {
+        opt.data = {}
+    }
+    opt = $.extend({}, {isAlert: true,}, opt)
+    fetch(opt.url, {
+        method: opt.method,
+        headers: opt.headers || {
+            'Content-Type': 'application/json',
+        },
+        body: opt.body || JSON.stringify(opt.data),
+    })
+        .then((response) => response.json())
+        .then((data) => {
+            console.log(`[${opt.url}]返回数据:`, data)
+            if (data.success) {
+                if (opt.isAlert) {
+                    MsgSuccess('操作成功', data.message || "")
+                }
+                if (opt.table) {
+                    IwbTable($(opt.table))
+                }
+                if (opt.modal) {
+                    $(opt.modal).modal('hide')
+                }
+                opt.success && opt.success(data)
+            } else {
+                console.error(opt.url, data.message)
+                if ((opt.isAlert && opt.isAlertError === undefined) || opt.isAlertError) {
+                    MsgError(data.message)
+                }
+            }
+        })
 }
 
 function IwbAjax_1(opt) {
-	opt.isAlert = false
-	opt.isAlertError = true
-	IwbAjax(opt)
+    opt.isAlert = false
+    opt.isAlertError = true
+    IwbAjax(opt)
 }
+
 function IwbAjax_2(opt, modal, table) {
-	opt.modal = modal || '#modal'
-	opt.table = table || '#table'
-	IwbAjax(opt)
+    opt.modal = modal || '#modal'
+    opt.table = table || '#table'
+    IwbAjax(opt)
 }
-function IwbTable(table, opts,isReload) {
-	const $table = $(table)
-	const $tableBox = $table.closest('.table-box')
-	if (table.length === 0) return
-	const defaultOptions = {
-		checkBox: false,
-		idFiled:'id',
-		tableCheckboxName:'table-id',
-		selected_ids:new Set(),
-		checkBoxWidth:50,
-		pageSize: 15,
-		pageNum: 1,
-		search: {
-			keyword: '',
-		},
-	}
-	const options = isReload ? defaultOptions : ($table.data('options') || defaultOptions)
-	const tableOptions = $.extend({}, options, opts || {})
-	let isSearch = false
-	const checkBoxDiv ='<div class="form-check form-check-sm form-check-custom form-check-solid"><input name="{1}" class="form-check-input" type="checkbox" {2} value="{0}" /></div>'
-	$table.data('init',true)
-
-	function ajaxTable(opt) {
-		if (isSearch) {
-			return
-		}
-		$table.data('options', opt)
-		loading()
-		const data = $.extend({}, opt.search, { pageNum: opt.pageNum, pageSize: opt.pageSize })
-		fetch(opt.url, {
-			method: 'POST',
-			headers: {
-				'Content-Type': 'application/json',
-			},
-			body: JSON.stringify(data),
-		})
-			.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)
-				}
-			})
-			.catch((error) => {
-				console.error(error)
-			})
-			.finally(() => {
-				clearLoading()
-			})
-	}
-	function loading() {
-		isSearch = true
-		$tableBox.addClass('table-loading').append(`<div class="table-loading-message"><span>正在加载中...</span></div>`)
-	}
-	function clearLoading() {
-		isSearch = false
-		$tableBox.removeClass('table-loading').find('.table-loading-message').remove()
-	}
-	function renderTable(rows, total) {
-		const opt = $table.data('options')
-		let head_str = '',
-			body_str = '',
-			check_all = true
-		// console.log('渲染表格:', opt.selected_ids,rows, total)
-		if(opt.checkBox){
-			head_str += `<th style="${opt.checkBoxWidth ? `width: ${opt.checkBoxWidth};` : ``}">
-						${checkBoxDiv.format('',`${opt.tableCheckboxName}_head`,'')}
+
+function IwbTable(table, opts, isReload) {
+    const $table = $(table)
+    const $tableBox = $table.closest('.table-box')
+    if (table.length === 0) return
+    const defaultOptions = {
+        checkBox: false,
+        idFiled: 'id',
+        tableCheckboxName: 'table-id',
+        selected_ids: new Set(),
+        selected_rows: new Set(),
+        rows: new Set(),
+        checkBoxWidth: 50,
+        pageSize: 15,
+        pageNum: 1,
+        search: {
+            keyword: '',
+        },
+    }
+    const options = isReload ? defaultOptions : ($table.data('options') || defaultOptions)
+    const tableOptions = $.extend({}, options, opts || {})
+    let isSearch = false
+    const checkBoxDiv = '<div class="form-check form-check-sm form-check-custom form-check-solid"><input name="{1}" class="form-check-input" type="checkbox" {2} value="{0}" /></div>'
+    $table.data('init', true)
+
+    function ajaxTable(opt) {
+        if (isSearch) {
+            return
+        }
+        $table.data('options', opt)
+        loading()
+        const data = $.extend({}, opt.search, {pageNum: opt.pageNum, pageSize: opt.pageSize})
+        fetch(opt.url, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            body: JSON.stringify(data),
+        })
+            .then((response) => response.json())
+            .then((res) => {
+                if (res.success) {
+                    opt.callBeforeRender && opt.callBeforeRender($table, res.data.rows)
+                    $table.data('rows', res.data.rows)
+                    opt.rows.add(res.data.rows)
+                    renderTable(res.data.rows, res.data.total)
+                    setTimeout(() => {
+                        opt.callAfterRender && opt.callAfterRender($table, res.data.rows)
+                    }, 1000)
+                } else {
+                    renderTable([], 0)
+                    $table.data('rows', [])
+                    console.error('加载表格出错:', res.message, opt.url)
+                }
+            })
+            .catch((error) => {
+                console.error(error)
+            })
+            .finally(() => {
+                $table.data('options', opt)
+                clearLoading()
+            })
+    }
+
+    function loading() {
+        isSearch = true
+        $tableBox.addClass('table-loading').append(`<div class="table-loading-message"><span>正在加载中...</span></div>`)
+    }
+
+    function clearLoading() {
+        isSearch = false
+        $tableBox.removeClass('table-loading').find('.table-loading-message').remove()
+    }
+
+    function renderTable(rows, total) {
+        const opt = $table.data('options')
+        let head_str = '',
+            body_str = '',
+            check_all = true
+        // console.log('渲染表格:', opt.selected_ids,rows, total)
+        if (opt.checkBox) {
+            head_str += `<th style="${opt.checkBoxWidth ? `width: ${opt.checkBoxWidth};` : ``}">
+						${checkBoxDiv.format('', `${opt.tableCheckboxName}_head`, '')}
 					</th>`
-		}
-		for (let i = 0; i < opt.columns.length; i++) {
-			const col = opt.columns[i]
-			head_str += `<th style="${col.width ? `width: ${col.width};` : ``}${col.style ? `${col.style};` : ``}">${col.title}</th>`
-		}
-		if (rows && rows.length) {
-			for (let i = 0; i < rows.length; i++) {
-				const row = rows[i]
-				body_str += '<tr>'
-				if(opt.checkBox){
-					if(opt.selected_ids.size && opt.selected_ids.has(row[opt.idFiled]+"")){
-						body_str +=`<td>${checkBoxDiv.format(row[opt.idFiled],`${opt.tableCheckboxName}`,'checked')}</td>`
-					}else{
-						check_all = false
-						body_str +=`<td>${checkBoxDiv.format(row[opt.idFiled],`${opt.tableCheckboxName}`,'')}</td>`
-					}
-				}
-				for (let j = 0; j < opt.columns.length; j++) {
-					const col = opt.columns[j]
-					if (col.render) {
-						body_str += `<td>${col.render(row, rows)}</td>`
-					} else {
-						body_str += `<td>${row[col.data]===undefined || row[col.data]===null ? '-':row[col.data]}</td>`
-					}
-				}
-				body_str += '</tr>'
-			}
-		} else {
-			body_str += '<tr><td colspan="' + opt.columns.length + '" class="text-center">暂无数据</td></tr>'
-		}
-		$tableBox.fadeOut(300, () => {
-			let page_str = formatPage(opt.pageNum, opt.pageSize, total)
-			$table.parent().find('.pagination-row').html(page_str)
-			$table.html(`<thead><tr>${head_str}</tr></thead><tbody>${body_str}</tbody>`)
-			$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]+""))
-				} else {
-					$tableBox.find(`input[name=${opt.tableCheckboxName}]`).prop('checked', false)
-					rows.forEach(row => opt.selected_ids.delete(row[opt.idFiled]+""))
-				}
-			})
-			$tableBox.find(`input[name=${opt.tableCheckboxName}]`).on('change', function () {
-				if ($tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:checked)`).length) {
-					$tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', false)
-				} else {
-					$tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', true)
-				}
-				const checked = $(this).is(':checked')
-				if (checked) {
-					opt.selected_ids.add($(this).val())
-				} else {
-					opt.selected_ids.delete($(this).val())
-				}
-			})
-			if(check_all){
-				$tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', true)
-			}
-			$tableBox.fadeIn(500,function (){
-				$tableBox.find(`[data-bs-toggle="tooltip"]`).each(function (){
-					new bootstrap.Tooltip(this)
-				})
-			})
-		})
-	}
-
-	function formatPage(pageNum, pageSize, total) {
-		const totalPages = Math.ceil(total / pageSize)
-		// const startIndex = (pageNum - 1) * pageSize + 1
-		// const endIndex = Math.min(pageNum * pageSize, total)
-
-		let str = '<div class=" d-flex align-items-center justify-content-center justify-content-md-start dt-toolbar">'
-		// 每页显示条数选择
-		str += `<div><select class="form-select form-select-solid form-select-sm" onchange="IwbTableChangePageSize(this)">`
-		;[15, 25, 50, 100].forEach((size) => {
-			str += `<option value="${size}" ${pageSize === size ? ' selected' : ''}>${size}</option>`
-		})
-		str += '</select></div>'
-
-		// 显示记录信息
-		str += `<div class="dt-info">当前第 ${pageNum}/${totalPages} 页  共 ${total} 条</div></div>`
-
-		// 分页导航
-		str += '<div class="d-flex align-items-center justify-content-center justify-content-md-end"><div class="dt-paging paging_simple_numbers"><nav aria-label="pagination"><ul class="pagination">'
-
-		// 上一页按钮
-		if (pageNum > 1) {
-			str += '<li class="dt-paging-button page-item"><button class="page-link previous" onclick="IwbTableJumpPage(this,' + (pageNum - 1) + ')"><i class="previous"></i></button></li>'
-		} else {
-			str += '<li class="dt-paging-button page-item disabled"><button class="page-link previous"><i class="previous"></i></button></li>'
-		}
-
-		// 计算显示的页码范围
-		let startPage = Math.max(1, pageNum - 2)
-		let endPage = Math.min(totalPages, pageNum + 2)
-
-		// 显示第一页和省略号
-		if (startPage > 1) {
-			str += '<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,1)">1</button></li>'
-			if (startPage > 2) {
-				str += '<li class="dt-paging-button page-item disabled"><button class="page-link ellipsis" tabindex="-1">…</button></li>'
-			}
-		}
-
-		// 显示页码
-		for (let i = startPage; i <= endPage; i++) {
-			if (i === pageNum) {
-				str += `<li class="dt-paging-button page-item active"><button class="page-link" onclick="IwbTableJumpPage(this,${i})">${i}</button></li>`
-			} else {
-				str += `<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,${i})">${i}</button></li>`
-			}
-		}
-
-		// 显示最后一页和省略号
-		if (endPage < totalPages) {
-			if (endPage < totalPages - 1) {
-				str += '<li class="dt-paging-button page-item disabled"><button class="page-link ellipsis" tabindex="-1">…</button></li>'
-			}
-			str += `<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,${totalPages})">${totalPages}</button></li>`
-		}
-
-		// 下一页按钮
-		if (pageNum < totalPages) {
-			str += '<li class="dt-paging-button page-item"><button class="page-link next" onclick="IwbTableJumpPage(this,' + (pageNum + 1) + ')"><i class="next"></i></button></li>'
-		} else {
-			str += '<li class="dt-paging-button page-item disabled"><button class="page-link next"><i class="next"></i></button></li>'
-		}
-
-		str += '</ul></nav></div></div>'
-		return str
-	}
-	ajaxTable(tableOptions)
+        }
+        for (let i = 0; i < opt.columns.length; i++) {
+            const col = opt.columns[i]
+            head_str += `<th style="${col.width ? `width: ${col.width};` : ``}${col.style ? `${col.style};` : ``}">${col.title}</th>`
+        }
+        if (rows && rows.length) {
+            for (let i = 0; i < rows.length; i++) {
+                const row = rows[i]
+                body_str += '<tr>'
+                if (opt.checkBox) {
+                    if (opt.selected_ids.size && opt.selected_ids.has(row[opt.idFiled] + "")) {
+                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, 'checked')}</td>`
+                    } else {
+                        check_all = false
+                        body_str += `<td>${checkBoxDiv.format(row[opt.idFiled], `${opt.tableCheckboxName}`, '')}</td>`
+                    }
+                }
+                for (let j = 0; j < opt.columns.length; j++) {
+                    const col = opt.columns[j]
+                    if (col.render) {
+                        body_str += `<td>${col.render(row, rows)}</td>`
+                    } else {
+                        body_str += `<td>${row[col.data] === undefined || row[col.data] === null ? '-' : row[col.data]}</td>`
+                    }
+                }
+                body_str += '</tr>'
+            }
+        } else {
+            body_str += '<tr><td colspan="' + opt.columns.length + '" class="text-center">暂无数据</td></tr>'
+        }
+        $tableBox.fadeOut(300, () => {
+            let page_str = formatPage(opt.pageNum, opt.pageSize, total)
+            $table.parent().find('.pagination-row').html(page_str)
+            $table.html(`<thead><tr>${head_str}</tr></thead><tbody>${body_str}</tbody>`)
+            $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)
+                    })
+                } else {
+                    $tableBox.find(`input[name=${opt.tableCheckboxName}]`).prop('checked', false)
+                    rows.forEach(row => {
+                        opt.selected_ids.delete(row[opt.idFiled] + "")
+                        opt.selected_rows.delete(row)
+                    })
+                }
+            })
+            $tableBox.find(`input[name=${opt.tableCheckboxName}]`).on('change', function () {
+                if ($tableBox.find(`input[name=${opt.tableCheckboxName}]:not(:checked)`).length) {
+                    $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', false)
+                } else {
+                    $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', true)
+                }
+                const checked = $(this).is(':checked')
+                if (checked) {
+                    opt.selected_ids.add($(this).val())
+                    opt.selected_rows.add(rows.find(row => row[opt.idFiled] + "" === $(this).val()))
+                } else {
+                    opt.selected_ids.delete($(this).val())
+                    opt.selected_rows.delete(rows.find(row => row[opt.idFiled] + "" === $(this).val()))
+                }
+            })
+            if (check_all) {
+                $tableBox.find(`input[name=${opt.tableCheckboxName}_head]`).prop('checked', true)
+            }
+            $tableBox.fadeIn(500, function () {
+                $tableBox.find(`[data-bs-toggle="tooltip"]`).each(function () {
+                    new bootstrap.Tooltip(this)
+                })
+            })
+        })
+    }
+
+    function formatPage(pageNum, pageSize, total) {
+        const totalPages = Math.ceil(total / pageSize)
+        // const startIndex = (pageNum - 1) * pageSize + 1
+        // const endIndex = Math.min(pageNum * pageSize, total)
+
+        let str = '<div class=" d-flex align-items-center justify-content-center justify-content-md-start dt-toolbar">'
+        // 每页显示条数选择
+        str += `<div><select class="form-select form-select-solid form-select-sm" onchange="IwbTableChangePageSize(this)">`
+        ;[15, 25, 50, 100].forEach((size) => {
+            str += `<option value="${size}" ${pageSize === size ? ' selected' : ''}>${size}</option>`
+        })
+        str += '</select></div>'
+
+        // 显示记录信息
+        str += `<div class="dt-info">当前第 ${pageNum}/${totalPages} 页  共 ${total} 条</div></div>`
+
+        // 分页导航
+        str += '<div class="d-flex align-items-center justify-content-center justify-content-md-end"><div class="dt-paging paging_simple_numbers"><nav aria-label="pagination"><ul class="pagination">'
+
+        // 上一页按钮
+        if (pageNum > 1) {
+            str += '<li class="dt-paging-button page-item"><button class="page-link previous" onclick="IwbTableJumpPage(this,' + (pageNum - 1) + ')"><i class="previous"></i></button></li>'
+        } else {
+            str += '<li class="dt-paging-button page-item disabled"><button class="page-link previous"><i class="previous"></i></button></li>'
+        }
+
+        // 计算显示的页码范围
+        let startPage = Math.max(1, pageNum - 2)
+        let endPage = Math.min(totalPages, pageNum + 2)
+
+        // 显示第一页和省略号
+        if (startPage > 1) {
+            str += '<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,1)">1</button></li>'
+            if (startPage > 2) {
+                str += '<li class="dt-paging-button page-item disabled"><button class="page-link ellipsis" tabindex="-1">…</button></li>'
+            }
+        }
+
+        // 显示页码
+        for (let i = startPage; i <= endPage; i++) {
+            if (i === pageNum) {
+                str += `<li class="dt-paging-button page-item active"><button class="page-link" onclick="IwbTableJumpPage(this,${i})">${i}</button></li>`
+            } else {
+                str += `<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,${i})">${i}</button></li>`
+            }
+        }
+
+        // 显示最后一页和省略号
+        if (endPage < totalPages) {
+            if (endPage < totalPages - 1) {
+                str += '<li class="dt-paging-button page-item disabled"><button class="page-link ellipsis" tabindex="-1">…</button></li>'
+            }
+            str += `<li class="dt-paging-button page-item"><button class="page-link" onclick="IwbTableJumpPage(this,${totalPages})">${totalPages}</button></li>`
+        }
+
+        // 下一页按钮
+        if (pageNum < totalPages) {
+            str += '<li class="dt-paging-button page-item"><button class="page-link next" onclick="IwbTableJumpPage(this,' + (pageNum + 1) + ')"><i class="next"></i></button></li>'
+        } else {
+            str += '<li class="dt-paging-button page-item disabled"><button class="page-link next"><i class="next"></i></button></li>'
+        }
+
+        str += '</ul></nav></div></div>'
+        return str
+    }
+
+    ajaxTable(tableOptions)
 }
+
 function IwbTableSearch(that) {
-	const $search = $(that).closest('form.search-box')
-	const $table = $(that).closest('.table-box').find('.table')
-	const search = $table.data('options').search || {}
-	$search.find('.form-control,.form-select').each((i, el) => {
-		const v = $(el).val()
-		if (v.trim() !== '') {
-			search[$(el).attr('name')] = v
-		} else {
-			delete search[$(el).attr('name')]
-		}
-	})
-	IwbTable($table, { search: search, pageNum: 1 })
+    const $search = $(that).closest('form.search-box')
+    const $table = $(that).closest('.table-box').find('.table')
+    const search = $table.data('options').search || {}
+    $search.find('.form-control,.form-select').each((i, el) => {
+        const v = $(el).val()
+        if (v.trim() !== '') {
+            search[$(el).attr('name')] = v
+        } else {
+            delete search[$(el).attr('name')]
+        }
+    })
+    IwbTable($table, {search: search, pageNum: 1})
 }
+
 function IwbTableResetSearch(that) {
-	const $search = $(that).closest('form.search-box')
-	$search[0].reset()
-	IwbTableSearch(that)
+    const $search = $(that).closest('form.search-box')
+    $search[0].reset()
+    IwbTableSearch(that)
 }
+
 function IwbTableJumpPage(that, pageNum) {
-	const $table = $(that).closest('.table-box').find('.table')
-	IwbTable($table, { pageNum: pageNum })
+    const $table = $(that).closest('.table-box').find('.table')
+    IwbTable($table, {pageNum: pageNum})
 }
+
 function IwbTableChangePageSize(that) {
-	const $table = $(that).closest('.table-box').find('.table')
-	const pageSize = parseInt($(that).val())
-	IwbTable($table, { pageSize: pageSize })
+    const $table = $(that).closest('.table-box').find('.table')
+    const pageSize = parseInt($(that).val())
+    IwbTable($table, {pageSize: pageSize})
 }
+
 function IwbTableGetSelectedIds(table) {
-	return $(table) && $(table).data('options') ? Array.from($(table).data('options').selected_ids).sort((a, b) => a - b) : []
+    return $(table) && $(table).data('options') ? Array.from($(table).data('options').selected_ids).sort((a, b) => (Number(a) || 0) - (Number(b) || 0)) : []
+}
+
+function IwbTableGetSelectedRows(table) {
+    const opt = $(table).data('options')
+    return $(table) && $(table).data('options') ? Array.from($(table).data('options').selected_rows).sort((a, b) => (Number(a[opt.idFiled]) || 0) - (Number(b[opt.idFiled]) || 0)) : []
 }
+
 function AddModal(modal, callback) {
-	const $modal = $(modal)
-	$modal.find('.modal-header .modal-title span.prefix').html('添加')
-	$modal.find('.modal-body .form-control').val('')
-	callback && callback($modal)
-	$modal.modal('show')
+    const $modal = $(modal)
+    $modal.find('.modal-header .modal-title span.prefix').html('添加')
+    $modal.find('.modal-body .form-control').val('')
+    callback && callback($modal)
+    $modal.modal('show')
 }
+
 function EditModal(modal, callback) {
-	const $modal = $(modal)
-	$modal.find('.modal-header .modal-title span.prefix').html('修改')
-	callback && callback($modal)
-	$modal.modal('show')
+    const $modal = $(modal)
+    $modal.find('.modal-header .modal-title span.prefix').html('修改')
+    callback && callback($modal)
+    $modal.modal('show')
 }
 
 function InitRangeDate($el) {
-	const start = moment().subtract(29, "days");
-	const end = moment();
-	function cb(start, end) {
-	   $el.html(start.format("YYYY-MM-DD") + " - " + end.format("YYYY-MM-DD"));
-	}
-	$el.daterangepicker({
-		startDate: start,
-		endDate: end,
-		locale:{
-			format: "YYYY-MM-DD", //设置显示格式
-			applyLabel: '确定', //确定按钮文本
-			cancelLabel: '取消', //取消按钮文本
-			customRangeLabel: '自定义',
-			daysOfWeek: ['日', '一', '二', '三', '四', '五', '六'],
-			monthNames: ['一月', '二月', '三月', '四月', '五月', '六月',
-				'七月', '八月', '九月', '十月', '十一月', '十二月'
-			],
-			firstDay: 1
-		},
-		ranges: {
-		"今天": [moment(), moment()],
-		"昨天": [moment().subtract(1, "days"), moment().subtract(1, "days")],
-		"近7天": [moment().subtract(6, "days"), moment()],
-		"近30天": [moment().subtract(29, "days"), moment()],
-		"本月": [moment().startOf("month"), moment().endOf("month")],
-		"上月": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")]
-		}
-	}, cb);
-	cb(start, end);
+    const start = moment().subtract(29, "days");
+    const end = moment();
+
+    function cb(start, end) {
+        $el.html(start.format("YYYY-MM-DD") + " - " + end.format("YYYY-MM-DD"));
+    }
+
+    $el.daterangepicker({
+        startDate: start,
+        endDate: end,
+        locale: {
+            format: "YYYY-MM-DD", //设置显示格式
+            applyLabel: '确定', //确定按钮文本
+            cancelLabel: '取消', //取消按钮文本
+            customRangeLabel: '自定义',
+            daysOfWeek: ['日', '一', '二', '三', '四', '五', '六'],
+            monthNames: ['一月', '二月', '三月', '四月', '五月', '六月',
+                '七月', '八月', '九月', '十月', '十一月', '十二月'
+            ],
+            firstDay: 1
+        },
+        ranges: {
+            "今天": [moment(), moment()],
+            "昨天": [moment().subtract(1, "days"), moment().subtract(1, "days")],
+            "近7天": [moment().subtract(6, "days"), moment()],
+            "近30天": [moment().subtract(29, "days"), moment()],
+            "本月": [moment().startOf("month"), moment().endOf("month")],
+            "上月": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")]
+        }
+    }, cb);
+    cb(start, end);
 }
-function Msg(msg, type){
-	const opts={
+
+function Msg(msg, type) {
+    const opts = {
         text: msg,
-        icon: type||"info",
+        icon: type || "info",
         buttonsStyling: false,
         confirmButtonText: "确定",
         customClass: {
             confirmButton: "btn btn-primary"
         },
-		toast:false
+        toast: false
+    }
+    if (type === 'success') {
+        opts.toast = true
+        opts.timer = 3000
+        opts.showConfirmButton = false
     }
-	if(type==='success'){
-		opts.toast=true
-		opts.timer=3000
-		opts.showConfirmButton=false
-	}
-	Swal.fire(opts)
-	// toastr.options = {
-	//   "closeButton": false,
-	//   "debug": false,
-	//   "newestOnTop": false,
-	//   "progressBar": false,
-	//   "positionClass": "toastr-top-center",
-	//   "preventDuplicates": false,
-	//   "onclick": null,
-	//   "showDuration": "300",
-	//   "hideDuration": "1000",
-	//   "timeOut": "2000",
-	//   "extendedTimeOut": "1000",
-	//   "showEasing": "swing",
-	//   "hideEasing": "linear",
-	//   "showMethod": "fadeIn",
-	//   "hideMethod": "fadeOut"
-	// };
-	// if (type==='success'){
-	// 	title ? toastr.success(msg,title):toastr.success(msg);
-	// }else if (type==='error'){
-	// 	toastr.options.timeOut = "0";
-	// 	title ? toastr.error(msg,title):toastr.error(msg);
-	// }else if(type==='warning'){
-	// 	toastr.options.timeOut = "5000";
-	// 	title ? toastr.warning(msg,title):toastr.warning(msg);
-	// }else{
-	// 	toastr.options.timeOut = "0";
-	// 	title ? toastr.info(msg,title):toastr.info(msg);
-	// }
+    Swal.fire(opts)
+    // toastr.options = {
+    //   "closeButton": false,
+    //   "debug": false,
+    //   "newestOnTop": false,
+    //   "progressBar": false,
+    //   "positionClass": "toastr-top-center",
+    //   "preventDuplicates": false,
+    //   "onclick": null,
+    //   "showDuration": "300",
+    //   "hideDuration": "1000",
+    //   "timeOut": "2000",
+    //   "extendedTimeOut": "1000",
+    //   "showEasing": "swing",
+    //   "hideEasing": "linear",
+    //   "showMethod": "fadeIn",
+    //   "hideMethod": "fadeOut"
+    // };
+    // if (type==='success'){
+    // 	title ? toastr.success(msg,title):toastr.success(msg);
+    // }else if (type==='error'){
+    // 	toastr.options.timeOut = "0";
+    // 	title ? toastr.error(msg,title):toastr.error(msg);
+    // }else if(type==='warning'){
+    // 	toastr.options.timeOut = "5000";
+    // 	title ? toastr.warning(msg,title):toastr.warning(msg);
+    // }else{
+    // 	toastr.options.timeOut = "0";
+    // 	title ? toastr.info(msg,title):toastr.info(msg);
+    // }
 }
+
 function MsgSuccess(msg, title) {
-	Msg(msg,  'success',title)
+    Msg(msg, 'success', title)
 }
+
 function MsgError(msg, title) {
-	Msg(msg,  'error',title)
+    Msg(msg, 'error', title)
 }
+
 function MsgWarning(msg, title) {
-	Msg(msg,  'warning',title)
+    Msg(msg, 'warning', title)
 }
+
 function Confirm(title, callback) {
-	const opts={
+    const opts = {
         text: title,
         icon: "info",
         buttonsStyling: false,
-		showCancelButton: true,
-		showConfirmButton: true,
+        showCancelButton: true,
+        showConfirmButton: true,
         cancelButtonText: "取消",
         confirmButtonText: "确定",
         customClass: {
-			cancelButton: "btn btn-light",
+            cancelButton: "btn btn-light",
             confirmButton: "btn btn-primary"
         },
-		toast:false
+        toast: false
     }
-	Swal.fire(opts).then((result)=>{
-		console.log("CONFIRM",result)
-		if(result.isConfirmed){
-			callback && callback()
-		}
-	});
+    Swal.fire(opts).then((result) => {
+        console.log("CONFIRM", result)
+        if (result.isConfirmed) {
+            callback && callback()
+        }
+    });
 }
 
 function ConfirmUrl(title, url, table) {
-	Confirm(title,function () {
-		IwbAjax({
-			url: url,
-			table: table || '#table',
-		})
-	})
+    Confirm(title, function () {
+        IwbAjax({
+            url: url,
+            table: table || '#table',
+        })
+    })
 }
 
 function ChangeHeadMenu(menu) {
-	$('#header_menu .menu-item').removeClass('here')
-	$('#header_menu ' + menu).addClass('here')
+    $('#header_menu .menu-item').removeClass('here')
+    $('#header_menu ' + menu).addClass('here')
 }
 
 function DownloadFile(url, fileName) {
-	fetch(url,{
-		method:'POST'
-	}).then((response) => {
-			if (!response.ok) {
-				return response.json().then((err) => {
-					throw new Error(err.message || '下载失败')
-				})
-			}
-			return response.blob()
-		})
-		.then((blob) => {
-			const downloadUrl = window.URL.createObjectURL(blob)
-			const a = document.createElement('a')
-			a.href = downloadUrl
-			a.download = fileName
-			document.body.appendChild(a)
-			a.click()
-			window.URL.revokeObjectURL(downloadUrl)
-			document.body.removeChild(a)
-		})
-		.catch((error) => {
-			MsgError(error.message)
-		})
+    fetch(url, {
+        method: 'POST'
+    }).then((response) => {
+        if (!response.ok) {
+            return response.json().then((err) => {
+                throw new Error(err.message || '下载失败')
+            })
+        }
+        return response.blob()
+    })
+        .then((blob) => {
+            const downloadUrl = window.URL.createObjectURL(blob)
+            const a = document.createElement('a')
+            a.href = downloadUrl
+            a.download = fileName
+            document.body.appendChild(a)
+            a.click()
+            window.URL.revokeObjectURL(downloadUrl)
+            document.body.removeChild(a)
+        })
+        .catch((error) => {
+            MsgError(error.message)
+        })
 }
 
 // 添加字符串序列化方法
 String.prototype.format = function () {
-	let args = arguments
-	return this.replace(/{(\d+)}/g, function (match, index) {
-		return typeof args[index] != 'undefined' ? args[index] : match
-	})
+    let args = arguments
+    return this.replace(/{(\d+)}/g, function (match, index) {
+        return typeof args[index] != 'undefined' ? args[index] : match
+    })
 }

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

@@ -1,4 +1,4 @@
-const task_modal_template=`
+const task_modal_template = `
 <div class="modal fade" id="modal" tabindex="-1" aria-hidden="true">
   <div class="modal-dialog modal-dialog-centered">
     <div class="modal-content rounded">
@@ -76,7 +76,7 @@ const task_modal_template=`
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
-        <button type="button" class="btn btn-success" onclick="SaveProject(true)">提交</button>
+        <button type="button" class="btn btn-light-success" onclick="SaveProject(true)">提交</button>
         <button type="button" class="btn btn-primary" onclick="SaveProject()">保存草稿</button>
       </div>
     </div>
@@ -86,279 +86,281 @@ $('.app-main .app-container').append(task_modal_template)
 const table_add_task_btn_template = `<button type="button" class="task_add_btn btn btn-primary btn-sm" onclick="Add('{0}')">添加任务</button>`
 
 const table = '#table',
-	$modal = $('#modal')
+    $modal = $('#modal')
 $modal.find('#budget_id').html($('#budget_id_options').html())
 $(function () {
-	InitFileUpload()
+    InitFileUpload()
 })
 
 
 let _fileUploadDropzone = null;
 
-function InitFileUpload(){
-	const id = "#modal";
-	const dropzone = document.querySelector(id);
-	// set the preview element template
-	const previewNode = dropzone.querySelector(".dropzone-item");
-	previewNode.id = "";
-	const previewTemplate = previewNode.parentNode.innerHTML;
-	previewNode.parentNode.removeChild(previewNode);
-	_fileUploadDropzone = new Dropzone(id, {
-		url:"/",
-		parallelUploads: 10,
-		previewTemplate: previewTemplate,
-		autoProcessQueue: false, //关闭自动上传功能
-		maxFiles: 5,
-		maxFilesize: 10, // MB
-		previewsContainer: id + " .dropzone-items", //
-    	clickable: id + " .dropzone-select", //
-		acceptedFiles: '.csv,.xls,.xlsx',
-		accept: function(file, done) {
-			console.log('DONE', file, done)
-			if(file.type.includes('csv') || file.type.includes('.xls') || file.type.includes('sheet')){
-				done()
-			}else{
-				done('文件类型非法,只允许.csv,.xls,.xlsx文件')
-			}
-		},
-		dictInvalidFileType: '文件类型非法,只允许.csv,.xls,.xlsx文件',
-		// dictRemoveFileConfirmation: '确定移除文件吗?',
-		dictFileTooBig: '文件[{{filesize}}M]过大,最大允许{{maxFilesize}}M。',
-		dictMaxFilesExceeded: '最多只能上传5个,每个文件不超过10M',
-	});
-	_fileUploadDropzone.on("addedfile", function (file) {
-		const dropzoneItems = dropzone.querySelectorAll('.dropzone-item');
-		dropzoneItems.forEach(dropzoneItem => {
-			dropzoneItem.style.display = '';
-		});
-		if(_fileUploadDropzone.getAcceptedFiles().findIndex(item=>item.name===file.name)>=0){
-			MsgWarning('文件已存在')
-			_fileUploadDropzone.removeFile(file);
-		}
-	});
-	// _fileUploadDropzone.on("removedfile", function (file) {
-	// 	_files = _files.filter(item=>item.name!==file.name)
-	// 	console.log('removedfile', file,_files)
-	// })
+function InitFileUpload() {
+    const id = "#modal";
+    const dropzone = document.querySelector(id);
+    // set the preview element template
+    const previewNode = dropzone.querySelector(".dropzone-item");
+    previewNode.id = "";
+    const previewTemplate = previewNode.parentNode.innerHTML;
+    previewNode.parentNode.removeChild(previewNode);
+    _fileUploadDropzone = new Dropzone(id, {
+        url: "/",
+        parallelUploads: 10,
+        previewTemplate: previewTemplate,
+        autoProcessQueue: false, //关闭自动上传功能
+        maxFiles: 5,
+        maxFilesize: 10, // MB
+        previewsContainer: id + " .dropzone-items", //
+        clickable: id + " .dropzone-select", //
+        acceptedFiles: '.csv,.xls,.xlsx',
+        accept: function (file, done) {
+            console.log('DONE', file, done)
+            if (file.type.includes('csv') || file.type.includes('.xls') || file.type.includes('sheet')) {
+                done()
+            } else {
+                done('文件类型非法,只允许.csv,.xls,.xlsx文件')
+            }
+        },
+        dictInvalidFileType: '文件类型非法,只允许.csv,.xls,.xlsx文件',
+        // dictRemoveFileConfirmation: '确定移除文件吗?',
+        dictFileTooBig: '文件[{{filesize}}M]过大,最大允许{{maxFilesize}}M。',
+        dictMaxFilesExceeded: '最多只能上传5个,每个文件不超过10M',
+    });
+    _fileUploadDropzone.on("addedfile", function (file) {
+        const dropzoneItems = dropzone.querySelectorAll('.dropzone-item');
+        dropzoneItems.forEach(dropzoneItem => {
+            dropzoneItem.style.display = '';
+        });
+        if (_fileUploadDropzone.getAcceptedFiles().findIndex(item => item.name === file.name) >= 0) {
+            MsgWarning('文件已存在')
+            _fileUploadDropzone.removeFile(file);
+        }
+    });
+    // _fileUploadDropzone.on("removedfile", function (file) {
+    // 	_files = _files.filter(item=>item.name!==file.name)
+    // 	console.log('removedfile', file,_files)
+    // })
 }
 
-function RenderRightBox_Custom(data){
-	$quotaBox.data("table-url",`/api/quota/list`)
-	$rightBoxHeader.find('[name="table_radio"]').prop("disabled",false).off('change.iwb')
-	$rightBoxHeader.find('[name="table_radio"]').on('change.iwb',function(){
-		const val = $(this).val()
-		if (val === 'quota') {
-			$taskBox.hide()
-			QuotaNavTab(data)
-		} else {
-			_taskTable(data)
-		}
-	})
-	if(data.children_count>0||data.chapter){
-		_renderTask(data)
-	}else{
-		$rightBoxHeader.find('.badge').text('定额输入明细').removeClass('badge-primary').addClass('badge-success')
-		$rightBoxHeader.find('#task_radio').prop("disabled",true)
-		$rightBoxHeader.find('#quota_radio').prop("checked",true)
-		QuotaNavTab(data)
-	}
-	function _renderTask(data){
-		$rightBoxHeader.find('.badge').text('任务列表').removeClass('badge-success').addClass('badge-primary')
-		$rightBoxHeader.find('#task_radio').prop("checked",true)
-		const budget_id =0
-		$taskBox.html(table_template.format(budget_id,table_add_task_btn_template.format(budget_id),table_run_select_template))
-		_taskTable(data)
-	}
-	function _taskTable(data){
-		$quotaBox.hide()
-		const $table = $taskBox.find('.table')
-		IwbTable($table, {
-				url: `/api/task/list/${project_id}/${data.item_code}`,
-				columns: [
-					{
-						title: '任务编号',
-						data: 'id',
-						width: '100px',
-					},
-					{
-						title: '任务名称',
-						data: 'task_name',
-						width: '240px',
-					},
-					{
-						title: '任务排序',
-						data: 'task_sort',
-						width: '100px',
-					},
-					{
-						title: '概算单元',
-						data: 'budget_id',
-						width: '130px',
-						render: (row) => {
-							const budget_name = $modal.find(`[name="budget_id"] option[value='${row.budget_id}']`)
-							return budget_name.length ? budget_name.text() : '-'
-						}
-					},
-					{
-						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] || ""
-									if (!path) continue
-									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: '180px',
-						render: (row) => {
-							let str = ``
-							if (row.process_status === 0) {
-								str += `<span class="badge badge-secondary">草稿</span>`
-							} else if (row.process_status === 1){
-								str += `<span class="badge badge-light-primary">等待运行</span>`
-							} else if (row.process_status === 2){
-								str += `<span class="badge badge-light-info">运行中</span>`
-								// if (row.send_status === 0) {
-								// 	str += `<span class="badge badge-light-primary ms-3">未发送</span>`
-								// } else if (row.send_status === 1){
-								// 	str += `<span class="badge badge-light-warning ms-3">发送中</span>`
-								// } else if (row.send_status === 2){
-								// 	str += `<span class="badge badge-light-success ms-3">发送完成</span>`
-								// } else if (row.send_status === 3){
-								// 	str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
-								// }
-								// if (row.process_status === 0) {
-								// 	str += `<span class="badge badge-light-primary ms-3">未处理</span>`
-								// } else if (row.process_status === 1){
-								// 	str += `<span class="badge badge-light-warning ms-3">处理中</span>`
-								// } else if (row.process_status === 2){
-								// 	str += `<span class="badge badge-light-success ms-3">处理完成</span>`
-								//
-								// } else if (row.process_status === 3){
-								// 	str += `<span class="badge badge-light-danger ms-3">处理失败</span>`
-								// }
-							} else if (row.process_status === 200){
-								str += `<span class="badge badge-light-success">运行成功</span>`
-							} else if (row.process_status === 4){
-								str += `<span class="badge badge-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.process_error}">运行失败</span>`
-							} else if (row.process_status === 5){
-								str += `<span class="badge badge-warning">取消运行</span>`
-							}
-							return str
-						}
-					},
-					{
-						title: '操作',
-						data: 'id',
-						width: '160px',
-						render: (row) => {
-							let str = ``
-							if (row.process_status === 0) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始任务" onclick="StarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							} else if (row.process_status === 1 || row.process_status === 2) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="取消任务" onclick="CancelTask(${row.id})"><i class="ki-duotone ki-cross-square fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-							}
-							else if (row.process_status === 200) {
-								// str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-								// if (row.process_status === 0) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								// } else if (row.process_status === 2) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								//
-								// } else if (row.process_status === 3) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								// } else if (row.process_status === 4) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								// }
-								// if (row.send_status === 0) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><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 btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><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 btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								// } else if (row.send_status === 4) {
-								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								// }
-							} else if (row.process_status === 4) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							}else if (row.process_status === 5) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							}
-							if (row.process_status === 0) {
-								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>`
-							}
-							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>`
-							}
-							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>`
-							}
-							return str
-						}
-					},
-				]
-			}, true)
-		$taskBox.find('.table-box').hide()
-		$taskBox.show()
-	}
+function RenderRightBox_Custom(data) {
+    $quotaBox.data("table-url", `/api/quota/list`)
+    $('.table_radio_box .form-check').hide()
+    $rightBoxHeader.find('[name="table_radio"]').prop("disabled", false).off('change.iwb')
+    $rightBoxHeader.find('[name="table_radio"]').on('change.iwb', function () {
+        const val = $(this).val()
+        if (val === 'quota') {
+            $taskBox.hide()
+            QuotaNavTab(data)
+        } else {
+            _taskTable(data)
+        }
+    })
+    if (data.children_count > 0 || data.chapter) {
+        _renderTask(data)
+    } else {
+        $rightBoxHeader.find('.badge').text('定额输入明细').removeClass('badge-primary').addClass('badge-success')
+        $rightBoxHeader.find('#task_radio').prop("disabled", true)
+        $rightBoxHeader.find('#quota_radio').prop("checked", true)
+        QuotaNavTab(data)
+    }
+
+    function _renderTask(data) {
+        $rightBoxHeader.find('.badge').text('任务列表').removeClass('badge-success').addClass('badge-primary')
+        $rightBoxHeader.find('#task_radio').prop("checked", true)
+        const budget_id = 0
+        $taskBox.html(table_template.format(budget_id, table_add_task_btn_template.format(budget_id), table_run_select_template))
+        _taskTable(data)
+    }
+
+    function _taskTable(data) {
+        $quotaBox.hide()
+        const $table = $taskBox.find('.table')
+        IwbTable($table, {
+            url: `/api/task/list/${project_id}/${data.item_code}`,
+            columns: [
+                {
+                    title: '任务编号',
+                    data: 'id',
+                    width: '100px',
+                },
+                {
+                    title: '任务名称',
+                    data: 'task_name',
+                    width: '240px',
+                },
+                {
+                    title: '任务排序',
+                    data: 'task_sort',
+                    width: '100px',
+                },
+                {
+                    title: '概算单元',
+                    data: 'budget_id',
+                    width: '130px',
+                    render: (row) => {
+                        const budget_name = $modal.find(`[name="budget_id"] option[value='${row.budget_id}']`)
+                        return budget_name.length ? budget_name.text() : '-'
+                    }
+                },
+                {
+                    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] || ""
+                                if (!path) continue
+                                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: '180px',
+                    render: (row) => {
+                        let str = ``
+                        if (row.process_status === 0) {
+                            str += `<span class="badge badge-secondary">草稿</span>`
+                        } else if (row.process_status === 1) {
+                            str += `<span class="badge badge-light-primary">等待运行</span>`
+                        } else if (row.process_status === 2) {
+                            str += `<span class="badge badge-light-info">运行中</span>`
+                            // if (row.send_status === 0) {
+                            // 	str += `<span class="badge badge-light-primary ms-3">未发送</span>`
+                            // } else if (row.send_status === 1){
+                            // 	str += `<span class="badge badge-light-warning ms-3">发送中</span>`
+                            // } else if (row.send_status === 2){
+                            // 	str += `<span class="badge badge-light-success ms-3">发送完成</span>`
+                            // } else if (row.send_status === 3){
+                            // 	str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
+                            // }
+                            // if (row.process_status === 0) {
+                            // 	str += `<span class="badge badge-light-primary ms-3">未处理</span>`
+                            // } else if (row.process_status === 1){
+                            // 	str += `<span class="badge badge-light-warning ms-3">处理中</span>`
+                            // } else if (row.process_status === 2){
+                            // 	str += `<span class="badge badge-light-success ms-3">处理完成</span>`
+                            //
+                            // } else if (row.process_status === 3){
+                            // 	str += `<span class="badge badge-light-danger ms-3">处理失败</span>`
+                            // }
+                        } else if (row.process_status === 200) {
+                            str += `<span class="badge badge-light-success">运行成功</span>`
+                        } else if (row.process_status === 4) {
+                            str += `<span class="badge badge-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.process_error}">运行失败</span>`
+                        } else if (row.process_status === 5) {
+                            str += `<span class="badge badge-warning">取消运行</span>`
+                        }
+                        return str
+                    }
+                },
+                {
+                    title: '操作',
+                    data: 'id',
+                    width: '160px',
+                    render: (row) => {
+                        let str = ``
+                        if (row.process_status === 0) {
+                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始任务" onclick="StarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                        } else if (row.process_status === 1 || row.process_status === 2) {
+                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="取消任务" onclick="CancelTask(${row.id})"><i class="ki-duotone ki-cross-square fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+                        } else if (row.process_status === 200) {
+                            // str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                            // if (row.process_status === 0) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                            // } else if (row.process_status === 2) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                            //
+                            // } else if (row.process_status === 3) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                            // } else if (row.process_status === 4) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                            // }
+                            // if (row.send_status === 0) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><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 btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><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 btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+                            // } else if (row.send_status === 4) {
+                            // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+                            // }
+                        } else if (row.process_status === 4) {
+                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                        } else if (row.process_status === 5) {
+                            str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+                        }
+                        if (row.process_status === 0) {
+                            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>`
+                        }
+                        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>`
+                        }
+                        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>`
+                        }
+                        return str
+                    }
+                },
+            ]
+        }, true)
+        $taskBox.find('.table-box').hide()
+        $taskBox.show()
+    }
 }
 
-function SetBudgetData($el){
-	const $tableBox = $(`#body_box .right-box`)
-	$el.find('[name="project_id"]').val($tableBox.find('input[name="project_id"]').val());
-	$el.find('[name="item_id"]').val($tableBox.find('input[name="item_id"]').val());
-	$el.find('[name="item_code"]').val($tableBox.find('input[name="item_code"]').val());
+function SetBudgetData($el) {
+    const $tableBox = $(`#body_box .right-box`)
+    $el.find('[name="project_id"]').val($tableBox.find('input[name="project_id"]').val());
+    $el.find('[name="item_id"]').val($tableBox.find('input[name="item_id"]').val());
+    $el.find('[name="item_code"]').val($tableBox.find('input[name="item_code"]').val());
 }
 
 function Add() {
-	_fileUploadDropzone.removeAllFiles()
-	AddModal($modal, () => {
-		$modal.find('[name="task_id"]').val('0');
-		$modal.find('[name="budget_id"]').val(0);
-		$modal.find('[name="task_sort"]').val(0);
-		$modal.find('#delete_file_box').hide();
-		// $modal.find('[name="delete_file"]').prop('checked',false)
-		SetBudgetData($modal)
-	})
+    _fileUploadDropzone.removeAllFiles()
+    AddModal($modal, () => {
+        $modal.find('[name="task_id"]').val('0');
+        $modal.find('[name="budget_id"]').val(0);
+        $modal.find('[name="task_sort"]').val(0);
+        $modal.find('#delete_file_box').hide();
+        // $modal.find('[name="delete_file"]').prop('checked',false)
+        SetBudgetData($modal)
+    })
 }
 
 function Edit(id) {
-	_fileUploadDropzone.removeAllFiles()
-    EditModal($modal,()=>{
+    _fileUploadDropzone.removeAllFiles()
+    EditModal($modal, () => {
         IwbAjax_1({
-            url:`/api/task/get/${id}`,
-            success:res=>{
-				if(!res.success){
-					console.error(res.message)
-					return
-				}
-				const data = res.data
-				// $modal.find('#delete_file_box').show();
-				// SetBudgetData(budget_id)
-        		$modal.find('[name="task_id"]').val(data.id);
-				$modal.find('[name="budget_id"]').val(data.budget_id);
-				$modal.find('[name="project_id"]').val(data.project_id);
-				$modal.find('[name="item_id"]').val(data.item_id);
-				$modal.find('[name="item_code"]').val(data.item_code);
+            url: `/api/task/get/${id}`,
+            success: res => {
+                if (!res.success) {
+                    console.error(res.message)
+                    return
+                }
+                const data = res.data
+                // $modal.find('#delete_file_box').show();
+                // SetBudgetData(budget_id)
+                $modal.find('[name="task_id"]').val(data.id);
+                $modal.find('[name="budget_id"]').val(data.budget_id);
+                $modal.find('[name="project_id"]').val(data.project_id);
+                $modal.find('[name="item_id"]').val(data.item_id);
+                $modal.find('[name="item_code"]').val(data.item_code);
                 $modal.find('[name="task_name"]').val(data.task_name);
                 $modal.find('[name="task_desc"]').val(data.task_desc);
                 $modal.find('[name="task_sort"]').val(data.task_sort);
-				// $modal.find('[name="delete_file"]').prop('checked',false)
+                // $modal.find('[name="delete_file"]').prop('checked',false)
             }
         })
     })
@@ -366,62 +368,63 @@ function Edit(id) {
 
 function SaveProject(is_submit) {
     const
-		formData = new FormData(),
-		budget_id = $modal.find('[name="budget_id"]').val(),
-		item_id = $modal.find('[name="item_id"]').val(),
-		item_code = $modal.find('[name="item_code"]').val(),
-		project_id = $modal.find('[name="project_id"]').val(),
-		task_id=  $modal.find('[name="task_id"]').val(),
-		task_name = $modal.find('[name="task_name"]').val(),
-		task_sort = $modal.find('[name="task_sort"]').val(),
-		task_desc = $modal.find('[name="task_desc"]').val(),
-		// delete_file = $modal.find('[name="delete_file"]').prop('checked')? 'true':'false',
-		files = _fileUploadDropzone.getAcceptedFiles();
-	// console.log("FILES",files)
+        formData = new FormData(),
+        budget_id = $modal.find('[name="budget_id"]').val(),
+        item_id = $modal.find('[name="item_id"]').val(),
+        item_code = $modal.find('[name="item_code"]').val(),
+        project_id = $modal.find('[name="project_id"]').val(),
+        task_id = $modal.find('[name="task_id"]').val(),
+        task_name = $modal.find('[name="task_name"]').val(),
+        task_sort = $modal.find('[name="task_sort"]').val(),
+        task_desc = $modal.find('[name="task_desc"]').val(),
+        // delete_file = $modal.find('[name="delete_file"]').prop('checked')? 'true':'false',
+        files = _fileUploadDropzone.getAcceptedFiles();
+    // console.log("FILES",files)
 
-	if(files&&files.length>0){
-		files.forEach((file) => {
-			formData.append('files', file)
-		})
-	}else{
-		MsgWarning('文件不能为空,请选择文件')
-		return
-	}
-	formData.append('budget_id', budget_id)
-	formData.append('item_id', item_id)
-	formData.append('item_code', item_code)
-	formData.append('project_id', project_id)
-	formData.append('task_id', task_id)
-	formData.append('task_name', task_name)
-	formData.append('task_sort', task_sort)
-	formData.append('task_desc', task_desc)
-	// formData.append('delete_old', delete_file)
-	if (is_submit) {
-		formData.append('run_now', 'true')
-	}
+    if (files && files.length > 0) {
+        files.forEach((file) => {
+            formData.append('files', file)
+        })
+    } else {
+        MsgWarning('文件不能为空,请选择文件')
+        return
+    }
+    formData.append('budget_id', budget_id)
+    formData.append('item_id', item_id)
+    formData.append('item_code', item_code)
+    formData.append('project_id', project_id)
+    formData.append('task_id', task_id)
+    formData.append('task_name', task_name)
+    formData.append('task_sort', task_sort)
+    formData.append('task_desc', task_desc)
+    // formData.append('delete_old', delete_file)
+    if (is_submit) {
+        formData.append('run_now', 'true')
+    }
 
     IwbAjax({
-        url:`/api/task/save/${task_id}`,
-		headers:{},
-        body:formData,
-		modal:"#modal",
-		table:`#table_0`
+        url: `/api/task/save/${task_id}`,
+        headers: {},
+        body: formData,
+        modal: "#modal",
+        table: `#table_0`
     })
 }
 
-function Delete(id){
-	ConfirmUrl('确定删除吗?',`/api/task/delete/${id}`,`#table_0`)
+function Delete(id) {
+    ConfirmUrl('确定删除吗?', `/api/task/delete/${id}`, `#table_0`)
 }
 
-function StarTask(id){
-	ConfirmUrl('确定开始运行任务吗?',`/api/task/start_task/${id}`,`#table_0`)
+function StarTask(id) {
+    ConfirmUrl('确定开始运行任务吗?', `/api/task/start_task/${id}`, `#table_0`)
 }
 
-function ReStarTask(id){
-	ConfirmUrl('确定重新开始运行任务吗?',`/api/task/start_task/${id}`,`#table_0`)
+function ReStarTask(id) {
+    ConfirmUrl('确定重新开始运行任务吗?', `/api/task/start_task/${id}`, `#table_0`)
 }
-function CancelTask(id){
-	ConfirmUrl('确定取消运行任务吗?',`/api/task/cancel_task/${id}`,`#table_0`)
+
+function CancelTask(id) {
+    ConfirmUrl('确定取消运行任务吗?', `/api/task/cancel_task/${id}`, `#table_0`)
 }
 
 

+ 421 - 392
SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js

@@ -73,21 +73,21 @@ const quota_modal_template = `
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
-        <button type="button" class="btn btn-primary" onclick="SaveQuota(true)">提交</button>
+        <button type="button" class="btn btn-light-success" onclick="SaveQuota(true)">提交</button>
         <button type="button" class="btn btn-primary" onclick="SaveQuota()">保存草稿</button>
       </div>
     </div>
   </div>
 </div>`
 $('.app-main .app-container').append(quota_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 = `
+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">
+    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">
 												<option value="">全部运行状态</option>
 												<option value="0">草稿</option>
 												<option value="1">等待运行</option>
@@ -97,7 +97,7 @@ const	nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 												<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">
 												<option value="">全部推送状态</option>
 												<option value="0">未推送</option>
 												<option value="1">推送中</option>
@@ -105,7 +105,7 @@ const	nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 												<option value="2">推送失败</option>
 												<option value="3">数据变更</option>
 											</select>`,
-	table_template = `<div class="table-box table-responsive" data-id="{0}" id="table_box_{0}">
+    table_template = `<div class="table-box table-responsive" data-id="{0}" id="table_box_{0}">
 								<div class="d-flex justify-content-between my-5">
 									<div class="">{1}</div>
 									<form class="search-box d-flex">
@@ -125,17 +125,17 @@ const	nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 							</div>`
 
 const $modalQuota = $('#modal_quota')
-let	$rightBox, $rightBoxHeader , $rightBoxBody, $taskBox, $quotaBox, budget_id, item_code
+let $rightBox, $rightBoxHeader, $rightBoxBody, $taskBox, $quotaBox, budget_id, item_code
 
 console.log(`加载项目:${project_id}`)
 InitBody()
 $(function () {
-	BuildChapterInfo()
-	$(window).on('resize', AdjustBoxHeight)
+    BuildChapterInfo()
+    $(window).on('resize', AdjustBoxHeight)
 })
 
-function InitBody(){
-	$('#body_box').html(`<div class="d-flex flex-row project-box w-100">
+function InitBody() {
+    $('#body_box').html(`<div class="d-flex flex-row project-box w-100">
 						<div class="flex-row-auto h-100 left-box">
 							<div class="tree-dom w-300px h-100 overflow-auto" id="js-tree"></div>
 						</div>
@@ -148,7 +148,7 @@ function InitBody(){
 									<input type="hidden" name="item_code" value="">
 								</section>
 								<span class="fw-bolder me-5 title fs-2"></span>
-								<span class="badge d-none badge-primary fs-5 me-5"></span>
+								<span class="badge badge-primary fs-6 me-5"></span>
 								<div class="d-flex table_radio_box" >
 									<div class="form-check form-check-custom form-check-primary form-check-solid">
 										<input class="form-check-input" name="table_radio" type="radio" value="task"  id="task_radio"/>
@@ -170,322 +170,334 @@ function InitBody(){
 							</div>
 						</div>
 					</div>`)
-	AdjustBoxHeight()
-	$rightBox= $('#body_box .right-box'),
-	$rightBoxHeader = $('#body_box .right-box .box-header'),
-	$rightBoxBody = $('#body_box .right-box .box-body'),
-	$taskBox = $rightBoxBody.find('.task'),
-	$quotaBox = $rightBoxBody.find('.quota')
+    AdjustBoxHeight()
+    $rightBox = $('#body_box .right-box'),
+        $rightBoxHeader = $('#body_box .right-box .box-header'),
+        $rightBoxBody = $('#body_box .right-box .box-body'),
+        $taskBox = $rightBoxBody.find('.task'),
+        $quotaBox = $rightBoxBody.find('.quota')
 }
-function  AdjustBoxHeight(){
-	const h= $('.app-wrapper').height()  - $('.app-body-header').height() - $('.app-footer').height() -5
-	$('#body_box .project-box').height(h)
+
+function AdjustBoxHeight() {
+    const h = $('.app-wrapper').height() - $('.app-body-header').height() - $('.app-footer').height() - 5
+    $('#body_box .project-box').height(h)
 }
-function BuildChapterInfo(){
-	const $tree = $(`#js-tree`)
-	const opt = {
-		core: {
-			themes: {
-				responsive: false,
-			},
-			strings:{
-				'Loading ...': '加载中...',
-			},
-			check_callback: true,
-			data: function (node, callback) {
-				// console.log('TREE_NODE', node)
-				IwbAjax_1({
-					url: `/api/project/chapter/${project_id}?c=${node?.data?.item_code || ''}`,
-					success: res => {
-						if (res.success) {
-							console.log('TREE', res.data)
-							callback(res.data)
-						} else {
-							console.error(res.message)
-						}
-					},
-				})
-			},
-		},
-		types: {
-			default: {
-				icon: 'ki-outline ki-folder text-primary',
-			},
-			file: {
-				icon: 'ki-outline ki-file  text-primary',
-			},
-		},
-		plugins: ['dnd', 'types'],
-	}
-	// $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)
-		RenderRightBox(data.node?.data)
-	})
-	$tree.jstree(opt)
+
+function BuildChapterInfo() {
+    const $tree = $(`#js-tree`)
+    const opt = {
+        core: {
+            themes: {
+                responsive: false,
+            },
+            strings: {
+                'Loading ...': '加载中...',
+            },
+            check_callback: true,
+            data: function (node, callback) {
+                // console.log('TREE_NODE', node)
+                IwbAjax_1({
+                    url: `/api/project/chapter/${project_id}?c=${node?.data?.item_code || ''}`,
+                    success: res => {
+                        if (res.success) {
+                            console.log('TREE', res.data)
+                            callback(res.data)
+                        } else {
+                            console.error(res.message)
+                        }
+                    },
+                })
+            },
+        },
+        types: {
+            default: {
+                icon: 'ki-outline ki-folder text-primary',
+            },
+            file: {
+                icon: 'ki-outline ki-file  text-primary',
+            },
+        },
+        plugins: ['dnd', 'types'],
+    }
+    // $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)
+        RenderRightBox(data.node?.data)
+    })
+    $tree.jstree(opt)
 }
 
-function RenderRightBox(data){
-	console.log('RenderRightBox', arguments)
-	$rightBoxBody.data('data',data)
-	$rightBox.find('input[name="budget_id"]').val(data.budget_id);
-	$rightBox.find('input[name="project_id"]').val(project_id);
-	$rightBox.find('input[name="item_id"]').val(data.item_id);
-	$rightBox.find('input[name="item_code"]').val(data.item_code);
-	const title = data.chapter ?`${data.chapter}、${data.project_name}`: data.section ?`${data.section}.${data.project_name}`: data.project_name
-	$rightBoxHeader.find('.title').text(title)
-	RenderRightBox_Custom(data)
-	$rightBox.show()
+function RenderRightBox(data) {
+    console.log('RenderRightBox', arguments)
+    $rightBoxBody.data('data', data)
+    $rightBox.find('input[name="budget_id"]').val(data.budget_id);
+    $rightBox.find('input[name="project_id"]').val(project_id);
+    $rightBox.find('input[name="item_id"]').val(data.item_id);
+    $rightBox.find('input[name="item_code"]').val(data.item_code);
+    const title = data.chapter ? `${data.chapter}、${data.project_name}` : data.section ? `${data.section}.${data.project_name}` : data.project_name
+    $rightBoxHeader.find('.title').text(title)
+    RenderRightBox_Custom(data)
+    $rightBox.show()
 }
-function  RenderRightBox_Custom(data){
-	$('.table_radio_box .form-check').hide()
-	$quotaBox.data("table-url",`/api/quota/list/task/${task_id}`)
-	QuotaNavTab(data)
+
+function RenderRightBox_Custom(data) {
+    $('.table_radio_box .form-check').hide()
+    $quotaBox.data("table-url", `/api/quota/list/task/${task_id}`)
+    QuotaNavTab(data)
 }
-function QuotaNavTab(){
-	$taskBox.hide()
-	if(!$quotaBox.find('#nav_tab').length){
-		$quotaBox.html(nav_template)
-		BuildBudgetInfo()
-	}else{
-		const budget = $quotaBox.find('#nav_tab').data('budget-info') || {}
-		$quotaBox.html(nav_template)
-		RenderTabs(budget)
-	}
-	$quotaBox.find('.table-box').hide()
-	$quotaBox.show()
+
+function QuotaNavTab() {
+    $taskBox.hide()
+    if (!$quotaBox.find('#nav_tab').length) {
+        $quotaBox.html(nav_template)
+        BuildBudgetInfo()
+    } else {
+        const budget = $quotaBox.find('#nav_tab').data('budget-info') || {}
+        $quotaBox.html(nav_template)
+        RenderTabs(budget)
+    }
+    $quotaBox.find('.table-box').hide()
+    $quotaBox.show()
 }
 
 function BuildBudgetInfo() {
-	IwbAjax_1({
-		url: `/api/project/budget/${project_id}`,
-		success: function (res) {
-			if (res.success) {
-				RenderTabs(res.data)
-			}else{
-				console.error(res.message)
-			}
-		},
-	})
+    IwbAjax_1({
+        url: `/api/project/budget/${project_id}`,
+        success: function (res) {
+            if (res.success) {
+                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)
-			const tableStr = table_template.format(item.budget_id,table_add_quota_btn_template.format(item.budget_id),table_send_select_template)
-			const tabContent = tab_content_template.format(item.budget_id, tableStr)
-			// console.log('TAB_CONTENT', tabContent)
-			str2 += tabContent
-		}
-	}
-	const $tab = $('#nav_tab'),$content =$('#tab_content')
-	$tab.html(str1).data('budget-info', data)
-	$content.html(str2)
-	const $tab_btn = $tab.find('li button[data-bs-toggle="tab"]')
-	$tab_btn.on('shown.bs.tab',(e)=>{
-		console.log('TAB', e)
-		const tab_id = $(e.target).data('id'),
-			data = $("#body_box .right-box .box-body").data('data')
-		budget_id = tab_id
-		item_code = data.item_code
-		RenderQuotaTable(data)
-	})
-	const firstTab = new bootstrap.Tab($tab_btn.eq(0))
-	firstTab.show()
+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)
+            const tableStr = table_template.format(item.budget_id, table_add_quota_btn_template.format(item.budget_id), table_send_select_template)
+            const tabContent = tab_content_template.format(item.budget_id, tableStr)
+            // console.log('TAB_CONTENT', tabContent)
+            str2 += tabContent
+        }
+    }
+    const $tab = $('#nav_tab'), $content = $('#tab_content')
+    $tab.html(str1).data('budget-info', data)
+    $content.html(str2)
+    const $tab_btn = $tab.find('li button[data-bs-toggle="tab"]')
+    $tab_btn.on('shown.bs.tab', (e) => {
+        console.log('TAB', e)
+        const tab_id = $(e.target).data('id'),
+            data = $("#body_box .right-box .box-body").data('data')
+        budget_id = tab_id
+        item_code = data.item_code
+        RenderQuotaTable(data)
+    })
+    const firstTab = new bootstrap.Tab($tab_btn.eq(0))
+    firstTab.show()
 }
 
-function RenderQuotaTable(data){
-	console.log('RenderQuotaTable', budget_id, data)
-	const $table = $quotaBox.find(`#table_${budget_id}`)
-	LoadQuotaTable($table)
+function RenderQuotaTable(data) {
+    console.log('RenderQuotaTable', budget_id, data)
+    const $table = $quotaBox.find(`#table_${budget_id}`)
+    LoadQuotaTable($table)
 }
 
-function  LoadQuotaTable(table){
-	const url = `${$quotaBox.data("table-url")}/${budget_id}/${project_id}/${item_code}`
-    IwbTable(table,{
-		url,
-		checkBox: true,
-		columns: [
-			{
-				title: '序号',
-				data: 'id',
-				width: '80px',
-			},
-			{
-				title: '工程或费用项目名称',
-				data: 'entry_name',
-				width: '210px',
-				render: (row) => {
-						return `<span class="one-line mw-200px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.entry_name}" >${row.entry_name}</span>`
-				}
-			},
-			{
-				title: '工程数量',
-				data: 'amount',
-				width: '100px',
-			},
-			{
-				title: '单位',
-				data: 'units',
-				width: '80px',
-			},
-			{
-				title: '定额编号',
-				data: 'quota_code',
-				width: '100px',
-			},
-			{
-				title: 'Excel文件',
-				data: 'ex_file',
-				width: '150px',
-				render: (row) => {
-					const path = row.ex_file
-					if (!path) {
-						return '-'
-					}
-					const names = path.split('/')
-					const file_name = names[names.length - 1]
-					return `<span class="one-line mw-150px"><a href="#" onclick="DownloadFile('/api/task/download?filename=${encodeURIComponent(path)}','${file_name}')"  class="link link-info px-2">${file_name}</a></span>`
-				}
-			},
-			{
-				title: 'Excel整行内容',
-				data: 'ex_row',
-				width: 'auto',
-				render: (row) => {
-					return `<span class="one-line w-300px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.ex_row}" >${row.ex_row}</span>`
-				}
-			},
-			{
-				title: 'Excel数量',
-				data: 'ex_amount',
-				width: '100px',
-			},
-			{
-				title: '数量位置',
-				data: 'ex_cell',
-				width: '80px',
-			},
-			{
-				title: '数量单位',
-				data: 'ex_unit',
-				width: '80px',
-			},
-			{
-				title: '状态',
-				data: 'status',
-				width: '100px',
-				render: (row) => {
-					let str=''
-					// if(row.process_status === 0){
-					// 	str+= `<span class="badge badge-primary">未处理</span>`
-					// }else if (row.process_status === 1){
-					// 	str+= `<span class="badge badge-warning">处理中</span>`
-					// }else if (row.process_status === 2){
-					// 	str+= `<span class="badge badge-success">已处理</span>`
-					// }else if (row.process_status === 3){
-					// 	str+= `<span class="badge badge-danger">处理失败</span>`
-					// }else if (row.process_status === 4){
-					// 	str+= `<span class="badge badge-danger">数据变更</span>`
-					// }
-					if(row.send_status === 0){
-						str+= `<span class="badge badge-primary ms-3">未推送</span>`
-					}else if (row.send_status === 1){
-						str+= `<span class="badge badge-warning ms-3">推送中</span>`
-					}else if (row.send_status === 200){
-						str+= `<span class="badge badge-success ms-3">推送成功</span>`
-					}else if (row.send_status === 2){
-						str+= `<span class="badge badge-danger ms-3">推送失败</span>`
-					}else if (row.send_status === 3){
-						str+= `<span class="badge badge-danger ms-3">数据变更</span>`
-					}
+function LoadQuotaTable(table) {
+    const url = `${$quotaBox.data("table-url")}/${budget_id}/${project_id}/${item_code}`
+    IwbTable(table, {
+        url,
+        checkBox: true,
+        columns: [
+            {
+                title: '序号',
+                data: 'id',
+                width: '80px',
+            },
+            {
+                title: '工程或费用项目名称',
+                data: 'entry_name',
+                width: '210px',
+                render: (row) => {
+                    return `<span class="one-line mw-200px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.entry_name}" >${row.entry_name}</span>`
+                }
+            },
+            {
+                title: '工程数量',
+                data: 'amount',
+                width: '100px',
+            },
+            {
+                title: '单位',
+                data: 'units',
+                width: '80px',
+            },
+            {
+                title: '定额编号',
+                data: 'quota_code',
+                width: '100px',
+                render: (row) => {
+                    if (row.quota_id) {
+                        return `<span class="badge badge-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="可覆盖系统数据:${row.quota_id}" >${row.quota_code}</span>`
+                    } else {
+                        return `<span class="badge badge-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="未关联系统数据" >${row.quota_code}</span>`
+                    }
+                }
+            },
+            {
+                title: 'Excel文件',
+                data: 'ex_file',
+                width: '150px',
+                render: (row) => {
+                    const path = row.ex_file
+                    if (!path) {
+                        return '-'
+                    }
+                    const names = path.split('/')
+                    const file_name = names[names.length - 1]
+                    return `<span class="one-line mw-150px"><a href="#" onclick="DownloadFile('/api/task/download?filename=${encodeURIComponent(path)}','${file_name}')"  class="link link-info px-2">${file_name}</a></span>`
+                }
+            },
+            {
+                title: 'Excel整行内容',
+                data: 'ex_row',
+                width: 'auto',
+                render: (row) => {
+                    return `<span class="one-line w-300px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.ex_row}" >${row.ex_row}</span>`
+                }
+            },
+            {
+                title: 'Excel数量',
+                data: 'ex_amount',
+                width: '100px',
+            },
+            {
+                title: '数量位置',
+                data: 'ex_cell',
+                width: '80px',
+            },
+            {
+                title: '数量单位',
+                data: 'ex_unit',
+                width: '80px',
+            },
+            {
+                title: '状态',
+                data: 'status',
+                width: '100px',
+                render: (row) => {
+                    let str = ''
+                    // if(row.process_status === 0){
+                    // 	str+= `<span class="badge badge-primary">未处理</span>`
+                    // }else if (row.process_status === 1){
+                    // 	str+= `<span class="badge badge-warning">处理中</span>`
+                    // }else if (row.process_status === 2){
+                    // 	str+= `<span class="badge badge-success">已处理</span>`
+                    // }else if (row.process_status === 3){
+                    // 	str+= `<span class="badge badge-danger">处理失败</span>`
+                    // }else if (row.process_status === 4){
+                    // 	str+= `<span class="badge badge-danger">数据变更</span>`
+                    // }
+                    if (row.send_status === 0) {
+                        str += `<span class="badge badge-primary ms-3">未推送</span>`
+                    } else if (row.send_status === 1) {
+                        str += `<span class="badge badge-warning ms-3">推送中</span>`
+                    } else if (row.send_status === 200) {
+                        str += `<span class="badge badge-success ms-3">推送成功</span>`
+                    } else if (row.send_status === 2) {
+                        str += `<span class="badge badge-danger ms-3" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.send_error}">推送失败</span>`
+                    } else if (row.send_status === 3) {
+                        str += `<span class="badge badge-danger ms-3">数据变更</span>`
+                    }
 
-					return str
-				}
-			},
-			{
-				title: '操作',
-				data: 'id',
-				width: '120px',
-				render: (row) => {
-					let str=''
-					// if (row.process_status === 0) {
-					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					// } else if (row.process_status === 2) {
-					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					//
-					// } else if (row.process_status === 3) {
-					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					// } else if (row.process_status === 4) {
-					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					// }
-					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>`
-					} 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>`
-					return str
-				}
-			},
-		]
-	}, true)
+                    return str
+                }
+            },
+            {
+                title: '操作',
+                data: 'id',
+                width: '120px',
+                render: (row) => {
+                    let str = ''
+                    // if (row.process_status === 0) {
+                    // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                    // } else if (row.process_status === 2) {
+                    // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                    //
+                    // } else if (row.process_status === 3) {
+                    // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                    // } else if (row.process_status === 4) {
+                    // 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+                    // }
+                    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>`
+                    } 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>`
+                    return str
+                }
+            },
+        ]
+    }, true)
 }
 
-function SetBudgetData($el){
-	const $tableBox = $(`.table-box`)
-	$el.find('[name="project_id"]').val($tableBox.find('input[name="project_id"]').val());
-	$el.find('[name="item_id"]').val($tableBox.find('input[name="item_id"]').val());
-	$el.find('[name="item_code"]').val($tableBox.find('input[name="item_code"]').val());
+function SetBudgetData($el) {
+    const $tableBox = $(`.table-box`)
+    $el.find('[name="project_id"]').val($tableBox.find('input[name="project_id"]').val());
+    $el.find('[name="item_id"]').val($tableBox.find('input[name="item_id"]').val());
+    $el.find('[name="item_code"]').val($tableBox.find('input[name="item_code"]').val());
 }
+
 function Add_Quota(budget_id,) {
-	AddModal($modalQuota, () => {
-		SetBudgetData($modalQuota)
-		$modalQuota.find('[name="id"]').val('0');
-		$modalQuota.find('[name="quota_id"]').val('0');
-		$modalQuota.find('[name="task_id"]').val('0');
-		$modalQuota.find('[name="budget_id"]').val(budget_id);
-		$modalQuota.find('[name="is_cover"]').val('0')
-		$modalQuota.find('.is_cover_box').hide()
-	})
+    AddModal($modalQuota, () => {
+        SetBudgetData($modalQuota)
+        $modalQuota.find('[name="id"]').val('0');
+        $modalQuota.find('[name="quota_id"]').val('0');
+        $modalQuota.find('[name="task_id"]').val('0');
+        $modalQuota.find('[name="budget_id"]').val(budget_id);
+        $modalQuota.find('[name="is_cover"]').val('0')
+        $modalQuota.find('.is_cover_box').hide()
+    })
 }
 
 function Edit_Quota(id) {
-	$modalQuota.find('.is_cover_box').hide()
-    EditModal($modalQuota,()=>{
+    $modalQuota.find('.is_cover_box').hide()
+    EditModal($modalQuota, () => {
         IwbAjax_1({
-            url:`/api/quota/get/${id}`,
-            success:res=>{
-				if(!res.success){
-					console.error(res.message)
-					return
-				}
-				const data = res.data,
-				// SetBudgetData(budget_id)
-				quota_id = data.quota_id
-        		$modalQuota.find('[name="id"]').val(data.id);
-        		$modalQuota.find('[name="quota_id"]').val(quota_id);
-        		$modalQuota.find('[name="task_id"]').val(data.task_id);
-				$modalQuota.find('[name="budget_id"]').val(data.budget_id);
-				$modalQuota.find('[name="project_id"]').val(data.project_id);
-				$modalQuota.find('[name="item_id"]').val(data.item_id);
-				$modalQuota.find('[name="item_code"]').val(data.item_code);
+            url: `/api/quota/get/${id}`,
+            success: res => {
+                if (!res.success) {
+                    console.error(res.message)
+                    return
+                }
+                const data = res.data,
+                    // SetBudgetData(budget_id)
+                    quota_id = data.quota_id
+                $modalQuota.find('[name="id"]').val(data.id);
+                $modalQuota.find('[name="quota_id"]').val(quota_id);
+                $modalQuota.find('[name="task_id"]').val(data.task_id);
+                $modalQuota.find('[name="budget_id"]').val(data.budget_id);
+                $modalQuota.find('[name="project_id"]').val(data.project_id);
+                $modalQuota.find('[name="item_id"]').val(data.item_id);
+                $modalQuota.find('[name="item_code"]').val(data.item_code);
                 $modalQuota.find('[name="entry_name"]').val(data.entry_name);
                 $modalQuota.find('[name="amount"]').val(data.amount);
                 $modalQuota.find('[name="units"]').val(data.units);
@@ -494,115 +506,132 @@ function Edit_Quota(id) {
                 $modalQuota.find('#ex_amount').html(data.ex_amount);
                 $modalQuota.find('#ex_cell').html(data.ex_cell);
                 $modalQuota.find('#ex_unit').html(data.ex_unit);
-				$modalQuota.find('[name="is_cover"]').val('0')
-				if(quota_id){
-					$modalQuota.find('.is_cover_box').show()
-				}
+                $modalQuota.find('[name="is_cover"][value="0"]').prop('checked', true)
+                if (quota_id) {
+                    $modalQuota.find('.is_cover_box').show()
+                }
             }
         })
     })
 }
 
-function SaveQuota(isSubmit){
-	const id = $modalQuota.find('[name="id"]').val(),
-		quota_id = $modalQuota.find('[name="quota_id"]').val(),
-		task_id = $modalQuota.find('[name="task_id"]').val(),
-		budget_id = $modalQuota.find('[name="budget_id"]').val(),
-		project_id = $modalQuota.find('[name="project_id"]').val(),
-		item_id = $modalQuota.find('[name="item_id"]').val(),
-		item_code = $modalQuota.find('[name="item_code"]').val(),
-		entry_name = $modalQuota.find('[name="entry_name"]').val(),
-		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"]').val()
-	IwbAjax({
-		url:`/api/quota/save`,
-		data:{
-			id,
-			quota_id,
-			task_id,
-			budget_id,
-			project_id,
-			item_id,
-			item_code,
-			entry_name,
-			amount,
-			units,
-			quota_code,
-			run_now: isSubmit?'true':'false',
-			is_cover: is_cover?'true':'false'
-		},
-		modal:$modalQuota,
-		table:`#table_${budget_id}`
-	})
+function SaveQuota(isSubmit) {
+    const id = $modalQuota.find('[name="id"]').val(),
+        quota_id = $modalQuota.find('[name="quota_id"]').val(),
+        task_id = $modalQuota.find('[name="task_id"]').val(),
+        budget_id = $modalQuota.find('[name="budget_id"]').val(),
+        project_id = $modalQuota.find('[name="project_id"]').val(),
+        item_id = $modalQuota.find('[name="item_id"]').val(),
+        item_code = $modalQuota.find('[name="item_code"]').val(),
+        entry_name = $modalQuota.find('[name="entry_name"]').val(),
+        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()
+    IwbAjax({
+        url: `/api/quota/save`,
+        data: {
+            id,
+            quota_id,
+            task_id,
+            budget_id,
+            project_id,
+            item_id,
+            item_code,
+            entry_name,
+            amount,
+            units,
+            quota_code,
+            run_now: isSubmit ? 'true' : 'false',
+            is_cover: is_cover === '1' || is_cover === 1 ? 'true' : 'false',
+        },
+        modal: $modalQuota,
+        table: `#table_${budget_id}`
+    })
 
 
 }
 
-function Delete_Quota(id,budget_id){
-	ConfirmUrl('确定删除吗?',`/api/quota/delete/${id}`,`#table_${budget_id}`)
+function Delete_Quota(id, budget_id) {
+    ConfirmUrl('确定删除吗?', `/api/quota/delete/${id}`, `#table_${budget_id}`)
 }
 
-// function StartProcessQuota(id,budget_id){
-// 	ConfirmUrl('确定开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
-// }
-// function ReStartProcessQuota(id,budget_id){
-// 	ConfirmUrl('确定重新开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
-// }
-
-function StartSendQuota(ids,budget_id,is_cover){
-	SendQuota('确定开始推送数据吗?',ids,budget_id,is_cover)
+function StartSendQuota(ids, budget_id, is_cover) {
+    SendQuota('确定开始推送数据吗?', ids, budget_id, is_cover)
 }
-function ReStartSendQuota(ids,budget_id,is_cover){
-	SendQuota('确定重新开始推送数据吗?',ids,budget_id,is_cover)
+
+function ReStartSendQuota(ids, budget_id, is_cover) {
+    SendQuota('确定重新开始推送数据吗?', ids, budget_id, is_cover)
 }
-function Send_Quota_Batch(budget_id){
-	const $table = `#table_${budget_id}`,
-		select_ids = IwbTableGetSelectedIds($table)
-	if(select_ids.length){
-		const select_ids_str = select_ids.join(',')
-		SendQuota(`确定批量推送这些数据吗?[${select_ids_str}]`,select_ids_str,budget_id,true)
-	}else{
-		MsgWarning('没有选择数据,请选择要推送的数据!')
-	}
+
+function Send_Quota_Batch(budget_id) {
+    const $table = `#table_${budget_id}`,
+        selectedRows = IwbTableGetSelectedRows($table)
+    if (selectedRows.length) {
+        const ids = [], insert_ids = [], update_ids = []
+        for (let i = 0; i < selectedRows.length; i++) {
+            const row = selectedRows[i]
+            if (row.quota_id > 0) {
+                update_ids.push(row.id)
+            } else {
+                insert_ids.push(row.id)
+            }
+            ids.push(row.id)
+        }
+        const ids_str = ids.join(','), insert_ids_str = insert_ids.join(','), update_ids_str = update_ids.join(',')
+        let str = `<div class="">确定批量推送这些数据吗?部分数据只能新增,无法覆盖,请谨慎操作!</div>`
+        if (insert_ids_str) {
+            str += `<div class="text-primary d-flex align-items-center"><strong style="white-space: nowrap">新增ID:</strong>${insert_ids_str}</div>`
+        }
+        if (update_ids_str) {
+            str += `<div class="text-danger d-flex align-items-center"><strong style="white-space: nowrap">覆盖(修改)ID:</strong>${update_ids_str}</div>`
+        }
+        SendQuota(str, ids_str, budget_id, true, true)
+    } else {
+        MsgWarning('没有选择数据,请选择要推送的数据!')
+    }
 }
-function SendQuota(title,ids,budget_id,is_cover){
-	const opts={
+
+function SendQuota(title, ids, budget_id, is_cover, is_html) {
+    const opts = {
         text: title,
         icon: "info",
         buttonsStyling: false,
-		showCancelButton: true,
-		showConfirmButton: true,
-		showDenyButton: !!is_cover,
+        showCancelButton: true,
+        showConfirmButton: true,
+        showDenyButton: !!is_cover,
         cancelButtonText: "取消",
         confirmButtonText: "新增",
         denyButtonText: "覆盖",
         customClass: {
-			cancelButton: "btn btn-light mx-2",
+            cancelButton: "btn btn-light mx-2",
             confirmButton: "btn btn-primary mx-2",
             denyButton: "btn btn-danger mx-2"
         },
-		toast:false
+        toast: false
+    }
+    if (is_html) {
+        opts.html = `${title}`
+    }
+    Swal.fire(opts).then((result) => {
+        console.log("CONFIRM", result)
+        if (result.isConfirmed) {
+            _send(false)
+        } else if (result.isDenied) {
+            _send(true)
+        }
+    });
+
+    function _send(is_cover) {
+        console.log("is_cover", is_cover, ids)
+        IwbAjax({
+            url: `/api/quota/start_send`,
+            data: {
+                ids: ids + "",
+                is_cover: is_cover ? 'true' : 'false'
+            },
+            modal: $modalQuota,
+            table: `#table_${budget_id}`
+        })
     }
-	Swal.fire(opts).then((result)=>{
-		console.log("CONFIRM",result)
-		if(result.isConfirmed){
-			_send(false)
-		}else if(result.isDenied){
-			_send(true)
-		}
-	});
-	function _send(is_cover){
-		console.log("is_cover",is_cover,ids)
-		IwbAjax({
-			url:`/api/quota/start_send`,
-			data:{
-				ids,
-				is_cover:is_cover?'true':'false'
-			},
-			modal:$modalQuota,
-			table:`#table_${budget_id}`
-		})
-	}
 }