Bläddra i källkod

项目结项功能

yusm 1 vecka sedan
förälder
incheckning
c0d500777e
19 ändrade filer med 5090 tillägg och 4430 borttagningar
  1. 5 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/PermissionController.java
  2. 7 7
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplierController.java
  3. 53 10
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplyController.java
  4. 9 3
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApplier.java
  5. 4 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApply.java
  6. 2 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/PermissionService.java
  7. 31 3
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplierService.java
  8. 2 2
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplyService.java
  9. 8 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/PermissionServiceImpl.java
  10. 149 18
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplierServiceImpl.java
  11. 83 6
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplyServiceImpl.java
  12. 2 2
      fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/ProjectClosureApplierMapper.xml
  13. 48 0
      fhKeeper/formulahousekeeper/management-platform/src/main/resources/sql/2026-05-13.sql
  14. 95 0
      fhKeeper/formulahousekeeper/timesheet/src/components/ClosureFilter.vue
  15. 10 33
      fhKeeper/formulahousekeeper/timesheet/src/views/corpreport/list.vue
  16. 65 6
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureApplyForm.vue
  17. 64 7
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureDetail.vue
  18. 205 53
      fhKeeper/formulahousekeeper/timesheet/src/views/project/closureList.vue
  19. 4248 4280
      fhKeeper/formulahousekeeper/timesheet/src/views/project/list.vue

+ 5 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/PermissionController.java

@@ -63,6 +63,11 @@ public class PermissionController {
         return permissionService.getRoleList(companyId);
     }
 
