Przeglądaj źródła

Merge branch 'master' of http://47.100.37.243:10191/quyueting/manHourHousekeeper

QuYueTing 1 tydzień temu
rodzic
commit
c27f84c993
16 zmienionych plików z 10546 dodań i 6785 usunięć
  1. 2 2
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplierController.java
  2. 24 177
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplyController.java
  3. 1 1
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApplier.java
  4. 24 2
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApply.java
  5. 11 1
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApprovalLog.java
  6. 12 6
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/TimeType.java
  7. 12 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplierService.java
  8. 18 70
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplyService.java
  9. 50 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplierServiceImpl.java
  10. 221 120
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplyServiceImpl.java
  11. 7 13
      fhKeeper/formulahousekeeper/timesheet/src/routes.js
  12. 11 18
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureApplyForm.vue
  13. 34 25
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureDetail.vue
  14. 384 137
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureList.vue
  15. 9735 6203
      fhKeeper/formulahousekeeper/timesheet/src/views/project/list.vue
  16. 0 10
      fhKeeper/formulahousekeeper/timesheet/src/views/settings/timetype.vue

+ 2 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplierController.java

@@ -55,7 +55,7 @@ public class ProjectClosureApplierController {
      */
     @PostMapping("/add")
     public HttpRespMsg add(@RequestParam Integer timeTypeId,
-                           @RequestParam Integer approverUserId,
+                           @RequestParam String approverUserId,
                            @RequestParam String approverName,
                            @RequestParam Integer sortOrder,
                            @RequestParam(defaultValue = "1") Integer status) {
@@ -81,7 +81,7 @@ public class ProjectClosureApplierController {
     @PostMapping("/update")
     public HttpRespMsg update(@RequestParam Integer id,
                               @RequestParam Integer timeTypeId,
-                              @RequestParam Integer approverUserId,
+                              @RequestParam String approverUserId,
                               @RequestParam String approverName,
                               @RequestParam Integer sortOrder,
                               @RequestParam Integer status) {

+ 24 - 177
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplyController.java

@@ -1,269 +1,116 @@
 package com.management.platform.controller;
 
-
-
 import com.baomidou.mybatisplus.core.metadata.IPage;
-
 import com.management.platform.entity.ProjectClosureApply;
-
 import com.management.platform.service.ProjectClosureApplyService;
-
 import com.management.platform.util.HttpRespMsg;
-
 import org.springframework.web.bind.annotation.*;
 
-
-
 import javax.annotation.Resource;
-
+import java.util.List;
 import java.util.Map;
 
-
-
 /**
-
- * <p>
-
- * 项目结项申请�?前端控制�?
- * </p>
-
- *
-
- * @author Seyason
-
- * @since 2026-05-12
-
+ * 项目结项申请与审批
  */
-
 @RestController
-
 @RequestMapping("/project-closure-apply")
-
 public class ProjectClosureApplyController {
 
-
-
     @Resource
-
     private ProjectClosureApplyService closureApplyService;
 
-
-
-    /**
-
-     * 提交结项申请
-
-     */
+    @Resource
+    private com.management.platform.service.ProjectClosureApplierService closureApplierService;
 
     @PostMapping("/submit")
-
     public HttpRespMsg submitApply(@RequestParam Integer projectId,
-
-                                   @RequestParam Integer userId,
-
+                                   @RequestParam String userId,
                                    @RequestParam(required = false) String reason,
-
                                    @RequestParam(required = false) String attachmentIds) {
-
         try {
-
             String[] ids = attachmentIds != null ? attachmentIds.split(",") : new String[0];
-
-            java.util.List<Integer> attachmentIdList = new java.util.ArrayList<>();
-
+            List<Integer> attachmentIdList = new java.util.ArrayList<>();
             for (String id : ids) {
-
                 if (id != null && !id.trim().isEmpty()) {
-
                     attachmentIdList.add(Integer.parseInt(id.trim()));
-
                 }
-
             }
-
             ProjectClosureApply apply = closureApplyService.submitApply(projectId, userId, reason, attachmentIdList);
-
             return HttpRespMsg.success("申请提交成功", apply);
-
         } catch (Exception e) {
-
-            return HttpRespMsg.error("申请提交失败" + e.getMessage());
-
+            return HttpRespMsg.error("申请提交失败:" + e.getMessage());
         }
-
     }
 
-
-
-    /**
-
-     * 获取申请详情
-
-     */
-
     @GetMapping("/detail/{id}")
-
-    public HttpRespMsg getDetail(@PathVariable Integer id) {
-
-        ProjectClosureApply apply = closureApplyService.getById(id);
-
+    public HttpRespMsg getDetail(@PathVariable Integer id,
+                                 @RequestParam(required = false) String viewerId) {
+        ProjectClosureApply apply = closureApplyService.getApplyView(id, viewerId);
         if (apply != null) {
-
             return HttpRespMsg.success(apply);
-
         }
-
         return HttpRespMsg.error("申请记录不存在");
-
     }
 
-
-
-    /**
-
-     * 获取审批记录列表
-
-     */
-
     @GetMapping("/approval-logs/{applyId}")
-
     public HttpRespMsg getApprovalLogs(@PathVariable Integer applyId) {
-
         try {
-
-            java.util.List<com.management.platform.entity.ProjectClosureApprovalLog> logs = closureApplyService.getApprovalLogs(applyId);
-
-            return HttpRespMsg.success(logs);
-
+            return HttpRespMsg.success(closureApplyService.getApprovalLogs(applyId));
         } catch (Exception e) {
-
-            return HttpRespMsg.error("获取审批记录失败" + e.getMessage());
-
+            return HttpRespMsg.error("获取审批记录失败:" + e.getMessage());
         }
-
     }
 
-
-
-    /**
-
-     * 获取审批进度
-
-     */
-
     @GetMapping("/progress/{applyId}")
-
-    public HttpRespMsg getProgress(@PathVariable Integer applyId) {
-
+    public HttpRespMsg getProgress(@PathVariable Integer applyId,
+                                   @RequestParam(required = false) String viewerId) {
         try {
-
-            Map<String, Object> progress = closureApplyService.getProgress(applyId);
-
+            Map<String, Object> progress = closureApplyService.getProgress(applyId, viewerId);
             return HttpRespMsg.success(progress);
-
         } catch (Exception e) {
-
-            return HttpRespMsg.error("获取审批进度失败" + e.getMessage());
-
+            return HttpRespMsg.error("获取审批进度失败:" + e.getMessage());
         }
-
     }
 
-
-
-    /**
-
-     * 审批操作
-
-     */
-
     @PostMapping("/approve")
-
     public HttpRespMsg approve(@RequestParam Integer applyId,
-
-                               @RequestParam Integer applierId,
-
+                               @RequestParam String applierId,
                                @RequestParam(required = false) String comment,
-
                                @RequestParam Integer action) {
-
         try {
-
             boolean result = closureApplyService.approve(applyId, applierId, comment, action);
-
             if (result) {
-
                 return HttpRespMsg.success("审批成功");
-
             }
-
             return HttpRespMsg.error("审批失败");
-
         } catch (Exception e) {
-
-            return HttpRespMsg.error("审批失败? + e.getMessage()");
-
+            return HttpRespMsg.error("审批失败:" + e.getMessage());
         }
-
     }
 
-
-
-    /**
-
-     * 获取我的待审批列�?
-     */
-
     @GetMapping("/my-pending")
-
-    public HttpRespMsg getMyPendingApproval(@RequestParam Integer userId,
-
+    public HttpRespMsg getMyPendingApproval(@RequestParam String userId,
                                             @RequestParam(defaultValue = "1") int current,
-
-                                            @RequestParam(defaultValue = "10") int size) {
-
+                                            @RequestParam(defaultValue = "10") int size,
+                                            @RequestParam(required = false, defaultValue = "0") Integer status) {
         try {
-
-            IPage<ProjectClosureApply> page = closureApplyService.getMyPendingApproval(userId, current, size);
-
+            IPage<ProjectClosureApply> page = closureApplyService.getMyPendingApproval(userId, current, size, status);
             return HttpRespMsg.success(page);
-
         } catch (Exception e) {
-
             return HttpRespMsg.error("获取待审批列表失败:" + e.getMessage());
-
         }
-
     }
 
-
-
-    /**
-
-     * 获取我的申请列表
-
-     */
-
     @GetMapping("/my-apply")
-
-    public HttpRespMsg getMyApplyList(@RequestParam Integer userId,
-
+    public HttpRespMsg getMyApplyList(@RequestParam String userId,
                                       @RequestParam(defaultValue = "1") int current,
-
                                       @RequestParam(defaultValue = "10") int size) {
-
         try {
-
             IPage<ProjectClosureApply> page = closureApplyService.getMyApplyList(userId, current, size);
-
             return HttpRespMsg.success(page);
-
         } catch (Exception e) {
-
-            return HttpRespMsg.error("获取申请列表失败" + e.getMessage());
-
+            return HttpRespMsg.error("获取申请列表失败:" + e.getMessage());
         }
-
     }
-
 }

+ 1 - 1
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApplier.java

@@ -42,7 +42,7 @@ public class ProjectClosureApplier extends Model<ProjectClosureApplier> {
      * 审批人用户ID
      */
     @TableField("approver_user_id")
-    private Integer approverUserId;
+    private String approverUserId;
 
     /**
      * 审批人姓名

+ 24 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApply.java

@@ -54,7 +54,7 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
      * 申请人用户ID
      */
     @TableField("applicant_id")
-    private Integer applicantId;
+    private String applicantId;
 
     /**
      * 申请人姓名
@@ -72,7 +72,7 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
      * 当前审批人用户ID
      */
     @TableField("current_approver_id")
-    private Integer currentApproverId;
+    private String currentApproverId;
 
     /**
      * 当前审批人姓名
@@ -98,6 +98,28 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
     @TableField("update_time")
     private LocalDateTime updateTime;
 
+    /** 前端列表/详情用:1待审批 2通过 3驳回 4撤销 */
+    @TableField(exist = false)
+    private Integer status;
+
+    @TableField(exist = false)
+    private String applyTime;
+
+    @TableField(exist = false)
+    private String reason;
+
+    @TableField(exist = false)
+    private Integer totalStep;
+
+    @TableField(exist = false)
+    private String currentApplierName;
+
+    @TableField(exist = false)
+    private String currentStepDisplay;
+
+    @TableField(exist = false)
+    private Boolean canApprove;
+
 
     @Override
     protected Serializable pkVal() {

+ 11 - 1
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApprovalLog.java

@@ -42,7 +42,7 @@ public class ProjectClosureApprovalLog extends Model<ProjectClosureApprovalLog>
      * 审批人ID
      */
     @TableField("approver_id")
-    private Integer approverId;
+    private String approverId;
 
     /**
      * 审批人姓名
@@ -65,6 +65,16 @@ public class ProjectClosureApprovalLog extends Model<ProjectClosureApprovalLog>
     @TableField("create_time")
     private LocalDateTime createTime;
 
+    /** 前端表格:与 approverName 一致 */
+    @TableField(exist = false)
+    private String applierName;
+
+    @TableField(exist = false)
+    private String comment;
+
+    @TableField(exist = false)
+    private String approveTime;
+
 
     @Override
     protected Serializable pkVal() {

+ 12 - 6
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/TimeType.java

@@ -1,16 +1,16 @@
 package com.management.platform.entity;
 
-import java.math.BigDecimal;
-import com.baomidou.mybatisplus.extension.activerecord.Model;
-import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableField;
-import java.io.Serializable;
-import java.util.List;
-
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
 
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
 /**
  * <p>
  * 
@@ -721,6 +721,12 @@ public class TimeType extends Model<TimeType> {
     @TableField("finish_task_in_report")
     private Boolean finishTaskInReport;
 
+    /**
+     * 日报内是否可以完成任务
+     */
+    @TableField("is_project_closure")
+    private Integer isProjectClosure;
+
 
 
     @TableField(exist = false)

+ 12 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplierService.java

@@ -3,6 +3,9 @@ package com.management.platform.service;
 import com.management.platform.entity.ProjectClosureApplier;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * <p>
  * 项目结项审批人设置表 服务类
@@ -13,4 +16,13 @@ import com.baomidou.mybatisplus.extension.service.IService;
  */
 public interface ProjectClosureApplierService extends IService<ProjectClosureApplier> {
 
+    /**
+     * 获取审批流程配置列表(按时间类型)
+     */
+    List<Map<String, Object>> getConfigList(Integer timeTypeId);
+
+    /**
+     * 保存审批流程配置
+     */
+    void saveConfig(Integer timeTypeId, List<Map<String, Object>> configSteps);
 }

+ 18 - 70
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplyService.java

@@ -1,89 +1,37 @@
 package com.management.platform.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.management.platform.entity.ProjectClosureApply;
+import com.management.platform.entity.ProjectClosureApplier;
+import com.management.platform.entity.ProjectClosureApprovalLog;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.List;
+import java.util.Map;
+
 /**
- * <p>
- * ????????????
- * </p>
- *
- * @author Seyason
- * @since 2026-05-12
+ * 项目结项申请服务
  */
 public interface ProjectClosureApplyService extends IService<ProjectClosureApply> {
 
-    /**
-     * ??????
-     *
-     * @param projectId ??ID
-     * @param userId ?????ID
-     * @param reason ????
-     * @param attachments ????
-     * @return ????
-     */
-    ProjectClosureApply submitApply(Integer projectId, Integer userId, String reason, java.util.List<Integer> attachmentIds);
+    ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<Integer> attachmentIds);
 
-    /**
-     * ???????? sort_order ??????????
-     *
-     * @param timeTypeId ????ID
-     * @return ??????
-     */
-    java.util.List<com.management.platform.entity.ProjectClosureApplier> getApprovalFlow(Integer timeTypeId);
+    List<ProjectClosureApplier> getApprovalFlow(Integer companyId);
 
-    /**
-     * ????????/????
-     *
-     * @param applyId ??ID
-     * @param applierId ???ID
-     * @param comment ????
-     * @param action ?? 1-?? 2-??
-     * @return ????
-     */
-    boolean approve(Integer applyId, Integer applierId, String comment, Integer action);
+    boolean approve(Integer applyId, String applierId, String comment, Integer action);
 
-    /**
-     * ????????
-     *
-     * @param applyId ??ID
-     * @return ????
-     */
-    java.util.Map<String, Object> getProgress(Integer applyId);
+    Map<String, Object> getProgress(Integer applyId, String viewerUserId);
 
-    /**
-     * ????????
-     *
-     * @param applyId ??ID
-     * @return ??????
-     */
-    java.util.List<com.management.platform.entity.ProjectClosureApprovalLog> getApprovalLogs(Integer applyId);
+    List<ProjectClosureApprovalLog> getApprovalLogs(Integer applyId);
 
-    /**
-     * ??????????
-     *
-     * @param userId ??ID
-     * @param current ??
-     * @param size ????
-     * @return ????
-     */
-    com.baomidou.mybatisplus.core.metadata.IPage<ProjectClosureApply> getMyPendingApproval(Integer userId, int current, int size);
+    IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status);
 
-    /**
-     * ????????
-     *
-     * @param userId ??ID
-     * @param current ??
-     * @param size ????
-     * @return ????
-     */
-    com.baomidou.mybatisplus.core.metadata.IPage<ProjectClosureApply> getMyApplyList(Integer userId, int current, int size);
+    IPage<ProjectClosureApply> getMyApplyList(String userId, int current, int size);
+
+    void onApprovalPassed(Integer applyId);
 
     /**
-     * ????????????
-     *
-     * @param applyId ??ID
+     * 详情(含前端展示字段)
      */
-    void onApprovalPassed(Integer applyId);
+    ProjectClosureApply getApplyView(Integer id, String viewerUserId);
 }
-

+ 50 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplierServiceImpl.java

@@ -1,11 +1,18 @@
 package com.management.platform.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.management.platform.entity.ProjectClosureApplier;
 import com.management.platform.mapper.ProjectClosureApplierMapper;
 import com.management.platform.service.ProjectClosureApplierService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  * <p>
  * 项目结项审批人设置表 服务实现类
@@ -17,4 +24,47 @@ import org.springframework.stereotype.Service;
 @Service
 public class ProjectClosureApplierServiceImpl extends ServiceImpl<ProjectClosureApplierMapper, ProjectClosureApplier> implements ProjectClosureApplierService {
 
+    @Override
+    public List<Map<String, Object>> getConfigList(Integer timeTypeId) {
+        QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
+        if (timeTypeId != null && timeTypeId > 0) {
+            wrapper.eq("time_type_id", timeTypeId);
+        }
+        wrapper.orderByAsc("sort_order");
+        List<ProjectClosureApplier> list = baseMapper.selectList(wrapper);
+        
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ProjectClosureApplier applier : list) {
+            Map<String, Object> step = new HashMap<>();
+            step.put("id", applier.getId());
+            step.put("roleId", applier.getApproverUserId());
+            step.put("roleName", applier.getApproverName());
+            step.put("stepOrder", applier.getSortOrder());
+            result.add(step);
+        }
+        return result;
+    }
+
+    @Override
+    public void saveConfig(Integer timeTypeId, List<Map<String, Object>> configSteps) {
+        // 先删除该时间类型下的旧配置
+        QueryWrapper<ProjectClosureApplier> deleteWrapper = new QueryWrapper<>();
+        deleteWrapper.eq("time_type_id", timeTypeId);
+        baseMapper.delete(deleteWrapper);
+        
+        // 添加新配置
+        LocalDateTime now = LocalDateTime.now();
+        for (int i = 0; i < configSteps.size(); i++) {
+            Map<String, Object> step = configSteps.get(i);
+            ProjectClosureApplier applier = new ProjectClosureApplier();
+            applier.setTimeTypeId(timeTypeId);
+            applier.setApproverUserId((String) step.get("roleId"));
+            applier.setApproverName((String) step.get("roleName"));
+            applier.setSortOrder((Integer) step.get("stepOrder"));
+            applier.setStatus(1);
+            applier.setCreateTime(now);
+            applier.setUpdateTime(now);
+            baseMapper.insert(applier);
+        }
+    }
 }

+ 221 - 120
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplyServiceImpl.java

@@ -4,222 +4,323 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.management.platform.entity.Project;
 import com.management.platform.entity.ProjectClosureApplier;
 import com.management.platform.entity.ProjectClosureApply;
 import com.management.platform.entity.ProjectClosureApprovalLog;
+import com.management.platform.entity.User;
 import com.management.platform.mapper.ProjectClosureApplierMapper;
 import com.management.platform.mapper.ProjectClosureApplyMapper;
 import com.management.platform.mapper.ProjectClosureApprovalLogMapper;
-import com.management.platform.mapper.ProjectClosureAttachmentMapper;
 import com.management.platform.service.ProjectClosureApplyService;
+import com.management.platform.service.ProjectService;
+import com.management.platform.service.UserService;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
- * <p>
- * 项目结项申请表 服务实现类
- * </p>
- *
- * @author Seyason
- * @since 2026-05-12
+ * 项目结项申请:提交、多级审批、通过后完成项目
  */
 @Service
 public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureApplyMapper, ProjectClosureApply> implements ProjectClosureApplyService {
 
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
     @Resource
     private ProjectClosureApplierMapper closureApplierMapper;
     @Resource
     private ProjectClosureApprovalLogMapper approvalLogMapper;
     @Resource
-    private ProjectClosureAttachmentMapper attachmentMapper;
+    private ProjectService projectService;
+    @Resource
+    private UserService userService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public ProjectClosureApply submitApply(Integer projectId, Integer userId, String reason, List<Integer> attachmentIds) {
-        // 1. 获取项目信息(需要通过项目Service获取实际项目名称和编码)
-        String projectName = "项目-" + projectId;
-        String projectCode = "CODE-" + projectId;
-        
-        // 2. 创建申请记录
+    public ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<Integer> attachmentIds) {
+        if (projectId == null || !StringUtils.hasText(userId)) {
+            throw new RuntimeException("参数不完整");
+        }
+        Project project = projectService.getById(projectId);
+        if (project == null) {
+            throw new RuntimeException("项目不存在");
+        }
+        if (project.getStatus() == null || project.getStatus() != 1) {
+            throw new RuntimeException("仅进行中的项目可发起结项申请");
+        }
+        boolean canApply = Objects.equals(userId, project.getCreatorId()) || Objects.equals(userId, project.getInchargerId());
+        if (!canApply) {
+            throw new RuntimeException("仅项目负责人或创建人可发起结项申请");
+        }
+        long pending = this.count(new QueryWrapper<ProjectClosureApply>()
+                .eq("project_id", projectId)
+                .in("approval_status", 0, 1));
+        if (pending > 0) {
+            throw new RuntimeException("该项目已有进行中的结项申请");
+        }
+        Integer companyId = project.getCompanyId();
+        List<ProjectClosureApplier> flow = getApprovalFlow(companyId);
+        if (CollectionUtils.isEmpty(flow)) {
+            throw new RuntimeException("未配置结项审批人,请在系统设置中配置后再申请");
+        }
+        User applicant = userService.getById(userId);
+        String applicantName = applicant != null && StringUtils.hasText(applicant.getName()) ? applicant.getName() : userId;
+
         ProjectClosureApply apply = new ProjectClosureApply();
         apply.setProjectId(projectId);
-        apply.setProjectName(projectName);
-        apply.setProjectCode(projectCode);
+        apply.setProjectName(project.getProjectName());
+        apply.setProjectCode(project.getProjectCode());
         apply.setApplicantId(userId);
-        apply.setApplicantName("用户" + userId); // TODO: 从用户表查询
+        apply.setApplicantName(applicantName);
         apply.setCurrentStep(0);
-        apply.setApprovalStatus(0); // 待审批
+        apply.setApprovalStatus(1);
         apply.setRemark(reason);
         apply.setCreateTime(LocalDateTime.now());
         apply.setUpdateTime(LocalDateTime.now());
-        
-        // 保存申请记录
+
+        ProjectClosureApplier first = flow.get(0);
+        apply.setCurrentApproverId(first.getApproverUserId());
+        apply.setCurrentApproverName(first.getApproverName());
+
         this.save(apply);
-        
-        // 3. 保存附件
         if (!CollectionUtils.isEmpty(attachmentIds)) {
-            for (Integer attachmentId : attachmentIds) {
-                // TODO: 从附件Service获取附件信息并保存到project_closure_attachment表
-            }
+            // 附件表关联可按 attachmentIds 扩展,当前仅保存申请主表
         }
-        
-        // 4. 获取审批流程,设置第一个审批人
-        this.setNextApprover(apply.getId());
-        
+        decorateApply(apply, userId, companyId);
         return apply;
     }
 
     @Override
-    public List<ProjectClosureApplier> getApprovalFlow(Integer timeTypeId) {
+    public List<ProjectClosureApplier> getApprovalFlow(Integer companyId) {
+        if (companyId == null) {
+            return new ArrayList<>();
+        }
         QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        wrapper.eq("time_type_id", timeTypeId)
-               .eq("status", 1) // 启用状态
-               .orderByAsc("sort_order");
+        wrapper.eq("time_type_id", companyId)
+                .eq("status", 1)
+                .orderByAsc("sort_order");
         return closureApplierMapper.selectList(wrapper);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean approve(Integer applyId, Integer applierId, String comment, Integer action) {
-        // 1. 获取申请记录
+    public boolean approve(Integer applyId, String applierId, String comment, Integer action) {
+        if (applyId == null || !StringUtils.hasText(applierId) || action == null) {
+            return false;
+        }
         ProjectClosureApply apply = this.getById(applyId);
         if (apply == null) {
             return false;
         }
-        
-        // 2. 检查是否是当前审批人
-        if (!applierId.equals(apply.getCurrentApproverId())) {
+        if (!Objects.equals(applierId, apply.getCurrentApproverId())) {
             throw new RuntimeException("您不是当前审批人");
         }
-        
-        // 3. 创建审批记录
+        int as = apply.getApprovalStatus() == null ? -1 : apply.getApprovalStatus();
+        if (as != 0 && as != 1) {
+            throw new RuntimeException("该申请已结束审批");
+        }
+        boolean pass = Objects.equals(action, 2) || Objects.equals(action, 1);
+        boolean reject = Objects.equals(action, 3);
+
+        User approver = userService.getById(applierId);
+        String approverName = approver != null && StringUtils.hasText(approver.getName()) ? approver.getName() : applierId;
+
         ProjectClosureApprovalLog log = new ProjectClosureApprovalLog();
         log.setApplyId(applyId);
         log.setApproverId(applierId);
-        log.setApproverName("用户" + applierId); // TODO: 从用户表查询
-        log.setAction(action);
+        log.setApproverName(approverName);
+        log.setAction(pass ? 2 : 3);
         log.setOpinion(comment);
         log.setCreateTime(LocalDateTime.now());
         approvalLogMapper.insert(log);
-        
-        // 4. 根据审批结果处理
-        if (action == 1) { // 同意
-            apply.setCurrentStep(apply.getCurrentStep() + 1);
-            
-            // 检查是否还有下一个审批人
-            ProjectClosureApplier nextApplier = this.getNextApprover(apply.getProjectId(), apply.getCurrentStep());
-            if (nextApplier != null) {
-                // 还有下一个审批人
-                apply.setCurrentApproverId(nextApplier.getApproverUserId());
-                apply.setCurrentApproverName(nextApplier.getApproverName());
-                apply.setApprovalStatus(1); // 审批中
-                this.updateById(apply);
-            } else {
-                // 所有审批人都同意
-                apply.setApprovalStatus(2); // 已通过
-                this.updateById(apply);
-                // 调用审批通过回调
-                this.onApprovalPassed(applyId);
-            }
-        } else { // 驳回
-            apply.setApprovalStatus(3); // 已驳回
+
+        Project project = projectService.getById(apply.getProjectId());
+        Integer companyId = project != null ? project.getCompanyId() : null;
+        List<ProjectClosureApplier> flow = companyId != null ? getApprovalFlow(companyId) : new ArrayList<>();
+
+        if (reject) {
+            apply.setApprovalStatus(3);
+            apply.setUpdateTime(LocalDateTime.now());
+            this.updateById(apply);
+            return true;
+        }
+        if (!pass) {
+            throw new RuntimeException("不支持的审批操作");
+        }
+
+        int nextStepIndex = (apply.getCurrentStep() == null ? 0 : apply.getCurrentStep()) + 1;
+        apply.setCurrentStep(nextStepIndex);
+        if (nextStepIndex >= flow.size()) {
+            apply.setApprovalStatus(2);
+            apply.setCurrentApproverId(null);
+            apply.setCurrentApproverName(null);
+            apply.setUpdateTime(LocalDateTime.now());
+            this.updateById(apply);
+            onApprovalPassed(applyId);
+        } else {
+            ProjectClosureApplier next = flow.get(nextStepIndex);
+            apply.setCurrentApproverId(next.getApproverUserId());
+            apply.setCurrentApproverName(next.getApproverName());
+            apply.setApprovalStatus(1);
+            apply.setUpdateTime(LocalDateTime.now());
             this.updateById(apply);
         }
-        
         return true;
     }
 
     @Override
-    public Map<String, Object> getProgress(Integer applyId) {
+    public Map<String, Object> getProgress(Integer applyId, String viewerUserId) {
+        Map<String, Object> progress = new HashMap<>();
         ProjectClosureApply apply = this.getById(applyId);
         if (apply == null) {
-            return new HashMap<>();
+            return progress;
+        }
+        Project project = projectService.getById(apply.getProjectId());
+        Integer companyId = project != null ? project.getCompanyId() : null;
+        List<ProjectClosureApplier> flowList = companyId != null ? getApprovalFlow(companyId) : new ArrayList<>();
+        List<Map<String, Object>> flow = new ArrayList<>();
+        for (ProjectClosureApplier step : flowList) {
+            Map<String, Object> row = new HashMap<>();
+            row.put("approverName", step.getApproverName());
+            row.put("approverUserId", step.getApproverUserId());
+            flow.add(row);
         }
-        
-        Map<String, Object> progress = new HashMap<>();
-        progress.put("apply", apply);
-        progress.put("logs", approvalLogMapper.selectList(
-            new QueryWrapper<ProjectClosureApprovalLog>()
-                .eq("apply_id", applyId)
-                .orderByDesc("create_time")
-        ));
-        
-        // 获取审批流程
-        ProjectClosureApply actualApply = this.getById(applyId);
-        // TODO: 需要通过项目ID获取timeTypeId,然后获取审批流程
-        List<ProjectClosureApplier> flow = new ArrayList<>();
         progress.put("flow", flow);
-        
+        boolean canApprove = StringUtils.hasText(viewerUserId)
+                && Objects.equals(viewerUserId, apply.getCurrentApproverId())
+                && (apply.getApprovalStatus() != null && (apply.getApprovalStatus() == 0 || apply.getApprovalStatus() == 1));
+        progress.put("canApprove", canApprove);
         return progress;
     }
 
     @Override
     public List<ProjectClosureApprovalLog> getApprovalLogs(Integer applyId) {
-        return approvalLogMapper.selectList(
-            new QueryWrapper<ProjectClosureApprovalLog>()
-                .eq("apply_id", applyId)
-                .orderByDesc("create_time")
-        );
+        List<ProjectClosureApprovalLog> logs = approvalLogMapper.selectList(
+                new QueryWrapper<ProjectClosureApprovalLog>()
+                        .eq("apply_id", applyId)
+                        .orderByAsc("create_time"));
+        for (ProjectClosureApprovalLog log : logs) {
+            log.setApplierName(log.getApproverName());
+            log.setComment(log.getOpinion());
+            if (log.getCreateTime() != null) {
+                log.setApproveTime(log.getCreateTime().format(DT_FMT));
+            }
+        }
+        return logs;
     }
 
     @Override
-    public IPage<ProjectClosureApply> getMyPendingApproval(Integer userId, int current, int size) {
+    public IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status) {
         Page<ProjectClosureApply> page = new Page<>(current, size);
+        User viewer = userService.getById(userId);
+        if (viewer == null || viewer.getCompanyId() == null) {
+            return page;
+        }
+        Integer companyId = viewer.getCompanyId();
         QueryWrapper<ProjectClosureApply> wrapper = new QueryWrapper<>();
-        wrapper.eq("current_approver_id", userId)
-               .in("approval_status", 0, 1) // 待审批或审批中
-               .orderByDesc("create_time");
-        return this.page(page, wrapper);
+        wrapper.inSql("project_id", "select id from project where company_id = " + companyId)
+                .and(w -> w.eq("current_approver_id", userId).or().eq("applicant_id", userId));
+        int st = status == null ? 0 : status;
+        if (st == 1) {
+            wrapper.in("approval_status", 0, 1);
+        } else if (st == 2) {
+            wrapper.eq("approval_status", 2);
+        } else if (st == 3) {
+            wrapper.eq("approval_status", 3);
+        } else if (st == 4) {
+            wrapper.eq("approval_status", 4);
+        }
+        wrapper.orderByDesc("create_time");
+        IPage<ProjectClosureApply> result = this.page(page, wrapper);
+        for (ProjectClosureApply row : result.getRecords()) {
+            decorateApply(row, userId, companyId);
+        }
+        return result;
     }
 
     @Override
-    public IPage<ProjectClosureApply> getMyApplyList(Integer userId, int current, int size) {
+    public IPage<ProjectClosureApply> getMyApplyList(String userId, int current, int size) {
         Page<ProjectClosureApply> page = new Page<>(current, size);
+        User viewer = userService.getById(userId);
+        Integer companyId = viewer != null ? viewer.getCompanyId() : null;
         QueryWrapper<ProjectClosureApply> wrapper = new QueryWrapper<>();
-        wrapper.eq("applicant_id", userId)
-               .orderByDesc("create_time");
-        return this.page(page, wrapper);
+        wrapper.eq("applicant_id", userId);
+        if (companyId != null) {
+            wrapper.inSql("project_id", "select id from project where company_id = " + companyId);
+        }
+        wrapper.orderByDesc("create_time");
+        IPage<ProjectClosureApply> result = this.page(page, wrapper);
+        for (ProjectClosureApply row : result.getRecords()) {
+            decorateApply(row, userId, companyId);
+        }
+        return result;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void onApprovalPassed(Integer applyId) {
-        // 审批通过后,更新项目状态
         ProjectClosureApply apply = this.getById(applyId);
-        if (apply != null) {
-            // TODO: 调用项目Service更新项目状态为"已结项"
-            // projectService.updateStatus(apply.getProjectId(), ProjectStatus.CLOSED);
+        if (apply == null) {
+            return;
         }
+        Project upd = new Project();
+        upd.setId(apply.getProjectId());
+        upd.setStatus(2);
+        upd.setFinishDate(LocalDate.now());
+        projectService.updateById(upd);
     }
-    
-    /**
-     * 获取下一个审批人
-     */
-    private ProjectClosureApplier getNextApprover(Integer projectId, int currentStep) {
-        // TODO: 需要通过项目获取timeTypeId,然后查询审批人
-        return null;
+
+    @Override
+    public ProjectClosureApply getApplyView(Integer id, String viewerUserId) {
+        ProjectClosureApply apply = this.getById(id);
+        if (apply == null) {
+            return null;
+        }
+        Project project = projectService.getById(apply.getProjectId());
+        Integer companyId = project != null ? project.getCompanyId() : null;
+        decorateApply(apply, viewerUserId, companyId);
+        return apply;
     }
-    
-    /**
-     * 设置下一个审批人
-     */
-    private void setNextApprover(Integer applyId) {
-        ProjectClosureApply apply = this.getById(applyId);
-        if (apply != null) {
-            ProjectClosureApplier firstApprover = this.getNextApprover(apply.getProjectId(), 0);
-            if (firstApprover != null) {
-                apply.setCurrentApproverId(firstApprover.getApproverUserId());
-                apply.setCurrentApproverName(firstApprover.getApproverName());
-                this.updateById(apply);
-            }
+
+    private void decorateApply(ProjectClosureApply apply, String viewerUserId, Integer companyId) {
+        int total = companyId != null ? getApprovalFlow(companyId).size() : 0;
+        apply.setTotalStep(total);
+        apply.setReason(apply.getRemark());
+        apply.setCurrentApplierName(apply.getCurrentApproverName());
+        if (apply.getCreateTime() != null) {
+            apply.setApplyTime(apply.getCreateTime().format(DT_FMT));
+        }
+        Integer s = apply.getApprovalStatus();
+        if (s == null) {
+            apply.setStatus(1);
+        } else if (s == 0 || s == 1) {
+            apply.setStatus(1);
+        } else if (s == 2) {
+            apply.setStatus(2);
+        } else if (s == 3) {
+            apply.setStatus(3);
+        } else if (s == 4) {
+            apply.setStatus(4);
+        } else {
+            apply.setStatus(1);
         }
+        int cs = apply.getCurrentStep() == null ? 0 : apply.getCurrentStep();
+        apply.setCurrentStepDisplay(total > 0 ? (cs + 1) + "/" + total : "-");
+        boolean can = StringUtils.hasText(viewerUserId)
+                && Objects.equals(viewerUserId, apply.getCurrentApproverId())
+                && (s != null && (s == 0 || s == 1));
+        apply.setCanApprove(can);
     }
-}
+}

+ 7 - 13
fhKeeper/formulahousekeeper/timesheet/src/routes.js

@@ -24,7 +24,9 @@ import task from './views/task/list.vue'
 import projectInside from  './views/project/projectInside.vue'
 import info from './views/project/info.vue'
 import projectGantt from './views/project/project_gantt.vue'
-import projectClosure from './views/project/closureList.vue'
+import closureList from './views/project/closureList.vue'
+import closureDetail from './views/project/closureDetail.vue'
+import closureApprovalConfig from './views/project/closureApprovalConfig.vue'
 
 // 立项管理
 import projectApproval from './views/projectApproval/projectApproval.vue'
@@ -316,6 +318,10 @@ export const allRouters = [//组织架构
         leaf: true,
         children: [
             { path: '/list', component: list, name: '项目管理' },
+            { path: '/closureList', component: closureList, name: '结项审批' },
+            { path: '/closureDetail/:id', component: closureDetail, name: '结项审批详情' },
+            { path: '/closureApprovalConfig', component: closureApprovalConfig, name: '审批流程配置' },
+            { path: '/closureApprovalConfig', component: closureApprovalConfig, name: '审批流程配置' },
         ],
         // 其他信息
         meta: { text: 'navigation.projectManagement' } 
@@ -332,18 +338,6 @@ export const allRouters = [//组织架构
         // 其他信息
         meta: { text: 'navigation.projectApproval' } 
     },
-    {
-        path: '/',
-        component: Home,
-        name: '结项审批',
-        iconCls: 'iconfont firerock-iconxiangmu',
-        leaf: true,
-        children: [
-            { path: '/projectClosure', component: projectClosure, name: '结项审批' },
-        ],
-        // 其他信息
-        meta: { text: 'navigation.projectClosure' } 
-    },
     //预算工时审核
     {
         path: '/',

+ 11 - 18
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureApplyForm.vue

@@ -71,26 +71,19 @@ export default {
             this.$refs.form.validate((valid) => {
                 if (!valid) return
                 const attachmentIds = this.fileList.map(f => f.response && f.response.data).filter(Boolean).join(',')
-                this.$axios.post(`/project-closure-apply/submit?projectId=${this.projectId}&userId=${this.userId}&reason=${encodeURIComponent(this.form.reason)}&attachmentIds=${attachmentIds}`)
-                    .then(res => {
-                        if (res.data.code === 200) {
-                            this.$message.success('申请提交成功')
-                            this.dialogVisible = false
-                            this.$emit('success')
-                        } else {
-                            this.$message.error(res.data.msg || '提交失败')
-                        }
-                    })
-                    .catch(err => {
-                        this.$message.error('提交失败:' + err.message)
-                    })
+                let user = JSON.parse(sessionStorage.getItem('user') || '{}')
+                let currentUserId = user.id || ''
+                this.http.post(`/project-closure-apply/submit?projectId=${this.projectId}&userId=${currentUserId}&reason=${encodeURIComponent(this.form.reason)}&attachmentIds=${attachmentIds}`, {}, (res) => {
+                    if (res.code === 200 || res.code === 'ok') {
+                        this.$message.success('申请提交成功')
+                        this.dialogVisible = false
+                        this.$emit('success')
+                    } else {
+                        this.$message.error(res.msg || '提交失败')
+                    }
+                })
             })
         }
-    },
-    computed: {
-        userId() {
-            return this.$store.state.user ? this.$store.state.user.id : null
-        }
     }
 }
 </script>

+ 34 - 25
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureDetail.vue

@@ -17,7 +17,7 @@
                     <el-tag v-else-if="detail.status === 3" type="danger">审批驳回</el-tag>
                     <el-tag v-else-if="detail.status === 4" type="info">已撤销</el-tag>
                 </el-descriptions-item>
-                <el-descriptions-item :label="$t('当前步骤') || '当前步骤'">{{ detail.currentStep }}/{{ detail.totalStep }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('当前步骤') || '当前步骤'">{{ detail.currentStepDisplay || '-' }}</el-descriptions-item>
                 <el-descriptions-item :label="$t('当前审批人') || '当前审批人'">{{ detail.currentApplierName || '-' }}</el-descriptions-item>
                 <el-descriptions-item :label="$t('申请原因') || '申请原因'" :span="2">{{ detail.reason || '-' }}</el-descriptions-item>
             </el-descriptions>
@@ -90,66 +90,75 @@ export default {
     },
     created() {
         this.loadDetail()
-        this.loadApprovalFlow()
+        this.loadProgress()
         this.loadApprovalLogs()
     },
     methods: {
+        apiOk(res) {
+            const c = res.data && res.data.code
+            return c === 200 || c === 'ok'
+        },
         loadDetail() {
-            this.$axios.get(`/project-closure-apply/detail/${this.applyId}`).then(res => {
-                if (res.data.code === 200) {
-                    this.detail = res.data.data
+            this.http.get(`/project-closure-apply/detail/${this.applyId}?viewerId=${this.userId}`, (res) => {
+                if (res.code === 200 || res.code === 'ok') {
+                    this.detail = res.data
                 }
             })
         },
-        loadApprovalFlow() {
-            this.$axios.get(`/project-closure-apply/approval-logs/${this.applyId}`).then(res => {
-                if (res.data.code === 200) {
-                    this.approvalLogs = res.data.data
+        loadApprovalLogs() {
+            this.http.get(`/project-closure-apply/approval-logs/${this.applyId}`, (res) => {
+                if (res.code === 200 || res.code === 'ok') {
+                    this.approvalLogs = res.data || []
                 }
             })
         },
-        loadApprovalLogs() {
-            this.$axios.get(`/project-closure-apply/progress/${this.applyId}`).then(res => {
-                if (res.data.code === 200) {
-                    const progress = res.data.data
+        loadProgress() {
+            this.http.get(`/project-closure-apply/progress/${this.applyId}?viewerId=${this.userId}`, (res) => {
+                if (res.code === 200 || res.code === 'ok') {
+                    const progress = res.data || {}
                     if (progress.flow) this.approvalFlow = progress.flow
-                    if (progress.canApprove) this.canApprove = progress.canApprove
+                    if (progress.canApprove !== undefined) this.canApprove = progress.canApprove
                 }
             })
         },
         getTimelineType(index) {
             const apply = this.detail
             if (!apply) return 'info'
-            if (index < apply.currentStep) return 'success'
-            if (index === apply.currentStep) return 'primary'
+            const step = apply.currentStep == null ? 0 : apply.currentStep
+            if (index < step) return 'success'
+            if (index === step) return 'primary'
             return 'info'
         },
         getStepTime(index) {
-            const log = this.approvalLogs.find(l => l.applierIndex === index)
-            return log ? log.approveTime : ''
+            const flow = this.approvalFlow
+            if (!flow || !flow[index]) return ''
+            const uid = flow[index].approverUserId
+            const log = this.approvalLogs.find(l => String(l.approverId) === String(uid))
+            return log ? (log.approveTime || '') : ''
         },
         getStepStatus(index) {
             const apply = this.detail
             if (!apply) return ''
-            if (index < apply.currentStep) return '已通过'
-            if (index === apply.currentStep) {
+            const step = apply.currentStep == null ? 0 : apply.currentStep
+            if (index < step) return '已通过'
+            if (index === step) {
                 if (apply.status === 1) return '待审批'
                 if (apply.status === 3) return '已驳回'
             }
             return '待审批'
         },
         doApprove(action) {
-            this.$axios.post(`/project-closure-apply/approve?applyId=${this.applyId}&applierId=${this.userId}&comment=${encodeURIComponent(this.comment)}&action=${action}`).then(res => {
-                if (res.data.code === 200) {
+            this.http.post(`/project-closure-apply/approve?applyId=${this.applyId}&applierId=${this.userId}&comment=${encodeURIComponent(this.comment)}&action=${action}`, {}, (res) => {
+                if (res.code === 200 || res.code === 'ok') {
                     this.$message.success('审批成功')
                     this.loadDetail()
-                    this.loadApprovalFlow()
+                    this.loadProgress()
                     this.loadApprovalLogs()
                 } else {
-                    this.$message.error(res.data.msg || '审批失败')
+                    this.$message.error(res.msg || '审批失败')
                 }
             })
         }
     }
 }
-</script>
+</script>

+ 384 - 137
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureList.vue

@@ -1,150 +1,397 @@
 <template>
-    <div>
-        <el-card>
-            <!--工具条-->
-            <el-col :span="24" class="toolbar">
-                <el-form :inline="true">
-                    <el-form-item :label="$t('状态') || '状态'">
-                        <el-select v-model="status" placeholder="请选择" clearable size="small" @change="loadData()">
-                            <el-option label="待审批" value=1></el-option>
-                            <el-option label="审批通过" value=2></el-option>
-                            <el-option label="审批驳回" value=3></el-option>
-                            <el-option label="已撤销" value=4></el-option>
-                            <el-option label="全部" value=0></el-option>
-                        </el-select>
-                    </el-form-item>
-                    <el-form-item>
-                        <el-button size="small" type="primary" @click="loadData">查询</el-button>
-                    </el-form-item>
-                </el-form>
-            </el-col>
-            
-            <!--列表-->
-            <el-table :data="list" v-loading="listLoading" border style="width: 100%;" :height="tableHeight">
-                <el-table-column prop="projectName" :label="$t('项目名称') || '项目名称'" min-width="200"></el-table-column>
-                <el-table-column prop="applicantName" :label="$t('申请人') || '申请人'" width="120"></el-table-column>
-                <el-table-column prop="applyTime" :label="$t('申请时间') || '申请时间'" width="160"></el-table-column>
-                <el-table-column prop="currentStep" :label="$t('当前步骤') || '当前步骤'" width="100"></el-table-column>
-                <el-table-column prop="status" :label="$t('状态') || '状态'" width="100">
-                    <template slot-scope="scope">
-                        <el-tag v-if="scope.row.status === 1" type="warning">待审批</el-tag>
-                        <el-tag v-else-if="scope.row.status === 2" type="success">审批通过</el-tag>
-                        <el-tag v-else-if="scope.row.status === 3" type="danger">审批驳回</el-tag>
-                        <el-tag v-else-if="scope.row.status === 4" type="info">已撤销</el-tag>
-                        <el-tag v-else>-</el-tag>
-                    </template>
-                </el-table-column>
-                <el-table-column :label="$t('操作') || '操作'" width="200" fixed="right">
-                    <template slot-scope="scope">
-                        <el-button size="mini" type="primary" @click="viewDetail(scope.row.id)">查看详情</el-button>
-                        <el-button v-if="scope.row.status === 1" size="mini" type="success" @click="handleApprove(scope.row)">审批</el-button>
-                    </template>
-                </el-table-column>
-            </el-table>
-            
-            <!--分页-->
-            <el-pagination
-                @size-change="handleSizeChange"
-                @current-change="handleCurrentChange"
-                :current-page="currentPage"
-                :page-sizes="[10, 20, 50]"
-                :page-size="pageSize"
-                layout="total, sizes, prev, pager, next, jumper"
-                :total="total"
-                style="float: right; margin-top: 10px;">
-            </el-pagination>
-        </el-card>
-        
-        <!--审批弹窗-->
-        <el-dialog :title="'审批结项申请'" :visible.sync="approveDialog" width="500px">
-            <el-form :model="approveForm" label-width="100px">
-                <el-form-item label="申请项目">
-                    <el-input :value="currentApply.projectName" disabled></el-input>
-                </el-form-item>
-                <el-form-item label="审批意见">
-                    <el-input type="textarea" v-model="approveForm.comment" rows="4"></el-input>
-                </el-form-item>
-            </el-form>
-            <div slot="footer">
-                <el-button @click="approveDialog = false">取消</el-button>
-                <el-button type="danger" @click="doApprove(3)">驳回</el-button>
-                <el-button type="success" @click="doApprove(2)">通过</el-button>
-            </div>
-        </el-dialog>
-    </div>
+  <div>
+    <el-card>
+      <!--工具条-->
+      <el-col :span="24" class="toolbar">
+        <el-form :inline="true">
+          <el-form-item :label="$t('状态') || '状态'">
+            <el-select
+              v-model="status"
+              placeholder="请选择"
+              clearable
+              size="small"
+              @change="loadData()"
+            >
+              <el-option label="待审批" value="1"></el-option>
+              <el-option label="审批通过" value="2"></el-option>
+              <el-option label="审批驳回" value="3"></el-option>
+              <el-option label="已撤销" value="4"></el-option>
+              <el-option label="全部" value="0"></el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button size="small" type="primary" @click="loadData"
+              >查询</el-button
+            >
+            <el-button size="small" type="warning" @click="openConfig"
+              >配置审批流程</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </el-col>
+
+      <!--列表-->
+      <el-table
+        :data="list"
+        v-loading="listLoading"
+        border
+        style="width: 100%"
+        :height="tableHeight"
+      >
+        <el-table-column
+          prop="projectName"
+          :label="$t('项目名称') || '项目名称'"
+          min-width="200"
+        ></el-table-column>
+        <el-table-column
+          prop="applicantName"
+          :label="$t('申请人') || '申请人'"
+          width="120"
+        ></el-table-column>
+        <el-table-column
+          prop="applyTime"
+          :label="$t('申请时间') || '申请时间'"
+          width="160"
+        ></el-table-column>
+        <el-table-column
+          prop="currentStepDisplay"
+          :label="$t('当前步骤') || '当前步骤'"
+          width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="status"
+          :label="$t('状态') || '状态'"
+          width="100"
+        >
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status === 1" type="warning">待审批</el-tag>
+            <el-tag v-else-if="scope.row.status === 2" type="success"
+              >审批通过</el-tag
+            >
+            <el-tag v-else-if="scope.row.status === 3" type="danger"
+              >审批驳回</el-tag
+            >
+            <el-tag v-else-if="scope.row.status === 4" type="info"
+              >已撤销</el-tag
+            >
+            <el-tag v-else>-</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          :label="$t('操作') || '操作'"
+          width="200"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="primary"
+              @click="viewDetail(scope.row.id)"
+              >查看详情</el-button
+            >
+            <el-button
+              v-if="scope.row.canApprove"
+              size="mini"
+              type="success"
+              @click="handleApprove(scope.row)"
+              >审批</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!--分页-->
+      <el-pagination
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-sizes="[10, 20, 50]"
+        :page-size="pageSize"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+        style="float: right; margin-top: 10px"
+      >
+      </el-pagination>
+    </el-card>
+
+    <!--审批弹窗-->
+    <el-dialog
+      :title="'审批结项申请'"
+      :visible.sync="approveDialog"
+      width="500px"
+    >
+      <el-form :model="approveForm" label-width="100px">
+        <el-form-item label="申请项目">
+          <el-input :value="currentApply.projectName" disabled></el-input>
+        </el-form-item>
+        <el-form-item label="审批意见">
+          <el-input
+            type="textarea"
+            v-model="approveForm.comment"
+            rows="4"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="approveDialog = false">取消</el-button>
+        <el-button type="danger" @click="doApprove(3)">驳回</el-button>
+        <el-button type="success" @click="doApprove(2)">通过</el-button>
+      </div>
+    </el-dialog>
+
+    <!--审批流程配置弹窗-->
+    <el-dialog
+      :title="'配置审批流程'"
+      :visible.sync="configDialog"
+      width="700px"
+    >
+      <div class="config-dialog-content">
+        <div class="config-toolbar">
+          <el-button
+            size="small"
+            type="primary"
+            icon="el-icon-plus"
+            @click="addStep"
+            >添加审批步骤</el-button
+          >
+        </div>
+        <el-table :data="configSteps" border size="small" class="config-table">
+          <el-table-column
+            type="index"
+            label="顺序"
+            width="60"
+            align="center"
+          ></el-table-column>
+          <el-table-column label="审批角色" min-width="180">
+            <template slot-scope="scope">
+              <el-select
+                v-model="scope.row.role"
+                placeholder="选择角色"
+                size="small"
+                class="role-select"
+              >
+                <el-option
+                  v-for="role in roleList"
+                  :key="role.id"
+                  :label="role.rolename"
+                  :value="role"
+                ></el-option>
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="280" align="center">
+            <template slot-scope="scope">
+              <div class="action-buttons">
+                <el-button
+                  size="mini"
+                  type="primary"
+                  plain
+                  @click="moveStep(scope.$index, -1)"
+                  :disabled="scope.$index === 0"
+                  >上移</el-button
+                >
+                <el-button
+                  size="mini"
+                  type="primary"
+                  plain
+                  @click="moveStep(scope.$index, 1)"
+                  :disabled="scope.$index === configSteps.length - 1"
+                  >下移</el-button
+                >
+                <el-button
+                  size="mini"
+                  type="danger"
+                  plain
+                  @click="removeStep(scope.$index)"
+                  >删除</el-button
+                >
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div slot="footer">
+        <el-button @click="configDialog = false">取消</el-button>
+        <el-button type="primary" @click="saveConfig">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
 </template>
 
 <script>
 export default {
-    data() {
-        return {
-            list: [],
-            listLoading: true,
-            status: 0,
-            currentPage: 1,
-            pageSize: 10,
-            total: 0,
-            approveDialog: false,
-            currentApply: {},
-            approveForm: {
-                comment: ''
-            }
-        }
+  data() {
+    return {
+      list: [],
+      listLoading: true,
+      status: 0,
+      currentPage: 1,
+      pageSize: 10,
+      total: 0,
+      approveDialog: false,
+      configDialog: false,
+      currentApply: {},
+      approveForm: {
+        comment: "",
+      },
+      configSteps: [],
+      roleList: [],
+    };
+  },
+  computed: {
+    tableHeight() {
+      return window.innerHeight - 300;
     },
-    computed: {
-        tableHeight() {
-            return window.innerHeight - 300
-        },
-        userId() {
-            return this.$store.state.user ? this.$store.state.user.id : null
-        }
+    userId() {
+      const user = JSON.parse(sessionStorage.getItem("user") || "{}");
+      return user.id || null;
     },
-    created() {
-        this.loadData()
+    user() {
+      return JSON.parse(sessionStorage.getItem("user") || "{}");
     },
-    methods: {
-        loadData() {
-            this.listLoading = true
-            const url = `/project-closure-apply/my-pending?userId=${this.userId}&current=${this.currentPage}&size=${this.pageSize}&status=${this.status}`
-            this.$axios.get(url).then(res => {
-                if (res.data.code === 200) {
-                    const data = res.data.data
-                    this.list = data.records || []
-                    this.total = data.total || 0
-                }
-                this.listLoading = false
-            }).catch(() => {
-                this.listLoading = false
-            })
-        },
-        handleSizeChange(val) {
-            this.pageSize = val
-            this.currentPage = 1
-            this.loadData()
+  },
+  created() {
+    this.loadData();
+  },
+  methods: {
+    loadData() {
+      this.listLoading = true;
+      const url = `/project-closure-apply/my-pending?userId=${this.userId}&current=${this.currentPage}&size=${this.pageSize}&status=${this.status}`;
+      this.http.get(
+        url,
+        (res) => {
+          if (res.code === 200 || res.code === "ok") {
+            const data = res.data;
+            this.list = data.records || [];
+            this.total = data.total || 0;
+          }
+          this.listLoading = false;
         },
-        handleCurrentChange(val) {
-            this.currentPage = val
-            this.loadData()
+        () => {
+          this.listLoading = false;
         },
-        viewDetail(id) {
-            this.$router.push('/project/closureDetail/' + id)
-        },
-        handleApprove(row) {
-            this.currentApply = row
-            this.approveForm.comment = ''
-            this.approveDialog = true
+      );
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.loadData();
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.loadData();
+    },
+    viewDetail(id) {
+      this.$router.push("/closureDetail/" + id);
+    },
+    handleApprove(row) {
+      this.currentApply = row;
+      this.approveForm.comment = "";
+      this.approveDialog = true;
+    },
+    doApprove(action) {
+      this.http.post(
+        `/project-closure-apply/approve?applyId=${
+          this.currentApply.id
+        }&applierId=${this.userId}&comment=${encodeURIComponent(
+          this.approveForm.comment,
+        )}&action=${action}`,
+        {},
+        (res) => {
+          if (res.code === 200 || res.code === "ok") {
+            this.$message.success("审批成功");
+            this.approveDialog = false;
+            this.loadData();
+            this.$emit("success");
+          } else {
+            this.$message.error(res.msg || "审批失败");
+          }
         },
-        doApprove(action) {
-            this.$axios.post(`/project-closure-apply/approve?applyId=${this.currentApply.id}&applierId=${this.userId}&comment=${encodeURIComponent(this.approveForm.comment)}&action=${action}`).then(res => {
-                if (res.data.code === 200) {
-                    this.$message.success('审批成功')
-                    this.approveDialog = false
-                    this.loadData()
-                } else {
-                    this.$message.error(res.data.msg || '审批失败')
-                }
-            })
+      );
+    },
+    // ========== 审批流程配置相关方法 ==========
+    openConfig() {
+      this.configDialog = true;
+      // 先加载角色列表,角色列表加载完成后再加载配置
+      const companyId = this.user.companyId;
+      this.http.post("/permission/getRoleList", { companyId }, (res) => {
+        if (res.code === 200 || res.code === "ok") {
+          this.roleList = res.data || [];
+          const roleIdToRole = {};
+          this.roleList.forEach((role) => {
+            roleIdToRole[role.id] = role;
+          });
+          // 角色列表加载完成后,再加载审批配置
+          this.http.get("/project-closure-apply/config/list", (res2) => {
+            if (res2.code === 200 || res2.code === "ok") {
+              this.configSteps = (res2.data || []).map((step) => ({
+                role: roleIdToRole[step.roleId] || null,
+              }));
+            }
+          });
+        } else {
+          this.$message.error(res.msg || "加载角色列表失败");
+          this.roleList = [];
         }
-    }
+      });
+    },
+    addStep() {
+      this.configSteps.push({ role: null });
+    },
+    removeStep(index) {
+      this.configSteps.splice(index, 1);
+    },
+    moveStep(index, direction) {
+      const newIndex = index + direction;
+      if (newIndex < 0 || newIndex >= this.configSteps.length) return;
+      // 使用 splice 方式确保 Vue 响应式更新
+      const item = this.configSteps.splice(index, 1)[0];
+      this.configSteps.splice(newIndex, 0, item);
+    },
+    saveConfig() {
+      const payload = this.configSteps.map((step, index) => ({
+        stepOrder: index + 1,
+        roleId: step.role ? step.role.id : null,
+        roleName: step.role ? step.role.roleName : "",
+      }));
+      this.http.post("/project-closure-apply/config/save", payload, (res) => {
+        if (res.code === 200 || res.code === "ok") {
+          this.$message.success("配置保存成功");
+          this.configDialog = false;
+        } else {
+          this.$message.error(res.msg || "保存失败");
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style scoped>
+.config-dialog-content {
+  padding: 0 10px;
+}
+
+.config-toolbar {
+  margin-bottom: 15px;
+}
+
+.config-table ::v-deep .el-table__body-wrapper {
+  overflow-x: visible;
+}
+
+.role-select {
+  width: 100%;
+}
+
+.action-buttons {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+}
+
+.action-buttons .el-button {
+  margin: 0;
 }
-</script>
+</style>

Plik diff jest za duży
+ 9735 - 6203
fhKeeper/formulahousekeeper/timesheet/src/views/project/list.vue


+ 0 - 10
fhKeeper/formulahousekeeper/timesheet/src/views/settings/timetype.vue

@@ -1193,16 +1193,6 @@
         "设置部门或人员的特殊节假日"
       }}</span>
     </div>
-    <!-- 项目结项申请 -->
-    <div class="yanjiu">
-      <p style="margin: 0 68px 0 10px; color: #666">{{ "项目结项" }}</p>
-      <el-switch
-        style="margin-left: 55px"
-        v-model="timeType.is_project_closure"
-        :active-color="themeColor"
-      >
-      </el-switch>
-    </div>
     <!-- 特殊人员填报设置 -->
     <div class="yanjiu" v-if="user.companyId == 1071">
       <p style="margin: 0 68px 0 10px; color: #666">{{ "特殊人员填报设置" }}</p>