Merge branch 'main' of http://49.232.154.205:10100/DeXun/fad_oa
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.hrm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
@@ -18,8 +19,10 @@ import com.ruoyi.hrm.domain.vo.HrmMyApplyVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmSealReqVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
|
||||
import com.ruoyi.hrm.domain.HrmFlowInstance;
|
||||
import com.ruoyi.hrm.mapper.HrmAppropriationReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmEmployeeMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmFlowInstanceMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmLeaveReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmReimburseReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmSealReqMapper;
|
||||
@@ -32,7 +35,9 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -48,6 +53,8 @@ public class HrmMyApplyController extends BaseController {
|
||||
private final HrmSealReqMapper sealReqMapper;
|
||||
private final HrmReimburseReqMapper reimburseReqMapper;
|
||||
private final HrmAppropriationReqMapper appropriationReqMapper;
|
||||
private final HrmFlowInstanceMapper flowInstanceMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmMyApplyVo> list(String bizType, String status, String keyword, PageQuery pageQuery) {
|
||||
@@ -80,6 +87,9 @@ public class HrmMyApplyController extends BaseController {
|
||||
all.addAll(mapAppropriation(appropriationReqMapper.selectVoWithProjectList(buildAppropriationBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
|
||||
// 用流程实例状态覆盖业务表状态,避免历史数据状态未同步
|
||||
overrideStatusByFlowInstance(all);
|
||||
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
String lower = keyword.toLowerCase();
|
||||
all = all.stream().filter(v -> contains(v, lower)).collect(Collectors.toList());
|
||||
@@ -93,6 +103,52 @@ public class HrmMyApplyController extends BaseController {
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
private void overrideStatusByFlowInstance(List<HrmMyApplyVo> all) {
|
||||
if (all == null || all.isEmpty()) return;
|
||||
Map<String, List<Long>> bizIdsByType = all.stream()
|
||||
.filter(v -> v.getBizType() != null && v.getBizId() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
HrmMyApplyVo::getBizType,
|
||||
Collectors.mapping(HrmMyApplyVo::getBizId, Collectors.toList())
|
||||
));
|
||||
Map<String, String> statusByKey = new HashMap<>();
|
||||
bizIdsByType.forEach((bizType, bizIds) -> {
|
||||
if (bizIds.isEmpty()) return;
|
||||
List<HrmFlowInstance> insts = flowInstanceMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<HrmFlowInstance>()
|
||||
.eq(HrmFlowInstance::getBizType, bizType)
|
||||
.in(HrmFlowInstance::getBizId, bizIds)
|
||||
);
|
||||
for (HrmFlowInstance inst : insts) {
|
||||
if (inst.getStatus() == null) continue;
|
||||
// 合并多个流程实例:优先终态 (approved/rejected/withdrawn) > 进行中 (running/pending),
|
||||
// 否则前面遗留的"running"会盖掉新的"approved",导致前端永远显示审批中
|
||||
statusByKey.merge(bizType + "_" + inst.getBizId(), inst.getStatus(),
|
||||
(oldS, newS) -> _statusPriority(newS) > _statusPriority(oldS) ? newS : oldS);
|
||||
}
|
||||
});
|
||||
for (HrmMyApplyVo v : all) {
|
||||
String s = statusByKey.get(v.getBizType() + "_" + v.getBizId());
|
||||
if (s != null) v.setStatus(s);
|
||||
}
|
||||
}
|
||||
|
||||
private int _statusPriority(String s) {
|
||||
if (s == null) return 0;
|
||||
switch (s) {
|
||||
case "approved":
|
||||
case "done":
|
||||
return 3;
|
||||
case "rejected":
|
||||
case "withdrawn":
|
||||
return 2;
|
||||
case "running":
|
||||
case "pending":
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(HrmMyApplyVo v, String lower) {
|
||||
return Objects.toString(v.getTitle(), "").toLowerCase().contains(lower)
|
||||
|| Objects.toString(v.getRemark(), "").toLowerCase().contains(lower)
|
||||
@@ -107,13 +163,14 @@ public class HrmMyApplyController extends BaseController {
|
||||
private HrmReimburseReqBo buildReimburseBo(Long empId, String status) { HrmReimburseReqBo bo = new HrmReimburseReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
|
||||
private HrmAppropriationReqBo buildAppropriationBo(Long empId, String status) { HrmAppropriationReqBo bo = new HrmAppropriationReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
|
||||
|
||||
private List<HrmMyApplyVo> mapLeave(List<HrmLeaveReqVo> list, String nickName) { return list.stream().map(v -> toVo("leave", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapTravel(List<HrmTravelReqVo> list, String nickName) { return list.stream().map(v -> toVo("travel", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapSeal(List<HrmSealReqVo> list, String nickName) { return list.stream().map(v -> toVo("seal", v.getBizId(), v.getEmpId(), nickName, v.getRemark(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapReimburse(List<HrmReimburseReqVo> list, String nickName) { return list.stream().map(v -> toVo("reimburse", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapAppropriation(List<HrmAppropriationReqVo> list, String nickName) { return list.stream().map(v -> toVo("appropriation", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapLeave(List<HrmLeaveReqVo> list, String nickName) { return list.stream().map(v -> toVo("leave", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapTravel(List<HrmTravelReqVo> list, String nickName) { return list.stream().map(v -> toVo("travel", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapSeal(List<HrmSealReqVo> list, String nickName) { return list.stream().map(v -> toVo("seal", v.getBizId(), v.getEmpId(), nickName, v.getPurpose() != null ? v.getPurpose() : v.getRemark(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapReimburse(List<HrmReimburseReqVo> list, String nickName) { return list.stream().map(v -> toVo("reimburse", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); }
|
||||
private List<HrmMyApplyVo> mapAppropriation(List<HrmAppropriationReqVo> list, String nickName) { return list.stream().map(v -> toVo("appropriation", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); }
|
||||
|
||||
private HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime) {
|
||||
@SuppressWarnings("unchecked")
|
||||
private HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime, Object source) {
|
||||
HrmMyApplyVo vo = new HrmMyApplyVo();
|
||||
vo.setBizType(bizType);
|
||||
vo.setBizId(bizId);
|
||||
@@ -122,6 +179,9 @@ public class HrmMyApplyController extends BaseController {
|
||||
vo.setTitle(title);
|
||||
vo.setStatus(status);
|
||||
vo.setCreateTime(createTime);
|
||||
if (source != null) {
|
||||
vo.setBizData(objectMapper.convertValue(source, Map.class));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class HrmFlowInstanceVo implements Serializable {
|
||||
@@ -41,4 +42,7 @@ public class HrmFlowInstanceVo implements Serializable {
|
||||
private Date createTime;
|
||||
private String updateBy;
|
||||
private Date updateTime;
|
||||
|
||||
/** 业务表回填的数据,用于列表关键信息展示 */
|
||||
private Map<String, Object> bizData;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class HrmMyApplyVo implements Serializable {
|
||||
@@ -21,4 +22,5 @@ public class HrmMyApplyVo implements Serializable {
|
||||
private Date endTime;
|
||||
private Date actualEndTime;
|
||||
private String remark;
|
||||
private Map<String, Object> bizData;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.service.UserService;
|
||||
@@ -27,7 +28,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -42,6 +45,11 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
private final UserService userService;
|
||||
private final HrmFlowCcMapper ccMapper;
|
||||
private final HrmTravelReqMapper travelReqMapper;
|
||||
private final HrmLeaveReqMapper leaveReqMapper;
|
||||
private final HrmSealReqMapper sealReqMapper;
|
||||
private final HrmReimburseReqMapper reimburseReqMapper;
|
||||
private final HrmAppropriationReqMapper appropriationReqMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Override
|
||||
@@ -188,9 +196,55 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
}
|
||||
}
|
||||
}
|
||||
fillInstanceBizData(result.getRecords());
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void fillInstanceBizData(List<HrmFlowInstanceVo> records) {
|
||||
if (records == null || records.isEmpty()) return;
|
||||
Map<String, List<Long>> bizIdsByType = records.stream()
|
||||
.filter(v -> v.getBizType() != null && v.getBizId() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
HrmFlowInstanceVo::getBizType,
|
||||
Collectors.mapping(HrmFlowInstanceVo::getBizId, Collectors.toList())
|
||||
));
|
||||
Map<String, Object> bizDataMap = new HashMap<>();
|
||||
bizIdsByType.forEach((bizType, bizIds) -> {
|
||||
if (bizIds.isEmpty()) return;
|
||||
switch (bizType) {
|
||||
case "leave":
|
||||
leaveReqMapper.selectBatchIds(bizIds).forEach(d ->
|
||||
bizDataMap.put("leave_" + d.getBizId(), objectMapper.convertValue(d, Map.class)));
|
||||
break;
|
||||
case "travel":
|
||||
travelReqMapper.selectBatchIds(bizIds).forEach(d ->
|
||||
bizDataMap.put("travel_" + d.getBizId(), objectMapper.convertValue(d, Map.class)));
|
||||
break;
|
||||
case "seal":
|
||||
sealReqMapper.selectBatchIds(bizIds).forEach(d ->
|
||||
bizDataMap.put("seal_" + d.getBizId(), objectMapper.convertValue(d, Map.class)));
|
||||
break;
|
||||
case "reimburse":
|
||||
reimburseReqMapper.selectBatchIds(bizIds).forEach(d ->
|
||||
bizDataMap.put("reimburse_" + d.getBizId(), objectMapper.convertValue(d, Map.class)));
|
||||
break;
|
||||
case "appropriation":
|
||||
appropriationReqMapper.selectBatchIds(bizIds).forEach(d ->
|
||||
bizDataMap.put("appropriation_" + d.getBizId(), objectMapper.convertValue(d, Map.class)));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
records.forEach(vo -> {
|
||||
Object data = bizDataMap.get(vo.getBizType() + "_" + vo.getBizId());
|
||||
if (data != null) {
|
||||
vo.setBizData((Map<String, Object>) data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HrmFlowInstanceVo> queryList(HrmFlowInstanceBo bo) {
|
||||
LambdaQueryWrapper<HrmFlowInstance> lqw = buildQueryWrapper(bo);
|
||||
|
||||
@@ -286,6 +286,7 @@ private void fillBizData(List<HrmFlowTaskVo> tasks) {
|
||||
sealReqService.stampWithJava(inst.getBizId(), stampBo);
|
||||
}
|
||||
}
|
||||
bizStatusSyncHelper.setBizApproved(inst.getBizType(), inst.getBizId());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ public class OaAiReviewController extends BaseController {
|
||||
@PostMapping(value = "/analyze", consumes = "multipart/form-data")
|
||||
public R<OaAiReviewVo> analyze(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("reviewType") String reviewType,
|
||||
@RequestParam(value = "position", required = false) String position) {
|
||||
return R.ok("审核完成", service.analyze(file, reviewType, position));
|
||||
@RequestParam(value = "position", required = false) String position,
|
||||
@RequestParam(value = "requirements", required = false) String requirements) {
|
||||
return R.ok("审核完成", service.analyze(file, reviewType, position, requirements));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +49,9 @@ public class OaAiReviewController extends BaseController {
|
||||
@PostMapping(value = "/analyzeStream", consumes = "multipart/form-data", produces = "text/event-stream;charset=UTF-8")
|
||||
public SseEmitter analyzeStream(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("reviewType") String reviewType,
|
||||
@RequestParam(value = "position", required = false) String position) {
|
||||
return service.analyzeStream(file, reviewType, position);
|
||||
@RequestParam(value = "position", required = false) String position,
|
||||
@RequestParam(value = "requirements", required = false) String requirements) {
|
||||
return service.analyzeStream(file, reviewType, position, requirements);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@@ -57,7 +59,8 @@ public class OaAiReviewController extends BaseController {
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
// 限定为数字,避免 /analyzeStream 等子路径被 {id} 误匹配(否则 POST 会得到 405)
|
||||
@GetMapping("/{id:\\d+}")
|
||||
public R<OaAiReviewVo> getInfo(@NotNull @PathVariable Long id) {
|
||||
return R.ok(service.queryById(id));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ruoyi.oa.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalActionBo;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalConfigBo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalConfigVo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo;
|
||||
import com.ruoyi.oa.service.IOaApprovalService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通用审批
|
||||
*/
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/oa/approval")
|
||||
public class OaApprovalController extends BaseController {
|
||||
|
||||
private final IOaApprovalService approvalService;
|
||||
|
||||
// ===== 配置 =====
|
||||
@GetMapping("/config/list")
|
||||
public R<List<OaApprovalConfigVo>> configList() {
|
||||
return R.ok(approvalService.listConfigs());
|
||||
}
|
||||
|
||||
@GetMapping("/config/{businessType}")
|
||||
public R<OaApprovalConfigVo> getConfig(@PathVariable String businessType) {
|
||||
return R.ok(approvalService.getConfig(businessType));
|
||||
}
|
||||
|
||||
@PostMapping("/config")
|
||||
public R<Void> saveConfig(@RequestBody OaApprovalConfigBo bo) {
|
||||
return toAjax(approvalService.saveConfig(bo));
|
||||
}
|
||||
|
||||
@DeleteMapping("/config/{id}")
|
||||
public R<Void> deleteConfig(@PathVariable Long id) {
|
||||
return toAjax(approvalService.deleteConfig(id));
|
||||
}
|
||||
|
||||
// ===== 审批 =====
|
||||
@PostMapping("/act")
|
||||
public R<Void> act(@Validated @RequestBody OaApprovalActionBo bo) {
|
||||
return toAjax(approvalService.act(bo));
|
||||
}
|
||||
|
||||
@PostMapping("/withdraw/{instanceId}")
|
||||
public R<Void> withdraw(@PathVariable Long instanceId) {
|
||||
return toAjax(approvalService.withdraw(instanceId));
|
||||
}
|
||||
|
||||
// ===== 查询 =====
|
||||
@GetMapping("/detail/{instanceId}")
|
||||
public R<OaApprovalInstanceVo> detail(@PathVariable Long instanceId) {
|
||||
return R.ok(approvalService.getDetail(instanceId));
|
||||
}
|
||||
|
||||
@GetMapping("/latest")
|
||||
public R<OaApprovalInstanceVo> latest(@NotNull @RequestParam String businessType,
|
||||
@NotNull @RequestParam Long businessId) {
|
||||
return R.ok(approvalService.getLatestByBusiness(businessType, businessId));
|
||||
}
|
||||
|
||||
@GetMapping("/mine/pending")
|
||||
public TableDataInfo<OaApprovalInstanceVo> myPending(PageQuery pageQuery,
|
||||
@RequestParam(required = false) String businessType) {
|
||||
return approvalService.pageMyPending(pageQuery, businessType);
|
||||
}
|
||||
|
||||
@GetMapping("/mine/done")
|
||||
public TableDataInfo<OaApprovalInstanceVo> myDone(PageQuery pageQuery,
|
||||
@RequestParam(required = false) String businessType) {
|
||||
return approvalService.pageMyDone(pageQuery, businessType);
|
||||
}
|
||||
|
||||
@GetMapping("/mine/submitted")
|
||||
public TableDataInfo<OaApprovalInstanceVo> mySubmitted(PageQuery pageQuery,
|
||||
@RequestParam(required = false) String businessType) {
|
||||
return approvalService.pageMySubmitted(pageQuery, businessType);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<OaApprovalInstanceVo> all(PageQuery pageQuery,
|
||||
@RequestParam(required = false) String businessType,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
return approvalService.pageAll(pageQuery, businessType, status);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@ public class OaAiReview extends BaseEntity {
|
||||
/** 简历审核的目标岗位 */
|
||||
private String position;
|
||||
|
||||
/** 本次审核的附加要求/审核重点 */
|
||||
private String requirements;
|
||||
|
||||
/** 简历匹配度评分 0-100(合同为空) */
|
||||
private Integer matchScore;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.oa.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 业务审批配置 oa_approval_config
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("oa_approval_config")
|
||||
public class OaApprovalConfig extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/** 业务类型 key */
|
||||
private String businessType;
|
||||
|
||||
/** 业务名称(展示用) */
|
||||
private String businessName;
|
||||
|
||||
/** 审批人 user_id 列表,逗号分隔 */
|
||||
private String approverIds;
|
||||
|
||||
/** 1或签 2会签 */
|
||||
private Integer signType;
|
||||
|
||||
/** 是否启用:0停用 1启用 */
|
||||
private Integer enabled;
|
||||
|
||||
private String remark;
|
||||
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.oa.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审批单实例 oa_approval_instance
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("oa_approval_instance")
|
||||
public class OaApprovalInstance extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
private String businessType;
|
||||
private Long businessId;
|
||||
private String businessTitle;
|
||||
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyTime;
|
||||
|
||||
/** 快照:提交时的审批人 */
|
||||
private String approverIds;
|
||||
/** 快照:1或签 2会签 */
|
||||
private Integer signType;
|
||||
|
||||
/** 0待审 1通过 2驳回 3撤回 */
|
||||
private Integer status;
|
||||
private Date finishTime;
|
||||
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.oa.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审批操作流水 oa_approval_record
|
||||
*/
|
||||
@Data
|
||||
@TableName("oa_approval_record")
|
||||
public class OaApprovalRecord implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
private Long instanceId;
|
||||
private Long approverId;
|
||||
private String approverName;
|
||||
|
||||
/** 1通过 2驳回 */
|
||||
private Integer action;
|
||||
private String comment;
|
||||
private Date opTime;
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -45,6 +45,10 @@ public class OaRequirements extends BaseEntity {
|
||||
* 挂接项目 ID,可选
|
||||
*/
|
||||
private Long projectId;
|
||||
/**
|
||||
* 关联物料 ID CSV -> sys_oa_warehouse.id,可选
|
||||
*/
|
||||
private String materialIds;
|
||||
/**
|
||||
* 需求描述
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.oa.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class OaApprovalActionBo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull
|
||||
private Long instanceId;
|
||||
|
||||
/** 1通过 2驳回 */
|
||||
@NotNull
|
||||
private Integer action;
|
||||
|
||||
private String comment;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.oa.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class OaApprovalConfigBo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String businessType;
|
||||
private String businessName;
|
||||
/** 审批人 user_id 列表,逗号分隔 */
|
||||
private String approverIds;
|
||||
/** 1或签 2会签 */
|
||||
private Integer signType;
|
||||
/** 0停用 1启用 */
|
||||
private Integer enabled;
|
||||
private String remark;
|
||||
}
|
||||
@@ -49,6 +49,11 @@ public class OaRequirementsBo extends BaseEntity {
|
||||
*/
|
||||
private Long projectId;
|
||||
|
||||
/**
|
||||
* 关联物料 ID CSV,多个物料用逗号分隔
|
||||
*/
|
||||
private String materialIds;
|
||||
|
||||
/**
|
||||
* 需求描述
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ public class OaAiReviewVo implements Serializable {
|
||||
private Long ossId;
|
||||
private String fileUrl;
|
||||
private String position;
|
||||
private String requirements;
|
||||
private Integer matchScore;
|
||||
private String riskLevel;
|
||||
private String summary;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class OaApprovalConfigVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String businessType;
|
||||
private String businessName;
|
||||
private String approverIds;
|
||||
/** 显示用:审批人昵称 列表 */
|
||||
private String approverNames;
|
||||
private Integer signType;
|
||||
private Integer enabled;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class OaApprovalInstanceVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String businessType;
|
||||
private String businessName;
|
||||
private Long businessId;
|
||||
private String businessTitle;
|
||||
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyTime;
|
||||
|
||||
private String approverIds;
|
||||
private String approverNames;
|
||||
private Integer signType;
|
||||
|
||||
/** 0待审 1通过 2驳回 3撤回 */
|
||||
private Integer status;
|
||||
private Date finishTime;
|
||||
|
||||
/** 审批流水 */
|
||||
private List<OaApprovalRecordVo> records;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class OaApprovalRecordVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private Long instanceId;
|
||||
private Long approverId;
|
||||
private String approverName;
|
||||
private Integer action;
|
||||
private String comment;
|
||||
private Date opTime;
|
||||
}
|
||||
@@ -89,8 +89,29 @@ public class OaRequirementsVo extends BaseEntity {
|
||||
private String ownerNickName;
|
||||
private String projectName;
|
||||
|
||||
/** 关联物料 ID CSV */
|
||||
private String materialIds;
|
||||
/** 关联物料明细(service 层 enrich,列表/详情都返回) */
|
||||
private java.util.List<MaterialItem> materials;
|
||||
|
||||
@Data
|
||||
public static class MaterialItem implements java.io.Serializable {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String model;
|
||||
private String unit;
|
||||
private Long inventory;
|
||||
private String brand;
|
||||
private String specifications;
|
||||
}
|
||||
|
||||
/** 附件文件列表(已联查 sys_oss,每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
|
||||
private String accessoryFiles;
|
||||
|
||||
/** 审批状态:null=未提交审批,0=待审,1=通过,2=驳回,3=撤回 */
|
||||
private Integer approvalStatus;
|
||||
/** 审批单 id(最新一次) */
|
||||
private Long approvalInstanceId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -138,4 +138,9 @@ public class SysOaContractVo {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/** 审批状态:null=未提交审批,0=待审,1=通过,2=驳回,3=撤回 */
|
||||
private Integer approvalStatus;
|
||||
/** 审批单 id(最新一次) */
|
||||
private Long approvalInstanceId;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ruoyi.oa.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.domain.OaApprovalConfig;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalConfigVo;
|
||||
|
||||
public interface OaApprovalConfigMapper extends BaseMapperPlus<OaApprovalConfigMapper, OaApprovalConfig, OaApprovalConfigVo> {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ruoyi.oa.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.domain.OaApprovalInstance;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo;
|
||||
|
||||
public interface OaApprovalInstanceMapper extends BaseMapperPlus<OaApprovalInstanceMapper, OaApprovalInstance, OaApprovalInstanceVo> {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ruoyi.oa.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.domain.OaApprovalRecord;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalRecordVo;
|
||||
|
||||
public interface OaApprovalRecordMapper extends BaseMapperPlus<OaApprovalRecordMapper, OaApprovalRecord, OaApprovalRecordVo> {
|
||||
}
|
||||
@@ -17,13 +17,14 @@ public interface IOaAiReviewService {
|
||||
* @param file PDF / Word 文件
|
||||
* @param reviewType contract / resume
|
||||
* @param position 简历审核的目标岗位(合同可空)
|
||||
* @param requirements 附加审核要求/重点(可空)
|
||||
*/
|
||||
OaAiReviewVo analyze(MultipartFile file, String reviewType, String position);
|
||||
OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements);
|
||||
|
||||
/**
|
||||
* 流式审核:边生成边推送(SSE),结束后落库
|
||||
*/
|
||||
SseEmitter analyzeStream(MultipartFile file, String reviewType, String position);
|
||||
SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements);
|
||||
|
||||
TableDataInfo<OaAiReviewVo> queryPageList(OaAiReviewBo bo, PageQuery pageQuery);
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.oa.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalActionBo;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalConfigBo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalConfigVo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通用审批 Service
|
||||
*/
|
||||
public interface IOaApprovalService {
|
||||
|
||||
// ===== 配置 =====
|
||||
List<OaApprovalConfigVo> listConfigs();
|
||||
|
||||
OaApprovalConfigVo getConfig(String businessType);
|
||||
|
||||
Boolean saveConfig(OaApprovalConfigBo bo);
|
||||
|
||||
Boolean deleteConfig(Long id);
|
||||
|
||||
// ===== 业务侧调用 =====
|
||||
|
||||
/**
|
||||
* 业务提交触发审批,若已有未结束的实例则直接返回。
|
||||
* @return 审批单 id
|
||||
*/
|
||||
Long submit(String businessType, Long businessId, String businessTitle);
|
||||
|
||||
/**
|
||||
* 审批操作(通过/驳回),自动判定或签/会签终结状态。
|
||||
*/
|
||||
Boolean act(OaApprovalActionBo bo);
|
||||
|
||||
/**
|
||||
* 撤回(仅申请人可在 status=0 时撤回)
|
||||
*/
|
||||
Boolean withdraw(Long instanceId);
|
||||
|
||||
/**
|
||||
* 查询某业务的最新审批单(用于业务详情展示)
|
||||
*/
|
||||
OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId);
|
||||
|
||||
/**
|
||||
* 批量查询业务最新审批状态:返回 businessId -> status
|
||||
*/
|
||||
Map<Long, Integer> batchLatestStatus(String businessType, Collection<Long> businessIds);
|
||||
|
||||
// ===== 列表 =====
|
||||
|
||||
/** 我待办(审批人是我,status=0) */
|
||||
TableDataInfo<OaApprovalInstanceVo> pageMyPending(PageQuery pageQuery, String businessType);
|
||||
|
||||
/** 我已办(出现在 record 里) */
|
||||
TableDataInfo<OaApprovalInstanceVo> pageMyDone(PageQuery pageQuery, String businessType);
|
||||
|
||||
/** 我发起的 */
|
||||
TableDataInfo<OaApprovalInstanceVo> pageMySubmitted(PageQuery pageQuery, String businessType);
|
||||
|
||||
/** 全部(管理员) */
|
||||
TableDataInfo<OaApprovalInstanceVo> pageAll(PageQuery pageQuery, String businessType, Integer status);
|
||||
|
||||
/** 单条详情(带流水) */
|
||||
OaApprovalInstanceVo getDetail(Long instanceId);
|
||||
}
|
||||
@@ -57,8 +57,8 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
private static final Pattern RISK_PATTERN = Pattern.compile("风险评级[::\\s]*([高中低])");
|
||||
|
||||
@Override
|
||||
public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position) {
|
||||
Prepared p = prepareSync(file, reviewType, position);
|
||||
public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
Prepared p = prepareSync(file, reviewType, position, requirements);
|
||||
buildPrompt(p);
|
||||
String result = p.images != null
|
||||
? miMoClient.chatMultimodal(p.system, p.userText, p.images)
|
||||
@@ -70,11 +70,11 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position) {
|
||||
public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
// 只在同步阶段做轻量校验 + 读取字节(multipart 必须在请求线程内消费);
|
||||
// 文档解析、渲染、大模型调用全部放到异步线程,任何异常都以 SSE error 事件返回,
|
||||
// 避免在返回 SseEmitter 之前抛异常被全局处理器包成 JSON(前端按流读取会“静默失败”)。
|
||||
Prepared p = prepareSync(file, reviewType, position);
|
||||
Prepared p = prepareSync(file, reviewType, position, requirements);
|
||||
String username = currentUsername();
|
||||
long timeoutMs = ((miMoProps.getTimeout() == null ? 180 : miMoProps.getTimeout()) + 60) * 1000L;
|
||||
SseEmitter emitter = new SseEmitter(timeoutMs);
|
||||
@@ -129,7 +129,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
}
|
||||
|
||||
/** 同步阶段:校验 + 读取字节(不解析,快) */
|
||||
private Prepared prepareSync(MultipartFile file, String reviewType, String position) {
|
||||
private Prepared prepareSync(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("请上传文件");
|
||||
}
|
||||
@@ -153,6 +153,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
Prepared p = new Prepared();
|
||||
p.reviewType = reviewType;
|
||||
p.position = position;
|
||||
p.requirements = StringUtils.trimToNull(requirements);
|
||||
p.fileName = fileName;
|
||||
p.bytes = bytes;
|
||||
return p;
|
||||
@@ -160,7 +161,12 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
|
||||
/** 解析文档 + 构建提示词(耗时/可能抛异常的部分) */
|
||||
private void buildPrompt(Prepared p) {
|
||||
p.system = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position);
|
||||
String base = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position);
|
||||
if (StringUtils.isNotBlank(p.requirements)) {
|
||||
base += "\n\n【本次额外审核要求 / 重点】请在上述固定结构基础上,务必额外重点关注并逐条回应以下要求:\n"
|
||||
+ p.requirements;
|
||||
}
|
||||
p.system = base;
|
||||
String text = truncate(DocumentParseUtil.extractText(p.fileName, p.bytes));
|
||||
if (StringUtils.length(text) >= MIN_TEXT_LEN) {
|
||||
p.userText = "contract".equals(p.reviewType)
|
||||
@@ -189,6 +195,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
entity.setFileUrl(oss.getUrl());
|
||||
}
|
||||
entity.setPosition(p.position);
|
||||
entity.setRequirements(p.requirements);
|
||||
entity.setResultMd(result);
|
||||
entity.setSummary(buildSummary(result));
|
||||
entity.setModel(miMoProps.getModel());
|
||||
@@ -242,6 +249,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
private static class Prepared {
|
||||
String reviewType;
|
||||
String position;
|
||||
String requirements;
|
||||
String fileName;
|
||||
byte[] bytes;
|
||||
String system;
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
package com.ruoyi.oa.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.oa.domain.OaApprovalConfig;
|
||||
import com.ruoyi.oa.domain.OaApprovalInstance;
|
||||
import com.ruoyi.oa.domain.OaApprovalRecord;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalActionBo;
|
||||
import com.ruoyi.oa.domain.bo.OaApprovalConfigBo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalConfigVo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo;
|
||||
import com.ruoyi.oa.domain.vo.OaApprovalRecordVo;
|
||||
import com.ruoyi.oa.mapper.OaApprovalConfigMapper;
|
||||
import com.ruoyi.oa.mapper.OaApprovalInstanceMapper;
|
||||
import com.ruoyi.oa.mapper.OaApprovalRecordMapper;
|
||||
import com.ruoyi.oa.service.IOaApprovalService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OaApprovalServiceImpl implements IOaApprovalService {
|
||||
|
||||
private final OaApprovalConfigMapper configMapper;
|
||||
private final OaApprovalInstanceMapper instanceMapper;
|
||||
private final OaApprovalRecordMapper recordMapper;
|
||||
private final ISysUserService userService;
|
||||
|
||||
// ===================== 配置 =====================
|
||||
|
||||
@Override
|
||||
public List<OaApprovalConfigVo> listConfigs() {
|
||||
List<OaApprovalConfigVo> list = configMapper.selectVoList(
|
||||
Wrappers.<OaApprovalConfig>lambdaQuery().orderByAsc(OaApprovalConfig::getBusinessType));
|
||||
list.forEach(this::fillApproverNames);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OaApprovalConfigVo getConfig(String businessType) {
|
||||
OaApprovalConfig cfg = configMapper.selectOne(
|
||||
Wrappers.<OaApprovalConfig>lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType));
|
||||
if (cfg == null) return null;
|
||||
OaApprovalConfigVo vo = BeanUtil.toBean(cfg, OaApprovalConfigVo.class);
|
||||
fillApproverNames(vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean saveConfig(OaApprovalConfigBo bo) {
|
||||
if (StringUtils.isBlank(bo.getBusinessType())) {
|
||||
throw new ServiceException("业务类型不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(bo.getApproverIds())) {
|
||||
throw new ServiceException("审批人不能为空");
|
||||
}
|
||||
if (bo.getSignType() == null) bo.setSignType(1);
|
||||
if (bo.getEnabled() == null) bo.setEnabled(1);
|
||||
|
||||
OaApprovalConfig exist = configMapper.selectOne(
|
||||
Wrappers.<OaApprovalConfig>lambdaQuery().eq(OaApprovalConfig::getBusinessType, bo.getBusinessType()));
|
||||
OaApprovalConfig entity = BeanUtil.toBean(bo, OaApprovalConfig.class);
|
||||
if (exist != null) {
|
||||
entity.setId(exist.getId());
|
||||
return configMapper.updateById(entity) > 0;
|
||||
}
|
||||
return configMapper.insert(entity) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteConfig(Long id) {
|
||||
return configMapper.deleteById(id) > 0;
|
||||
}
|
||||
|
||||
// ===================== 业务侧 =====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long submit(String businessType, Long businessId, String businessTitle) {
|
||||
OaApprovalConfig cfg = configMapper.selectOne(
|
||||
Wrappers.<OaApprovalConfig>lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType));
|
||||
if (cfg == null || cfg.getEnabled() != null && cfg.getEnabled() == 0) {
|
||||
// 未配置 / 未启用:跳过审批,不报错
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(cfg.getApproverIds())) {
|
||||
throw new ServiceException("业务【" + businessType + "】未配置审批人");
|
||||
}
|
||||
|
||||
// 若已存在未结束实例则复用
|
||||
OaApprovalInstance pending = instanceMapper.selectOne(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, businessType)
|
||||
.eq(OaApprovalInstance::getBusinessId, businessId)
|
||||
.eq(OaApprovalInstance::getStatus, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (pending != null) return pending.getId();
|
||||
|
||||
OaApprovalInstance inst = new OaApprovalInstance();
|
||||
inst.setBusinessType(businessType);
|
||||
inst.setBusinessId(businessId);
|
||||
inst.setBusinessTitle(businessTitle);
|
||||
inst.setApplyUserId(LoginHelper.getUserId());
|
||||
inst.setApplyUserName(LoginHelper.getNickName());
|
||||
inst.setApplyTime(new Date());
|
||||
inst.setApproverIds(cfg.getApproverIds());
|
||||
inst.setSignType(cfg.getSignType());
|
||||
inst.setStatus(0);
|
||||
instanceMapper.insert(inst);
|
||||
return inst.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean act(OaApprovalActionBo bo) {
|
||||
OaApprovalInstance inst = instanceMapper.selectById(bo.getInstanceId());
|
||||
if (inst == null) throw new ServiceException("审批单不存在");
|
||||
if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("审批已终结");
|
||||
|
||||
Long me = LoginHelper.getUserId();
|
||||
Set<Long> approvers = parseIds(inst.getApproverIds());
|
||||
if (!approvers.contains(me)) throw new ServiceException("无审批权限");
|
||||
|
||||
// 防重:同一人对同一单只能操作一次
|
||||
Long already = recordMapper.selectCount(
|
||||
Wrappers.<OaApprovalRecord>lambdaQuery()
|
||||
.eq(OaApprovalRecord::getInstanceId, inst.getId())
|
||||
.eq(OaApprovalRecord::getApproverId, me));
|
||||
if (already != null && already > 0) throw new ServiceException("您已审批过");
|
||||
|
||||
OaApprovalRecord rec = new OaApprovalRecord();
|
||||
rec.setInstanceId(inst.getId());
|
||||
rec.setApproverId(me);
|
||||
rec.setApproverName(LoginHelper.getNickName());
|
||||
rec.setAction(bo.getAction());
|
||||
rec.setComment(bo.getComment());
|
||||
rec.setOpTime(new Date());
|
||||
recordMapper.insert(rec);
|
||||
|
||||
// 终结判定
|
||||
int finalStatus = decideFinalStatus(inst, bo.getAction(), me, approvers);
|
||||
if (finalStatus != 0) {
|
||||
inst.setStatus(finalStatus);
|
||||
inst.setFinishTime(new Date());
|
||||
instanceMapper.updateById(inst);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int decideFinalStatus(OaApprovalInstance inst, Integer action, Long me, Set<Long> approvers) {
|
||||
// 驳回:无论或签会签都立即驳回
|
||||
if (action != null && action == 2) return 2;
|
||||
// 通过场景
|
||||
if (inst.getSignType() != null && inst.getSignType() == 1) {
|
||||
// 或签:任一人通过即通过
|
||||
return 1;
|
||||
}
|
||||
// 会签:所有人都通过才通过
|
||||
Long passCount = recordMapper.selectCount(
|
||||
Wrappers.<OaApprovalRecord>lambdaQuery()
|
||||
.eq(OaApprovalRecord::getInstanceId, inst.getId())
|
||||
.eq(OaApprovalRecord::getAction, 1));
|
||||
return passCount != null && passCount >= approvers.size() ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean withdraw(Long instanceId) {
|
||||
OaApprovalInstance inst = instanceMapper.selectById(instanceId);
|
||||
if (inst == null) throw new ServiceException("审批单不存在");
|
||||
if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("已终结,无法撤回");
|
||||
if (!Objects.equals(inst.getApplyUserId(), LoginHelper.getUserId())) throw new ServiceException("仅申请人可撤回");
|
||||
inst.setStatus(3);
|
||||
inst.setFinishTime(new Date());
|
||||
return instanceMapper.updateById(inst) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId) {
|
||||
OaApprovalInstance inst = instanceMapper.selectOne(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, businessType)
|
||||
.eq(OaApprovalInstance::getBusinessId, businessId)
|
||||
.orderByDesc(OaApprovalInstance::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (inst == null) return null;
|
||||
OaApprovalInstanceVo vo = toInstanceVo(inst);
|
||||
vo.setRecords(loadRecords(inst.getId()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, Integer> batchLatestStatus(String businessType, Collection<Long> businessIds) {
|
||||
if (businessIds == null || businessIds.isEmpty()) return Collections.emptyMap();
|
||||
List<OaApprovalInstance> list = instanceMapper.selectList(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, businessType)
|
||||
.in(OaApprovalInstance::getBusinessId, businessIds)
|
||||
.orderByAsc(OaApprovalInstance::getId));
|
||||
// 后面的覆盖前面的,得到最新
|
||||
Map<Long, Integer> result = new HashMap<>();
|
||||
for (OaApprovalInstance i : list) {
|
||||
result.put(i.getBusinessId(), i.getStatus());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===================== 列表 =====================
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaApprovalInstanceVo> pageMyPending(PageQuery pageQuery, String businessType) {
|
||||
Long me = LoginHelper.getUserId();
|
||||
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getStatus, 0)
|
||||
.and(w -> w.like(OaApprovalInstance::getApproverIds, "," + me + ",")
|
||||
.or().likeRight(OaApprovalInstance::getApproverIds, me + ",")
|
||||
.or().likeLeft(OaApprovalInstance::getApproverIds, "," + me)
|
||||
.or().eq(OaApprovalInstance::getApproverIds, String.valueOf(me)))
|
||||
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
|
||||
.orderByDesc(OaApprovalInstance::getId);
|
||||
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
|
||||
return buildInstancePage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaApprovalInstanceVo> pageMyDone(PageQuery pageQuery, String businessType) {
|
||||
Long me = LoginHelper.getUserId();
|
||||
List<Long> instIds = recordMapper.selectObjs(
|
||||
Wrappers.<OaApprovalRecord>lambdaQuery()
|
||||
.select(OaApprovalRecord::getInstanceId)
|
||||
.eq(OaApprovalRecord::getApproverId, me))
|
||||
.stream().filter(Objects::nonNull).map(o -> Long.parseLong(o.toString())).distinct().collect(Collectors.toList());
|
||||
if (instIds.isEmpty()) return TableDataInfo.build(new ArrayList<>());
|
||||
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.in(OaApprovalInstance::getId, instIds)
|
||||
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
|
||||
.orderByDesc(OaApprovalInstance::getId);
|
||||
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
|
||||
return buildInstancePage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaApprovalInstanceVo> pageMySubmitted(PageQuery pageQuery, String businessType) {
|
||||
Long me = LoginHelper.getUserId();
|
||||
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getApplyUserId, me)
|
||||
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
|
||||
.orderByDesc(OaApprovalInstance::getId);
|
||||
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
|
||||
return buildInstancePage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaApprovalInstanceVo> pageAll(PageQuery pageQuery, String businessType, Integer status) {
|
||||
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
|
||||
.eq(status != null, OaApprovalInstance::getStatus, status)
|
||||
.orderByDesc(OaApprovalInstance::getId);
|
||||
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
|
||||
return buildInstancePage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OaApprovalInstanceVo getDetail(Long instanceId) {
|
||||
OaApprovalInstance inst = instanceMapper.selectById(instanceId);
|
||||
if (inst == null) return null;
|
||||
OaApprovalInstanceVo vo = toInstanceVo(inst);
|
||||
vo.setRecords(loadRecords(instanceId));
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ===================== 内部 =====================
|
||||
|
||||
private TableDataInfo<OaApprovalInstanceVo> buildInstancePage(Page<OaApprovalInstance> page) {
|
||||
List<OaApprovalInstanceVo> vos = page.getRecords().stream().map(this::toInstanceVo).collect(Collectors.toList());
|
||||
Page<OaApprovalInstanceVo> p = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
p.setRecords(vos);
|
||||
return TableDataInfo.build(p);
|
||||
}
|
||||
|
||||
private OaApprovalInstanceVo toInstanceVo(OaApprovalInstance inst) {
|
||||
OaApprovalInstanceVo vo = BeanUtil.toBean(inst, OaApprovalInstanceVo.class);
|
||||
vo.setApproverNames(resolveNames(inst.getApproverIds()));
|
||||
OaApprovalConfig cfg = configMapper.selectOne(
|
||||
Wrappers.<OaApprovalConfig>lambdaQuery().eq(OaApprovalConfig::getBusinessType, inst.getBusinessType()));
|
||||
if (cfg != null) vo.setBusinessName(cfg.getBusinessName());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<OaApprovalRecordVo> loadRecords(Long instanceId) {
|
||||
return recordMapper.selectVoList(
|
||||
Wrappers.<OaApprovalRecord>lambdaQuery()
|
||||
.eq(OaApprovalRecord::getInstanceId, instanceId)
|
||||
.orderByAsc(OaApprovalRecord::getOpTime));
|
||||
}
|
||||
|
||||
private void fillApproverNames(OaApprovalConfigVo vo) {
|
||||
vo.setApproverNames(resolveNames(vo.getApproverIds()));
|
||||
}
|
||||
|
||||
private String resolveNames(String csvIds) {
|
||||
Set<Long> ids = parseIds(csvIds);
|
||||
if (ids.isEmpty()) return "";
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Long id : ids) {
|
||||
try {
|
||||
SysUser u = userService.selectUserById(id);
|
||||
names.add(u != null ? u.getNickName() : String.valueOf(id));
|
||||
} catch (Exception e) {
|
||||
names.add(String.valueOf(id));
|
||||
}
|
||||
}
|
||||
return String.join(",", names);
|
||||
}
|
||||
|
||||
private Set<Long> parseIds(String csv) {
|
||||
if (StringUtils.isBlank(csv)) return Collections.emptySet();
|
||||
Set<Long> set = new LinkedHashSet<>();
|
||||
for (String s : csv.split(",")) {
|
||||
String t = s.trim();
|
||||
if (t.isEmpty()) continue;
|
||||
try { set.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,25 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
|
||||
import com.ruoyi.oa.domain.vo.OaRequirementsVo;
|
||||
import com.ruoyi.oa.domain.OaApprovalInstance;
|
||||
import com.ruoyi.oa.domain.OaRequirements;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||
import com.ruoyi.oa.mapper.OaApprovalInstanceMapper;
|
||||
import com.ruoyi.oa.mapper.OaRequirementsMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||
import com.ruoyi.oa.service.IOaApprovalService;
|
||||
import com.ruoyi.oa.service.IOaRequirementsService;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* OA 需求Service业务层处理
|
||||
@@ -33,16 +44,97 @@ import java.util.Collection;
|
||||
@Service
|
||||
public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
|
||||
public static final String BIZ_TYPE = "purchase_req";
|
||||
|
||||
private final OaRequirementsMapper baseMapper;
|
||||
|
||||
private final OaWarehouseAuditService auditService;
|
||||
|
||||
private final SysOaWarehouseMapper warehouseMapper;
|
||||
|
||||
private final IOaApprovalService approvalService;
|
||||
|
||||
private final OaApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
/**
|
||||
* 查询OA 需求
|
||||
*/
|
||||
@Override
|
||||
public OaRequirementsVo queryById(Long requirementId){
|
||||
return baseMapper.selectVoById(requirementId);
|
||||
OaRequirementsVo vo = baseMapper.selectVoById(requirementId);
|
||||
enrichMaterials(Collections.singletonList(vo));
|
||||
enrichApproval(Collections.singletonList(vo));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/** 给 VO 列表注入审批状态(最新一条实例) */
|
||||
private void enrichApproval(List<OaRequirementsVo> list) {
|
||||
if (list == null || list.isEmpty()) return;
|
||||
List<Long> ids = list.stream().filter(v -> v != null && v.getRequirementId() != null)
|
||||
.map(OaRequirementsVo::getRequirementId).collect(Collectors.toList());
|
||||
if (ids.isEmpty()) return;
|
||||
List<OaApprovalInstance> insts = approvalInstanceMapper.selectList(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, BIZ_TYPE)
|
||||
.in(OaApprovalInstance::getBusinessId, ids)
|
||||
.orderByAsc(OaApprovalInstance::getId));
|
||||
Map<Long, OaApprovalInstance> latest = new HashMap<>();
|
||||
for (OaApprovalInstance i : insts) latest.put(i.getBusinessId(), i);
|
||||
for (OaRequirementsVo v : list) {
|
||||
if (v == null) continue;
|
||||
OaApprovalInstance i = latest.get(v.getRequirementId());
|
||||
if (i != null) {
|
||||
v.setApprovalStatus(i.getStatus());
|
||||
v.setApprovalInstanceId(i.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据 material_ids(CSV)批量补全物料明细到 VO.materials */
|
||||
private void enrichMaterials(List<OaRequirementsVo> list) {
|
||||
if (list == null || list.isEmpty()) return;
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (OaRequirementsVo v : list) {
|
||||
if (v == null) continue;
|
||||
for (Long id : parseCsv(v.getMaterialIds())) ids.add(id);
|
||||
}
|
||||
if (ids.isEmpty()) return;
|
||||
List<SysOaWarehouse> wList = warehouseMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysOaWarehouse>()
|
||||
.in("id", ids));
|
||||
Map<Long, SysOaWarehouse> wMap = wList.stream()
|
||||
.collect(Collectors.toMap(SysOaWarehouse::getId, w -> w, (a, b) -> a));
|
||||
for (OaRequirementsVo v : list) {
|
||||
if (v == null) continue;
|
||||
List<Long> mids = parseCsv(v.getMaterialIds());
|
||||
if (mids.isEmpty()) continue;
|
||||
List<OaRequirementsVo.MaterialItem> items = new ArrayList<>();
|
||||
for (Long id : mids) {
|
||||
SysOaWarehouse w = wMap.get(id);
|
||||
if (w == null) continue;
|
||||
OaRequirementsVo.MaterialItem it = new OaRequirementsVo.MaterialItem();
|
||||
it.setId(w.getId());
|
||||
it.setName(w.getName());
|
||||
it.setModel(w.getModel());
|
||||
it.setUnit(w.getUnit());
|
||||
it.setInventory(w.getInventory());
|
||||
it.setBrand(w.getBrand());
|
||||
it.setSpecifications(w.getSpecifications());
|
||||
items.add(it);
|
||||
}
|
||||
v.setMaterials(items);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Long> parseCsv(String csv) {
|
||||
if (csv == null || csv.isEmpty()) return Collections.emptyList();
|
||||
List<Long> r = new ArrayList<>();
|
||||
for (String s : csv.split(",")) {
|
||||
String t = s.trim();
|
||||
if (t.isEmpty()) continue;
|
||||
try { r.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +144,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
public TableDataInfo<OaRequirementsVo> queryPageList(OaRequirementsBo bo, PageQuery pageQuery) {
|
||||
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
||||
Page<OaRequirementsVo> result = baseMapper.selectVoListPage(pageQuery.build(), lqw);
|
||||
enrichMaterials(result.getRecords());
|
||||
enrichApproval(result.getRecords());
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
@@ -61,7 +155,10 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
@Override
|
||||
public List<OaRequirementsVo> queryList(OaRequirementsBo bo) {
|
||||
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoListForExport(lqw);
|
||||
List<OaRequirementsVo> list = baseMapper.selectVoListForExport(lqw);
|
||||
enrichMaterials(list);
|
||||
enrichApproval(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private QueryWrapper<OaRequirements> buildQueryWrapper(OaRequirementsBo bo) {
|
||||
@@ -101,6 +198,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
bo.setRequirementId(add.getRequirementId());
|
||||
auditService.log(OpType.REQ_CREATE, OpType.REF_REQUIREMENT, add.getRequirementId(),
|
||||
"新建采购需求:" + add.getTitle());
|
||||
// 触发审批流
|
||||
approvalService.submit(BIZ_TYPE, add.getRequirementId(), add.getTitle());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -112,6 +211,18 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
public Boolean updateByBo(OaRequirementsBo bo) {
|
||||
OaRequirements update = BeanUtil.toBean(bo, OaRequirements.class);
|
||||
validEntityBeforeSave(update);
|
||||
// 推进到采购中/完成 前需审批通过;
|
||||
// 若审批未启用/未配置 (无实例),则放行,避免「停用审批反而卡住业务」。
|
||||
if (bo.getStatus() != null && (bo.getStatus() == 1 || bo.getStatus() == 2)) {
|
||||
OaApprovalInstance i = approvalInstanceMapper.selectOne(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, BIZ_TYPE)
|
||||
.eq(OaApprovalInstance::getBusinessId, bo.getRequirementId())
|
||||
.orderByDesc(OaApprovalInstance::getId).last("LIMIT 1"));
|
||||
if (i != null && (i.getStatus() == null || i.getStatus() != 1)) {
|
||||
throw new com.ruoyi.common.exception.ServiceException("该采购需求尚未审批通过,无法推进状态");
|
||||
}
|
||||
}
|
||||
boolean ok = baseMapper.updateById(update) > 0;
|
||||
if (ok) {
|
||||
String op = OpType.REQ_UPDATE;
|
||||
|
||||
@@ -16,12 +16,19 @@ import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.SysOaContractBo;
|
||||
import com.ruoyi.oa.domain.vo.SysOaContractVo;
|
||||
import com.ruoyi.oa.domain.SysOaContract;
|
||||
import com.ruoyi.oa.domain.OaApprovalInstance;
|
||||
import com.ruoyi.oa.mapper.SysOaContractMapper;
|
||||
import com.ruoyi.oa.mapper.OaApprovalInstanceMapper;
|
||||
import com.ruoyi.oa.service.ISysOaContractService;
|
||||
import com.ruoyi.oa.service.IOaApprovalService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 合同管理Service业务层处理
|
||||
@@ -33,16 +40,26 @@ import java.util.Collection;
|
||||
@Service
|
||||
public class SysOaContractServiceImpl implements ISysOaContractService {
|
||||
|
||||
public static final String BIZ_TYPE = "contract";
|
||||
|
||||
@Autowired
|
||||
private SysOaContractMapper contractMapper;
|
||||
|
||||
@Autowired
|
||||
private IOaApprovalService approvalService;
|
||||
|
||||
@Autowired
|
||||
private OaApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
|
||||
/**
|
||||
* 查询合同管理
|
||||
*/
|
||||
@Override
|
||||
public SysOaContractVo queryById(Long contractId){
|
||||
return contractMapper.selectVoById(contractId);
|
||||
SysOaContractVo vo = contractMapper.selectVoById(contractId);
|
||||
enrichApproval(Collections.singletonList(vo));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +69,7 @@ public class SysOaContractServiceImpl implements ISysOaContractService {
|
||||
public TableDataInfo<SysOaContractVo> queryPageList(SysOaContractBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<SysOaContract> lqw = buildQueryWrapper(bo);
|
||||
Page<SysOaContractVo> result = contractMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
enrichApproval(result.getRecords());
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
@@ -61,7 +79,32 @@ public class SysOaContractServiceImpl implements ISysOaContractService {
|
||||
@Override
|
||||
public List<SysOaContractVo> queryList(SysOaContractBo bo) {
|
||||
LambdaQueryWrapper<SysOaContract> lqw = buildQueryWrapper(bo);
|
||||
return contractMapper.selectVoList(lqw);
|
||||
List<SysOaContractVo> list = contractMapper.selectVoList(lqw);
|
||||
enrichApproval(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 给 VO 列表注入审批状态(最新一条实例) */
|
||||
private void enrichApproval(List<SysOaContractVo> list) {
|
||||
if (list == null || list.isEmpty()) return;
|
||||
List<Long> ids = list.stream().filter(v -> v != null && v.getContractId() != null)
|
||||
.map(SysOaContractVo::getContractId).collect(Collectors.toList());
|
||||
if (ids.isEmpty()) return;
|
||||
List<OaApprovalInstance> insts = approvalInstanceMapper.selectList(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, BIZ_TYPE)
|
||||
.in(OaApprovalInstance::getBusinessId, ids)
|
||||
.orderByAsc(OaApprovalInstance::getId));
|
||||
Map<Long, OaApprovalInstance> latest = new HashMap<>();
|
||||
for (OaApprovalInstance i : insts) latest.put(i.getBusinessId(), i);
|
||||
for (SysOaContractVo v : list) {
|
||||
if (v == null) continue;
|
||||
OaApprovalInstance i = latest.get(v.getContractId());
|
||||
if (i != null) {
|
||||
v.setApprovalStatus(i.getStatus());
|
||||
v.setApprovalInstanceId(i.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +162,8 @@ public class SysOaContractServiceImpl implements ISysOaContractService {
|
||||
boolean flag = contractMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setContractId(add.getContractId());
|
||||
// 触发审批流(依赖 oa_approval_config 里 contract 配置;未配置/未启用会跳过)
|
||||
approvalService.submit(BIZ_TYPE, add.getContractId(), add.getContractName());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -146,6 +191,17 @@ public class SysOaContractServiceImpl implements ISysOaContractService {
|
||||
public Boolean updateByBo(SysOaContractBo bo) {
|
||||
SysOaContract update = BeanUtil.toBean(bo, SysOaContract.class);
|
||||
validEntityBeforeSave(update);
|
||||
// 标记「完结」(1) 前必须审批通过;若审批未启用/未配置 (无实例),放行。
|
||||
if (bo.getContractStatus() != null && "1".equals(bo.getContractStatus())) {
|
||||
OaApprovalInstance i = approvalInstanceMapper.selectOne(
|
||||
Wrappers.<OaApprovalInstance>lambdaQuery()
|
||||
.eq(OaApprovalInstance::getBusinessType, BIZ_TYPE)
|
||||
.eq(OaApprovalInstance::getBusinessId, bo.getContractId())
|
||||
.orderByDesc(OaApprovalInstance::getId).last("LIMIT 1"));
|
||||
if (i != null && (i.getStatus() == null || i.getStatus() != 1)) {
|
||||
throw new com.ruoyi.common.exception.ServiceException("该合同尚未审批通过,无法标记为完结");
|
||||
}
|
||||
}
|
||||
return contractMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ public class SysOaWarehouseServiceImpl implements ISysOaWarehouseService {
|
||||
.like("sow.brand", bo.getName())
|
||||
.or()
|
||||
.like("sow.model", bo.getName())
|
||||
.or()
|
||||
.like("sow.specifications", bo.getName())
|
||||
)
|
||||
.eq(StringUtils.isNotBlank(bo.getModel()), "sow.model", bo.getModel())
|
||||
.eq(StringUtils.isNotBlank(bo.getBrand()), "sow.brand", bo.getBrand());
|
||||
|
||||
43
ruoyi-ui/src/api/oa/approval.js
Normal file
43
ruoyi-ui/src/api/oa/approval.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ===== 审批配置 =====
|
||||
export function listApprovalConfig() {
|
||||
return request({ url: '/oa/approval/config/list', method: 'get' })
|
||||
}
|
||||
export function getApprovalConfig(businessType) {
|
||||
return request({ url: '/oa/approval/config/' + businessType, method: 'get' })
|
||||
}
|
||||
export function saveApprovalConfig(data) {
|
||||
return request({ url: '/oa/approval/config', method: 'post', data })
|
||||
}
|
||||
export function delApprovalConfig(id) {
|
||||
return request({ url: '/oa/approval/config/' + id, method: 'delete' })
|
||||
}
|
||||
|
||||
// ===== 我的审批 =====
|
||||
export function listMyPending(query) {
|
||||
return request({ url: '/oa/approval/mine/pending', method: 'get', params: query })
|
||||
}
|
||||
export function listMyDone(query) {
|
||||
return request({ url: '/oa/approval/mine/done', method: 'get', params: query })
|
||||
}
|
||||
export function listMySubmitted(query) {
|
||||
return request({ url: '/oa/approval/mine/submitted', method: 'get', params: query })
|
||||
}
|
||||
export function listAllApproval(query) {
|
||||
return request({ url: '/oa/approval/list', method: 'get', params: query })
|
||||
}
|
||||
|
||||
// ===== 操作 =====
|
||||
export function actApproval(data) {
|
||||
return request({ url: '/oa/approval/act', method: 'post', data })
|
||||
}
|
||||
export function withdrawApproval(instanceId) {
|
||||
return request({ url: '/oa/approval/withdraw/' + instanceId, method: 'post' })
|
||||
}
|
||||
export function getApprovalDetail(instanceId) {
|
||||
return request({ url: '/oa/approval/detail/' + instanceId, method: 'get' })
|
||||
}
|
||||
export function getLatestApproval(businessType, businessId) {
|
||||
return request({ url: '/oa/approval/latest', method: 'get', params: { businessType, businessId } })
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
|
||||
@toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav" />
|
||||
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
|
||||
<sub-menu-bar v-if="!topNav" />
|
||||
|
||||
<div class="right-menu">
|
||||
<template v-if="device !== 'mobile'">
|
||||
@@ -51,6 +51,7 @@ import RuoYiGit from "@/components/RuoYi/Git";
|
||||
import Screenfull from "@/components/Screenfull";
|
||||
import SizeSelect from "@/components/SizeSelect";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import SubMenuBar from "@/layout/components/SubMenuBar/index.vue";
|
||||
import AIChat from "@/layout/components/AIChat/index.vue";
|
||||
import FeedbackEntry from "@/layout/components/FeedbackEntry.vue";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
@@ -65,6 +66,7 @@ export default {
|
||||
// ChatComponent,
|
||||
Breadcrumb,
|
||||
TopNav,
|
||||
SubMenuBar,
|
||||
Hamburger,
|
||||
Screenfull,
|
||||
SizeSelect,
|
||||
|
||||
95
ruoyi-ui/src/layout/components/SubMenuBar/index.vue
Normal file
95
ruoyi-ui/src/layout/components/SubMenuBar/index.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="sub-menu-bar" v-if="items.length">
|
||||
<router-link
|
||||
v-for="item in items"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="sub-menu-item"
|
||||
:class="{ active: isActive(item) }"
|
||||
>{{ item.title }}</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "SubMenuBar",
|
||||
computed: {
|
||||
...mapGetters(["sidebarRouters"]),
|
||||
activeTopPath () {
|
||||
const path = this.$route.path || "";
|
||||
if (!path || path === "/") return "";
|
||||
const seg = path.split("/").filter(Boolean)[0];
|
||||
return seg ? "/" + seg : "";
|
||||
},
|
||||
items () {
|
||||
const top = this.findTop(this.sidebarRouters, this.activeTopPath);
|
||||
if (!top) return [];
|
||||
const children = (top.children || []).filter(c => !c.hidden);
|
||||
return children.map(c => ({
|
||||
path: this.resolvePath(top.path, c.path),
|
||||
title: c.meta?.title
|
||||
})).filter(it => it.title);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findTop (routes, activePath) {
|
||||
if (!routes || !activePath) return null;
|
||||
for (const r of routes) {
|
||||
if (r.hidden) continue;
|
||||
const rp = r.path?.startsWith("/") ? r.path : "/" + r.path;
|
||||
if (rp === activePath) return r;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
resolvePath (parent, child) {
|
||||
if (!child) return parent;
|
||||
if (/^https?:\/\//.test(child)) return child;
|
||||
if (child.startsWith("/")) return child;
|
||||
return (parent || "").replace(/\/+$/, "") + "/" + child;
|
||||
},
|
||||
isActive (item) {
|
||||
return this.$route.path === item.path || this.$route.path.startsWith(item.path + "/");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-menu-bar {
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 12px;
|
||||
overflow-x: auto;
|
||||
max-width: calc(100% - 360px);
|
||||
}
|
||||
|
||||
.sub-menu-bar::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
color: #5a5e66;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.sub-menu-item:hover {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sub-menu-item.active {
|
||||
color: #409eff;
|
||||
border-bottom-color: #409eff;
|
||||
}
|
||||
</style>
|
||||
@@ -4,13 +4,13 @@
|
||||
<div slot="header" class="card-header">
|
||||
<span>{{ bizTitle }}</span>
|
||||
<div class="actions">
|
||||
<el-button v-if="!preview" size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
|
||||
<el-button v-if="!isPreview" size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadDetail">刷新</el-button>
|
||||
|
||||
<el-button v-if="!preview && canApprove" type="success" size="mini" :loading="actionLoading" @click="handleApprove">
|
||||
<el-button v-if="!isPreview && canApprove" type="success" size="mini" :loading="actionLoading" @click="handleApprove">
|
||||
通过
|
||||
</el-button>
|
||||
<el-button v-if="!preview && canApprove" type="danger" size="mini" :loading="actionLoading" @click="handleReject">
|
||||
<el-button v-if="!isPreview && canApprove" type="danger" size="mini" :loading="actionLoading" @click="handleReject">
|
||||
驳回
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<ProjectInfo :info="detail" />
|
||||
</el-card>
|
||||
|
||||
<div v-if="!preview">
|
||||
<div v-if="!isPreview">
|
||||
<div class="block-title">审批操作</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
<div v-if="currentTask" class="btn-row">
|
||||
@@ -75,7 +75,7 @@
|
||||
<span>操作汇报</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!preview" class="comment-form">
|
||||
<div v-if="!isPreview" class="comment-form">
|
||||
<editor v-model="commentForm.commentContent" placeholder="填写操作汇报(可选)" />
|
||||
<file-upload v-model="commentForm.attachments" />
|
||||
<div class="form-actions">
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="comment-meta">
|
||||
<span class="comment-operator">{{ item.createByName }}</span>
|
||||
<span class="comment-time">{{ item.createTime }}</span>
|
||||
<el-button v-if="!preview && isSelf(item)" type="danger" size="mini" @click="handleDeleteComment(item.commentId)"
|
||||
<el-button v-if="!isPreview && isSelf(item)" type="danger" size="mini" @click="handleDeleteComment(item.commentId)"
|
||||
:loading="buttonLoading">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,6 +160,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPreview () {
|
||||
return this.preview || !!this.$route?.query?.preview
|
||||
},
|
||||
currentBizId () {
|
||||
return this.bizId
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
if (routePath) {
|
||||
this.$router.push({
|
||||
path: routePath,
|
||||
query: { bizId: bizId }
|
||||
query: { bizId: bizId, preview: 1 }
|
||||
})
|
||||
} else {
|
||||
this.$message.warning('无法确定申请类型对应的详情页面')
|
||||
|
||||
@@ -138,7 +138,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getRowTitle (row) {
|
||||
return row.title || row.remark || '-'
|
||||
const t = (row.title || row.remark || '').trim()
|
||||
if (t) return t
|
||||
return this.summarizeBiz(row) || '-'
|
||||
},
|
||||
summarizeBiz (row) {
|
||||
const b = row.bizData || {}
|
||||
switch (row.bizType) {
|
||||
case 'leave': {
|
||||
const dur = b.hours ? `${b.hours}h` : ''
|
||||
return [b.leaveType || '请假', dur, b.reason].filter(Boolean).join(' · ')
|
||||
}
|
||||
case 'travel':
|
||||
return [b.travelType || '出差', b.destination, b.reason].filter(Boolean).join(' · ')
|
||||
case 'seal':
|
||||
return [b.sealType || '用印', b.purpose].filter(Boolean).join(' · ')
|
||||
case 'reimburse':
|
||||
return [b.reimburseType || '报销', b.totalAmount != null ? '¥' + b.totalAmount : null, b.reason].filter(Boolean).join(' · ')
|
||||
case 'appropriation':
|
||||
return [b.appropriationType || '拨款', b.amount != null ? '¥' + b.amount : null, b.reason].filter(Boolean).join(' · ')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
isTravelCompleted (row) {
|
||||
if (!row || row.bizType !== 'travel') return false
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
stripe
|
||||
border
|
||||
@row-dblclick="handleRowClick">
|
||||
<el-table-column label="编号" prop="instId" width="80" />
|
||||
<el-table-column label="类型" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.bizType)" size="mini">{{ getTypeText(scope.row.bizType) }}</el-tag>
|
||||
@@ -80,6 +79,9 @@
|
||||
<el-table-column label="申请时间" prop="createTime" width="140">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" width="140">
|
||||
<template slot-scope="scope">{{ formatFinishTime(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" width="80">
|
||||
<template slot-scope="scope">{{ formatElapsed(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
@@ -174,7 +176,7 @@ export default {
|
||||
case 'travel':
|
||||
return `${b.travelType || '出差'} · ${b.destination || '未填目的地'}${b.startTime ? ' · ' + this.formatDate(b.startTime) : ''}`
|
||||
case 'seal':
|
||||
return `${b.sealType || '用印'}${b.fileName ? ' · ' + b.fileName : ''}${b.useReason ? ' · ' + b.useReason : ''}`
|
||||
return `${b.sealType || '用印'}${b.purpose ? ' · ' + b.purpose : ''}`
|
||||
case 'reimburse':
|
||||
return `${b.reimburseType || '报销'} · ¥${b.totalAmount != null ? b.totalAmount : 0}${b.reason ? ' · ' + b.reason : ''}`
|
||||
case 'appropriation':
|
||||
@@ -192,9 +194,19 @@ export default {
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
flowEndTime (row) {
|
||||
const finalStatus = ['approved', 'rejected', 'revoked', 'finished', 'complete']
|
||||
if (row.bizType === 'travel' && row.actualEndTime) return row.actualEndTime
|
||||
if (finalStatus.includes(row.status)) return row.updateTime || null
|
||||
return null
|
||||
},
|
||||
formatFinishTime (row) {
|
||||
const end = this.flowEndTime(row)
|
||||
return end ? this.formatDate(end) : '-'
|
||||
},
|
||||
formatElapsed (row) {
|
||||
if (!row.createTime) return '-'
|
||||
const end = row.finishTime || row.endTime || Date.now()
|
||||
const end = this.flowEndTime(row) || Date.now()
|
||||
const ms = new Date(end).getTime() - new Date(row.createTime).getTime()
|
||||
if (ms <= 0) return '-'
|
||||
const h = ms / 3600000
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 盖章操作(审批通过后显示) -->
|
||||
<div v-if="canStamp" class="block-title">盖章操作</div>
|
||||
<el-card v-if="canStamp" class="inner-card" shadow="never">
|
||||
<div v-if="canStamp && !$route.query.preview" class="block-title">盖章操作</div>
|
||||
<el-card v-if="canStamp && !$route.query.preview" class="inner-card" shadow="never">
|
||||
<div class="stamp-section">
|
||||
<div class="stamp-config">
|
||||
<el-form :model="stampForm" label-width="120px" size="small">
|
||||
|
||||
@@ -30,6 +30,23 @@
|
||||
: '评估候选人,分析与目标岗位的匹配度、优势、短板与面试建议。' }}
|
||||
支持 PDF / Word(.doc/.docx),≤ 20MB。
|
||||
</div>
|
||||
|
||||
<!-- 审核重点 / 附加要求 -->
|
||||
<div class="req-area">
|
||||
<div class="req-line">
|
||||
<span class="req-label">审核重点</span>
|
||||
<el-checkbox-group v-model="checkedItems" size="mini" :disabled="streaming" class="req-items">
|
||||
<el-checkbox v-for="it in itemOptions" :key="it.value" :label="it.label" border>{{ it.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<span class="req-tip">可在「系统管理→字典管理」增删审核项</span>
|
||||
</div>
|
||||
<div class="req-line">
|
||||
<span class="req-label">附加要求</span>
|
||||
<el-input v-model="extraText" type="textarea" :rows="2" :disabled="streaming"
|
||||
class="req-extra" maxlength="500" show-word-limit
|
||||
placeholder="可补充本次审核的特殊关注点,例如:重点核查付款比例是否对我方有利、是否有自动续约陷阱…" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="12" class="body">
|
||||
@@ -96,6 +113,7 @@
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { getDicts } from '@/api/system/dict/data'
|
||||
const marked = require('marked')
|
||||
|
||||
export default {
|
||||
@@ -107,6 +125,11 @@ export default {
|
||||
rawFile: null,
|
||||
fileName: '',
|
||||
|
||||
// 审核重点(字典) + 附加要求(自由文本)
|
||||
itemDicts: { contract: [], resume: [] },
|
||||
checkedItems: [],
|
||||
extraText: '',
|
||||
|
||||
streaming: false,
|
||||
done: false,
|
||||
reasoning: '',
|
||||
@@ -124,15 +147,37 @@ export default {
|
||||
computed: {
|
||||
renderedMd () {
|
||||
try { return marked(this.content) } catch (e) { return this.content }
|
||||
},
|
||||
itemOptions () {
|
||||
return this.itemDicts[this.reviewType] || []
|
||||
},
|
||||
requirements () {
|
||||
const parts = []
|
||||
if (this.checkedItems.length) parts.push('重点审核项:' + this.checkedItems.join('、'))
|
||||
if (this.extraText && this.extraText.trim()) parts.push('其他要求:' + this.extraText.trim())
|
||||
return parts.join('\n')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 切换类型时清空已选审核项(两套字典不同)
|
||||
reviewType () { this.checkedItems = [] }
|
||||
},
|
||||
created () {
|
||||
marked.setOptions({ breaks: true })
|
||||
this.loadItemDicts()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl)
|
||||
},
|
||||
methods: {
|
||||
loadItemDicts () {
|
||||
getDicts('oa_ai_review_item_contract').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, contract: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
getDicts('oa_ai_review_item_resume').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, resume: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
},
|
||||
goBack () { this.$router.push('/hint/aiReview') },
|
||||
goDetail () { if (this.savedId) this.$router.push('/hint/aiReview/detail/' + this.savedId) },
|
||||
riskTagType (r) { return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success') },
|
||||
@@ -167,6 +212,7 @@ export default {
|
||||
fd.append('file', this.rawFile)
|
||||
fd.append('reviewType', this.reviewType)
|
||||
if (this.reviewType === 'resume' && this.position) fd.append('position', this.position)
|
||||
if (this.requirements) fd.append('requirements', this.requirements)
|
||||
|
||||
try {
|
||||
const resp = await fetch(process.env.VUE_APP_BASE_API + '/oa/aiReview/analyzeStream', {
|
||||
@@ -264,6 +310,18 @@ export default {
|
||||
}
|
||||
.bar-hint { font-size: 12px; color: #909399; margin-top: 8px; }
|
||||
|
||||
.req-area { margin-top: 10px; border-top: 1px dashed #ebeef5; padding-top: 8px; }
|
||||
.req-line { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
.req-label { flex: 0 0 56px; font-size: 12px; color: #606266; padding-top: 5px; font-weight: 600; }
|
||||
.req-items { flex: 1; display: flex; flex-wrap: wrap; gap: 6px 0;
|
||||
::v-deep .el-checkbox { margin-right: 8px; margin-left: 0; }
|
||||
::v-deep .el-checkbox.is-bordered { padding: 4px 10px 4px 8px; height: auto; }
|
||||
}
|
||||
.req-tip { flex: 0 0 auto; font-size: 11px; color: #c0c4cc; padding-top: 5px; }
|
||||
.req-extra { flex: 1; }
|
||||
|
||||
.body { margin-top: 0; }
|
||||
.panel { ::v-deep .el-card__header { padding: 9px 14px; } }
|
||||
.hd { display: flex; justify-content: space-between; align-items: center;
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<el-descriptions-item label="模型">{{ info.model || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">{{ info.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核人">{{ info.createBy || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="info.requirements" label="审核重点 / 附加要求" :span="3">
|
||||
<span style="white-space: pre-wrap">{{ info.requirements }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="info" class="md-body" v-html="renderedMd" />
|
||||
|
||||
169
ruoyi-ui/src/views/oa/approval/config/index.vue
Normal file
169
ruoyi-ui/src/views/oa/approval/config/index.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert
|
||||
title="配置每个业务的审批人和或签/会签规则。审批人 user_id 多选;保存后立即生效,但只影响新提交的审批单。"
|
||||
type="info" show-icon :closable="false" style="margin-bottom: 12px;" />
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新增业务配置</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务类型 key" prop="businessType" width="180" />
|
||||
<el-table-column label="业务名称" prop="businessName" width="180" />
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-for="(name, i) in (row.approverNames || '').split(',').filter(Boolean)"
|
||||
:key="i" size="mini" style="margin-right: 4px;">{{ name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规则" prop="signType" width="100" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.signType === 1" type="success" size="mini">或签</el-tag>
|
||||
<el-tag v-else type="warning" size="mini">会签</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" prop="enabled" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.enabled === 1" size="mini" type="success">启用</el-tag>
|
||||
<el-tag v-else size="mini" type="info">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(row)">改审批人</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDel(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :title="form.id ? '修改审批配置' : '新增审批配置'" :visible.sync="open" width="640px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
|
||||
<el-form-item label="业务类型 key" prop="businessType">
|
||||
<el-input v-model="form.businessType" :disabled="!!form.id" placeholder="如 purchase_req / contract" />
|
||||
<div class="form-tip">代码内调用 approvalService.submit 时使用的 key,建表后不要修改。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务名称" prop="businessName">
|
||||
<el-input v-model="form.businessName" placeholder="如 采购需求" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人" prop="approverIdList">
|
||||
<el-select v-model="form.approverIdList" multiple filterable
|
||||
placeholder="选择一个或多个审批人" style="width: 100%">
|
||||
<el-option v-for="u in userOptions" :key="u.userId"
|
||||
:label="u.nickName + (u.userName ? ('('+u.userName+')') : '')" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则" prop="signType">
|
||||
<el-radio-group v-model="form.signType">
|
||||
<el-radio :label="1">或签(任一人通过即通过)</el-radio>
|
||||
<el-radio :label="2">会签(全部通过才通过)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="enabled">
|
||||
<el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="open = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listApprovalConfig, saveApprovalConfig, delApprovalConfig } from '@/api/oa/approval'
|
||||
import { listUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalConfig',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
userOptions: [],
|
||||
open: false,
|
||||
form: this.emptyForm(),
|
||||
rules: {
|
||||
businessType: [{ required: true, message: '业务类型不能为空', trigger: 'blur' }],
|
||||
businessName: [{ required: true, message: '业务名称不能为空', trigger: 'blur' }],
|
||||
approverIdList: [{ required: true, type: 'array', min: 1, message: '至少选择一个审批人', trigger: 'change' }],
|
||||
signType: [{ required: true, message: '请选择规则', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.loadUsers()
|
||||
},
|
||||
methods: {
|
||||
emptyForm() {
|
||||
return { id: null, businessType: '', businessName: '', approverIdList: [], signType: 1, enabled: 1, remark: '' }
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listApprovalConfig().then(res => {
|
||||
this.list = res.data || []
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
loadUsers() {
|
||||
listUser({ pageNum: 1, pageSize: 1000, status: '0' }).then(res => {
|
||||
this.userOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
handleAdd() {
|
||||
this.form = this.emptyForm()
|
||||
this.open = true
|
||||
},
|
||||
handleEdit(row) {
|
||||
const ids = (row.approverIds || '').split(',').filter(Boolean).map(s => Number(s))
|
||||
this.form = {
|
||||
id: row.id,
|
||||
businessType: row.businessType,
|
||||
businessName: row.businessName,
|
||||
approverIdList: ids,
|
||||
signType: row.signType,
|
||||
enabled: row.enabled,
|
||||
remark: row.remark
|
||||
}
|
||||
this.open = true
|
||||
},
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
const payload = {
|
||||
id: this.form.id,
|
||||
businessType: this.form.businessType,
|
||||
businessName: this.form.businessName,
|
||||
approverIds: (this.form.approverIdList || []).join(','),
|
||||
signType: this.form.signType,
|
||||
enabled: this.form.enabled,
|
||||
remark: this.form.remark
|
||||
}
|
||||
saveApprovalConfig(payload).then(() => {
|
||||
this.$modal.msgSuccess('保存成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDel(row) {
|
||||
this.$modal.confirm('确认删除业务【' + row.businessName + '】的审批配置?').then(() =>
|
||||
delApprovalConfig(row.id)
|
||||
).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip { color: #909399; font-size: 12px; line-height: 1.4; margin-top: 4px; }
|
||||
</style>
|
||||
168
ruoyi-ui/src/views/oa/approval/pending/index.vue
Normal file
168
ruoyi-ui/src/views/oa/approval/pending/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tabs v-model="tab" @tab-click="onTabClick" class="compact-tabs">
|
||||
<el-tab-pane name="pending" label="待我审批" />
|
||||
<el-tab-pane name="done" label="我已审批" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" size="mini" :inline="true" class="compact-search">
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="queryParams.businessType" clearable placeholder="全部" style="width: 200px;">
|
||||
<el-option v-for="c in configs" :key="c.businessType"
|
||||
:label="c.businessName + '(' + c.businessType + ')'" :value="c.businessType" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务" width="160">
|
||||
<template slot-scope="{ row }">
|
||||
{{ row.businessName || row.businessType }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="businessTitle" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="申请人" prop="applyUserName" width="100" />
|
||||
<el-table-column label="申请时间" prop="applyTime" width="160" />
|
||||
<el-table-column label="规则" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="row.signType === 1 ? 'success' : 'warning'">
|
||||
{{ row.signType === 1 ? '或签' : '会签' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="tab === 'pending'" type="success" size="mini" @click="openAct(row, 1)">通过</el-button>
|
||||
<el-button v-if="tab === 'pending'" type="danger" size="mini" @click="openAct(row, 2)">驳回</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-document" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 审批操作弹窗 -->
|
||||
<el-dialog :title="actForm.action === 1 ? '审批通过' : '审批驳回'" :visible.sync="actOpen" width="500px" append-to-body>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<b>{{ actForm.businessName }} - {{ actForm.businessTitle }}</b>
|
||||
</div>
|
||||
<el-input v-model="actForm.comment" type="textarea" :rows="3" placeholder="意见(可选)" />
|
||||
<div slot="footer">
|
||||
<el-button @click="actOpen = false">取消</el-button>
|
||||
<el-button :type="actForm.action === 1 ? 'success' : 'danger'" @click="doAct">
|
||||
{{ actForm.action === 1 ? '确认通过' : '确认驳回' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog title="审批详情" :visible.sync="detailOpen" width="640px" append-to-body>
|
||||
<div v-if="detail">
|
||||
<p><b>业务:</b>{{ detail.businessName || detail.businessType }} #{{ detail.businessId }}</p>
|
||||
<p><b>标题:</b>{{ detail.businessTitle }}</p>
|
||||
<p><b>申请人:</b>{{ detail.applyUserName }}({{ detail.applyTime }})</p>
|
||||
<p><b>审批人:</b>{{ detail.approverNames }}({{ detail.signType === 1 ? '或签' : '会签' }})</p>
|
||||
<p><b>状态:</b>
|
||||
<el-tag size="mini" :type="statusTag(detail.status)">{{ statusText(detail.status) }}</el-tag>
|
||||
</p>
|
||||
<el-divider>审批流水</el-divider>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="r in (detail.records || [])" :key="r.id"
|
||||
:type="r.action === 1 ? 'success' : 'danger'"
|
||||
:timestamp="r.opTime">
|
||||
<b>{{ r.approverName }}</b> {{ r.action === 1 ? '通过' : '驳回' }}
|
||||
<div v-if="r.comment" style="color:#666;">{{ r.comment }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listMyPending, listMyDone, listApprovalConfig,
|
||||
actApproval, getApprovalDetail
|
||||
} from '@/api/oa/approval'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalPending',
|
||||
data() {
|
||||
return {
|
||||
tab: 'pending',
|
||||
loading: false,
|
||||
total: 0,
|
||||
list: [],
|
||||
configs: [],
|
||||
queryParams: { pageNum: 1, pageSize: 10, businessType: undefined },
|
||||
actOpen: false,
|
||||
actForm: { instanceId: null, action: 1, comment: '', businessName: '', businessTitle: '' },
|
||||
detailOpen: false,
|
||||
detail: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
listApprovalConfig().then(res => { this.configs = res.data || [] })
|
||||
},
|
||||
methods: {
|
||||
statusText(s) {
|
||||
return ({ 0: '待审', 1: '通过', 2: '驳回', 3: '撤回' })[s] || '-'
|
||||
},
|
||||
statusTag(s) {
|
||||
return ({ 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' })[s] || ''
|
||||
},
|
||||
onTabClick() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
reset() { this.queryParams = { pageNum: 1, pageSize: 10, businessType: undefined }; this.getList() },
|
||||
getList() {
|
||||
this.loading = true
|
||||
const fn = this.tab === 'pending' ? listMyPending : listMyDone
|
||||
fn(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
openAct(row, action) {
|
||||
this.actForm = {
|
||||
instanceId: row.id, action, comment: '',
|
||||
businessName: row.businessName || row.businessType,
|
||||
businessTitle: row.businessTitle
|
||||
}
|
||||
this.actOpen = true
|
||||
},
|
||||
doAct() {
|
||||
actApproval({
|
||||
instanceId: this.actForm.instanceId,
|
||||
action: this.actForm.action,
|
||||
comment: this.actForm.comment
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已提交')
|
||||
this.actOpen = false
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
openDetail(row) {
|
||||
getApprovalDetail(row.id).then(res => {
|
||||
this.detail = res.data
|
||||
this.detailOpen = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
112
ruoyi-ui/src/views/oa/approval/submitted/index.vue
Normal file
112
ruoyi-ui/src/views/oa/approval/submitted/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" size="mini" :inline="true" class="compact-search">
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="queryParams.businessType" clearable placeholder="全部" style="width: 200px;">
|
||||
<el-option v-for="c in configs" :key="c.businessType"
|
||||
:label="c.businessName + '(' + c.businessType + ')'" :value="c.businessType" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务" width="160">
|
||||
<template slot-scope="{ row }">{{ row.businessName || row.businessType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="businessTitle" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="申请时间" prop="applyTime" width="160" />
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成时间" prop="finishTime" width="160" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="row.status === 0" type="text" size="mini" style="color:#f56c6c"
|
||||
@click="withdraw(row)">撤回</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-document" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<el-dialog title="审批详情" :visible.sync="detailOpen" width="640px" append-to-body>
|
||||
<div v-if="detail">
|
||||
<p><b>业务:</b>{{ detail.businessName || detail.businessType }} #{{ detail.businessId }}</p>
|
||||
<p><b>标题:</b>{{ detail.businessTitle }}</p>
|
||||
<p><b>申请人:</b>{{ detail.applyUserName }}({{ detail.applyTime }})</p>
|
||||
<p><b>审批人:</b>{{ detail.approverNames }}({{ detail.signType === 1 ? '或签' : '会签' }})</p>
|
||||
<p><b>状态:</b>
|
||||
<el-tag size="mini" :type="statusTag(detail.status)">{{ statusText(detail.status) }}</el-tag>
|
||||
</p>
|
||||
<el-divider>审批流水</el-divider>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="r in (detail.records || [])" :key="r.id"
|
||||
:type="r.action === 1 ? 'success' : 'danger'"
|
||||
:timestamp="r.opTime">
|
||||
<b>{{ r.approverName }}</b> {{ r.action === 1 ? '通过' : '驳回' }}
|
||||
<div v-if="r.comment" style="color:#666;">{{ r.comment }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listMySubmitted, listApprovalConfig,
|
||||
withdrawApproval, getApprovalDetail
|
||||
} from '@/api/oa/approval'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalSubmitted',
|
||||
data() {
|
||||
return {
|
||||
loading: false, total: 0, list: [], configs: [],
|
||||
queryParams: { pageNum: 1, pageSize: 10, businessType: undefined },
|
||||
detailOpen: false, detail: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
listApprovalConfig().then(res => { this.configs = res.data || [] })
|
||||
},
|
||||
methods: {
|
||||
statusText(s) { return ({ 0: '待审', 1: '通过', 2: '驳回', 3: '撤回' })[s] || '-' },
|
||||
statusTag(s) { return ({ 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' })[s] || '' },
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
reset() { this.queryParams = { pageNum: 1, pageSize: 10, businessType: undefined }; this.getList() },
|
||||
getList() {
|
||||
this.loading = true
|
||||
listMySubmitted(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
withdraw(row) {
|
||||
this.$modal.confirm('确认撤回审批单?').then(() =>
|
||||
withdrawApproval(row.id)
|
||||
).then(() => {
|
||||
this.$modal.msgSuccess('已撤回')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
openDetail(row) {
|
||||
getApprovalDetail(row.id).then(res => {
|
||||
this.detail = res.data
|
||||
this.detailOpen = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
278
ruoyi-ui/src/views/oa/docs/contract/index.vue
Normal file
278
ruoyi-ui/src/views/oa/docs/contract/index.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="app-container docs-page">
|
||||
<h1>合同流 · 使用说明</h1>
|
||||
<el-alert type="info" :closable="false" show-icon
|
||||
title="合同从「谁起草」到「谁批」到「正式生效」的全过程。本页让业务员、法务、领导都清楚自己在哪一步。"
|
||||
style="margin-bottom: 20px;" />
|
||||
|
||||
<!-- ============ 泳道图 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>一、流程全景(泳道图)</b></div>
|
||||
<div class="swimlane-wrap">
|
||||
<svg viewBox="0 0 1180 560" class="swimlane-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#606266" /></marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#f56c6c" /></marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#67c23a" /></marker>
|
||||
<marker id="arrow-dash" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#909399" /></marker>
|
||||
</defs>
|
||||
|
||||
<!-- 泳道背景 -->
|
||||
<g class="lanes">
|
||||
<rect x="120" y="20" width="1040" height="100" fill="#f9f6ff" stroke="#d3adf7" />
|
||||
<text x="20" y="76" class="lane-label">管理员</text>
|
||||
<text x="20" y="92" class="lane-sub">配置审批人</text>
|
||||
|
||||
<rect x="120" y="125" width="1040" height="120" fill="#eef9ff" stroke="#91d5ff" />
|
||||
<text x="20" y="185" class="lane-label">起草人</text>
|
||||
<text x="20" y="201" class="lane-sub">业务员 / 法务</text>
|
||||
|
||||
<rect x="120" y="250" width="1040" height="150" fill="#fff7e6" stroke="#ffd591" />
|
||||
<text x="20" y="325" class="lane-label">系统</text>
|
||||
<text x="20" y="341" class="lane-sub">自动派单 / 提醒</text>
|
||||
|
||||
<rect x="120" y="405" width="1040" height="135" fill="#f6ffed" stroke="#b7eb8f" />
|
||||
<text x="20" y="475" class="lane-label">审批人</text>
|
||||
<text x="20" y="491" class="lane-sub">领导 / 法务 / 财务</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 管理员 ===== -->
|
||||
<g class="node node-admin">
|
||||
<rect x="140" y="40" width="180" height="60" rx="6" />
|
||||
<text x="230" y="64" class="t1">① 配置「谁来批合同」</text>
|
||||
<text x="230" y="82" class="t2">选定审批人</text>
|
||||
<text x="230" y="96" class="t2">设置或签 / 会签</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 起草人 ===== -->
|
||||
<g class="node node-user">
|
||||
<rect x="140" y="150" width="180" height="70" rx="6" />
|
||||
<text x="230" y="178" class="t1">② 起草合同</text>
|
||||
<text x="230" y="196" class="t2">填合同名/甲乙方/金额</text>
|
||||
<text x="230" y="210" class="t2">/合同管理/新增</text>
|
||||
</g>
|
||||
|
||||
<g class="node node-user">
|
||||
<rect x="900" y="150" width="210" height="70" rx="6" />
|
||||
<text x="1005" y="178" class="t1">⑦ 合同生效执行</text>
|
||||
<text x="1005" y="196" class="t2">签订 / 履约 / 归档</text>
|
||||
<text x="1005" y="210" class="t2">(审批通过后才发出)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 系统 ===== -->
|
||||
<g class="node node-sys">
|
||||
<rect x="350" y="270" width="170" height="70" rx="6" />
|
||||
<text x="435" y="296" class="t1">③ 自动生成审批单</text>
|
||||
<text x="435" y="314" class="t2">把当前审批人 +</text>
|
||||
<text x="435" y="328" class="t2">或签/会签规则定下来</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="560" y="270" width="160" height="70" rx="6" />
|
||||
<text x="640" y="296" class="t1">④ 派单给审批人</text>
|
||||
<text x="640" y="314" class="t2">在「我的审批」</text>
|
||||
<text x="640" y="328" class="t2">出现待办</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="900" y="350" width="210" height="40" rx="6" />
|
||||
<text x="1005" y="376" class="t1">⑥' 判定整单结果(或/会签)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 审批人 ===== -->
|
||||
<g class="node node-app">
|
||||
<rect x="560" y="430" width="180" height="80" rx="6" />
|
||||
<text x="650" y="456" class="t1">⑤ 审批人处理</text>
|
||||
<text x="650" y="474" class="t2">通过 ✓ 或 驳回 ✗</text>
|
||||
<text x="650" y="490" class="t2">可写意见</text>
|
||||
<text x="650" y="506" class="t2">流水自动留痕</text>
|
||||
</g>
|
||||
|
||||
<!-- 配置 → 生成(虚线) -->
|
||||
<path d="M 320 80 C 400 80, 400 240, 435 270"
|
||||
fill="none" stroke="#909399" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#arrow-dash)" />
|
||||
<text x="345" y="160" class="arrow-label dim">仅影响后续新单</text>
|
||||
|
||||
<!-- ② → ③ -->
|
||||
<path d="M 320 200 C 360 200, 360 250, 435 270"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ③ → ④ -->
|
||||
<line x1="520" y1="305" x2="560" y2="305" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ④ → ⑤ -->
|
||||
<path d="M 640 340 C 640 380, 650 405, 650 430"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
<text x="660" y="395" class="arrow-label">收到待办</text>
|
||||
|
||||
<!-- ⑤ → ⑥' -->
|
||||
<path d="M 740 460 C 850 460, 880 410, 950 390"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ⑥' → ⑦ 通过 -->
|
||||
<path d="M 1005 350 L 1005 220"
|
||||
fill="none" stroke="#67c23a" stroke-width="2" marker-end="url(#arrow-green)" />
|
||||
<text x="1018" y="290" class="arrow-label ok">通过</text>
|
||||
|
||||
<!-- ⑥' → ② 驳回(闭环) -->
|
||||
<path d="M 900 370 C 700 370, 400 380, 230 240"
|
||||
fill="none" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#arrow-red)" />
|
||||
<text x="540" y="408" class="arrow-label bad">驳回 → 改完再提,自动开新单</text>
|
||||
|
||||
<!-- ⑦ → 完成 -->
|
||||
<g class="node node-end">
|
||||
<circle cx="1130" cy="185" r="22" />
|
||||
<text x="1130" y="190" class="t-end">归档</text>
|
||||
</g>
|
||||
<line x1="1110" y1="185" x2="1108" y2="185" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><i class="dot dot-admin"></i>管理员动作</span>
|
||||
<span><i class="dot dot-user"></i>起草人动作</span>
|
||||
<span><i class="dot dot-sys"></i>系统自动</span>
|
||||
<span><i class="dot dot-app"></i>审批人动作</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#67c23a" stroke-width="2"/></svg> 通过</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"/></svg> 驳回退回</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 审批状态 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>二、合同列表里「审批」列的含义</b></div>
|
||||
<p class="hint">合同管理列表会显示一列「审批」,告诉你这份合同走到流程哪一步了。</p>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「审批」列<span class="state-owner">(合同到底批没批)</span></h4>
|
||||
<div class="state-row"><el-tag size="small" type="info">未提交</el-tag><span>合同审批未启用 / 未配置,合同直接生效</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">待审</el-tag><span>已派给审批人,等他们处理</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">通过</el-tag><span>合同可以正式签订 / 发出</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="danger">驳回</el-tag><span>修改合同内容后再保存,系统会自动新开一张审批单</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">撤回</el-tag><span>起草人主动撤回,本单作废</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card warn">
|
||||
<h4>⚠️ 卡点:未审批通过不能标记「完结」</h4>
|
||||
<p>合同的字段(甲乙方/金额/条款等)<b>不受审批拦截</b>,可以照常编辑;但要把合同状态推进到 <el-tag size="mini" type="success">完结</el-tag> 时,<b>最新审批必须是「通过」</b>,否则系统会拒绝并提示「该合同尚未审批通过」。</p>
|
||||
<p class="hint">如果整个合同审批没启用 / 未配置(列表显示"未提交"),则不会有任何拦截,完结照常推进。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 角色操作速查 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>三、不同角色怎么用</b></div>
|
||||
<el-table :data="roleSteps" border size="small">
|
||||
<el-table-column label="你是" prop="role" width="100" />
|
||||
<el-table-column label="去哪儿" prop="menu" width="240" />
|
||||
<el-table-column label="做什么" prop="action" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 或签 vs 会签 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>四、或签 vs 会签(在合同场景下怎么选)</b></div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>或签 <el-tag size="mini" type="success">任一人即可</el-tag></h4>
|
||||
<p>审批人列表里 <b>任意一人</b> 通过 → 整张合同审批通过。</p>
|
||||
<p class="hint">适用:日常采购合同、小额合同、找最快有空的领导批即可的场景。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>会签 <el-tag size="mini" type="warning">必须全员通过</el-tag></h4>
|
||||
<p>审批人列表里 <b>所有人</b> 都通过 → 整张合同审批才通过。</p>
|
||||
<p class="hint">适用:大额合同 / 跨部门合同 / 重大法务事项 — 法务 + 财务 + 主管领导都要背书。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ FAQ ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>五、常见问题</b></div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="我新建了合同,但不想走审批,怎么办?">
|
||||
找管理员在「审批配置」里把「合同」这一行的「启用」开关关掉,新建合同就不再产生审批单,可以直接执行。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="合同已经审批通过了,但条款要改怎么办?">
|
||||
目前实现不会自动重新发起审批。如果改动比较大、有原则性变化,建议起草人在「我的发起」撤回旧单(如果还能撤回),或者重新建一份合同记录走新流程,留好痕迹。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="审批被驳回了,合同记录还在吗?">
|
||||
在。合同本身不会消失,改完字段重新保存时系统会自动新开一张审批单(流程图里那条红色虚线)。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="谁审过、什么意见去哪看?">
|
||||
起草人去「审批中心 → 我的发起」找到这张合同的审批单点「详情」,里面有完整的审批流水(谁、什么时间、通过/驳回、意见)。审批人本人在「我的审批 → 我已审批」也能看。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我换了合同审批人,老合同的审批单上人没变?">
|
||||
单子生成时已经把审批人和或签/会签规则定下来了 —— <b>不会回溯</b>。修改只对之后新提交的合同生效。
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OaDocContractFlow',
|
||||
data() {
|
||||
return {
|
||||
roleSteps: [
|
||||
{ role: '管理员', menu: '审批中心 → 审批配置', action: '选定合同的审批人;切换或签 / 会签;启用或停用合同审批。修改不影响已发起的旧单。' },
|
||||
{ role: '起草人', menu: '合同管理 → 新增', action: '填合同信息,保存后系统自动派单给审批人;列表的「审批」列会出现「待审」。' },
|
||||
{ role: '起草人', menu: '审批中心 → 我的发起', action: '查自己提交过的所有合同审批单 + 流水 + 意见;待审中可撤回。' },
|
||||
{ role: '审批人', menu: '审批中心 → 我的审批', action: '「待我审批」tab 里点通过 / 驳回并写意见;「我已审批」可回看历史。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docs-page { max-width: 1240px; }
|
||||
.docs-page h1 { font-size: 22px; margin: 0 0 16px; }
|
||||
.doc-card { margin-bottom: 16px; }
|
||||
|
||||
.swimlane-wrap { width: 100%; overflow-x: auto; }
|
||||
.swimlane-svg { width: 100%; min-width: 1100px; height: auto; display: block; }
|
||||
.swimlane-svg .lane-label { font-size: 15px; font-weight: 600; fill: #303133; }
|
||||
.swimlane-svg .lane-sub { font-size: 11px; fill: #909399; }
|
||||
.swimlane-svg .node rect { stroke-width: 1.3; }
|
||||
.swimlane-svg .node .t1 { font-size: 13px; font-weight: 600; text-anchor: middle; fill: #303133; }
|
||||
.swimlane-svg .node .t2 { font-size: 11px; text-anchor: middle; fill: #606266; }
|
||||
.swimlane-svg .node-admin rect { fill: #fff; stroke: #b37feb; }
|
||||
.swimlane-svg .node-user rect { fill: #fff; stroke: #40a9ff; }
|
||||
.swimlane-svg .node-sys rect { fill: #fff; stroke: #ffa940; }
|
||||
.swimlane-svg .node-app rect { fill: #fff; stroke: #73d13d; }
|
||||
.swimlane-svg .node-end circle { fill: #67c23a; stroke: #67c23a; }
|
||||
.swimlane-svg .node-end .t-end { font-size: 12px; fill: #fff; font-weight: 600; text-anchor: middle; }
|
||||
.swimlane-svg .arrow-label { font-size: 11px; fill: #606266; }
|
||||
.swimlane-svg .arrow-label.dim { fill: #909399; }
|
||||
.swimlane-svg .arrow-label.ok { fill: #67c23a; font-weight: 600; }
|
||||
.swimlane-svg .arrow-label.bad { fill: #f56c6c; font-weight: 600; }
|
||||
|
||||
.legend { margin-top: 12px; padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
display: flex; flex-wrap: wrap; gap: 18px; font-size: 12px; color: #606266; align-items: center; }
|
||||
.legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }
|
||||
.legend .dot-sys { background: #ffa940; }
|
||||
.legend .dot-user { background: #40a9ff; }
|
||||
.legend .dot-app { background: #73d13d; }
|
||||
.legend .dot-admin { background: #b37feb; }
|
||||
|
||||
.state-card { background: #fafafa; border-radius: 6px; padding: 14px 16px; height: 100%; }
|
||||
.state-card.warn { background: #fff8e6; }
|
||||
.state-card h4 { margin: 0 0 10px; font-size: 14px; }
|
||||
.state-card .state-owner { font-weight: normal; color: #909399; margin-left: 6px; font-size: 12px; }
|
||||
.state-card .state-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 13px; color: #606266; }
|
||||
.state-card p { line-height: 1.7; color: #303133; margin: 6px 0; }
|
||||
.state-card p.hint { color: #909399; font-size: 12px; }
|
||||
.docs-page .hint { color: #909399; font-size: 13px; margin-top: 0; }
|
||||
</style>
|
||||
295
ruoyi-ui/src/views/oa/docs/purchase/index.vue
Normal file
295
ruoyi-ui/src/views/oa/docs/purchase/index.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="app-container docs-page">
|
||||
<h1>采购流 · 使用说明</h1>
|
||||
<el-alert type="info" :closable="false" show-icon
|
||||
title="一张图看懂:采购需求从「谁提」到「谁审」再到「采购完成」全过程。下方的状态对照和速查表帮你弄清自己当下该做什么。"
|
||||
style="margin-bottom: 20px;" />
|
||||
|
||||
<!-- ============ 泳道图 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>一、流程全景(泳道图)</b></div>
|
||||
<div class="swimlane-wrap">
|
||||
<svg viewBox="0 0 1180 560" class="swimlane-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#606266" />
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#f56c6c" />
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#67c23a" />
|
||||
</marker>
|
||||
<marker id="arrow-dash" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#909399" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- 泳道背景 -->
|
||||
<g class="lanes">
|
||||
<rect x="120" y="20" width="1040" height="100" fill="#f9f6ff" stroke="#d3adf7" />
|
||||
<text x="20" y="76" class="lane-label">管理员</text>
|
||||
<text x="20" y="92" class="lane-sub">配置审批人</text>
|
||||
|
||||
<rect x="120" y="125" width="1040" height="120" fill="#eef9ff" stroke="#91d5ff" />
|
||||
<text x="20" y="185" class="lane-label">发起人</text>
|
||||
<text x="20" y="201" class="lane-sub">提需求 / 推进</text>
|
||||
|
||||
<rect x="120" y="250" width="1040" height="150" fill="#fff7e6" stroke="#ffd591" />
|
||||
<text x="20" y="325" class="lane-label">系统</text>
|
||||
<text x="20" y="341" class="lane-sub">自动派单 / 提醒</text>
|
||||
|
||||
<rect x="120" y="405" width="1040" height="135" fill="#f6ffed" stroke="#b7eb8f" />
|
||||
<text x="20" y="475" class="lane-label">审批人</text>
|
||||
<text x="20" y="491" class="lane-sub">通过 / 驳回</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 管理员 ===== -->
|
||||
<g class="node node-admin">
|
||||
<rect x="140" y="40" width="180" height="60" rx="6" />
|
||||
<text x="230" y="64" class="t1">① 配置「谁来审」</text>
|
||||
<text x="230" y="82" class="t2">选定审批人</text>
|
||||
<text x="230" y="96" class="t2">设置或签 / 会签</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 发起人 ===== -->
|
||||
<g class="node node-user">
|
||||
<rect x="140" y="150" width="170" height="70" rx="6" />
|
||||
<text x="225" y="178" class="t1">② 新建采购需求</text>
|
||||
<text x="225" y="196" class="t2">填标题/物料/截止</text>
|
||||
<text x="225" y="210" class="t2">/任务/采购需求</text>
|
||||
</g>
|
||||
|
||||
<g class="node node-user">
|
||||
<rect x="900" y="150" width="210" height="70" rx="6" />
|
||||
<text x="1005" y="178" class="t1">⑦ 推进业务状态</text>
|
||||
<text x="1005" y="196" class="t2">未采购 → 采购中 → 完成</text>
|
||||
<text x="1005" y="210" class="t2">(审批通过后才放开)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 系统 ===== -->
|
||||
<g class="node node-sys">
|
||||
<rect x="350" y="270" width="170" height="70" rx="6" />
|
||||
<text x="435" y="296" class="t1">③ 自动生成审批单</text>
|
||||
<text x="435" y="314" class="t2">把当前审批人 +</text>
|
||||
<text x="435" y="328" class="t2">或签/会签规则定下来</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="560" y="270" width="160" height="70" rx="6" />
|
||||
<text x="640" y="296" class="t1">④ 派单给审批人</text>
|
||||
<text x="640" y="314" class="t2">在「我的审批」</text>
|
||||
<text x="640" y="328" class="t2">出现待办</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="900" y="350" width="210" height="40" rx="6" />
|
||||
<text x="1005" y="376" class="t1">⑥' 判定整单结果(或/会签)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 审批人 ===== -->
|
||||
<g class="node node-app">
|
||||
<rect x="560" y="430" width="180" height="80" rx="6" />
|
||||
<text x="650" y="456" class="t1">⑤ 审批人处理</text>
|
||||
<text x="650" y="474" class="t2">通过 ✓ 或 驳回 ✗</text>
|
||||
<text x="650" y="490" class="t2">可写意见</text>
|
||||
<text x="650" y="506" class="t2">流水自动留痕</text>
|
||||
</g>
|
||||
|
||||
<!-- ============ 箭头 ============ -->
|
||||
<!-- 配置 → 审批单生成(虚线,从管理员到 ③) -->
|
||||
<path d="M 320 80 C 400 80, 400 240, 435 270"
|
||||
fill="none" stroke="#909399" stroke-width="1.4"
|
||||
stroke-dasharray="5 4" marker-end="url(#arrow-dash)" />
|
||||
<text x="345" y="160" class="arrow-label dim">仅影响后续新单</text>
|
||||
|
||||
<!-- ② → ③ -->
|
||||
<path d="M 310 200 C 360 200, 360 250, 435 270"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ③ → ④ -->
|
||||
<line x1="520" y1="305" x2="560" y2="305" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ④ → ⑤ -->
|
||||
<path d="M 640 340 C 640 380, 650 405, 650 430"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
<text x="660" y="395" class="arrow-label">收到待办</text>
|
||||
|
||||
<!-- ⑤ → ⑥' 终结判定 -->
|
||||
<path d="M 740 460 C 850 460, 880 410, 950 390"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ⑥' → ⑦ 通过 -->
|
||||
<path d="M 1005 350 L 1005 220"
|
||||
fill="none" stroke="#67c23a" stroke-width="2" marker-end="url(#arrow-green)" />
|
||||
<text x="1018" y="290" class="arrow-label ok">通过</text>
|
||||
|
||||
<!-- ⑥' → ② 驳回(闭环 / 红色) -->
|
||||
<path d="M 900 370 C 700 370, 400 380, 225 240"
|
||||
fill="none" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"
|
||||
marker-end="url(#arrow-red)" />
|
||||
<text x="540" y="408" class="arrow-label bad">驳回 → 改完再提,自动开新单</text>
|
||||
|
||||
<!-- ⑦ → 完成 -->
|
||||
<g class="node node-end">
|
||||
<circle cx="1130" cy="185" r="22" />
|
||||
<text x="1130" y="190" class="t-end">完成</text>
|
||||
</g>
|
||||
<line x1="1110" y1="185" x2="1108" y2="185" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><i class="dot dot-admin"></i>管理员动作</span>
|
||||
<span><i class="dot dot-user"></i>发起人动作</span>
|
||||
<span><i class="dot dot-sys"></i>系统自动</span>
|
||||
<span><i class="dot dot-app"></i>审批人动作</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#67c23a" stroke-width="2"/></svg> 通过</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"/></svg> 驳回退回</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 状态对照 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>二、两类状态对照(务必区分)</b></div>
|
||||
<p class="hint">列表里你会看到两列状态:左边「审批」表示这条采购需求 <b>谁来批、批了没</b>;右边「状态」表示 <b>采购到哪一步了</b>。两者独立但有关联——审批没通过,业务状态推不动。</p>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「审批」列<span class="state-owner">(谁来批 · 批了没)</span></h4>
|
||||
<div class="state-row"><el-tag size="small" type="info">未提交</el-tag><span>该业务尚未配置审批,或审批未启用</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">待审</el-tag><span>已派给审批人,等他们处理</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">通过</el-tag><span>审批结束,可以推进业务了</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="danger">驳回</el-tag><span>审批人不通过,改完字段再保存会自动开新单</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">撤回</el-tag><span>发起人自己撤回了,本单作废</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「状态」列<span class="state-owner">(采购到哪一步了)</span></h4>
|
||||
<div class="state-row"><el-tag size="small">未采购</el-tag><span>刚新建,等审批通过后才能往下推</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">采购中</el-tag><span>开始下单 / 联系供应商</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">完成</el-tag><span>采购到货,闭环结束</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">取消</el-tag><span>不再需要的需求,可直接取消(无需审批)</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 角色操作速查 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>三、不同角色怎么用(按你是谁找对应行)</b></div>
|
||||
<el-table :data="roleSteps" border size="small">
|
||||
<el-table-column label="你是" prop="role" width="100" />
|
||||
<el-table-column label="去哪儿" prop="menu" width="240" />
|
||||
<el-table-column label="做什么 / 看什么" prop="action" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 或签 vs 会签 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>四、或签 vs 会签</b></div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>或签 <el-tag size="mini" type="success">任一人即可</el-tag></h4>
|
||||
<p>审批人列表里 <b>任意一人</b> 点通过,整张单子就通过;任一人点驳回,整张单子立即驳回。</p>
|
||||
<p class="hint">适用:日常事务、找最快有空的同事盖章即可。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>会签 <el-tag size="mini" type="warning">必须全员通过</el-tag></h4>
|
||||
<p>审批人列表里 <b>所有人</b> 都点通过,整张单子才通过;只要有一个人驳回,整张单子立即驳回。</p>
|
||||
<p class="hint">适用:跨部门、需要每个负责人都背书的事项。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 常见问题 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>五、常见问题</b></div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="状态下拉点不动,提示「尚未审批通过」?">
|
||||
采购需求必须审批通过才能进入「采购中/完成」。去「审批中心 → 我的发起」看看自己这单是不是还在「待审」或者已经被「驳回」。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我换了审批人,为什么旧单子上的人没变?">
|
||||
单子在生成那一刻就把当时的审批人定下来了,<b>不会回溯</b>,这是为了避免审批中途偷偷换人。你的修改对后面新提交的单子生效。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="审批被驳回了,我的采购需求还在吗?">
|
||||
在。需求本身不会消失,你改完字段重新保存,系统会自动开一张新审批单。(就是流程图里那条红色虚线的回流路径。)
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我的事项太急,能不走审批吗?">
|
||||
找管理员在「审批配置」把对应业务的「启用」临时关掉,新提交的就直接生效不走审批。事项处理完再让管理员开回来。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我发起后又不想审批了,能撤回吗?">
|
||||
可以。在「审批中心 → 我的发起」找到这张单子,只要还是「待审」状态就能撤回;已经被审批人处理过的(通过/驳回)不能撤回。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="为什么我在「我的审批」里看不到任何待办?">
|
||||
说明目前没人提交需要你审的单。也可能管理员没把你配进审批人列表里,去问下管理员。
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OaDocPurchaseFlow',
|
||||
data() {
|
||||
return {
|
||||
roleSteps: [
|
||||
{ role: '管理员', menu: '审批中心 → 审批配置', action: '选定每个业务的审批人;切换或签 / 会签;启用或停用某个业务的审批。修改不影响已经发起的旧单。' },
|
||||
{ role: '发起人', menu: '任务 → 采购需求', action: '点「新增」填写需求,保存后系统自动派给审批人;列表里会出现「待审」徽标。' },
|
||||
{ role: '发起人', menu: '审批中心 → 我的发起', action: '看自己提交过的所有审批单 + 完整流水 + 审批意见;待审中可撤回。' },
|
||||
{ role: '审批人', menu: '审批中心 → 我的审批', action: '「待我审批」tab 里点通过 / 驳回并写意见;「我已审批」可回看自己处理过的历史。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docs-page { max-width: 1240px; }
|
||||
.docs-page h1 { font-size: 22px; margin: 0 0 16px; }
|
||||
.doc-card { margin-bottom: 16px; }
|
||||
|
||||
.swimlane-wrap { width: 100%; overflow-x: auto; }
|
||||
.swimlane-svg { width: 100%; min-width: 1100px; height: auto; display: block; }
|
||||
.swimlane-svg .lane-label { font-size: 15px; font-weight: 600; fill: #303133; }
|
||||
.swimlane-svg .lane-sub { font-size: 11px; fill: #909399; }
|
||||
.swimlane-svg .node rect { stroke-width: 1.3; }
|
||||
.swimlane-svg .node .t1 { font-size: 13px; font-weight: 600; text-anchor: middle; fill: #303133; }
|
||||
.swimlane-svg .node .t2 { font-size: 11px; text-anchor: middle; fill: #606266; }
|
||||
.swimlane-svg .node-admin rect { fill: #fff; stroke: #b37feb; }
|
||||
.swimlane-svg .node-user rect { fill: #fff; stroke: #40a9ff; }
|
||||
.swimlane-svg .node-sys rect { fill: #fff; stroke: #ffa940; }
|
||||
.swimlane-svg .node-app rect { fill: #fff; stroke: #73d13d; }
|
||||
.swimlane-svg .node-end circle { fill: #67c23a; stroke: #67c23a; }
|
||||
.swimlane-svg .node-end .t-end { font-size: 12px; fill: #fff; font-weight: 600; text-anchor: middle; }
|
||||
.swimlane-svg .arrow-label { font-size: 11px; fill: #606266; }
|
||||
.swimlane-svg .arrow-label.dim { fill: #909399; }
|
||||
.swimlane-svg .arrow-label.ok { fill: #67c23a; font-weight: 600; }
|
||||
.swimlane-svg .arrow-label.bad { fill: #f56c6c; font-weight: 600; }
|
||||
|
||||
.legend {
|
||||
margin-top: 12px; padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
display: flex; flex-wrap: wrap; gap: 18px; font-size: 12px; color: #606266; align-items: center;
|
||||
}
|
||||
.legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }
|
||||
.legend .dot-sys { background: #ffa940; }
|
||||
.legend .dot-user { background: #40a9ff; }
|
||||
.legend .dot-app { background: #73d13d; }
|
||||
.legend .dot-admin { background: #b37feb; }
|
||||
|
||||
.state-card { background: #fafafa; border-radius: 6px; padding: 14px 16px; height: 100%; }
|
||||
.state-card h4 { margin: 0 0 10px; font-size: 14px; }
|
||||
.state-card .state-owner { font-weight: normal; color: #909399; margin-left: 6px; font-size: 12px; }
|
||||
.state-card .state-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 13px; color: #606266; }
|
||||
.state-card p { line-height: 1.7; color: #303133; margin: 6px 0; }
|
||||
.state-card p.hint { color: #909399; font-size: 12px; }
|
||||
.docs-page .hint { color: #909399; font-size: 13px; margin-top: 0; }
|
||||
</style>
|
||||
@@ -93,6 +93,15 @@
|
||||
<span>{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批" align="center" prop="approvalStatus" width="90">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.approvalStatus == null" size="mini" type="info">未提交</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 0" size="mini" type="warning">待审</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 1" size="mini" type="success">通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 2" size="mini" type="danger">驳回</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 3" size="mini" type="info">撤回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="contractStatus" width="120">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_show_hide" :value="scope.row.contractStatus"/>
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
<span @click="gotoPurchase" style="cursor: pointer;">如要发放采购相关需求请移步采购需求管理(点击此处快速跳转)</span>
|
||||
</el-alert>
|
||||
|
||||
<el-row :gutter="20" v-loading="loading">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="item in taskList" :key="item.taskId">
|
||||
<el-row :gutter="10" v-loading="loading">
|
||||
<el-col class="task-col" v-for="item in taskList" :key="item.taskId">
|
||||
<el-card class="task-card" shadow="hover" @click.native="handleLookTask(item)">
|
||||
<!-- 卡片头部 -->
|
||||
<template slot="header">
|
||||
@@ -99,6 +99,11 @@
|
||||
</div>
|
||||
<span v-else>{{ item.workerNickName }}</span>
|
||||
</div>
|
||||
<div class="info-item attachment-row" @click.stop>
|
||||
<span class="info-label">附件:</span>
|
||||
<file-preview v-if="item.accessory" :value="item.accessory" class="card-attachment" />
|
||||
<el-tag v-else type="info" size="mini">无附件</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作按钮 -->
|
||||
@@ -352,11 +357,7 @@
|
||||
附件
|
||||
</template>
|
||||
<div class="attachment-box">
|
||||
<div v-if="form.accessory" class="attachment-item">
|
||||
<el-link :href="form.accessory" :underline="false" target="_blank">
|
||||
<i class="el-icon-document"></i> 查看附件
|
||||
</el-link>
|
||||
</div>
|
||||
<file-preview v-if="form.accessory" v-model="form.accessory" />
|
||||
<div v-else class="no-attachment">暂无附件...</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
@@ -374,6 +375,7 @@ import { addTask, delTask, getTask, listTask, updateTask } from "@/api/oa/task";
|
||||
import { deptTreeSelect, selectUser } from "@/api/system/user";
|
||||
import UserSelect from "@/components/UserSelect";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import FilePreview from "@/components/FilePreview";
|
||||
import OperationLogDrawer from "@/views/oa/project/operationLog/OperationLogDrawer.vue";
|
||||
|
||||
|
||||
@@ -382,6 +384,7 @@ export default {
|
||||
components: {
|
||||
UserSelect,
|
||||
ProjectSelect,
|
||||
FilePreview,
|
||||
OperationLogDrawer
|
||||
},
|
||||
dicts: ['sys_project_type', 'sys_project_status', 'sys_work_type', 'sys_sort_grade'],
|
||||
@@ -426,7 +429,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
searchTime: [],
|
||||
},
|
||||
userList: [],
|
||||
@@ -765,12 +768,116 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-col {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
margin-bottom: 20px;
|
||||
height: 320px;
|
||||
margin-bottom: 12px;
|
||||
height: 300px;
|
||||
/* 固定卡片高度 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-card >>> .el-card__header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.task-card >>> .el-card__body {
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-card .task-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-card .info-label {
|
||||
font-size: 12px;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.task-card .info-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.task-card .card-footer .el-button {
|
||||
margin-right: 4px;
|
||||
padding: 4px 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.task-col {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.task-col {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-col {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.task-col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-attachment {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item {
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-attachment >>> .ele-upload-list__item-content-action {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item:hover .ele-upload-list__item-content-action {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-icon-document {
|
||||
color: #409eff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -817,6 +924,7 @@ export default {
|
||||
padding: 10px 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
|
||||
@@ -66,40 +66,25 @@
|
||||
|
||||
<!-- 新增提示组件 -->
|
||||
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange"
|
||||
@expand-change="onExpandChange">
|
||||
<el-table-column type="expand" width="36">
|
||||
<template slot-scope="props">
|
||||
<div style="padding: 8px 24px; background:#fafafa;">
|
||||
<div style="font-weight:600; margin-bottom:6px;">
|
||||
已入库批次({{ (batchMap[props.row.requirementId] || []).length }} 批)
|
||||
</div>
|
||||
<el-table v-loading="batchLoading[props.row.requirementId]"
|
||||
:data="batchMap[props.row.requirementId] || []" size="mini" stripe>
|
||||
<el-table-column label="入库时间" prop="signTime" width="160">
|
||||
<template slot-scope="s">{{ parseTime(s.row.signTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入库单" prop="masterNum" width="160" />
|
||||
<el-table-column label="入库人" prop="signUser" width="100" />
|
||||
<el-table-column label="物料概览" prop="summary" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="总数量" prop="totalQty" width="80" align="right" />
|
||||
<el-table-column label="总金额" width="110" align="right">
|
||||
<template slot-scope="s">¥{{ Number(s.row.totalAmount || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="(batchMap[props.row.requirementId] || []).length === 0
|
||||
&& !batchLoading[props.row.requirementId]"
|
||||
style="text-align:center; color:#909399; padding:12px 0;">
|
||||
暂无入库批次
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="采购物料" align="left" min-width="240">
|
||||
<template slot-scope="{ row }">
|
||||
<template v-if="row.materials && row.materials.length">
|
||||
<div v-for="m in row.materials" :key="m.id" class="mat-row">
|
||||
<span class="mat-name">{{ m.name }}<span v-if="m.model" class="mat-model"> / {{ m.model }}</span></span>
|
||||
<span class="mat-stock" :class="{ low: (m.inventory||0) <= 0 }">
|
||||
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">未关联</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
|
||||
@@ -129,6 +114,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 表格中的状态列(替换原有列) -->
|
||||
<el-table-column label="审批" align="center" prop="approvalStatus" width="90">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.approvalStatus == null" size="mini" type="info">未提交</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 0" size="mini" type="warning">待审</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 1" size="mini" type="success">通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 2" size="mini" type="danger">驳回</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 3" size="mini" type="info">撤回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="160">
|
||||
<template slot-scope="{ row }">
|
||||
<el-select v-model="row.status" size="mini" placeholder="选择状态"
|
||||
@@ -153,9 +147,6 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
|
||||
v-if="scope.row.status !== 2 && scope.row.status !== 3"
|
||||
@click="handleGoToInbound(scope.row)">执行入库</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
|
||||
v-if="scope.row.status === 1">完成</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||
@@ -184,6 +175,25 @@
|
||||
<el-form-item label="关联项目" prop="projectId">
|
||||
<project-select v-model="form.projectId" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="采购物料" prop="materialIdArr">
|
||||
<div class="mat-trigger">
|
||||
<el-button size="mini" icon="el-icon-search" @click="openMaterialPicker">
|
||||
选择物料{{ selectedMaterials.length ? `(已选 ${selectedMaterials.length})` : '' }}
|
||||
</el-button>
|
||||
<span v-if="!selectedMaterials.length" class="mat-trigger-hint">可多选;留空也可保存,后续再关联</span>
|
||||
</div>
|
||||
<div v-if="selectedMaterials.length" class="mat-stock-panel">
|
||||
<div v-for="m in selectedMaterials" :key="m.id" class="mat-stock-row">
|
||||
<span class="mat-stock-name">{{ m.name }}<span v-if="m.model" style="color:#909399;"> / {{ m.model }}</span></span>
|
||||
<span class="mat-stock-tag" :class="{ low: (m.inventory||0) <= 0 }">
|
||||
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||
</span>
|
||||
<span v-if="m.brand" class="mat-stock-meta">品牌:{{ m.brand }}</span>
|
||||
<span v-if="m.specifications" class="mat-stock-meta">规格:{{ m.specifications }}</span>
|
||||
<i class="el-icon-close mat-remove" title="移除" @click="removeSelectedMaterial(m.id)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="需求描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
|
||||
</el-form-item>
|
||||
@@ -205,6 +215,84 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 物料选择器 -->
|
||||
<el-dialog title="选择采购物料" :visible.sync="materialPickerOpen" width="780px"
|
||||
append-to-body :close-on-click-modal="false" @open="onPickerOpen">
|
||||
<div class="picker-toolbar">
|
||||
<el-input v-model="picker.kw" placeholder="搜索 名称 / 型号 / 品牌 / 规格"
|
||||
size="mini" clearable prefix-icon="el-icon-search"
|
||||
style="width: 320px;" @input="onPickerSearch" @clear="onPickerSearch" />
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-plus"
|
||||
style="margin-left:8px;" @click="openInlineNew">未找到?新增物料</el-button>
|
||||
<span class="picker-tip" v-if="picker.tempSelected.length">
|
||||
已勾选 <b>{{ picker.tempSelected.length }}</b> 项
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 内嵌新增物料表单 -->
|
||||
<el-card v-if="newMat.show" shadow="never" class="new-mat-card">
|
||||
<div slot="header" class="new-mat-header">
|
||||
<span><i class="el-icon-plus" /> 新增物料到库存</span>
|
||||
<el-button type="text" size="mini" icon="el-icon-close" @click="newMat.show = false">收起</el-button>
|
||||
</div>
|
||||
<el-form :model="newMat" :rules="newMatRules" ref="newMatForm" size="mini"
|
||||
label-width="70px" :inline="true">
|
||||
<el-form-item label="名称" prop="name" required>
|
||||
<el-input v-model="newMat.name" placeholder="物料名称" style="width:180px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="型号" prop="model">
|
||||
<el-input v-model="newMat.model" placeholder="可选" style="width:140px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格" prop="specifications">
|
||||
<el-input v-model="newMat.specifications" placeholder="可选" style="width:140px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌" prop="brand">
|
||||
<el-input v-model="newMat.brand" placeholder="可选" style="width:120px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="newMat.unit" placeholder="个/箱/kg" style="width:100px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="初始库存" prop="inventory" required>
|
||||
<el-input-number v-model="newMat.inventory" :min="0" :step="1" size="mini" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" type="primary" :loading="newMat.saving" @click="submitNewMat">保存并选中</el-button>
|
||||
<el-button size="mini" @click="newMat.show = false">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="picker.list" v-loading="picker.loading" size="mini"
|
||||
ref="pickerTable" border highlight-current-row max-height="380"
|
||||
row-key="id" :row-class-name="pickerRowClass"
|
||||
@row-click="onPickerRowClick"
|
||||
@selection-change="onPickerSelectionChange">
|
||||
<el-table-column type="selection" width="42" reserve-selection />
|
||||
<el-table-column label="名称" prop="name" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="型号" prop="model" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="规格" prop="specifications" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="品牌" prop="brand" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||
<el-table-column label="库存" width="90" align="right">
|
||||
<template slot-scope="s">
|
||||
<span :style="{color: (s.row.inventory||0) <= 0 ? '#f56c6c' : '#67c23a', fontWeight:600}">
|
||||
{{ s.row.inventory == null ? 0 : s.row.inventory }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="picker-pager" v-if="picker.total > picker.pageSize">
|
||||
<el-pagination small background layout="prev, pager, next, total"
|
||||
:total="picker.total" :page-size="picker.pageSize" :current-page.sync="picker.pageNum"
|
||||
@current-change="loadPickerList" />
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button @click="materialPickerOpen = false">取 消</el-button>
|
||||
<el-button type="primary" @click="confirmPicker">确定({{ picker.tempSelected.length }})</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 需求详情对话框 -->
|
||||
<el-dialog title="需求详情" :visible.sync="detailDialog" width="600px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -233,8 +321,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import { listOaWarehouse, getOaWarehouse, addOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
|
||||
import FilePreview from '@/components/FilePreview';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
@@ -244,9 +333,35 @@ export default {
|
||||
components: { FileUpload, FilePreview, ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
// 入库批次(按 requirementId 缓存)
|
||||
batchMap: {},
|
||||
batchLoading: {},
|
||||
// 当前已选物料明细(用于库存展示)
|
||||
selectedMaterials: [],
|
||||
// 物料选择器
|
||||
materialPickerOpen: false,
|
||||
picker: {
|
||||
kw: '',
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tempSelected: [], // 选择器内部勾选项(含完整物料对象)
|
||||
searchTimer: null,
|
||||
},
|
||||
// 新增物料内嵌表单
|
||||
newMat: {
|
||||
show: false,
|
||||
saving: false,
|
||||
name: '',
|
||||
model: '',
|
||||
specifications: '',
|
||||
brand: '',
|
||||
unit: '',
|
||||
inventory: 0,
|
||||
},
|
||||
newMatRules: {
|
||||
name: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
|
||||
inventory: [{ required: true, message: '初始库存不能为空', trigger: 'change' }],
|
||||
},
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
@@ -324,11 +439,120 @@ export default {
|
||||
this.refreshStat();
|
||||
},
|
||||
methods: {
|
||||
// 跳到入库明细页面,并预填该采购需求
|
||||
handleGoToInbound (row) {
|
||||
this.$router.push({
|
||||
path: '/step/in',
|
||||
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
|
||||
// ====== 物料选择器 ======
|
||||
openMaterialPicker () {
|
||||
this.picker.kw = ''
|
||||
this.picker.pageNum = 1
|
||||
// 当前已选项作为初始勾选
|
||||
this.picker.tempSelected = this.selectedMaterials.slice()
|
||||
this.newMat.show = false
|
||||
this.materialPickerOpen = true
|
||||
},
|
||||
onPickerOpen () {
|
||||
this.loadPickerList().then(() => this.syncPickerSelectionUI())
|
||||
},
|
||||
loadPickerList () {
|
||||
this.picker.loading = true
|
||||
const params = {
|
||||
pageNum: this.picker.pageNum,
|
||||
pageSize: this.picker.pageSize,
|
||||
}
|
||||
if (this.picker.kw && this.picker.kw.trim()) params.name = this.picker.kw.trim()
|
||||
return listOaWarehouse(params).then(res => {
|
||||
this.picker.list = res.rows || []
|
||||
this.picker.total = res.total || 0
|
||||
this.$nextTick(() => this.syncPickerSelectionUI())
|
||||
}).finally(() => { this.picker.loading = false })
|
||||
},
|
||||
onPickerSearch () {
|
||||
clearTimeout(this.picker.searchTimer)
|
||||
this.picker.searchTimer = setTimeout(() => {
|
||||
this.picker.pageNum = 1
|
||||
this.loadPickerList()
|
||||
}, 250)
|
||||
},
|
||||
// 表格分页/刷新后把已勾选项重新打勾
|
||||
syncPickerSelectionUI () {
|
||||
const tbl = this.$refs.pickerTable
|
||||
if (!tbl) return
|
||||
tbl.clearSelection()
|
||||
const ids = new Set(this.picker.tempSelected.map(m => m.id))
|
||||
for (const row of this.picker.list) {
|
||||
if (ids.has(row.id)) tbl.toggleRowSelection(row, true)
|
||||
}
|
||||
},
|
||||
onPickerSelectionChange (rows) {
|
||||
// 当前页勾选 + 之前其它页保留下来的(不在 list 中的)
|
||||
const curIds = new Set(this.picker.list.map(m => m.id))
|
||||
const keepOther = this.picker.tempSelected.filter(m => !curIds.has(m.id))
|
||||
this.picker.tempSelected = keepOther.concat(rows)
|
||||
},
|
||||
onPickerRowClick (row) {
|
||||
const tbl = this.$refs.pickerTable
|
||||
if (!tbl) return
|
||||
const idx = this.picker.tempSelected.findIndex(m => m.id === row.id)
|
||||
tbl.toggleRowSelection(row, idx === -1)
|
||||
},
|
||||
pickerRowClass ({ row }) {
|
||||
return this.picker.tempSelected.find(m => m.id === row.id) ? 'picker-row-checked' : ''
|
||||
},
|
||||
confirmPicker () {
|
||||
this.selectedMaterials = this.picker.tempSelected.slice()
|
||||
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||
this.materialPickerOpen = false
|
||||
},
|
||||
removeSelectedMaterial (id) {
|
||||
this.selectedMaterials = this.selectedMaterials.filter(m => m.id !== id)
|
||||
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||
},
|
||||
// ====== 新增物料 ======
|
||||
openInlineNew () {
|
||||
this.newMat.show = true
|
||||
this.newMat.name = this.picker.kw || ''
|
||||
this.newMat.model = ''
|
||||
this.newMat.specifications = ''
|
||||
this.newMat.brand = ''
|
||||
this.newMat.unit = ''
|
||||
this.newMat.inventory = 0
|
||||
this.$nextTick(() => { this.$refs.newMatForm && this.$refs.newMatForm.clearValidate() })
|
||||
},
|
||||
submitNewMat () {
|
||||
this.$refs.newMatForm.validate(valid => {
|
||||
if (!valid) return
|
||||
this.newMat.saving = true
|
||||
const payload = {
|
||||
name: this.newMat.name.trim(),
|
||||
model: this.newMat.model || undefined,
|
||||
specifications: this.newMat.specifications || undefined,
|
||||
brand: this.newMat.brand || undefined,
|
||||
unit: this.newMat.unit || undefined,
|
||||
inventory: Number(this.newMat.inventory) || 0,
|
||||
}
|
||||
addOaWarehouse(payload).then(res => {
|
||||
// 后端返回的 data 可能是 id 或对象,统一兜底再 get 一次
|
||||
const newId = (res && (res.data && (res.data.id || typeof res.data === 'number'))) ? (res.data.id || res.data) : null
|
||||
const finish = (full) => {
|
||||
if (!full) return
|
||||
// 直接放进当前页顶端 + 勾选
|
||||
if (!this.picker.list.find(m => m.id === full.id)) {
|
||||
this.picker.list.unshift(full)
|
||||
}
|
||||
if (!this.picker.tempSelected.find(m => m.id === full.id)) {
|
||||
this.picker.tempSelected.push(full)
|
||||
}
|
||||
this.syncPickerSelectionUI()
|
||||
this.newMat.show = false
|
||||
this.$modal.msgSuccess('已新增并自动选中')
|
||||
}
|
||||
if (newId) {
|
||||
getOaWarehouse(newId).then(r => finish(r.data))
|
||||
} else {
|
||||
// 兜底:用刚提交的字段拼一个临时对象
|
||||
finish({ ...payload, id: Date.now() })
|
||||
// 重新查一下保证 id 真实
|
||||
this.loadPickerList()
|
||||
}
|
||||
}).finally(() => { this.newMat.saving = false })
|
||||
})
|
||||
},
|
||||
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
||||
@@ -368,18 +592,6 @@ export default {
|
||||
if (file && file.url) window.open(file.url, '_blank')
|
||||
}
|
||||
},
|
||||
// 展开行:加载该需求的入库批次
|
||||
onExpandChange (row, expanded) {
|
||||
if (!expanded || !expanded.length) return
|
||||
const id = row.requirementId
|
||||
if (this.batchMap[id]) return // 已有缓存
|
||||
this.$set(this.batchLoading, id, true)
|
||||
getRequirementBatches(id).then(res => {
|
||||
this.$set(this.batchMap, id, res.data || [])
|
||||
}).finally(() => {
|
||||
this.$set(this.batchLoading, id, false)
|
||||
})
|
||||
},
|
||||
async onStatusChange (row, newVal) {
|
||||
row._updating = true
|
||||
// 如果后端需要字符串,可改为 String(newVal)
|
||||
@@ -453,6 +665,8 @@ export default {
|
||||
requesterId: undefined,
|
||||
ownerId: undefined,
|
||||
projectId: undefined,
|
||||
materialIds: undefined,
|
||||
materialIdArr: [],
|
||||
description: undefined,
|
||||
deadline: undefined,
|
||||
status: 0,
|
||||
@@ -488,6 +702,7 @@ export default {
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.form.requesterId = this.$store.state.user.id;
|
||||
this.selectedMaterials = [];
|
||||
this.open = true;
|
||||
this.title = "添加OA 需求";
|
||||
},
|
||||
@@ -504,10 +719,21 @@ export default {
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
this.selectedMaterials = [];
|
||||
const requirementId = row.requirementId || this.ids
|
||||
getRequirements(requirementId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
const data = response.data || {};
|
||||
const ids = (data.materialIds || '').split(',').map(s => Number(s)).filter(n => !isNaN(n) && n)
|
||||
data.materialIdArr = ids
|
||||
this.form = data;
|
||||
if (data.materials && data.materials.length) {
|
||||
this.selectedMaterials = data.materials.slice()
|
||||
} else if (ids.length) {
|
||||
// 兜底:逐个拉
|
||||
Promise.all(ids.map(id => getOaWarehouse(id).then(r => r.data).catch(() => null)))
|
||||
.then(list => { this.selectedMaterials = list.filter(Boolean) })
|
||||
}
|
||||
this.open = true;
|
||||
this.title = "修改OA 需求";
|
||||
});
|
||||
@@ -517,6 +743,8 @@ export default {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
const arr = Array.isArray(this.form.materialIdArr) ? this.form.materialIdArr : []
|
||||
this.form.materialIds = arr.length ? arr.join(',') : null
|
||||
if (this.form.requirementId != null) {
|
||||
updateRequirements(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
@@ -652,6 +880,67 @@ export default {
|
||||
cursor: pointer;
|
||||
&:hover { color: #409eff; text-decoration: underline; }
|
||||
}
|
||||
.mat-trigger {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
.mat-trigger-hint { font-size: 12px; color: #909399; }
|
||||
}
|
||||
.mat-remove {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
color: #c0c4cc;
|
||||
&:hover { color: #f56c6c; }
|
||||
}
|
||||
.picker-toolbar {
|
||||
display: flex; align-items: center; margin-bottom: 10px;
|
||||
.picker-tip { margin-left: auto; font-size: 12px; color: #606266; }
|
||||
}
|
||||
.picker-pager { text-align: right; margin-top: 10px; }
|
||||
.new-mat-card {
|
||||
margin-bottom: 10px;
|
||||
::v-deep .el-card__header { padding: 6px 12px; background: #f5f7fa; }
|
||||
.new-mat-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 13px; color: #303133;
|
||||
}
|
||||
}
|
||||
::v-deep .picker-row-checked > td { background: #ecf5ff !important; }
|
||||
.mat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
& + .mat-row { border-top: 1px dashed #ebeef5; }
|
||||
.mat-name { flex: 1; color: #303133; }
|
||||
.mat-model { color: #909399; margin-left: 2px; }
|
||||
.mat-stock {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
background: #f0f9eb; color: #67c23a;
|
||||
&.low { background: #fef0f0; color: #f56c6c; }
|
||||
}
|
||||
}
|
||||
.mat-stock-panel {
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
background: #fafafa;
|
||||
border-radius: 3px;
|
||||
.mat-stock-row {
|
||||
display: flex; flex-wrap: wrap; gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
& + .mat-stock-row { border-top: 1px dashed #ebeef5; }
|
||||
}
|
||||
.mat-stock-name { flex: 1; min-width: 120px; color: #303133; }
|
||||
.mat-stock-tag {
|
||||
padding: 0 6px; border-radius: 3px;
|
||||
background: #f0f9eb; color: #67c23a;
|
||||
&.low { background: #fef0f0; color: #f56c6c; }
|
||||
}
|
||||
.mat-stock-meta { color: #909399; }
|
||||
}
|
||||
.accessory-link {
|
||||
display: inline-block;
|
||||
max-width: 160px;
|
||||
|
||||
@@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
`oss_id` bigint(20) DEFAULT NULL COMMENT 'OSS文件ID(原件留存)',
|
||||
`file_url` varchar(500) DEFAULT NULL COMMENT 'OSS文件地址',
|
||||
`position` varchar(255) DEFAULT NULL COMMENT '简历目标岗位',
|
||||
`requirements` varchar(1000) DEFAULT NULL COMMENT '本次审核的附加要求/审核重点',
|
||||
`match_score` int(11) DEFAULT NULL COMMENT '简历匹配度评分 0-100',
|
||||
`risk_level` varchar(10) DEFAULT NULL COMMENT '合同风险评级 高/中/低',
|
||||
`summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)',
|
||||
@@ -31,8 +32,38 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 审核记录(合同/简历)';
|
||||
|
||||
-- 若表已存在(旧版本),补加 summary 列(MySQL 不支持 ADD COLUMN IF NOT EXISTS,重复执行报错可忽略):
|
||||
-- 若表已存在(旧版本),补加列(MySQL 不支持 ADD COLUMN IF NOT EXISTS,已存在则报错可忽略):
|
||||
-- ALTER TABLE `oa_ai_review` ADD COLUMN `summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)' AFTER `risk_level`;
|
||||
-- ALTER TABLE `oa_ai_review` ADD COLUMN `requirements` varchar(1000) DEFAULT NULL COMMENT '本次审核的附加要求/审核重点' AFTER `position`;
|
||||
|
||||
-- ---------------- 字典:可选审核重点(合同 / 简历),用户可在字典管理增删 ----------------
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES (2063920000000000001, 'AI合同审核项', 'oa_ai_review_item_contract', '0', 'admin', NOW(), 'AI合同审核可选审核重点'),
|
||||
(2063920000000000002, 'AI简历审核项', 'oa_ai_review_item_resume', '0', 'admin', NOW(), 'AI简历审核可选审核重点');
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_data`
|
||||
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES
|
||||
(2063920000000000010, 1, '付款条款与节点', '付款条款与节点', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000011, 2, '违约责任与赔偿', '违约责任与赔偿', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000012, 3, '知识产权归属', '知识产权归属', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000013, 4, '保密义务', '保密义务', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000014, 5, '交付与验收标准', '交付与验收标准', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000015, 6, '质保与售后', '质保与售后', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000016, 7, '价格与税费', '价格与税费', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000017, 8, '争议解决与管辖', '争议解决与管辖', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000018, 9, '不可抗力', '不可抗力', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000019, 10,'合同解除条件', '合同解除条件', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000040, 1, '技术能力匹配', '技术能力匹配', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000041, 2, '项目经验深度', '项目经验深度', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000042, 3, '工作稳定性', '工作稳定性', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000043, 4, '团队协作与管理', '团队协作与管理', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000044, 5, '学历与专业背景', '学历与专业背景', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000045, 6, '行业相关经验', '行业相关经验', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000046, 7, '薪资期望匹配', '薪资期望匹配', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000047, 8, '成长潜力', '成长潜力', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000048, 9, '离职风险', '离职风险', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000049, 10,'外语能力', '外语能力', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), '');
|
||||
|
||||
-- ---------------- 菜单:信息 > AI审核 ----------------
|
||||
-- 父菜单 1774989374680858626 = 「信息」
|
||||
|
||||
91
sql/oa_approval.sql
Normal file
91
sql/oa_approval.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- ============================================================
|
||||
-- 通用审批框架(轻量自建)
|
||||
-- 1. oa_approval_config 业务-审批人配置(可改)
|
||||
-- 2. oa_approval_instance 审批单实例(一次提交一条)
|
||||
-- 3. oa_approval_record 审批操作流水(一人一条)
|
||||
-- ============================================================
|
||||
|
||||
DROP TABLE IF EXISTS oa_approval_record;
|
||||
DROP TABLE IF EXISTS oa_approval_instance;
|
||||
DROP TABLE IF EXISTS oa_approval_config;
|
||||
|
||||
CREATE TABLE oa_approval_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
business_type VARCHAR(64) NOT NULL COMMENT '业务类型 key(如 purchase_req/contract)',
|
||||
business_name VARCHAR(128) NOT NULL COMMENT '业务名称(展示用)',
|
||||
approver_ids VARCHAR(512) NOT NULL COMMENT '审批人 sys_user.user_id 列表,逗号分隔',
|
||||
sign_type TINYINT NOT NULL DEFAULT 1 COMMENT '1或签 2会签',
|
||||
enabled TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0停用 1启用',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
create_by VARCHAR(64) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64) DEFAULT NULL,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
del_flag TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_biz_type (business_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务审批配置';
|
||||
|
||||
CREATE TABLE oa_approval_instance (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
business_type VARCHAR(64) NOT NULL COMMENT '业务类型 key',
|
||||
business_id BIGINT NOT NULL COMMENT '业务表主键',
|
||||
business_title VARCHAR(255) DEFAULT NULL COMMENT '业务标题(冗余展示)',
|
||||
apply_user_id BIGINT NOT NULL COMMENT '申请人 user_id',
|
||||
apply_user_name VARCHAR(64) DEFAULT NULL,
|
||||
apply_time DATETIME NOT NULL,
|
||||
approver_ids VARCHAR(512) NOT NULL COMMENT '快照:提交时的审批人',
|
||||
sign_type TINYINT NOT NULL COMMENT '快照:1或签 2会签',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回 3撤回',
|
||||
finish_time DATETIME DEFAULT NULL,
|
||||
remark VARCHAR(255) DEFAULT NULL,
|
||||
create_by VARCHAR(64) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64) DEFAULT NULL,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
del_flag TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_biz (business_type, business_id),
|
||||
KEY idx_status (status),
|
||||
KEY idx_apply_user (apply_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批单实例';
|
||||
|
||||
CREATE TABLE oa_approval_record (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
instance_id BIGINT NOT NULL COMMENT '审批单 id',
|
||||
approver_id BIGINT NOT NULL COMMENT '审批人 user_id',
|
||||
approver_name VARCHAR(64) DEFAULT NULL,
|
||||
action TINYINT NOT NULL COMMENT '1通过 2驳回',
|
||||
comment VARCHAR(512) DEFAULT NULL,
|
||||
op_time DATETIME NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_inst (instance_id),
|
||||
KEY idx_approver (approver_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批操作流水';
|
||||
|
||||
-- 默认业务审批配置(先用 admin=1 占位,部署后到「审批配置」里改成真实审批人)
|
||||
INSERT INTO oa_approval_config (business_type, business_name, approver_ids, sign_type, enabled, remark) VALUES
|
||||
('purchase_req', '采购需求', '1', 1, 1, '默认或签,请在「审批配置」修改审批人'),
|
||||
('contract', '合同', '1', 1, 1, '默认或签,请在「审批配置」修改审批人');
|
||||
|
||||
-- ============================================================
|
||||
-- 菜单:审批中心 / 我的审批 / 我的发起 / 审批配置
|
||||
-- 注意:id 段 2063900001000001~9 已避开 dev 占用,prod 部署前请确认未冲突
|
||||
-- ============================================================
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES
|
||||
(2063900001000001, '审批中心', 0, 50, 'approval', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'edit', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000002, '我的审批', 2063900001000001, 1, 'pending', 'oa/approval/pending/index', NULL, 1, 0, 'C', '0', '0', 'oa:approval:list', 'checkbox', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000003, '我的发起', 2063900001000001, 2, 'submitted', 'oa/approval/submitted/index', NULL, 1, 0, 'C', '0', '0', 'oa:approval:submitted', 'documentation', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000004, '审批配置', 2063900001000001, 3, 'config', 'oa/approval/config/index', NULL, 1, 0, 'C', '0', '0', 'oa:approval:config', 'edit', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000005, '审批配置新增', 2063900001000004, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'oa:approval:configAdd', '#', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000006, '审批配置修改', 2063900001000004, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'oa:approval:configEdit', '#', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000007, '审批配置删除', 2063900001000004, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'oa:approval:configRemove', '#', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000008, '审批操作', 2063900001000002, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'oa:approval:act', '#', 'admin', NOW(), '通用审批菜单'),
|
||||
(2063900001000009, '审批撤回', 2063900001000003, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'oa:approval:withdraw', '#', 'admin', NOW(), '通用审批菜单');
|
||||
|
||||
-- 「说明」顶级菜单(用户使用说明 / 数据流向)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES
|
||||
(2063900002000001, '说明', 0, 60, 'docs', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'question', 'admin', NOW(), '说明文档'),
|
||||
(2063900002000002, '采购流', 2063900002000001, 1, 'purchase', 'oa/docs/purchase/index', NULL, 1, 0, 'C', '0', '0', 'oa:docs:purchase', 'documentation', 'admin', NOW(), '说明文档'),
|
||||
(2063900002000003, '合同流', 2063900002000001, 2, 'contract', 'oa/docs/contract/index', NULL, 1, 0, 'C', '0', '0', 'oa:docs:contract', 'documentation', 'admin', NOW(), '说明文档');
|
||||
7
sql/requirement_add_material.sql
Normal file
7
sql/requirement_add_material.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- 采购需求关联物料库存(支持多物料,CSV)
|
||||
ALTER TABLE oa_requirements
|
||||
ADD COLUMN material_ids VARCHAR(1000) DEFAULT NULL COMMENT '关联物料ID CSV -> sys_oa_warehouse.id' AFTER project_id;
|
||||
|
||||
-- 已应用过单物料版本(material_id)的环境,执行下面这句把旧字段迁移并删除
|
||||
-- UPDATE oa_requirements SET material_ids = CAST(material_id AS CHAR) WHERE material_id IS NOT NULL AND (material_ids IS NULL OR material_ids = '');
|
||||
-- ALTER TABLE oa_requirements DROP COLUMN material_id;
|
||||
Reference in New Issue
Block a user