+    @RequestMapping("/getRoleUserList")
+    public HttpRespMsg getRoleUserList(@RequestParam Integer roleId, @RequestParam Integer companyId) {
+        return permissionService.getRoleUserList(roleId, companyId);
+    }
+
     @RequestMapping("/editRole")
     public HttpRespMsg editRole(Integer id, @RequestParam String name, @RequestParam String description, Integer companyId) {
         return permissionService.editRole(id, name, description, companyId);

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

@@ -28,10 +28,10 @@ public class ProjectClosureApplierController {
      * 获取审批人配置列表
      */
     @GetMapping("/list")
-    public HttpRespMsg list(@RequestParam(required = false) Integer timeTypeId) {
+    public HttpRespMsg list(@RequestParam(required = false) Integer companyId) {
         QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        if (timeTypeId != null && timeTypeId > 0) {
-            wrapper.eq("time_type_id", timeTypeId);
+        if (companyId != null && companyId > 0) {
+            wrapper.eq("company_id", companyId);
         }
         wrapper.orderByAsc("sort_order");
         List<ProjectClosureApplier> list = closureApplierMapper.selectList(wrapper);
@@ -54,14 +54,14 @@ public class ProjectClosureApplierController {
      * 添加审批人配置
      */
     @PostMapping("/add")
-    public HttpRespMsg add(@RequestParam Integer timeTypeId,
+    public HttpRespMsg add(@RequestParam Integer companyId,
                            @RequestParam String approverUserId,
                            @RequestParam String approverName,
                            @RequestParam Integer sortOrder,
                            @RequestParam(defaultValue = "1") Integer status) {
         try {
             ProjectClosureApplier applier = new ProjectClosureApplier();
-            applier.setTimeTypeId(timeTypeId);
+            applier.setCompanyId(companyId);
             applier.setApproverUserId(approverUserId);
             applier.setApproverName(approverName);
             applier.setSortOrder(sortOrder);
@@ -80,7 +80,7 @@ public class ProjectClosureApplierController {
      */
     @PostMapping("/update")
     public HttpRespMsg update(@RequestParam Integer id,
-                              @RequestParam Integer timeTypeId,
+                              @RequestParam Integer companyId,
                               @RequestParam String approverUserId,
                               @RequestParam String approverName,
                               @RequestParam Integer sortOrder,
@@ -88,7 +88,7 @@ public class ProjectClosureApplierController {
         try {
             ProjectClosureApplier applier = new ProjectClosureApplier();
             applier.setId(id);
-            applier.setTimeTypeId(timeTypeId);
+            applier.setCompanyId(companyId);
             applier.setApproverUserId(approverUserId);
             applier.setApproverName(approverName);
             applier.setSortOrder(sortOrder);

+ 53 - 10
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplyController.java

@@ -1,9 +1,14 @@
 package com.management.platform.controller;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.management.platform.entity.Project;
+import com.management.platform.entity.ProjectClosureApplier;
 import com.management.platform.entity.ProjectClosureApply;
+import com.management.platform.service.ProjectClosureApplierService;
 import com.management.platform.service.ProjectClosureApplyService;
+import com.management.platform.service.ProjectService;
 import com.management.platform.util.HttpRespMsg;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -21,7 +26,10 @@ public class ProjectClosureApplyController {
     private ProjectClosureApplyService closureApplyService;
 
     @Resource
-    private com.management.platform.service.ProjectClosureApplierService closureApplierService;
+    private ProjectClosureApplierService closureApplierService;
+
+    @Resource
+    private ProjectService projectService;
 
     @PostMapping("/submit")
     public HttpRespMsg submitApply(@RequestParam Integer projectId,
@@ -29,14 +37,21 @@ public class ProjectClosureApplyController {
                                    @RequestParam(required = false) String reason,
                                    @RequestParam(required = false) String attachmentIds) {
         try {
-            String[] ids = attachmentIds != null ? attachmentIds.split(",") : new String[0];
-            List<Integer> attachmentIdList = new java.util.ArrayList<>();
-            for (String id : ids) {
-                if (id != null && !id.trim().isEmpty()) {
-                    attachmentIdList.add(Integer.parseInt(id.trim()));
+            List<String> attachmentIdList = new java.util.ArrayList<>();
+            if (attachmentIds != null && !attachmentIds.trim().isEmpty()) {
+                String[] ids = attachmentIds.split(",");
+                for (String id : ids) {
+                    if (id != null && !id.trim().isEmpty()) {
+                        attachmentIdList.add(id.trim());
+                    }
                 }
             }
             ProjectClosureApply apply = closureApplyService.submitApply(projectId, userId, reason, attachmentIdList);
+            Project byId = projectService.getById(projectId);
+            if (byId != null) {
+                byId.setStatus(5);
+                projectService.updateById(byId);
+            }
             return HttpRespMsg.success("申请提交成功", apply);
         } catch (Exception e) {
             return HttpRespMsg.error("申请提交失败:" + e.getMessage());
@@ -75,11 +90,11 @@ public class ProjectClosureApplyController {
 
     @PostMapping("/approve")
     public HttpRespMsg approve(@RequestParam Integer applyId,
-                               @RequestParam String applierId,
+                               @RequestParam String approverUserId,
                                @RequestParam(required = false) String comment,
                                @RequestParam Integer action) {
         try {
-            boolean result = closureApplyService.approve(applyId, applierId, comment, action);
+            boolean result = closureApplyService.approve(applyId, approverUserId, comment, action);
             if (result) {
                 return HttpRespMsg.success("审批成功");
             }
@@ -93,9 +108,10 @@ public class ProjectClosureApplyController {
     public HttpRespMsg getMyPendingApproval(@RequestParam String userId,
                                             @RequestParam(defaultValue = "1") int current,
                                             @RequestParam(defaultValue = "10") int size,
-                                            @RequestParam(required = false, defaultValue = "0") Integer status) {
+                                            @RequestParam(required = false, defaultValue = "0") Integer status,
+                                            @RequestParam(required = false) String projectKeyword) {
         try {
-            IPage<ProjectClosureApply> page = closureApplyService.getMyPendingApproval(userId, current, size, status);
+            IPage<ProjectClosureApply> page = closureApplyService.getMyPendingApproval(userId, current, size, status, projectKeyword);
             return HttpRespMsg.success(page);
         } catch (Exception e) {
             return HttpRespMsg.error("获取待审批列表失败:" + e.getMessage());
@@ -113,4 +129,31 @@ public class ProjectClosureApplyController {
             return HttpRespMsg.error("获取申请列表失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 获取审批人配置列表
+     */
+    @GetMapping("/config/list")
+    public HttpRespMsg getConfigList(@RequestParam(required = false) Integer companyId) {
+        try {
+            List<Map<String, Object>> list = closureApplierService.getConfigList(companyId);
+            return HttpRespMsg.success(list);
+        } catch (Exception e) {
+            return HttpRespMsg.error("获取配置列表失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 保存审批人配置(批量)
+     */
+    @PostMapping("/config/save")
+    @Transactional
+    public HttpRespMsg saveConfigList(Integer companyId,@RequestBody List<ProjectClosureApplier> configList) {
+        try {
+            closureApplierService.saveConfig(companyId, configList);
+            return HttpRespMsg.success("配置保存成功");
+        } catch (Exception e) {
+            return HttpRespMsg.error("配置保存失败:" + e.getMessage());
+        }
+    }
 }

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

@@ -33,10 +33,10 @@ public class ProjectClosureApplier extends Model<ProjectClosureApplier> {
     private Integer id;
 
     /**
-     * 关联time_type.id(按企业配置)
+     * 关联企业companyId(按企业配置)
      */
-    @TableField("time_type_id")
-    private Integer timeTypeId;
+    @TableField("company_id")
+    private Integer companyId;
 
     /**
      * 审批人用户ID
@@ -50,6 +50,12 @@ public class ProjectClosureApplier extends Model<ProjectClosureApplier> {
     @TableField("approver_name")
     private String approverName;
 
+    /**
+     * 角色ID
+     */
+    @TableField("role_id")
+    private String roleId;
+
     /**
      * 排序(多审批人顺序)
      */

+ 4 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApply.java

@@ -10,6 +10,7 @@ import lombok.experimental.Accessors;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * <p>
@@ -120,6 +121,9 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
     @TableField(exist = false)
     private Boolean canApprove;
 
+    @TableField(exist = false)
+    private List<ProjectClosureAttachment> attachments;
+
 
     @Override
     protected Serializable pkVal() {

+ 2 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/PermissionService.java

@@ -23,6 +23,8 @@ public interface PermissionService extends IService<Permission> {
 
     HttpRespMsg getRoleList(Integer companyId);
 
+    HttpRespMsg getRoleUserList(Integer roleId, Integer companyId);
+
     HttpRespMsg editRole(Integer id, String name, String description, Integer companyId);
 
     HttpRespMsg deleteRole(Integer id);

+ 31 - 3
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplierService.java

@@ -1,28 +1,56 @@
 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>
+
  * 项目结项审批人设置表 服务类
+
  * </p>
+
  *
+
  * @author Seyason
+
  * @since 2026-05-12
+
  */
+
 public interface ProjectClosureApplierService extends IService<ProjectClosureApplier> {
 
+
+
     /**
-     * 获取审批流程配置列表(按时间类型)
+
+     * 获取审批流程配置列表(按企业)
+
      */
-    List<Map<String, Object>> getConfigList(Integer timeTypeId);
+
+    List<Map<String, Object>> getConfigList(Integer companyId);
+
+
 
     /**
+
      * 保存审批流程配置
+
      */
-    void saveConfig(Integer timeTypeId, List<Map<String, Object>> configSteps);
+
+    void saveConfig(Integer companyId, List<ProjectClosureApplier> configSteps);
+
 }
+

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

@@ -14,7 +14,7 @@ import java.util.Map;
  */
 public interface ProjectClosureApplyService extends IService<ProjectClosureApply> {
 
-    ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<Integer> attachmentIds);
+    ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<String> attachmentIds);
 
     List<ProjectClosureApplier> getApprovalFlow(Integer companyId);
 
@@ -24,7 +24,7 @@ public interface ProjectClosureApplyService extends IService<ProjectClosureApply
 
     List<ProjectClosureApprovalLog> getApprovalLogs(Integer applyId);
 
-    IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status);
+    IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status, String projectKeyword);
 
     IPage<ProjectClosureApply> getMyApplyList(String userId, int current, int size);
 

+ 8 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/PermissionServiceImpl.java

@@ -112,6 +112,14 @@ public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permiss
         return httpRespMsg;
     }
 
+    @Override
+    public HttpRespMsg getRoleUserList(Integer roleId, Integer companyId) {
+        HttpRespMsg httpRespMsg = new HttpRespMsg();
+        List<User> userList = userMapper.selectList(new QueryWrapper<User>().eq("role_id", roleId).eq("company_id", companyId));
+        httpRespMsg.data = userList;
+        return httpRespMsg;
+    }
+
     @Override
     public HttpRespMsg editRole(Integer id, String name, String description, Integer companyId) {
         HttpRespMsg msg = new HttpRespMsg();

+ 149 - 18
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplierServiceImpl.java

@@ -1,70 +1,201 @@
 package com.management.platform.service.impl;
 
+
+
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+
 import com.management.platform.entity.ProjectClosureApplier;
+
+import com.management.platform.entity.User;
+
 import com.management.platform.mapper.ProjectClosureApplierMapper;
+
+import com.management.platform.service.PermissionService;
+
 import com.management.platform.service.ProjectClosureApplierService;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+
+import com.management.platform.service.UserService;
+
+import com.management.platform.util.HttpRespMsg;
+
 import org.springframework.stereotype.Service;
 
+import org.springframework.util.StringUtils;
+
+
+
+import javax.annotation.Resource;
+
 import java.time.LocalDateTime;
+
 import java.util.ArrayList;
+
 import java.util.HashMap;
+
 import java.util.List;
+
 import java.util.Map;
 
+
+
 /**
+
  * <p>
+
  * 项目结项审批人设置表 服务实现类
+
  * </p>
+
  *
+
  * @author Seyason
+
  * @since 2026-05-12
+
  */
+
 @Service
+
 public class ProjectClosureApplierServiceImpl extends ServiceImpl<ProjectClosureApplierMapper, ProjectClosureApplier> implements ProjectClosureApplierService {
 
+
+
+    @Resource
+
+    PermissionService permissionService;
+
+
+
     @Override
-    public List<Map<String, Object>> getConfigList(Integer timeTypeId) {
+
+    public List<Map<String, Object>> getConfigList(Integer companyId) {
+
         QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        if (timeTypeId != null && timeTypeId > 0) {
-            wrapper.eq("time_type_id", timeTypeId);
+
+        if (companyId != null && companyId > 0) {
+
+            wrapper.eq("company_id", companyId);
+
         }
+
         wrapper.orderByAsc("sort_order");
+
         List<ProjectClosureApplier> list = baseMapper.selectList(wrapper);
-        
+
         List<Map<String, Object>> result = new ArrayList<>();
-        for (ProjectClosureApplier applier : list) {
+
+        for (int i = 0; i < list.size(); i++) {
+
+            ProjectClosureApplier applier = list.get(i);
+
             Map<String, Object> step = new HashMap<>();
+
             step.put("id", applier.getId());
-            step.put("roleId", applier.getApproverUserId());
-            step.put("roleName", applier.getApproverName());
+
+            step.put("roleId", applier.getRoleId());
+
+            step.put("approverUserId", applier.getApproverUserId());
+
+            step.put("approverName", applier.getApproverName());
+
             step.put("stepOrder", applier.getSortOrder());
+
+            step.put("stepLabel", "第" + (i + 1) + "步");
+
+
+
+            // 下一步节点标志
+
+            if (i + 1 < list.size()) {
+
+                ProjectClosureApplier nextApplier = list.get(i + 1);
+
+                step.put("nextStepLabel", "下一步:" + nextApplier.getApproverName() + "审批");
+
+            } else {
+
+                step.put("nextStepLabel", "无");
+
+            }
+
+
+
+            // 获取该节点的角色用户列表
+
+            if (StringUtils.hasText(applier.getRoleId())) {
+
+                HttpRespMsg roleUserList = permissionService.getRoleUserList(Integer.valueOf(applier.getRoleId()), companyId);
+
+                if (roleUserList != null && roleUserList.getData() != null) {
+
+                    step.put("userList", roleUserList.getData());
+
+                } else {
+
+                    step.put("userList", new ArrayList<>());
+
+                }
+
+            } else {
+
+                step.put("userList", new ArrayList<>());
+
+            }
+
             result.add(step);
+
         }
+
         return result;
+
     }
 
+
+
     @Override
-    public void saveConfig(Integer timeTypeId, List<Map<String, Object>> configSteps) {
-        // 先删除该时间类型下的旧配置
+
+    public void saveConfig(Integer companyId, List<ProjectClosureApplier> configSteps) {
+
+        // 先删除该企业下的旧配置
+
         QueryWrapper<ProjectClosureApplier> deleteWrapper = new QueryWrapper<>();
-        deleteWrapper.eq("time_type_id", timeTypeId);
+
+        deleteWrapper.eq("company_id", companyId);
+
         baseMapper.delete(deleteWrapper);
-        
+
         // 添加新配置
+
         LocalDateTime now = LocalDateTime.now();
+
         for (int i = 0; i < configSteps.size(); i++) {
-            Map<String, Object> step = configSteps.get(i);
+
+            ProjectClosureApplier projectClosureApplier = 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.setCompanyId(companyId);
+
+            applier.setRoleId(projectClosureApplier.getRoleId());
+
+            applier.setApproverUserId(projectClosureApplier.getApproverUserId());
+
+            applier.setApproverName(projectClosureApplier.getApproverName());
+
+            applier.setSortOrder(projectClosureApplier.getSortOrder());
+
             applier.setStatus(1);
+
             applier.setCreateTime(now);
+
             applier.setUpdateTime(now);
+
             baseMapper.insert(applier);
+
         }
+
     }
-}
+
+}

+ 83 - 6
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplyServiceImpl.java

@@ -8,10 +8,12 @@ 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.ProjectClosureAttachment;
 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;
@@ -43,13 +45,15 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
     @Resource
     private ProjectClosureApprovalLogMapper approvalLogMapper;
     @Resource
+    private ProjectClosureAttachmentMapper attachmentMapper;
+    @Resource
     private ProjectService projectService;
     @Resource
     private UserService userService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<Integer> attachmentIds) {
+    public ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<String> attachmentIds) {
         if (projectId == null || !StringUtils.hasText(userId)) {
             throw new RuntimeException("参数不完整");
         }
@@ -95,8 +99,40 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         apply.setCurrentApproverName(first.getApproverName());
 
         this.save(apply);
+
+        project.setStatus(5);
+        projectService.updateById(project);
+
         if (!CollectionUtils.isEmpty(attachmentIds)) {
-            // 附件表关联可按 attachmentIds 扩展,当前仅保存申请主表
+            for (String attachmentPath : attachmentIds) {
+                if (!StringUtils.hasText(attachmentPath)) {
+                    continue;
+                }
+                ProjectClosureAttachment attachment = new ProjectClosureAttachment();
+                attachment.setApplyId(apply.getId());
+                String raw = attachmentPath.trim();
+                String fileUrl = raw;
+                String fileName = raw;
+                int sep = raw.indexOf("@@");
+                if (sep > -1) {
+                    fileUrl = raw.substring(0, sep);
+                    fileName = raw.substring(sep + 2);
+                } else {
+                    int slash = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
+                    if (slash >= 0 && slash < fileName.length() - 1) {
+                        fileName = fileName.substring(slash + 1);
+                    }
+                }
+                attachment.setFileUrl(fileUrl);
+                attachment.setFileName(fileName);
+                try {
+                    attachment.setUploadUserId(Integer.valueOf(userId));
+                } catch (Exception ignore) {
+                    attachment.setUploadUserId(null);
+                }
+                attachment.setCreateTime(LocalDateTime.now());
+                attachmentMapper.insert(attachment);
+            }
         }
         decorateApply(apply, userId, companyId);
         return apply;
@@ -108,7 +144,7 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
             return new ArrayList<>();
         }
         QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        wrapper.eq("time_type_id", companyId)
+        wrapper.eq("company_id", companyId)
                 .eq("status", 1)
                 .orderByAsc("sort_order");
         return closureApplierMapper.selectList(wrapper);
@@ -154,6 +190,12 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
             apply.setApprovalStatus(3);
             apply.setUpdateTime(LocalDateTime.now());
             this.updateById(apply);
+            if (project != null) {
+                Project rejected = new Project();
+                rejected.setId(project.getId());
+                rejected.setStatus(1);
+                projectService.updateById(rejected);
+            }
             return true;
         }
         if (!pass) {
@@ -191,10 +233,12 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         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) {
+        for (int i = 0; i < flowList.size(); i++) {
+            ProjectClosureApplier step = flowList.get(i);
             Map<String, Object> row = new HashMap<>();
             row.put("approverName", step.getApproverName());
             row.put("approverUserId", step.getApproverUserId());
+            row.put("stepOrder", i);
             flow.add(row);
         }
         progress.put("flow", flow);
@@ -222,7 +266,7 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
     }
 
     @Override
-    public IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status) {
+    public IPage<ProjectClosureApply> getMyPendingApproval(String userId, int current, int size, Integer status, String projectKeyword) {
         Page<ProjectClosureApply> page = new Page<>(current, size);
         User viewer = userService.getById(userId);
         if (viewer == null || viewer.getCompanyId() == null) {
@@ -232,6 +276,9 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         QueryWrapper<ProjectClosureApply> wrapper = new QueryWrapper<>();
         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));
+        if (StringUtils.hasText(projectKeyword)) {
+            wrapper.like("project_name", projectKeyword.trim());
+        }
         int st = status == null ? 0 : status;
         if (st == 1) {
             wrapper.in("approval_status", 0, 1);
@@ -291,6 +338,14 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         Project project = projectService.getById(apply.getProjectId());
         Integer companyId = project != null ? project.getCompanyId() : null;
         decorateApply(apply, viewerUserId, companyId);
+
+        // 查询附件列表
+        List<ProjectClosureAttachment> attachments = attachmentMapper.selectList(
+                new QueryWrapper<ProjectClosureAttachment>()
+                        .eq("apply_id", id)
+                        .orderByDesc("create_time"));
+        apply.setAttachments(attachments);
+
         return apply;
     }
 
@@ -318,9 +373,31 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         }
         int cs = apply.getCurrentStep() == null ? 0 : apply.getCurrentStep();
         apply.setCurrentStepDisplay(total > 0 ? (cs + 1) + "/" + total : "-");
+
+        // 优化当前步骤显示
+        if (companyId != null && total > 0) {
+            List<ProjectClosureApplier> flow = getApprovalFlow(companyId);
+            if (s != null && s == 2) {
+                // 已通过 - 当前步骤显示"暂无"
+                apply.setCurrentStepDisplay("暂无");
+            } else if (s != null && s == 3) {
+                // 已拒绝 - 当前步骤显示"暂无"
+                apply.setCurrentStepDisplay("暂无");
+            } else {
+                // 进行中 - 当前步骤显示"XXX审核"
+                if (cs < flow.size()) {
+                    ProjectClosureApplier currentApplier = flow.get(cs);
+                    String currentStepText = "第" + (cs + 1) + "步:" + currentApplier.getApproverName() + "审核";
+                    apply.setCurrentStepDisplay(currentStepText);
+                } else {
+                    apply.setCurrentStepDisplay("暂无");
+                }
+            }
+        }
+
         boolean can = StringUtils.hasText(viewerUserId)
                 && Objects.equals(viewerUserId, apply.getCurrentApproverId())
                 && (s != null && (s == 0 || s == 1));
         apply.setCanApprove(can);
     }
-}
+}

+ 2 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/ProjectClosureApplierMapper.xml

@@ -5,7 +5,7 @@
     <!-- 通用查询映射结果 -->
     <resultMap id="BaseResultMap" type="com.management.platform.entity.ProjectClosureApplier">
         <id column="id" property="id" />
-        <result column="time_type_id" property="timeTypeId" />
+        <result column="company_id" property="companyId" />
         <result column="approver_user_id" property="approverUserId" />
         <result column="approver_name" property="approverName" />
         <result column="sort_order" property="sortOrder" />
@@ -16,7 +16,7 @@
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, time_type_id, approver_user_id, approver_name, sort_order, status, create_time, update_time
+        id, company_id, approver_user_id, approver_name, sort_order, status, create_time, update_time
     </sql>
 
 </mapper>

+ 48 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/resources/sql/2026-05-13.sql

@@ -0,0 +1,48 @@
+create table `project_closure_applier` (
+	`id` int (20),
+	`company_id` int (20),
+	`approver_user_id` varchar (600),
+	`role_id` varchar (150),
+	`approver_name` varchar (300),
+	`sort_order` int (11),
+	`status` tinyint (4),
+	`create_time` datetime ,
+	`update_time` datetime
+);
+create table `project_closure_apply` (
+	`id` int (11),
+	`project_id` int (20),
+	`project_name` varchar (600),
+	`project_code` varchar (300),
+	`applicant_id` varchar (300),
+	`applicant_name` varchar (300),
+	`current_step` int (11),
+	`current_approver_id` varchar (300),
+	`current_approver_name` varchar (300),
+	`approval_status` int (4),
+	`remark` varchar (1500),
+	`create_time` datetime ,
+	`update_time` datetime
+);
+create table `project_closure_approval_log` (
+	`id` int (20),
+	`apply_id` int (20),
+	`approver_id` varchar (600),
+	`approver_name` varchar (300),
+	`action` int (4),
+	`opinion` varchar (1500),
+	`create_time` datetime
+);
+create table `project_closure_attachment` (
+	`id` int (20),
+	`apply_id` int (20),
+	`file_name` varchar (600),
+	`file_url` varchar (1500),
+	`file_size` int (20),
+	`file_type` varchar (150),
+	`upload_user_id` int (20),
+	`create_time` datetime
+);
+
+
+ALTER TABLE time_type ADD `is_project_closure` INT(10) DEFAULT 0 COMMENT '是否开启项目结项功能'

+ 95 - 0
fhKeeper/formulahousekeeper/timesheet/src/components/ClosureFilter.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="closure-filter">
+    <el-form :inline="true" size="small" class="closure-filter-form">
+      <div class="closure-filter-left">
+        <el-form-item :label="$t('状态') || '状态'">
+          <el-select
+            v-model="localStatus"
+            placeholder="请选择"
+            clearable
+            @change="emitSearch"
+          >
+            <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 :label="$t('项目名称') || '项目名称'">
+          <el-input
+            v-model="localProjectKeyword"
+            clearable
+            placeholder="请输入项目名称"
+            @input="$emit('project-change', localProjectKeyword)"
+            @keyup.enter.native="emitSearch"
+          ></el-input>
+        </el-form-item>
+      </div>
+      <div class="closure-filter-right">
+        <slot name="actions">
+          <el-button type="primary" @click="emitSearch">查询</el-button>
+        </slot>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ClosureFilter",
+  props: {
+    status: {
+      type: String,
+      default: "0",
+    },
+    projectKeyword: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      localStatus: this.status,
+      localProjectKeyword: this.projectKeyword,
+    };
+  },
+  methods: {
+    emitSearch() {
+      this.$emit("search", {
+        status: this.localStatus,
+        projectKeyword: this.localProjectKeyword,
+      });
+    },
+  },
+  watch: {
+    status(val) {
+      this.localStatus = val;
+    },
+    projectKeyword(val) {
+      this.localProjectKeyword = val;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.closure-filter {
+  padding: 10px 0;
+}
+
+.closure-filter-form {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  width: 100%;
+}
+
+.closure-filter-left,
+.closure-filter-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+</style>

+ 10 - 33
fhKeeper/formulahousekeeper/timesheet/src/views/corpreport/list.vue

@@ -6207,26 +6207,6 @@
             </template>
             <!-- 员工考勤表 -->
             <template v-if="ins == 44">
-              <div class="corp-time-report-title">
-                <div class="corp-time-report-title__main">
-                  {{
-                    user.company && user.company.companyName
-                      ? user.company.companyName
-                      : ""
-                  }}
-                  {{
-                    monthPersonnel || dayjs(new Date()).format("YYYY-MM")
-                  }}月考勤表
-                </div>
-                <div class="corp-time-report-title__sub">
-                  <span
-                    >日期范围:{{
-                      monthPersonnel || dayjs(new Date()).format("YYYY-MM")
-                    }}</span
-                  >
-                  <span>单位:天</span>
-                </div>
-              </div>
               <div class="corp-time-table-wrap">
                 <el-table
                   key="44"
@@ -6277,19 +6257,10 @@
                   >
                     <template slot-scope="scope">
                       <span v-if="user.userNameNeedTranslate == '1'">
-                        <span
-                          v-for="(item, index) in scope.row.departmentName"
-                          :key="index"
-                        >
-                          <TranslationOpenDataText
-                            type="departmentName"
-                            :openid="item"
-                          ></TranslationOpenDataText>
-                          <span
-                            v-if="index < scope.row.departmentName.length - 1"
-                            >/</span
-                          >
-                        </span>
+                        <TranslationOpenDataText
+                          type="departmentName"
+                          :openid="item"
+                        ></TranslationOpenDataText>
                       </span>
                       <span v-if="user.userNameNeedTranslate != '1'">{{
                         scope.row.departmentName
@@ -7415,6 +7386,8 @@ export default {
         "部门成本表",
         "员工出勤表",
         "施工进度表",
+        "员工假勤表",
+        "员工考勤表",
       ],
 
       shuzArr: [
@@ -7459,6 +7432,10 @@ export default {
         "日报明细表",
         "项目薪资成本表",
         "部门成本表",
+        "员工出勤表",
+        "施工进度表",
+        "员工假勤表",
+        "员工考勤表",
       ],
 
       ins: 10000,

+ 65 - 6
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureApplyForm.vue

@@ -22,12 +22,22 @@
                         class="upload-demo"
                         action="#"
                         :file-list="fileList"
-                        :on-change="handleChange"
-                        :auto-upload="false"
+                        :http-request="uploadAttachment"
+                        :on-remove="handleRemove"
                         multiple>
                         <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
-                        <div slot="tip" class="el-upload__tip">只能上传文件,支持多种格式</div>
+                        <div slot="tip" class="el-upload__tip">支持多文件上传,上传后可点击文件名下载</div>
                     </el-upload>
+                    <div v-if="fileList.length" style="margin-top: 10px;">
+                        <el-link
+                            v-for="item in fileList"
+                            :key="item.uid"
+                            type="primary"
+                            :underline="false"
+                            @click="downloadFile(item)">
+                            {{ item.fileName || item.name }}
+                        </el-link>
+                    </div>
                 </el-form-item>
             </el-form>
             <div slot="footer" class="dialog-footer">
@@ -46,6 +56,7 @@ export default {
             projectId: null,
             projectName: '',
             fileList: [],
+            uploadedFiles: [],
             form: {
                 reason: ''
             },
@@ -61,23 +72,71 @@ export default {
             this.projectId = projectId
             this.projectName = projectName
             this.fileList = []
+            this.uploadedFiles = []
             this.form.reason = ''
             this.dialogVisible = true
         },
-        handleChange(file, fileList) {
+        handleRemove(file, fileList) {
             this.fileList = fileList
+            this.uploadedFiles = fileList
+        },
+        uploadAttachment(option) {
+            const formData = new FormData()
+            formData.append('multipartFile', option.file)
+            this.http.uploadFile('/common/uploadFile', formData, (res) => {
+                if (res.code === 'ok') {
+                    const raw = res.data && res.data.url ? res.data.url : res.data
+                    const url = typeof raw === 'string' ? raw : (raw && raw.fileUrl) || ''
+                    const fileName = option.file.name
+                    const fileInfo = {
+                        uid: option.file.uid,
+                        name: fileName,
+                        url,
+                        fileUrl: url,
+                        fileName,
+                        response: { data: { fileUrl: url, fileName } }
+                    }
+                    this.uploadedFiles.push(fileInfo)
+                    this.fileList = [...this.uploadedFiles]
+                    option.onSuccess && option.onSuccess(res, option.file)
+                    this.$message.success('上传成功')
+                } else {
+                    option.onError && option.onError(new Error(res.msg || '上传失败'))
+                    this.$message.error(res.msg || '上传失败')
+                }
+            }, (error) => {
+                option.onError && option.onError(error)
+                this.$message.error(error)
+            })
+        },
+        downloadFile(file) {
+            const url = file && (file.fileUrl || file.url)
+            if (!url) return
+            const a = document.createElement('a')
+            a.setAttribute('download', file.fileName || file.name || '附件')
+            a.setAttribute('href', url)
+            a.click()
+            a.remove()
         },
         submitForm() {
             this.$refs.form.validate((valid) => {
                 if (!valid) return
-                const attachmentIds = this.fileList.map(f => f.response && f.response.data).filter(Boolean).join(',')
+                const attachmentIds = this.fileList
+                    .map(f => {
+                        const url = f.fileUrl || (f.response && f.response.data && f.response.data.fileUrl) || f.url
+                        if (!url) return ''
+                        const name = f.fileName || f.name || ''
+                        return name ? `${url}@@${name}` : url
+                    })
+                    .filter(Boolean)
+                    .join(',')
                 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')
+                        this.$emit('success', { projectId: this.projectId, projectName: this.projectName })
                     } else {
                         this.$message.error(res.msg || '提交失败')
                     }

+ 64 - 7
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureDetail.vue

@@ -1,7 +1,7 @@
 <template>
     <div>
         <el-card>
-            <el-page-header @back="$router.go(-1)" :title="$t('返回') || '返回'">
+            <el-page-header @back="goBack" :title="$t('返回') || '返回'">
                 <template slot="content">
                     <span>{{ $t('结项审批详情') || '结项审批详情' }}</span>
                 </template>
@@ -20,6 +20,20 @@
                 <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-item :label="$t('附件') || '附件'" :span="2">
+                    <div v-if="attachments.length">
+                        <el-link
+                            v-for="item in attachments"
+                            :key="item.id || item.fileUrl || item.url"
+                            type="primary"
+                            :underline="false"
+                            style="margin-right: 12px;"
+                            @click="downloadFile(item)">
+                            {{ item.fileName || item.name || '附件' }}
+                        </el-link>
+                    </div>
+                    <span v-else>-</span>
+                </el-descriptions-item>
             </el-descriptions>
         </el-card>
         
@@ -77,7 +91,8 @@ export default {
             approvalFlow: [],
             approvalLogs: [],
             comment: '',
-            canApprove: false
+            canApprove: false,
+            attachments: []
         }
     },
     computed: {
@@ -94,17 +109,41 @@ export default {
         this.loadApprovalLogs()
     },
     methods: {
-        apiOk(res) {
-            const c = res.data && res.data.code
-            return c === 200 || c === 'ok'
-        },
         loadDetail() {
             this.http.get(`/project-closure-apply/detail/${this.applyId}?viewerId=${this.userId}`, (res) => {
                 if (res.code === 200 || res.code === 'ok') {
-                    this.detail = res.data
+                    this.detail = res.data || {}
+                    this.attachments = this.normalizeAttachments(res.data)
+                }
+            })
+        },
+        normalizeAttachments(detail) {
+            const list = (detail && (detail.attachments || detail.attachmentList || [])) || []
+            return list.map((item) => {
+                const rawUrl = item.fileUrl || item.url || item.filePath || ''
+                const fileUrl = this.resolveFileUrl(rawUrl)
+                return {
+                    ...item,
+                    fileUrl,
+                    fileName: item.fileName || item.name || this.getFileName(rawUrl),
                 }
             })
         },
+        resolveFileUrl(url) {
+            if (!url) return ''
+            const str = String(url)
+            if (/^(https?:)?\/\//i.test(str) || str.startsWith('/') || str.startsWith('blob:') || str.startsWith('data:')) {
+                return str
+            }
+            return `./upload/${str}`
+        },
+        getFileName(url) {
+            if (!url) return '附件'
+            const pure = String(url).split('?')[0]
+            const idx = Math.max(pure.lastIndexOf('/'), pure.lastIndexOf('\\'))
+            return idx >= 0 ? pure.substring(idx + 1) : pure
+        }, 
+
         loadApprovalLogs() {
             this.http.get(`/project-closure-apply/approval-logs/${this.applyId}`, (res) => {
                 if (res.code === 200 || res.code === 'ok') {
@@ -147,9 +186,27 @@ export default {
             }
             return '待审批'
         },
+        downloadFile(file) {
+            const url = file && (file.fileUrl || file.url || file.filePath)
+            if (!url) return
+            const a = document.createElement('a')
+            a.setAttribute('download', file.fileName || file.name || this.getFileName(url))
+            a.setAttribute('href', url)
+            a.target = '_self'
+            document.body.appendChild(a)
+            a.click()
+            a.remove()
+        },
+        goBack() {
+            localStorage.projectTab = '1'
+            this.$router.push('/list')
+        },
         doApprove(action) {
             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') {
+                    if (action === 3 && (this.detail.currentStep === 0 || this.detail.currentStep === '0')) {
+                        this.http.post(`/project/updateStatus?projectId=${this.detail.projectId}&status=2`, {}, () => {})
+                    }
                     this.$message.success('审批成功')
                     this.loadDetail()
                     this.loadProgress()

+ 205 - 53
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureList.vue

@@ -1,34 +1,26 @@
 <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-button size="small" type="warning" @click="openConfig"
-              >配置审批流程</el-button
-            >
-          </el-form-item>
-        </el-form>
-      </el-col>
+      <closure-filter
+        :status="status"
+        :project-keyword="projectKeyword"
+        @change="statusChange"
+        @project-change="projectChange"
+        @search="searchList"
+      >
+        <template #actions>
+          <el-button size="small" type="primary" @click="searchList"
+            >查询</el-button
+          >
+          <el-button
+            v-if="isSuperAdministrator"
+            size="small"
+            type="warning"
+            @click="openConfig"
+            >配置审批流程</el-button
+          >
+        </template>
+      </closure-filter>
 
       <!--列表-->
       <el-table
@@ -56,7 +48,7 @@
         <el-table-column
           prop="currentStepDisplay"
           :label="$t('当前步骤') || '当前步骤'"
-          width="100"
+          width="160"
         ></el-table-column>
         <el-table-column
           prop="status"
@@ -162,13 +154,15 @@
             width="60"
             align="center"
           ></el-table-column>
-          <el-table-column label="审批角色" min-width="180">
+          <el-table-column label="审批角色" min-width="160">
             <template slot-scope="scope">
               <el-select
                 v-model="scope.row.role"
+                value-key="id"
                 placeholder="选择角色"
                 size="small"
                 class="role-select"
+                @change="onRoleChange(scope.$index)"
               >
                 <el-option
                   v-for="role in roleList"
@@ -179,6 +173,27 @@
               </el-select>
             </template>
           </el-table-column>
+          <el-table-column label="审批人员" min-width="160">
+            <template slot-scope="scope">
+              <el-select
+                v-model="scope.row.approver"
+                value-key="id"
+                placeholder="选择人员"
+                size="small"
+                class="user-select"
+                :disabled="!scope.row.role"
+                :loading="scope.row.userLoading"
+                @change="onApproverChange(scope.$index)"
+              >
+                <el-option
+                  v-for="user in scope.row.userList"
+                  :key="user.id"
+                  :label="user.name"
+                  :value="user"
+                ></el-option>
+              </el-select>
+            </template>
+          </el-table-column>
           <el-table-column label="操作" width="280" align="center">
             <template slot-scope="scope">
               <div class="action-buttons">
@@ -219,12 +234,17 @@
 </template>
 
 <script>
+import ClosureFilter from "@/components/ClosureFilter.vue";
 export default {
+  components: {
+    ClosureFilter,
+  },
   data() {
     return {
       list: [],
       listLoading: true,
-      status: 0,
+      status: "",
+      projectKeyword: "",
       currentPage: 1,
       pageSize: 10,
       total: 0,
@@ -236,6 +256,7 @@ export default {
       },
       configSteps: [],
       roleList: [],
+      roleUserCache: {},
     };
   },
   computed: {
@@ -249,6 +270,10 @@ export default {
     user() {
       return JSON.parse(sessionStorage.getItem("user") || "{}");
     },
+    isSuperAdministrator() {
+      const roleName = this.user.roleName || this.user.rolename || "";
+      return roleName === this.$t("role.superAdministrator") || roleName === "超级管理员";
+    },
   },
   created() {
     this.loadData();
@@ -256,13 +281,19 @@ export default {
   methods: {
     loadData() {
       this.listLoading = true;
-      const url = `/project-closure-apply/my-pending?userId=${this.userId}&current=${this.currentPage}&size=${this.pageSize}&status=${this.status}`;
+      const url = `/project-closure-apply/my-pending?userId=${this.userId}&current=${this.currentPage}&size=${this.pageSize}&status=${this.status}${this.projectKeyword ? `&projectKeyword=${encodeURIComponent(this.projectKeyword)}` : ''}`;
       this.http.get(
         url,
         (res) => {
           if (res.code === 200 || res.code === "ok") {
             const data = res.data;
-            this.list = data.records || [];
+            this.list = (data.records || []).map((item) => ({
+              ...item,
+              currentStepDisplay:
+                item.currentStepDisplay && item.currentStepDisplay !== "1/1"
+                  ? item.currentStepDisplay
+                  : item.currentStepDisplay,
+            }));
             this.total = data.total || 0;
           }
           this.listLoading = false;
@@ -277,6 +308,24 @@ export default {
       this.currentPage = 1;
       this.loadData();
     },
+    statusChange(val) {
+      this.status = val;
+      this.loadData();
+    },
+    projectChange(val) {
+      this.projectKeyword = val;
+    },
+    searchList(payload) {
+      if (payload && typeof payload === 'object') {
+        if (payload.status !== undefined) {
+          this.status = payload.status;
+        }
+        if (payload.projectKeyword !== undefined) {
+          this.projectKeyword = payload.projectKeyword;
+        }
+      }
+      this.loadData();
+    },
     handleCurrentChange(val) {
       this.currentPage = val;
       this.loadData();
@@ -293,7 +342,7 @@ export default {
       this.http.post(
         `/project-closure-apply/approve?applyId=${
           this.currentApply.id
-        }&applierId=${this.userId}&comment=${encodeURIComponent(
+        }&approverUserId=${this.userId}&comment=${encodeURIComponent(
           this.approveForm.comment,
         )}&action=${action}`,
         {},
@@ -303,15 +352,32 @@ export default {
             this.approveDialog = false;
             this.loadData();
             this.$emit("success");
+            const isFirstNodeReject =
+              action === 3 &&
+              (this.currentApply.currentStep === 0 ||
+                this.currentApply.currentStep === "0");
+            if (isFirstNodeReject) {
+              this.updateProjectStatus(2);
+            }
           } else {
             this.$message.error(res.msg || "审批失败");
           }
         },
       );
     },
+    updateProjectStatus(status) {
+      const projectId = this.currentApply.projectId || this.currentApply.id;
+      if (!projectId) return;
+      this.http.post(
+        `/project/updateStatus?projectId=${projectId}&status=${status}`,
+        {},
+        () => {},
+      );
+    },
     // ========== 审批流程配置相关方法 ==========
     openConfig() {
       this.configDialog = true;
+      this.roleUserCache = {};
       // 先加载角色列表,角色列表加载完成后再加载配置
       const companyId = this.user.companyId;
       this.http.post("/permission/getRoleList", { companyId }, (res) => {
@@ -320,15 +386,29 @@ export default {
           const roleIdToRole = {};
           this.roleList.forEach((role) => {
             roleIdToRole[role.id] = role;
+            roleIdToRole[String(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,
-              }));
-            }
-          });
+          const companyId = this.user.companyId;
+          this.http.get(
+            "/project-closure-apply/config/list?companyId=" + companyId,
+            (res2) => {
+              if (res2.code === 200 || res2.code === "ok") {
+                this.configSteps = (res2.data || []).map((step) => ({
+                  role: roleIdToRole[step.roleId] || null,
+                  approver: step.userList
+                    ? step.userList.find((u) => u.id === step.approverUserId) ||
+                      null
+                    : null,
+                  userList: step.userList || [],
+                  userLoading: false,
+                  roleId: step.roleId || null,
+                  approverUserId: step.approverUserId || null,
+                  approverName: step.approverName || "",
+                }));
+              }
+            },
+          );
         } else {
           this.$message.error(res.msg || "加载角色列表失败");
           this.roleList = [];
@@ -336,7 +416,15 @@ export default {
       });
     },
     addStep() {
-      this.configSteps.push({ role: null });
+      this.configSteps.push({
+        role: null,
+        approver: null,
+        userList: [],
+        userLoading: false,
+        roleId: null,
+        approverUserId: null,
+        approverName: "",
+      });
     },
     removeStep(index) {
       this.configSteps.splice(index, 1);
@@ -348,20 +436,83 @@ export default {
       const item = this.configSteps.splice(index, 1)[0];
       this.configSteps.splice(newIndex, 0, item);
     },
+    onRoleChange(index) {
+      const step = this.configSteps[index];
+      // 重置人员选择
+      step.approver = null;
+      step.approverUserId = null;
+      step.approverName = "";
+      step.userList = [];
+      if (!step.role) {
+        return;
+      }
+      step.roleId = step.role.id;
+      step.userLoading = true;
+      // 如果已经缓存了该角色的用户,直接使用
+      if (this.roleUserCache[step.role.id]) {
+        step.userList = this.roleUserCache[step.role.id];
+        step.userLoading = false;
+        return;
+      }
+      // 否则从后端获取该角色的用户列表
+      const companyId = this.user.companyId;
+      this.http.post(
+        "/permission/getRoleUserList",
+        { roleId: step.role.id, companyId },
+        (res) => {
+          if (res.code === 200 || res.code === "ok") {
+            step.userList = res.data || [];
+            this.$set(this.roleUserCache, step.role.id, step.userList);
+          } else {
+            step.userList = [];
+            this.$message.error(res.msg || "获取用户列表失败");
+          }
+          step.userLoading = false;
+        },
+        () => {
+          step.userLoading = false;
+        },
+      );
+    },
+    onApproverChange(index) {
+      const step = this.configSteps[index];
+      step.approverUserId = step.approver ? step.approver.id : null;
+      step.approverName = step.approver ? step.approver.name : "";
+    },
     saveConfig() {
+      // 校验
+      for (let i = 0; i < this.configSteps.length; i++) {
+        const step = this.configSteps[i];
+        if (!step.role) {
+          this.$message.warning(`第${i + 1}步请选择审批角色`);
+          return;
+        }
+        if (!step.approver) {
+          this.$message.warning(`第${i + 1}步请选择审批人员`);
+          return;
+        }
+      }
       const payload = this.configSteps.map((step, index) => ({
         stepOrder: index + 1,
-        roleId: step.role ? step.role.id : null,
-        roleName: step.role ? step.role.roleName : "",
+        roleId: step.roleId || (step.role ? step.role.id : null),
+        roleName: step.role ? step.role.rolename : "",
+        approverUserId:
+          step.approverUserId || (step.approver ? step.approver.id : null),
+        approverName:
+          step.approverName || (step.approver ? step.approver.name : ""),
       }));
-      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 || "保存失败");
-        }
-      });
+      this.http.JSONPost(
+        "/project-closure-apply/config/save?companyId=" + this.user.companyId,
+        payload,
+        (res) => {
+          if (res.code === 200 || res.code === "ok") {
+            this.$message.success("配置保存成功");
+            this.configDialog = false;
+          } else {
+            this.$message.error(res.msg || "保存失败");
+          }
+        },
+      );
     },
   },
 };
@@ -380,7 +531,8 @@ export default {
   overflow-x: visible;
 }
 
-.role-select {
+.role-select,
+.user-select {
   width: 100%;
 }
 

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 4248 - 4280
fhKeeper/formulahousekeeper/timesheet/src/views/project/list.vue