Эх сурвалжийг харах

update 完善最新需求:模板任务

yue 2 долоо хоног өмнө
parent
commit
cc60a97fb3
19 өөрчлөгдсөн 624 нэмэгдсэн , 109 устгасан
  1. 1 1
      SourceCode/IntelligentRailwayCosting/.script/cmd/Mysql_BuildRcApp_1.x.run.xml
  2. 2 0
      SourceCode/IntelligentRailwayCosting/.script/init_mysql.sql
  3. 8 0
      SourceCode/IntelligentRailwayCosting/.script/update.sql
  4. 1 1
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose_mysql.yml
  5. 6 3
      SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py
  6. 9 1
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py
  7. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/task.py
  8. 8 1
      SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py
  9. 2 1
      SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py
  10. 10 0
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  11. 84 26
      SourceCode/IntelligentRailwayCosting/app/routes/project_task.py
  12. 50 0
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  13. 106 26
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  14. 2 1
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py
  15. 61 2
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_task.py
  16. 58 8
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py
  17. 44 5
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js
  18. 151 26
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  19. 19 7
      SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js

+ 1 - 1
SourceCode/IntelligentRailwayCosting/.script/cmd/Mysql_BuildRcApp_1.x.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="Mysql_BuildRcApp_1.x" type="docker-deploy" factoryName="dockerfile" server-name="104">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="railway_costing-app_mysql:1.0.7" />
+        <option name="imageTag" value="railway_costing-app_mysql:1.0.9" />
         <option name="buildOnly" value="true" />
         <option name="contextFolderPath" value="." />
         <option name="sourceFilePath" value="docker/Dockerfile" />

+ 2 - 0
SourceCode/IntelligentRailwayCosting/.script/init_mysql.sql

@@ -9,8 +9,10 @@ CREATE TABLE IF NOT EXISTS project_task (
     task_sort INT DEFAULT 0 COMMENT '任务排序',
     task_desc VARCHAR(1000) COMMENT '任务描述',
     task_type INT NOT NULL DEFAULT 0 COMMENT '任务类型',
+    is_template TINYINT DEFAULT 0 COMMENT '是否模板',
     is_cover TINYINT DEFAULT 0 COMMENT '是否覆盖',
     project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    project_name VARCHAR(500) NOT NULL COMMENT '项目名称',
     budget_id INT NOT NULL DEFAULT 0 COMMENT '概算序号',
     item_id INT NOT NULL COMMENT '条目序号',
     item_code VARCHAR(255) NOT NULL COMMENT '条目编号',

+ 8 - 0
SourceCode/IntelligentRailwayCosting/.script/update.sql

@@ -0,0 +1,8 @@
+
+-- 添加 is_template 字段
+ALTER TABLE project_task
+ADD COLUMN  is_template TINYINT DEFAULT 0 COMMENT '是否模板' AFTER task_type;
+
+-- 添加 project_name 字段
+ALTER TABLE project_task
+ADD COLUMN  project_name VARCHAR(500) NOT NULL COMMENT '项目名称' AFTER project_id;

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

@@ -22,7 +22,7 @@ services:
     build:
       context: ../
       dockerfile: .
-    image: railway_costing-app_mysql:1.0.7
+    image: railway_costing-app_mysql:1.0.9
     container_name: railway_costing-app_mysql
     depends_on:
       - rc-mysql

+ 6 - 3
SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py

@@ -56,12 +56,13 @@ class ExcelParseDto:
     def __init__(
         self,
         task_id: int,
+        parent_id: int,
         version: str,
         project_id: str,
         project_name: str,
         project_stage: str,
         selected_zgs_id: int,
-        is_matching_task: int,  # 0-普通任务,1-匹配任务
+        task_type: int,  # 0-普通任务,1-匹配任务
         zgs_list: list[ExcelParseZgsDto],
         selected_chapter: ExcelParseItemDto,
         # hierarchy: list[ExcelParseItemDto],
@@ -69,13 +70,14 @@ class ExcelParseDto:
         files: list[ExcelParseFileDto],
     ):
         self.task_id = task_id
+        self.parent_id = parent_id
         self.version = version
         self.project_id = project_id
         self.project_name = project_name
         self.project_stage = project_stage
         self.selected_zgs_id = -1 if selected_zgs_id == 0 else selected_zgs_id
         self.zgs_list = zgs_list
-        self.is_matching_task = is_matching_task
+        self.task_type = task_type
         self.selected_chapter = selected_chapter
         # self.hierarchy = hierarchy
         # self.components = components
@@ -84,12 +86,13 @@ class ExcelParseDto:
     def to_dict(self):
         data = {
             "task_id": self.task_id,
+            "parent_id": self.parent_id,
             "version": self.version,
             "project_id": self.project_id,
             "project_name": self.project_name,
             "project_stage": self.project_stage,
             "selected_zgs_id": self.selected_zgs_id,
-            "is_matching_task": self.is_matching_task,
+            "task_type": self.task_type,
             "files": [file.to_dict() for file in self.files],
             "zgs_list": [zgs.to_dict() for zgs in self.zgs_list],
             "selected_chapter": self.selected_chapter.to_dict(),

+ 9 - 1
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py

@@ -13,8 +13,10 @@ class ProjectTaskDto(BaseModel):
     task_sort: int = 0
     task_desc: Optional[str] = None
     task_type: int = 0
+    is_template: Optional[int] = 0
     is_cover: Optional[int] = 0
     project_id: str
+    project_name: Optional[str] = None
     budget_id: Optional[int] = 0
     item_id: int
     item_code: Optional[str] = None
@@ -32,6 +34,7 @@ class ProjectTaskDto(BaseModel):
     created_at: Optional[datetime] = None
     updated_by: Optional[str] = None
     updated_at: Optional[datetime] = None
+    can_operate: Optional[bool] = False
 
     @classmethod
     def from_model(cls, model: ProjectTaskModel) -> "ProjectTaskDto":
@@ -43,8 +46,10 @@ class ProjectTaskDto(BaseModel):
             task_sort=model.task_sort,
             task_desc=model.task_desc,
             task_type=model.task_type,
+            is_template=model.is_template,
             is_cover=model.is_cover,
             project_id=model.project_id,
+            project_name=model.project_name,
             budget_id=model.budget_id,
             item_id=model.item_id,
             item_code=model.item_code,
@@ -73,7 +78,10 @@ class ProjectTaskDto(BaseModel):
 
     def to_dict(self) -> dict:
         """转换为字典格式"""
-        return self.model_dump()
+        data = self.model_dump()
+        if self.created_at:
+            data["created_at"] = self.created_at.strftime("%Y-%m-%d %H:%M:%S")
+        return data
 
     def get_path(self):
         return f"{self.project_id}_{self.item_id}({self.item_code})_{self.id}/{datetime.now().strftime('%Y%m%d%H%M%S')}"

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/task.py

@@ -9,6 +9,7 @@ class TaskStatusEnum(Enum):
     FAILURE = 4
     CANCELED = 5
     CHANGE = 6
+    TEMPLATE = 7
 
     @classmethod
     def get_name(cls, status: int):
@@ -27,6 +28,7 @@ class TaskStatusEnum(Enum):
             "运行失败": cls.FAILURE,
             "取消运行": cls.CANCELED,
             "已修改": cls.CHANGE,
+            "模板": cls.TEMPLATE,
         }
 
 

+ 8 - 1
SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py

@@ -14,12 +14,19 @@ class ProjectTaskModel(Base):
     task_sort = Column(Integer, nullable=False, default=0, comment="任务排序")
     task_desc = Column(String(1000), comment="任务描述")
     task_type = Column(
-        Integer, nullable=False, default=0, comment="任务类型 0:普通任务 1:匹配任务"
+        Integer,
+        nullable=False,
+        default=0,
+        comment="任务类型 0:普通任务 1:匹配任务 2:模板任务",
+    )
+    is_template = Column(
+        Integer, nullable=False, default=0, comment="是否模板任务(0:否, 1:是)"
     )
     is_cover = Column(
         Integer, nullable=False, default=0, comment="是否覆盖(0:不覆盖, 1:覆盖)"
     )
     project_id = Column(String(50), nullable=False, comment="项目编号")
+    project_name = Column(String(500), nullable=False, comment="项目名称")
     budget_id = Column(Integer, comment="概算序号")
     item_id = Column(Integer, nullable=False, comment="条目序号")
     item_code = Column(String(255), nullable=False, comment="条目编号")

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

@@ -207,12 +207,13 @@ class TaskProcessor:
             chapter_data = ExcelParseItemDto.from_dto(chapter)
             data = ExcelParseDto(
                 task_id=task.id or 0,
+                parent_id=task.parent_id or 0,
                 version=configs.app.version or "2020",
                 project_id=task.project_id,
                 project_name=project.project_name,
                 project_stage=project.design_stage,
                 selected_zgs_id=task.budget_id or 0,
-                is_matching_task=0 if task.task_type == 0 else 1,
+                task_type=task.task_type,
                 zgs_list=budgets_data,
                 selected_chapter=chapter_data,
                 # hierarchy=parents_data,

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

@@ -126,6 +126,16 @@ def delete_quota(quota_id: int):
         return ResponseBase.error(f"删除定额条目失败:{str(e)}")
 
 
+@project_quota_api.route("/delete_batch/<quota_ids>", methods=["POST"])
+@Permission.authorize
+def delete_quota_batch(quota_ids: str):
+    try:
+        quota_service.delete_quota_batch(quota_ids)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"批量删除定额条目失败:{str(e)}")
+
+
 @project_quota_api.route("/start_send", methods=["POST"])
 @Permission.authorize
 def start_send():

+ 84 - 26
SourceCode/IntelligentRailwayCosting/app/routes/project_task.py

@@ -45,12 +45,14 @@ def get_task(task_id: int):
 def check_is_all_send(task_id: int):
     try:
         task = task_service.get_task(task_id)
-        if task.task_type != 0:
-            return ResponseBase.error("该任务类型不能重新提交!")
+        # if task.task_type != 0:
+        #     return ResponseBase.error("该任务类型不能重新提交!")
         if task.process_status != TaskStatusEnum.SUCCESS.value:
             return ResponseBase.error("该任务未完成运行,不能重新提交!")
         if not task_service.is_all_send(task_id):
-            return ResponseBase.error("该任务中还有未推送的条目,不能重新提交!")
+            return ResponseBase.error(
+                "该任务中还有未推送(未确认)的条目,不能重新提交!"
+            )
         return ResponseBase.success({"check": True})
     except Exception as e:
         return ResponseBase.error(f"检查任务是否全部推送失败:{str(e)}")
@@ -78,6 +80,8 @@ def save_task(task_id: int):
         # 验证必要参数
         if not all([project_id, task_name]):
             return ResponseBase.error("缺少必要参数:project_id、task_name")
+        # if not files or len(files) == 0:
+        #     return ResponseBase.error("缺少必要参数:files")
         # 构建任务DTO
         task_dto = ProjectTaskDto(
             item_id=item_id,
@@ -85,10 +89,12 @@ def save_task(task_id: int):
             project_id=project_id,
             item_code=item_code,
             parent_id=parent_id,
+            project_name=None,
             task_name=task_name,
             task_desc=task_desc,
             task_sort=task_sort,
             task_type=task_type,
+            is_template=0,
             file_path=None,
         )
 
@@ -142,29 +148,6 @@ def download_file():
     )
 
 
-# @project_task_api.route('/start_collect/<int:task_id>', methods=['POST'])
-# @Permission.authorize
-# def start_collect(task_id:int):
-#     try:
-#         msg = task_service.start_run_task(task_id)
-#         if msg:
-#             return ResponseBase.error(msg)
-#         return ResponseBase.success('启动采集成功')
-#     except Exception as e:
-#         return ResponseBase.error(f'启动采集失败:{str(e)}')
-
-# @project_task_api.route('/start_process/<int:task_id>', methods=['POST'])
-# @Permission.authorize
-# def start_process(task_id:int):
-#     try:
-#         msg = task_service.start_process(task_id)
-#         if msg:
-#             return ResponseBase.error(msg)
-#         return ResponseBase.success('启动处理成功')
-#     except Exception as e:
-#         return ResponseBase.error(f'启动处理失败:{str(e)}')
-
-
 @project_task_api.route("/start_task/<int:task_id>", methods=["POST"])
 @Permission.authorize
 def start_task(task_id: int):
@@ -203,3 +186,78 @@ def start_send(task_id: int):
         return ResponseBase.success(message="启动发送成功")
     except Exception as e:
         return ResponseBase.error(f"启动发送失败:{str(e)}")
+
+
+@project_task_api.route("/template/list", methods=["POST"])
+@Permission.authorize
+def get_task_template_paginated():
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        project_name = data.get("projectName")
+        keyword = data.get("keyword")
+        task, total_count = task_service.get_task_template_paginated(
+            project_name, page, per_page, keyword
+        )
+        return TableResponse.success(task, total_count)
+    except Exception as e:
+        return ResponseBase.error(f"获取任务列表失败:{str(e)}")
+
+
+@project_task_api.route("/template/add/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def create_template(task_id: int):
+    try:
+        task_service.create_template(task_id)
+        return ResponseBase.success("创建模板任务成功")
+    except Exception as e:
+        return ResponseBase.error(f"创建模板任务失败:{str(e)}")
+
+
+@project_task_api.route("/template/delete/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def delete_template(task_id: int):
+    try:
+        task_service.delete_template(task_id)
+        return ResponseBase.success("删除模板任务成功")
+    except Exception as e:
+        return ResponseBase.error(f"创建模板任务失败:{str(e)}")
+
+
+@project_task_api.route("/template/copy/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def create_from_template(task_id: int):
+    try:
+        data = request.get_json()
+        project_id = data.get("project_id")
+        if not project_id:
+            return ResponseBase.error("缺少必要参数:project_id")
+        budget_id = int(data.get("budget_id")) if data.get("budget_id") else 0
+        item_id = int(data.get("item_id")) if data.get("item_id") else None
+        if not item_id:
+            return ResponseBase.error("缺少必要参数:item_id")
+        item_code = data.get("item_code")
+        if not item_code:
+            return ResponseBase.error("缺少必要参数:item_code")
+
+        # task_name = request.form.get("task_name")
+        template_task = task_service.get_task(task_id)
+        task_dto = ProjectTaskDto.from_model(template_task)
+        task_dto.id = None
+        task_dto.parent_id = template_task.id
+        task_dto.project_id = project_id
+        task_dto.project_name = None
+        task_dto.item_id = item_id
+        task_dto.item_code = item_code
+        task_dto.budget_id = budget_id
+        task_dto.task_name = "复制于模板=>" + template_task.task_name
+        task_dto.task_sort = 0
+        task_dto.is_template = 0
+        task_dto.task_type = 2
+        task_dto.file_path = template_task.file_path
+
+        task_service.create_task(task_dto)
+        return ResponseBase.success("创建模板任务成功")
+    except Exception as e:
+        return ResponseBase.error(f"创建模板任务失败:{str(e)}")

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

@@ -260,6 +260,15 @@ class ProjectQuotaService:
             setattr(quota_dto, key, value)
             if quota_dto.is_change == 0 and key == "entry_name":
                 quota_dto.is_change = 0
+            elif key == "item_code":
+                new_item = self.chapter_store.get_chapter_item_by_item_code(
+                    quota_dto.project_id, value
+                )
+                if not new_item:
+                    raise ValueError("未查询到章节条目")
+                self.save_change_chapter(
+                    quota_dto.project_id, str(quota_dto.id), new_item.item_id
+                )
             else:
                 quota_dto.is_change = 1
             quota_dto.send_status = (
@@ -327,6 +336,47 @@ class ProjectQuotaService:
             )
             return msg
 
+    def delete_quota_batch(self, ids: str):
+        try:
+            if not ids:
+                return "请选择要删除的定额条目"
+            ids = ids.split(",")
+            msg = ""
+            for id in ids:
+                try:
+                    quota_dto = self.get_quota_dto(int(id))
+                    if not quota_dto:
+                        msg += f"{id}不存在;"
+                        continue
+                    self.delete_quota(int(id))
+                except Exception as e:
+                    msg += f"{id}: {str(e)};"
+            if msg == "":
+                LogRecordHelper.log_success(
+                    OperationType.DELETE,
+                    OperationModule.QUOTA,
+                    f"批量删除定额: {ids}",
+                    utils.to_str({"ids": ids}),
+                )
+            else:
+                LogRecordHelper.log_fail(
+                    OperationType.DELETE,
+                    OperationModule.QUOTA,
+                    f"批量删除定额: {ids}",
+                    utils.to_str({"ids": ids}),
+                )
+            return None if msg == "" else msg
+        except Exception as e:
+            msg = f"批量删除定额失败: {str(e)}"
+            self._logger.error(msg)
+            LogRecordHelper.log_fail(
+                OperationType.DELETE,
+                OperationModule.QUOTA,
+                f"批量删除定额失败: {str(e)}",
+                utils.to_str({"ids": ids}),
+            )
+            return msg
+
     def delete_quota(self, quota_id: int) -> bool:
         """删除项目定额
 

+ 106 - 26
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -5,14 +5,15 @@ from core.log.log_record import LogRecordHelper
 from core.enum import OperationModule, OperationType, TaskStatusEnum, SendStatusEnum
 from core.dtos import ProjectTaskDto
 from core.models import ProjectTaskModel
-from stores import ProjectTaskStore
+from core.user_session import UserSession
+from stores import ProjectTaskStore, ProjectStore
 import executor
 
 
 class ProjectTaskService:
     def __init__(self):
         self.store = ProjectTaskStore()
-
+        self.project_store = ProjectStore()
         self._logger = utils.get_logger()
         self._task_locks = {}
         self._lock = threading.Lock()
@@ -191,7 +192,7 @@ class ProjectTaskService:
     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)
+            task = self.create_task(task_dto)
         else:
             task = self.get_task_dto(task_id)
             if task.process_status != TaskStatusEnum.NEW.value:
@@ -206,6 +207,8 @@ class ProjectTaskService:
                 raise Exception("项目任务不存在")
             task_dto.id = task_id
             task = self.store.update_task(task_dto)
+        if task_id != 0 and (not files or len(files) == 0):
+            return task
         try:
             if task:
                 paths = self._process_file_upload(task, files)
@@ -259,6 +262,19 @@ class ProjectTaskService:
             )
             raise e
 
+    def create_task(self, task_dto: ProjectTaskDto):
+        try:
+            if not task_dto.project_name:
+                project = self.project_store.get(task_dto.project_id)
+                task_dto.project_name = (
+                    project.project_name if project else task_dto.project_id
+                )
+            task = self.store.create_task(task_dto)
+            return task
+        except Exception as e:
+            self._logger.error(f"创建任务失败: {str(e)}")
+            raise
+
     def _process_file_upload(self, task: ProjectTaskDto, files: list) -> str:
         """处理文件上传流程"""
 
@@ -315,30 +331,16 @@ class ProjectTaskService:
                 file_paths.append(file_path)
         return ",".join(file_paths)
 
-    def _create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
-        """创建项目任务
-
-        Args:
-            task_dto: 任务DTO对象
-
-        Returns:
-            ProjectTaskDto: 创建后的任务DTO对象
-        """
-        try:
-            # 业务验证
-            if not task_dto.project_id or not task_dto.budget_id:
-                raise ValueError("项目编号和概算序号不能为空")
-            if not task_dto.task_name:
-                raise ValueError("任务名称不能为空")
-            if task_dto.parent_id > 0 and self.is_all_send(task_dto.parent_id):
-                raise ValueError("父任务还未完成全部推送")
-            return self.store.create_task(task_dto)
-        except Exception as e:
-            self._logger.error(f"创建项目任务失败: {str(e)}")
-            raise
-
     def is_all_send(self, task_id) -> bool:
-        return self.store.is_all_send(task_id)
+        task = self.store.get_task(task_id)
+        if not task:
+            return False
+        status = (
+            SendStatusEnum.SUCCESS.value
+            if task.task_type == 0 or task.task_type == 2
+            else SendStatusEnum.CONFIRMED.value
+        )
+        return self.store.is_all_send(task_id, status)
 
     def _update_task(self, task_dto: ProjectTaskDto) -> Optional[ProjectTaskDto]:
         """更新项目任务
@@ -434,3 +436,81 @@ class ProjectTaskService:
             return msg
         else:
             return "没有查询到任务"
+
+    def get_task_template_paginated(
+        self,
+        project_name: str,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+    ):
+        """获取项目任务模板列表
+        Args:
+            project_name: 项目名称
+            page: 页码
+            page_size: 每页数量
+            keyword: 关键字
+
+        """
+        try:
+            data = self.store.get_task_template_paginated(
+                project_name=project_name,
+                page=page,
+                page_size=page_size,
+                keyword=keyword,
+            )
+            data_list = []
+            for task in data.get("data", []):
+                task = ProjectTaskDto.from_model(task)
+                username = UserSession.get_current_user().username
+                task.can_operate = username == "admin" or task.created_by == username
+                data_list.append(task.to_dict())
+            return data_list, data.get("total", 0)
+        except Exception as e:
+            self._logger.error(f"获取模板任务列表失败: {str(e)}")
+            raise
+
+    def create_template(self, task_id: int) -> ProjectTaskDto:
+        """创建模板任务"""
+        try:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                raise ValueError("任务不存在")
+            if task.process_status != TaskStatusEnum.SUCCESS.value:
+                raise ValueError("任务处理未完成")
+            new_task = ProjectTaskDto(
+                parent_id=task.id,
+                task_name=task.task_name,
+                task_sort=task.task_sort,
+                task_desc=task.task_desc,
+                task_type=task.task_type,
+                is_template=1,
+                is_cover=task.is_cover,
+                project_id=task.project_id,
+                project_name=task.project_name,
+                item_id=task.item_id,
+                item_code=task.item_code,
+                file_path=task.file_path,
+                process_status=task.process_status,
+            )
+            return self.create_task(new_task)
+        except Exception as e:
+            self._logger.error(f"创建模板任务失败: {str(e)}")
+            raise
+
+    def delete_template(self, task_id: int) -> bool:
+        """删除模板任务"""
+        try:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                raise ValueError("任务不存在")
+            username = UserSession.get_current_user().username
+            if task.created_by != username and username != "admin":
+                raise ValueError("没有权限删除该任务")
+            if task.is_template != 1:
+                raise ValueError("任务不是模板任务")
+
+            return self.store.delete_task(task_id)
+        except Exception as e:
+            self._logger.error(f"删除模板任务失败: {str(e)}")
+            raise

+ 2 - 1
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py

@@ -1,4 +1,4 @@
-from sqlalchemy import and_, or_, desc, func, Integer
+from sqlalchemy import and_, or_, func, Integer
 from datetime import datetime
 from typing import Optional
 
@@ -106,6 +106,7 @@ class ProjectQuotaStore:
                     or_(
                         ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
                         ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
+                        ProjectQuotaModel.item_code.like(f"%{keyword}%"),
                     )
                 )
             if send_status is not None:

+ 61 - 2
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_task.py

@@ -1,3 +1,5 @@
+from operator import or_
+
 from sqlalchemy import and_
 from datetime import datetime
 from typing import Optional
@@ -52,6 +54,7 @@ class ProjectTaskStore:
             conditions = [
                 ProjectTaskModel.is_del == 0,
                 ProjectTaskModel.project_id == project_id,
+                ProjectTaskModel.is_template == 0,
                 # ProjectTaskModel.budget_id == budget_id,
                 ProjectTaskModel.item_code.like(f"{item_code}%"),
             ]
@@ -79,6 +82,60 @@ class ProjectTaskStore:
 
             return {"total": total_count, "data": tasks}
 
+    def get_task_template_paginated(
+        self,
+        project_name: str,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+    ):
+        """分页查询任务列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            project_name: 项目编号
+            keyword: 关键字
+        Returns:
+
+        """
+        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.is_template == 1)
+            )
+            if project_name:
+                query = query.filter(
+                    ProjectTaskModel.project_name.like(f"%{project_name}%")
+                )
+            if keyword:
+                query = query.filter(
+                    or_(
+                        or_(
+                            ProjectTaskModel.task_name.like(f"%{keyword}%"),
+                            ProjectTaskModel.created_by.like(f"%{keyword}%"),
+                        ),
+                        ProjectTaskModel.project_id.like(f"%{keyword}%"),
+                    )
+                )
+
+            # 计算总数
+            total_count = query.count()
+
+            # 分页
+            query = (
+                query.order_by(ProjectTaskModel.task_sort.desc())
+                .order_by(ProjectTaskModel.created_at.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+            )
+
+            tasks = query.all()
+
+            return {"total": total_count, "data": tasks}
+
     def get_task(self, task_id: int) -> Optional[ProjectTaskModel]:
         with db_helper.mysql_query_session(self._database) as db_session:
             task = (
@@ -139,13 +196,13 @@ class ProjectTaskStore:
             tasks = query.all()
             return [ProjectTaskDto.from_model(task) for task in tasks]
 
-    def is_all_send(self, task_id) -> bool:
+    def is_all_send(self, task_id, status) -> bool:
         with db_helper.mysql_query_session(self._database) as db_session:
             query = db_session.query(ProjectQuotaModel)
             query = query.filter(
                 and_(
                     ProjectQuotaModel.is_del == 0,
-                    ProjectQuotaModel.send_status != SendStatusEnum.SUCCESS.value,
+                    ProjectQuotaModel.send_status != status,
                     ProjectQuotaModel.task_id == task_id,
                 )
             )
@@ -166,6 +223,8 @@ class ProjectTaskStore:
             task_desc=task_dto.task_desc,
             task_sort=task_dto.task_sort,
             task_type=task_dto.task_type,
+            is_template=task_dto.is_template,
+            project_name=task_dto.project_name,
             project_id=task_dto.project_id,
             budget_id=task_dto.budget_id,
             item_id=task_dto.item_id,

+ 58 - 8
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py

@@ -22,14 +22,14 @@ class ProjectTaskStore:
         return self._current_user
 
     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,
+        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,
     ):
         """分页查询任务列表
 
@@ -79,6 +79,53 @@ class ProjectTaskStore:
 
             return {"total": total_count, "data": tasks}
 
+    def get_task_template_paginated(
+        self,
+        project_name: str,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+    ):
+        """分页查询任务列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            project_name: 项目编号
+            keyword: 关键字
+        Returns:
+
+        """
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            query = db_session.query(ProjectTaskModel)
+
+            # 构建查询条件
+            conditions = [
+                ProjectTaskModel.is_del == 0,
+                ProjectTaskModel.is_template == 1,
+                ProjectTaskModel.project_name.like(f"%{project_name}%"),
+            ]
+            if keyword:
+                conditions.append(ProjectTaskModel.task_name.like(f"%{keyword}%"))
+                conditions.append(ProjectTaskModel.created_by.like(f"%{keyword}%"))
+
+            query = query.filter(and_(*conditions))
+
+            # 计算总数
+            total_count = query.count()
+
+            # 分页
+            query = (
+                query.order_by(ProjectTaskModel.task_sort.desc())
+                .order_by(ProjectTaskModel.created_at.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+            )
+
+            tasks = query.all()
+
+            return {"total": total_count, "data": tasks}
+
     def get_task(self, task_id: int) -> Optional[ProjectTaskModel]:
         with db_helper.sqlserver_query_session(self._database) as db_session:
             task = (
@@ -151,8 +198,11 @@ class ProjectTaskStore:
         task = ProjectTaskModel(
             task_name=task_dto.task_name,
             task_desc=task_dto.task_desc,
+            task_type=task_dto.task_type,
+            is_template=task_dto.is_template,
             task_sort=task_dto.task_sort,
             project_id=task_dto.project_id,
+            project_name=task_dto.project_name,
             budget_id=task_dto.budget_id,
             item_id=task_dto.item_id,
             item_code=task_dto.item_code,

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

@@ -6,6 +6,14 @@ function GoTo(url, isNewWindow) {
 	}
 }
 
+function loadingAjax() {
+	$('body').append('<div class="loading loading-g" style="background: rgba(0, 0, 0, .2);position: fixed; top: 0;left: 0;right: 0;bottom: 0;display: flex;justify-content: center;align-items: center;"><div class="spinner-border text-primary" role="status"></div><span class="text-primary px-5">加载中</span></div>')
+}
+
+function clearLoadingAjax() {
+	$('.loading-g').remove()
+}
+
 function IwbAjax(opt) {
 	opt = opt || {}
 	if (!opt.url) {
@@ -17,7 +25,11 @@ function IwbAjax(opt) {
 	if (opt.data === undefined) {
 		opt.data = {}
 	}
-	opt = $.extend({}, {isAlert: true,}, opt)
+	opt = $.extend({}, {isLoading: true, isAlert: true, reloadTable: false}, opt)
+
+	if (opt.isLoading) {
+		loadingAjax()
+	}
 	fetch(opt.url, {
 		method: opt.method,
 		headers: opt.headers || {
@@ -33,7 +45,11 @@ function IwbAjax(opt) {
 					MsgSuccess('操作成功', data.message || "")
 				}
 				if (opt.table) {
-					IwbTableReload($(opt.table))
+					if (opt.reloadTable) {
+						IwbTableReload(opt.table)
+					} else {
+						IwbTable($(opt.table))
+					}
 				}
 				if (opt.modal) {
 					$(opt.modal).modal('hide')
@@ -50,6 +66,11 @@ function IwbAjax(opt) {
 				opt.error && opt.error(data)
 			}
 		})
+		.finally(() => {
+			if (opt.isLoading) {
+				clearLoadingAjax()
+			}
+		})
 }
 
 function IwbAjax_1(opt) {
@@ -58,9 +79,10 @@ function IwbAjax_1(opt) {
 	IwbAjax(opt)
 }
 
-function IwbAjax_2(opt, modal, table) {
+function IwbAjax_2(opt, modal, table, reloadTable) {
 	opt.modal = modal || '#modal'
 	opt.table = table || '#table'
+	opt.reloadTable = reloadTable || false
 	IwbAjax(opt)
 }
 
@@ -297,11 +319,22 @@ function IwbTable(table, opts, isReload) {
 				})
 				reSizeTdOneLine($table)
 
-				if (opt.isDragColumn) {
+				if (opt.isDragColumn && rows.length > 0) {
 					dragTableColumn($table)
 				}
 
 			})
+
+			$tableBox.find(`.search-box .form-control`).off('keyup.search').on('keyup.search', function (event) {
+				event.preventDefault()
+				if (event.keyCode === 13) {
+					IwbTableSearch(this)
+				}
+			})
+			$tableBox.find(`.search-box .form-select`).off('change.search').on('change.search', function (event) {
+				event.preventDefault()
+				IwbTableSearch(this)
+			})
 		})
 	}
 
@@ -439,7 +472,7 @@ function IwbTable(table, opts, isReload) {
 }
 
 function IwbTableSearch(that) {
-	const $search = $(that).closest('form.search-box')
+	const $search = $(that).closest('.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) => {
@@ -665,3 +698,9 @@ String.prototype.format = function () {
 		return typeof args[index] != 'undefined' ? args[index] : match
 	})
 }
+window.addEventListener('scroll', () => {
+	const tooltips = document.querySelectorAll('.tooltip.show');
+	tooltips.forEach(item => {
+		item.style.display = 'none';
+	});
+});

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

@@ -25,6 +25,7 @@ const task_modal_template = `
               <select class="form-select "  name="task_type" id="task_type" data-placeholder="请选择任务类型">
                 <option value="0" selected>普通任务</option>
                 <option value="1">匹配任务</option>
+                <option value="2">模板任务</option>
               </select>
             </div>
             <div class="fv-row form-group mb-3">
@@ -89,18 +90,65 @@ const task_modal_template = `
       </div>
     </div>
   </div>
+</div>`,
+	template_modal_template2 = `<div class="modal fade" id="modal_template" tabindex="-1" aria-hidden="true">
+  <div class="modal-dialog modal-lg modal-dialog-centered" style="max-width:1200px;">
+    <div class="modal-content rounded">
+      <div class="modal-header">
+        <h3 class="modal-title">模板管理</h3>
+        <div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
+          <i class="ki-duotone ki-cross fs-1">
+            <span class="path1"></span>
+            <span class="path2"></span>
+          </i>
+        </div>
+      </div>
+      <div class="modal-body py-0">
+          <section>
+            <input type="hidden" name="budget_id" value="">
+            <input type="hidden" name="project_id" value="">
+            <input type="hidden" name="item_id" value="">
+            <input type="hidden" name="item_code" value="">
+            <input type="hidden" name="task_id" value="">
+            <input type="hidden" name="parent_id" value="">
+          </section>
+          <div class="table-box table-responsive" data-id="" id="table_box_task_template">
+								<div class="d-flex justify-content-between my-5">
+									<div class=""></div>
+									<form class="search-box d-flex">
+										<div class="d-flex">
+											<input type="text" class="form-control form-control-sm w-200px" placeholder="请输入项目名称" name="projectName" />
+											<input type="text" class="form-control form-control-sm w-200px ms-5" placeholder="请输入关键字" name="keyword" />
+										</div>
+										<div class="btn-group ms-5">
+											<button type="button" class="btn btn-primary btn-sm" onclick="IwbTableSearch(this);clear()">查询</button>
+											<button type="button" class="btn btn-danger btn-sm" onclick="IwbTableResetSearch(this);clear()">重置</button>
+										</div>
+									</form>
+								</div>
+								<table class="table table-striped table-bordered table-hover  table-rounded" id="table_task_template">
+								</table>
+								<div class="pagination-row"></div>
+							</div>
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
+      </div>
+    </div>
+  </div>
 </div>`
 $('.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>`
+$('.app-main .app-container').append(template_modal_template2)
+let table_add_task_btn_template = `<button type="button" class="task_add_btn btn btn-primary btn-sm" onclick="Add('{0}')">添加任务</button>`
+table_add_task_btn_template += `<button type="button" class="task_add_btn btn btn-warning btn-sm ms-5" onclick="AddFromTemplate('{0}')">模板管理</button>`
 
 const table = '#table',
-	$modal = $('#modal')
+	$modal = $('#modal'), $modal_template = $('#modal_template')
 $modal.find('#budget_id').html($('#budget_id_options').html())
 $(function () {
 	InitFileUpload()
 })
 
-
 let _fileUploadDropzone = null;
 
 function InitFileUpload() {
@@ -163,13 +211,15 @@ function RenderRightBox_Custom(data) {
 			_taskTable(data)
 		}
 	})
+	_renderTask(data)
+
 	if (data.children_count > 0 || data.chapter) {
-		_renderTask(data)
+		// _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)
+		// $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) {
@@ -187,6 +237,9 @@ function RenderRightBox_Custom(data) {
 			url: `/api/task/list/${project_id}/${data.item_code}`,
 			trClass: (row) => {
 				if (row.parent_id && row.parent_id !== 0) {
+					if (row.task_type === 2) {
+						return 'tr-warning'
+					}
 					return 'tr-primary'
 				}
 			},
@@ -206,7 +259,7 @@ function RenderRightBox_Custom(data) {
 					data: 'task_type',
 					width: '120px',
 					render: (row) => {
-						return row.task_type === 0 ? '<span class="badge badge-light-primary">普通任务</span>' : '<span class="badge badge-light-success">匹配任务</span>'
+						return row.task_type === 0 ? '<span class="badge badge-light-primary">普通任务</span>' : row.task_type === 1 ? '<span class="badge badge-light-success">匹配任务</span>' : '<span class="badge badge-light-info">模板任务</span>'
 					}
 				},
 				{
@@ -327,9 +380,11 @@ function RenderRightBox_Custom(data) {
 						str += `<!--<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>-->`
 						if (row.process_status === 2 || row.process_status === 200) {
 							str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary"  data-bs-toggle="tooltip" data-bs-placement="top" title="定额输入列表" onclick="GoTo('/${row.task_type === 0 ? 'quota_info' : 'quota_match_info'}/${project_id}/${row.id}',0)"><i class="ki-duotone ki-eye fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							if (row.task_type === 0) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-info"  data-bs-toggle="tooltip" data-bs-placement="top" title="再次提交任务" onclick="ReCreate(${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.task_type === 0) {
+							// }else {
+							// }
+							str += `<button type="button" class="btn btn-icon btn-sm btn-light-info"  data-bs-toggle="tooltip" data-bs-placement="top" title="再次提交任务" onclick="ReCreate(${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>`
+							str += `<button type="button" class="btn btn-icon btn-sm btn-light-success" data-bs-toggle="tooltip" data-bs-placement="top" title="保存为模板" onclick="SaveToTemplate(${row.id})"><i class="ki-duotone ki-archive-tick fs-1"><span class="path1"></span><span class="path2"></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>`
@@ -440,7 +495,7 @@ function SaveProject(is_submit) {
 		files.forEach((file) => {
 			formData.append('files', file)
 		})
-	} else {
+	} else if (task_id === '0') {
 		MsgWarning('文件不能为空,请选择文件')
 		return
 	}
@@ -472,6 +527,10 @@ function Delete(id) {
 	ConfirmUrl('确定删除吗?', `/api/task/delete/${id}`, `#table_0`)
 }
 
+function SaveToTemplate(id) {
+	ConfirmUrl('确定保存为模板吗?', `/api/task/template/add/${id}`, `#table_0`)
+}
+
 function StarTask(id) {
 	clear()
 	ConfirmUrl('确定开始运行任务吗?', `/api/task/start_task/${id}`, `#table_0`, function () {
@@ -506,18 +565,84 @@ function auto_refresh_table(id) {
 	}, 10 * 1000)
 }
 
-// function StartProcessTask(id){
-// 	ConfirmUrl('确定开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
-// }
-//
-// function ReStartProcessTask(id){
-// 	ConfirmUrl('确定重新开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
-// }
+function AddFromTemplate(budget_id) {
+	$modal_template.find('[name="budget_id"]').val(budget_id);
+	SetBudgetData($modal_template)
+	IwbTable('#table_task_template', {
+		url: '/api/task/template/list',
+		columns: [
+			{
+				title: '模板任务名称',
+				data: 'task_name',
+				width: `250px`,
+				render: (row) => {
+					return `<span class="one-line " data-bs-toggle="tooltip" data-bs-placement="top" title="${row.task_name}" >${row.task_name}</span>`
+				}
+			},
+			{
+				title: '项目名称',
+				data: 'project_name',
+				render: (row) => {
+					return `<span class="one-line " data-bs-toggle="tooltip" data-bs-placement="top" title="${row.project_name}" >${row.project_name}</span>`
+				}
+			},
+			{
+				title: '项目编号',
+				data: 'project_id',
+				width: '200px',
+			},
+			{
+				title: '创建人',
+				data: 'created_by',
+				width: '100px',
+			},
+			{
+				title: '创建时间',
+				data: 'created_at',
+				width: '160px',
+			},
+			{
+				title: '操作',
+				data: 'id',
+				width: '90px',
+				render: (row) => {
+					let str = `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="创建任务" onclick="SaveCreateTemplate(${row.id})"><i class="ki-duotone ki-plus-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					if (row.can_operate) {
+						str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="DeleteTemplate(${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
+				}
+			}
+
+		]
+	})
+	$modal_template.modal('show')
+}
 
-// function StartSendTask(id){
-// 	ConfirmUrl('确定开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
-// }
-// function ReStartSendTask(id){
-// 	ConfirmUrl('确定重新开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
-// }
+function DeleteTemplate(id) {
+	ConfirmUrl('确定删除模板吗?', `/api/task/template/delete/${id}`, `#table_task_template`)
+}
 
+function SaveCreateTemplate(id) {
+	Confirm('确定使用模板创建任务吗?', function () {
+		const budget_id = $modal_template.find('[name="budget_id"]').val(),
+			item_id = $modal_template.find('[name="item_id"]').val(),
+			item_code = $modal_template.find('[name="item_code"]').val(),
+			project_id = $modal_template.find('[name="project_id"]').val()
+		IwbAjax({
+			url: `/api/task/template/copy/${id}`,
+			data: {
+				budget_id,
+				item_id,
+				item_code,
+				project_id
+			},
+			success: res => {
+				if (res.success) {
+					$modal_template.modal('hide')
+					IwbTableReload(`#table_0`)
+				}
+			}
+		})
+	})
+}

+ 19 - 7
SourceCode/IntelligentRailwayCosting/app/views/static/project/quota_info.js

@@ -190,7 +190,7 @@ const nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 	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">
+									<div class="search-box d-flex">
 										<div class="d-flex">
 											{2}
 											<input type="text" class="form-control form-control-sm w-200px" placeholder="请输入关键字" name="keyword" />
@@ -199,7 +199,7 @@ const nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 											<button type="button" class="btn btn-primary btn-sm" onclick="IwbTableSearch(this);clear()">查询</button>
 											<button type="button" class="btn btn-danger btn-sm" onclick="IwbTableResetSearch(this);clear()">重置</button>
 										</div>
-									</form>
+									</div>
 								</div>
 								<div class="pagination-row"></div>
 								<table class="table table-striped table-bordered table-hover  table-rounded" id="table_{0}">
@@ -218,6 +218,7 @@ let table_add_quota_btn_template = ''
 table_add_quota_btn_template += `<button type="button" class="quota_add_btn btn btn-info btn-sm" onclick="Send_Quota_All('{0}')">全部推送</button>`;
 table_add_quota_btn_template += `<button type="button" class="ms-5 quota_add_btn btn btn-primary btn-sm" onclick="Send_Quota_Batch('{0}')">批量推送</button>`;
 table_add_quota_btn_template += `<button type="button" class="ms-5 btn btn-warning btn-sm" onclick="Edit_Quota_Code_Batch('{0}')">批量修改定额编号</button>`
+table_add_quota_btn_template += `<button type="button" class="ms-5 btn btn-danger btn-sm" onclick="Delete_Quota_Batch('{0}')">批量删除</button>`
 // table_add_quota_btn_template += `<button type="button" class="ms-5 btn btn-warning btn-sm" onclick="Edit_QuotaChapter(null,'{0}')">批量修改章节</button>`
 if (task_type === "1") {
 	table_add_quota_btn_template = `<button type="button" class="ms-5 quota_add_btn btn btn-primary btn-sm" onclick="StartConfirmQuotaBatch('{0}')">批量确认</button>`;
@@ -473,6 +474,7 @@ function LoadQuotaTable(table) {
 				data: 'item_code',
 				width: '190px',
 				tdStyle: "text-align:right;padding-right:15px;",
+				isEdit: true,
 				render: (row) => {
 					return `<span class="one-line edit-label" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.item_code}" ><span class="edit-text">${row.item_code}</span></span>`
 				}
@@ -508,7 +510,7 @@ function LoadQuotaTable(table) {
 						if (row.copy_quota_id > 0 || row.copy_quota_id === -1 && task_type === "1") {
 							return `<span class="edit-label"><span class="edit-text">-</span></span>`
 						}
-						const text = task_type === "0" ? "未查询到" : "未匹配"
+						const text = task_type === "0" || task_type === "2" ? "未查询到" : "未匹配"
 						return `<span class="badge badge-light-danger edit-label" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.note ? `说明:${row.note}` : text}" ><span class="edit-text">${text}</span></span>`
 					}
 					if (row.quota_id) {
@@ -575,7 +577,7 @@ function LoadQuotaTable(table) {
 					// 	str+= `<span class="badge badge-danger">数据变更</span>`
 					// }
 					if (row.send_status === 0) {
-						str += `<span class="badge badge-primary ms-3">${task_type === "0" ? "未推送" : "未确认"}</span>`
+						str += `<span class="badge badge-primary ms-3">${task_type === "0" || task_type === "2" ? "未推送" : "未确认"}</span>`
 					} else if (row.send_status === 1) {
 						str += `<span class="badge badge-warning ms-3">推送中</span>`
 					} else if (row.send_status === 200) {
@@ -607,7 +609,7 @@ function LoadQuotaTable(table) {
 					// } 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 (task_type === "0") {
+					if (task_type === "0" || task_type === "2") {
 						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) {
@@ -620,7 +622,7 @@ function LoadQuotaTable(table) {
 							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-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="修改条目" onclick="Edit_QuotaChapter(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-burger-menu-1 fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+						//str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="修改条目" onclick="Edit_QuotaChapter(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-burger-menu-1 fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 						str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning"  data-bs-toggle="tooltip" data-bs-placement="top" title="复制条目" onclick="CopyQuota(${row.id}, ${row.budget_id})"><i class="ki-duotone ki-copy fs-1"></i></button>`
 						if (row.send_status !== 200 && row.send_status !== 3) {
 							// 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>`
@@ -742,9 +744,19 @@ function SaveQuota(isSubmit) {
 }
 
 function Delete_Quota(id, budget_id) {
+
 	ConfirmUrl('确定删除吗?', `/api/quota/delete/${id}`, `#table_${budget_id}`)
 }
 
+function Delete_Quota_Batch(budget_id) {
+	const ids = IwbTableGetSelectedIds('#table_' + budget_id).join(",")
+	if (!ids) {
+		MsgWarning('没有选择数据,请选择要修改的数据!')
+		return
+	}
+	ConfirmUrl('确定批量删除吗?', `/api/quota/delete_batch/${ids}`, `#table_${budget_id}`)
+}
+
 function StartSendQuota(ids, budget_id, is_cover) {
 	SendQuota('确定开始推送数据吗?', ids, budget_id, is_cover)
 }
@@ -837,6 +849,7 @@ function Edit_Quota_Code_Batch(budget_id) {
 	}
 	Swal.fire({
 		title: '批量修改定额编号',
+		position: 'top',
 		html: '<input id="batch_quota_code" class="form-control form-control-sm" placeholder="请输入定额编号"/>',
 		showCancelButton: true,
 		focusConfirm: false,
@@ -945,7 +958,6 @@ function StartConfirmQuotaBatch(budget_id) {
 	ConfirmUrl('确定批量确认数据吗?', `/api/quota/confirm/${ids}`, $table)
 }
 
-
 function EditQuotaMap(id, budget_id) {
 	$modalQuotaInput.find(`input[name="quota_input_id"]`).val(id)
 	$modalQuotaInput.find('input[name="budget_id"]').val(budget_id);