Compare commits
32 Commits
365fc08b62
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3613b6d83a | |||
| 4faad94c79 | |||
| f6b5940a27 | |||
| db90e2a084 | |||
| f48818b14d | |||
| 602928dc0b | |||
| 455f3bbf09 | |||
| e0e31c765b | |||
| 335dc88a2a | |||
| 50527f68e0 | |||
| 8b78e82a80 | |||
| 8b3e016568 | |||
| c1c3fdba68 | |||
| 2d86713971 | |||
| f3d5556196 | |||
| f831f29b63 | |||
| 03b0e20301 | |||
| ba5796984c | |||
| a69c1f0cb2 | |||
| e728a98dcc | |||
| 54b820cc40 | |||
| f73a002f0f | |||
| 0400398361 | |||
| edca68136c | |||
| 1584d7e06d | |||
| 7c261c3028 | |||
| 5d0c056449 | |||
| fd72c18d48 | |||
| dfd912bf07 | |||
| 8b627c000f | |||
| 56f7a6abb9 | |||
| ebb57f4f26 |
@@ -6,6 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
|
||||
import com.ruoyi.hrm.service.IHrmAppropriationReqService;
|
||||
@@ -31,6 +32,7 @@ public class HrmAppropriationReqController extends BaseController {
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmAppropriationReqVo> list(HrmAppropriationReqBo bo, PageQuery pageQuery) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ public class HrmAppropriationReqController extends BaseController {
|
||||
|
||||
@GetMapping("/all")
|
||||
public R<List<HrmAppropriationReqVo>> all(HrmAppropriationReqBo bo) {
|
||||
bo.setCreateBy(String.valueOf(LoginHelper.getUserId()));
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,9 @@ public class HrmEmployeeController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/byUserId/{userId}")
|
||||
public R<HrmEmployeeVo> getByUserId(@PathVariable @NotNull Long userId) {
|
||||
HrmEmployeeBo bo = new HrmEmployeeBo();
|
||||
bo.setUserId(userId);
|
||||
List<HrmEmployeeVo> list = service.queryList(bo);
|
||||
if (list != null && !list.isEmpty()) {
|
||||
return R.ok(list.get(0));
|
||||
HrmEmployeeVo vo = service.queryByUserId(userId);
|
||||
if (vo != null) {
|
||||
return R.ok(vo);
|
||||
}
|
||||
return R.fail("未找到该用户对应的员工信息");
|
||||
}
|
||||
|
||||
@@ -66,7 +66,14 @@ public class HrmFlowCcController extends BaseController {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return toAjax(service.markRead(ccId, userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记抄送未读(新增)
|
||||
*/
|
||||
@PostMapping("/{ccId}/unread")
|
||||
public R<Void> unread(@PathVariable Long ccId) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return toAjax(service.markUnread(ccId, userId));
|
||||
}
|
||||
@GetMapping("/ping")
|
||||
public R<String> ping(@RequestParam @NotNull String x) {
|
||||
return R.ok(x);
|
||||
|
||||
@@ -56,6 +56,16 @@ public class HrmFlowTaskController extends BaseController {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页使用:按 bizType + bizId 查询审批概要、当前待办和历史记录
|
||||
*/
|
||||
@GetMapping("/detailByBiz")
|
||||
public R<?> detailByBiz(@RequestParam @NotNull String bizType,
|
||||
@RequestParam @NotNull Long bizId,
|
||||
@RequestParam(required = false) Long assigneeUserId) {
|
||||
return R.ok(service.queryDetailByBiz(bizType, bizId, assigneeUserId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
public R<HrmFlowTaskVo> getInfo(@PathVariable @NotNull Long taskId) {
|
||||
return R.ok(service.queryById(taskId));
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.hrm.domain.HrmLeaveReq;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmLeaveReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmLeaveReqVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmLeaveStatsVo;
|
||||
@@ -30,6 +30,7 @@ public class HrmLeaveReqController extends BaseController {
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmLeaveReqVo> list(HrmLeaveReqBo bo, PageQuery pageQuery) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ public class HrmLeaveReqController extends BaseController {
|
||||
|
||||
@GetMapping("/all")
|
||||
public R<List<HrmLeaveReqVo>> all(HrmLeaveReqBo bo) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.ruoyi.hrm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.HrmEmployee;
|
||||
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmLeaveReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmSealReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmLeaveReqVo;
|
||||
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.mapper.HrmAppropriationReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmEmployeeMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmLeaveReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmReimburseReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmSealReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmTravelReqMapper;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/hrm/my-apply")
|
||||
public class HrmMyApplyController extends BaseController {
|
||||
|
||||
private final HrmEmployeeMapper employeeMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final HrmLeaveReqMapper leaveReqMapper;
|
||||
private final HrmTravelReqMapper travelReqMapper;
|
||||
private final HrmSealReqMapper sealReqMapper;
|
||||
private final HrmReimburseReqMapper reimburseReqMapper;
|
||||
private final HrmAppropriationReqMapper appropriationReqMapper;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmMyApplyVo> list(String bizType, String status, String keyword, PageQuery pageQuery) {
|
||||
Long currentUserId = LoginHelper.getUserId();
|
||||
if (currentUserId == null) {
|
||||
return TableDataInfo.build();
|
||||
}
|
||||
HrmEmployee emp = employeeMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<HrmEmployee>()
|
||||
.eq(HrmEmployee::getUserId, currentUserId));
|
||||
if (emp == null) {
|
||||
return TableDataInfo.build();
|
||||
}
|
||||
SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId());
|
||||
String nickName = sysUser != null ? sysUser.getNickName() : null;
|
||||
|
||||
List<HrmMyApplyVo> all = new ArrayList<>();
|
||||
if (bizType == null || bizType.isEmpty() || "leave".equals(bizType)) {
|
||||
all.addAll(mapLeave(leaveReqMapper.selectVoWithProjectList(buildLeaveBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
if (bizType == null || bizType.isEmpty() || "travel".equals(bizType)) {
|
||||
all.addAll(mapTravel(travelReqMapper.selectVoWithProjectList(buildTravelBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
if (bizType == null || bizType.isEmpty() || "seal".equals(bizType)) {
|
||||
all.addAll(mapSeal(sealReqMapper.selectVoWithProjectList(buildSealBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
if (bizType == null || bizType.isEmpty() || "reimburse".equals(bizType)) {
|
||||
all.addAll(mapReimburse(reimburseReqMapper.selectVoWithProjectList(buildReimburseBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
if (bizType == null || bizType.isEmpty() || "appropriation".equals(bizType)) {
|
||||
all.addAll(mapAppropriation(appropriationReqMapper.selectVoWithProjectList(buildAppropriationBo(emp.getEmpId(), status)), nickName));
|
||||
}
|
||||
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
String lower = keyword.toLowerCase();
|
||||
all = all.stream().filter(v -> contains(v, lower)).collect(Collectors.toList());
|
||||
}
|
||||
all.sort(Comparator.comparing(HrmMyApplyVo::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())).reversed());
|
||||
|
||||
long start = (pageQuery.getPageNum() - 1L) * pageQuery.getPageSize();
|
||||
long end = Math.min(start + pageQuery.getPageSize(), all.size());
|
||||
Page<HrmMyApplyVo> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize(), all.size());
|
||||
page.setRecords(start >= all.size() ? new ArrayList<>() : all.subList((int) start, (int) end));
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
private boolean contains(HrmMyApplyVo v, String lower) {
|
||||
return Objects.toString(v.getTitle(), "").toLowerCase().contains(lower)
|
||||
|| Objects.toString(v.getRemark(), "").toLowerCase().contains(lower)
|
||||
|| Objects.toString(v.getNickName(), "").toLowerCase().contains(lower)
|
||||
|| Objects.toString(v.getEmpName(), "").toLowerCase().contains(lower)
|
||||
|| Objects.toString(v.getBizId(), "").contains(lower);
|
||||
}
|
||||
|
||||
private HrmLeaveReqBo buildLeaveBo(Long empId, String status) { HrmLeaveReqBo bo = new HrmLeaveReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
|
||||
private HrmTravelReqBo buildTravelBo(Long empId, String status) { HrmTravelReqBo bo = new HrmTravelReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
|
||||
private HrmSealReqBo buildSealBo(Long empId, String status) { HrmSealReqBo bo = new HrmSealReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; }
|
||||
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 HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime) {
|
||||
HrmMyApplyVo vo = new HrmMyApplyVo();
|
||||
vo.setBizType(bizType);
|
||||
vo.setBizId(bizId);
|
||||
vo.setEmpId(empId);
|
||||
vo.setNickName(nickName);
|
||||
vo.setTitle(title);
|
||||
vo.setStatus(status);
|
||||
vo.setCreateTime(createTime);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
|
||||
import com.ruoyi.hrm.service.IHrmReimburseReqService;
|
||||
@@ -31,6 +32,7 @@ public class HrmReimburseReqController extends BaseController {
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmReimburseReqVo> list(HrmReimburseReqBo bo, PageQuery pageQuery) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ public class HrmReimburseReqController extends BaseController {
|
||||
|
||||
@GetMapping("/all")
|
||||
public R<List<HrmReimburseReqVo>> all(HrmReimburseReqBo bo) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmSealReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmSealStampBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmSealReqVo;
|
||||
@@ -32,6 +33,7 @@ public class HrmSealReqController extends BaseController {
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmSealReqVo> list(HrmSealReqBo bo, PageQuery pageQuery) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@@ -40,6 +42,11 @@ public class HrmSealReqController extends BaseController {
|
||||
return R.ok(service.queryById(bizId));
|
||||
}
|
||||
|
||||
@GetMapping("/{bizId}/pdfPages")
|
||||
public R<Integer> pdfPages(@PathVariable @NotNull Long bizId) {
|
||||
return R.ok(service.queryPdfPageTotal(bizId));
|
||||
}
|
||||
|
||||
@Log(title = "用印申请", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<HrmSealReqVo> add(@Validated @RequestBody HrmSealReqBo bo) {
|
||||
|
||||
@@ -6,10 +6,12 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
|
||||
import com.ruoyi.hrm.service.IHrmTravelReqService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -28,9 +30,17 @@ import java.util.List;
|
||||
public class HrmTravelReqController extends BaseController {
|
||||
|
||||
private final IHrmTravelReqService service;
|
||||
private final IHrmTravelReqService hrmTravelReqService;
|
||||
|
||||
@Value("${fad.amap.webkey}")
|
||||
private String amapKey;
|
||||
|
||||
@Value("${fad.amap.securitykey}")
|
||||
private String amapSecurityKey;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmTravelReqVo> list(HrmTravelReqBo bo, PageQuery pageQuery) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@@ -56,9 +66,24 @@ public class HrmTravelReqController extends BaseController {
|
||||
public R<Void> remove(@PathVariable @NotEmpty Long[] bizIds) {
|
||||
return toAjax(service.deleteWithValidByIds(Arrays.asList(bizIds), true));
|
||||
}
|
||||
@PutMapping("/earlyEnd/{bizId}")
|
||||
public R<Void> earlyEnd(@PathVariable Long bizId) {
|
||||
return toAjax(hrmTravelReqService.earlyEnd(bizId));
|
||||
}
|
||||
|
||||
@GetMapping("/amapKey")
|
||||
public R<String> getAmapKey() {
|
||||
return R.ok(amapKey);
|
||||
}
|
||||
|
||||
@GetMapping("/amapSecurityKey")
|
||||
public R<String> getAmapSecurityKey() {
|
||||
return R.ok(amapSecurityKey);
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public R<List<HrmTravelReqVo>> all(HrmTravelReqBo bo) {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("hrm_flow_action")
|
||||
public class HrmFlowAction extends BaseEntity implements Serializable {
|
||||
public class HrmFlowAction extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用印申请
|
||||
@@ -15,7 +14,7 @@ import java.io.Serializable;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("hrm_seal_req")
|
||||
public class HrmSealReq extends BaseEntity implements Serializable {
|
||||
public class HrmSealReq extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -29,7 +28,7 @@ public class HrmSealReq extends BaseEntity implements Serializable {
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 用印类型(公章/合同章/财务章等) */
|
||||
/** 用印类型(对应印章文件名) */
|
||||
private String sealType;
|
||||
|
||||
/** 用途说明 */
|
||||
|
||||
@@ -32,6 +32,7 @@ public class HrmTravelReq extends BaseEntity {
|
||||
private String bankName;
|
||||
private String bankAccount;
|
||||
private String remark;
|
||||
private Date actualEndTime;
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class HrmSealReqBo extends BaseEntity {
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 用印类型 */
|
||||
/** 用印类型(对应印章文件名) */
|
||||
@NotBlank(message = "用印类型不能为空")
|
||||
private String sealType;
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.hrm.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class HrmFlowActionTimelineVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long actionId;
|
||||
private Long taskId;
|
||||
private Long instId;
|
||||
private Long actionUserId;
|
||||
private String actionUserName;
|
||||
private Long assigneeUserId;
|
||||
private String assigneeUserName;
|
||||
private String action;
|
||||
private String actionText;
|
||||
private String remark;
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
private Long nodeId;
|
||||
private String nodeName;
|
||||
private String taskStatus;
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -34,6 +34,8 @@ public class HrmFlowInstanceVo implements Serializable {
|
||||
private Date endTime;
|
||||
private BigDecimal hours;
|
||||
private String procStatus;
|
||||
private Date actualEndTime;
|
||||
private String statusName;
|
||||
|
||||
private String createBy;
|
||||
private Date createTime;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ruoyi.hrm.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class HrmFlowTaskDetailVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 当前业务对应的待办任务 */
|
||||
private HrmFlowTaskVo currentTask;
|
||||
|
||||
/** 当前业务对应的全部任务历史 */
|
||||
private List<HrmFlowTaskVo> taskHistory;
|
||||
|
||||
/** 当前流程实例状态 */
|
||||
private String flowStatus;
|
||||
|
||||
/** 当前节点ID */
|
||||
private Long currentNodeId;
|
||||
|
||||
/** 当前节点名称 */
|
||||
private String currentNodeName;
|
||||
|
||||
/** 审批是否通过 */
|
||||
private Boolean approved;
|
||||
|
||||
/** 流程动作历史(更细粒度) */
|
||||
private List<HrmFlowActionTimelineVo> actionTimeline;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.hrm.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class HrmMyApplyVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
private Long empId;
|
||||
private String empName;
|
||||
private Long userId;
|
||||
private String nickName;
|
||||
private String title;
|
||||
private String status;
|
||||
private Date createTime;
|
||||
private Date endTime;
|
||||
private Date actualEndTime;
|
||||
private String remark;
|
||||
}
|
||||
@@ -88,6 +88,9 @@ public class HrmSealReqVo implements Serializable {
|
||||
@Excel(name = "回执附件ID列表")
|
||||
private String receiptFileIds;
|
||||
|
||||
@Excel(name = "PDF页数")
|
||||
private Integer pdfPageTotal;
|
||||
|
||||
@Excel(name = "状态")
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ public class HrmTravelReqVo implements Serializable {
|
||||
private String bankAccount;
|
||||
@Excel(name = "备注")
|
||||
private String remark;
|
||||
|
||||
private Date actualEndTime;
|
||||
private String createBy;
|
||||
private Date createTime;
|
||||
private String updateBy;
|
||||
|
||||
@@ -15,6 +15,8 @@ public interface IHrmEmployeeService {
|
||||
|
||||
List<HrmEmployeeVo> queryList(HrmEmployeeBo bo);
|
||||
|
||||
HrmEmployeeVo queryByUserId(Long userId);
|
||||
|
||||
Boolean insertByBo(HrmEmployeeBo bo);
|
||||
|
||||
Boolean updateByBo(HrmEmployeeBo bo);
|
||||
|
||||
@@ -27,5 +27,9 @@ public interface IHrmFlowCcService {
|
||||
* 标记已读
|
||||
*/
|
||||
Boolean markRead(Long ccId, Long userId);
|
||||
/**
|
||||
* 标记未读
|
||||
*/
|
||||
Boolean markUnread(Long ccId, Long userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.ruoyi.hrm.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.hrm.domain.HrmFlowTask;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowTaskBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowTaskDetailVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -51,4 +51,9 @@ public interface IHrmFlowTaskService {
|
||||
* 根据业务类型 + 业务ID 查询当前待办任务(pending),用于详情页自动带出 currentTaskId
|
||||
*/
|
||||
HrmFlowTaskVo queryTodoByBiz(String bizType, Long bizId, Long assigneeUserId);
|
||||
|
||||
/**
|
||||
* 按业务查询详情:当前待办、状态和历史审批记录
|
||||
*/
|
||||
HrmFlowTaskDetailVo queryDetailByBiz(String bizType, Long bizId, Long assigneeUserId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.hrm.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmMyApplyVo;
|
||||
|
||||
public interface IHrmMyApplyService {
|
||||
TableDataInfo<HrmMyApplyVo> queryPageList(String bizType, String status, String keyword, PageQuery pageQuery);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ public interface IHrmSealReqService {
|
||||
|
||||
HrmSealReqVo queryById(Long bizId);
|
||||
|
||||
Integer queryPdfPageTotal(Long bizId);
|
||||
|
||||
TableDataInfo<HrmSealReqVo> queryPageList(HrmSealReqBo bo, PageQuery pageQuery);
|
||||
|
||||
List<HrmSealReqVo> queryList(HrmSealReqBo bo);
|
||||
|
||||
@@ -22,5 +22,7 @@ public interface IHrmTravelReqService {
|
||||
|
||||
Boolean updateByBo(HrmTravelReqBo bo);
|
||||
|
||||
int earlyEnd(Long bizId);
|
||||
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService {
|
||||
return baseMapper.selectVoById(empId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HrmEmployeeVo queryByUserId(Long userId) {
|
||||
return baseMapper.selectVoOne(Wrappers.<HrmEmployee>lambdaQuery().eq(HrmEmployee::getUserId, userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<HrmEmployeeVo> queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<HrmEmployee> lqw = buildQueryWrapper(bo);
|
||||
|
||||
@@ -99,6 +99,23 @@ public class HrmFlowCcServiceImpl implements IHrmFlowCcService {
|
||||
.eq(HrmFlowCc::getDelFlag, 0)
|
||||
) > 0;
|
||||
}
|
||||
/**
|
||||
* 标记未读
|
||||
*/
|
||||
@Override
|
||||
public Boolean markUnread(Long ccId, Long userId) {
|
||||
if (ccId == null || userId == null) {
|
||||
return false;
|
||||
}
|
||||
return baseMapper.update(
|
||||
null,
|
||||
Wrappers.<HrmFlowCc>lambdaUpdate()
|
||||
.set(HrmFlowCc::getReadFlag, 0)
|
||||
.eq(HrmFlowCc::getCcId, ccId)
|
||||
.eq(HrmFlowCc::getCcUserId, userId)
|
||||
.eq(HrmFlowCc::getDelFlag, 0)
|
||||
) > 0;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<HrmFlowCc> buildQueryWrapper(HrmFlowCcBo bo) {
|
||||
LambdaQueryWrapper<HrmFlowCc> lqw = Wrappers.lambdaQuery();
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.core.service.UserService;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.HrmFlowInstance;
|
||||
import com.ruoyi.hrm.domain.HrmFlowNode;
|
||||
import com.ruoyi.hrm.domain.HrmFlowTask;
|
||||
@@ -15,6 +16,7 @@ import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowInstanceBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowInstanceVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
|
||||
import com.ruoyi.hrm.mapper.*;
|
||||
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -37,6 +39,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
private final FlowAssigneeHelper assigneeHelper;
|
||||
private final UserService userService;
|
||||
private final HrmFlowCcMapper ccMapper;
|
||||
private final HrmTravelReqMapper travelReqMapper;
|
||||
|
||||
@Override
|
||||
public HrmFlowInstanceVo queryById(Long instId) {
|
||||
@@ -75,6 +78,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
task.setNodeId(0L);
|
||||
task.setAssigneeUserId(bo.getManualAssigneeUserId());
|
||||
task.setStatus("pending");
|
||||
task.setRemark("自选审批人一次性审批");
|
||||
// 关键:写入业务关联字段,便于审批中心联查业务数据
|
||||
task.setBizType(bo.getBizType());
|
||||
task.setBizId(bo.getBizId());
|
||||
@@ -121,6 +125,7 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
task.setNodeId(firstNode.getNodeId());
|
||||
task.setAssigneeUserId(assignees.get(0));
|
||||
task.setStatus("pending");
|
||||
task.setRemark(firstNode.getRemark());
|
||||
// 关键:写入业务关联字段,便于审批中心联查业务数据
|
||||
task.setBizType(bo.getBizType());
|
||||
task.setBizId(bo.getBizId());
|
||||
@@ -140,12 +145,36 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService {
|
||||
public TableDataInfo<HrmFlowInstanceVo> queryMyInstancePageList(HrmFlowInstanceBo bo, PageQuery pageQuery) {
|
||||
// “我的申请”= 当前登录用户发起的流程实例
|
||||
// 这里不信任前端传 startUserId,统一以登录态为准
|
||||
Long userId = com.ruoyi.common.helper.LoginHelper.getUserId();
|
||||
Long userId = LoginHelper.getUserId();
|
||||
LambdaQueryWrapper<HrmFlowInstance> lqw = buildQueryWrapper(bo);
|
||||
lqw.eq(userId != null, HrmFlowInstance::getStartUserId, userId);
|
||||
// 默认按发起时间倒序(如果表里没createTime字段,这里可改成instId倒序)
|
||||
lqw.orderByDesc(HrmFlowInstance::getInstId);
|
||||
Page<HrmFlowInstanceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
for (HrmFlowInstanceVo record : result.getRecords()) {
|
||||
|
||||
// 写入出差相关的时间,用于在页面中可以提前结束
|
||||
if (record.getBizType().equals("travel")){
|
||||
HrmTravelReqVo hrmTravelReqVo = travelReqMapper.selectVoById(record.getBizId());
|
||||
record.setActualEndTime(hrmTravelReqVo.getActualEndTime());
|
||||
}
|
||||
}
|
||||
for (HrmFlowInstanceVo vo : result.getRecords()) {
|
||||
if ("travel".equals(vo.getBizType())) {
|
||||
HrmTravelReqVo travel = travelReqMapper.selectVoById(vo.getBizId());
|
||||
if (travel != null) {
|
||||
vo.setActualEndTime(travel.getActualEndTime());
|
||||
// 只有流程已完成,才替换显示文字
|
||||
if ("complete".equals(vo.getStatus())) {
|
||||
if (travel.getActualEndTime() == null) {
|
||||
vo.setStatusName("出差中");
|
||||
} else {
|
||||
vo.setStatusName("已结束");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,15 @@ import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.*;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowTaskBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmSealStampBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowActionTimelineVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowTaskDetailVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmFlowTaskVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmEmployeeVo;
|
||||
import com.ruoyi.hrm.mapper.*;
|
||||
import com.ruoyi.hrm.service.IHrmFlowTaskService;
|
||||
import com.ruoyi.hrm.service.IHrmSealReqService;
|
||||
import com.ruoyi.hrm.service.IHrmFlowCcService;
|
||||
import com.ruoyi.hrm.service.IHrmEmployeeService;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowCcBo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -38,7 +42,6 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
private final FlowAssigneeHelper assigneeHelper;
|
||||
private final BizStatusSyncHelper bizStatusSyncHelper;
|
||||
|
||||
private final HrmFlowTaskMapper hrmFlowTaskMapper;
|
||||
// 注入五个业务Mapper
|
||||
private final HrmLeaveReqMapper leaveReqMapper;
|
||||
private final HrmTravelReqMapper travelReqMapper;
|
||||
@@ -47,6 +50,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
private final HrmAppropriationReqMapper appropriationReqMapper;
|
||||
private final ObjectMapper objectMapper; // Spring Boot 默认提供
|
||||
private final UserService userService;
|
||||
private final IHrmEmployeeService employeeService;
|
||||
|
||||
@Override
|
||||
public HrmFlowTaskVo queryById(Long taskId) {
|
||||
@@ -90,19 +94,39 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
if (bizIds.isEmpty()) return;
|
||||
switch (bizType) {
|
||||
case "leave":
|
||||
leaveReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("leave_" + d.getBizId(), d));
|
||||
leaveReqMapper.selectBatchIds(bizIds).forEach(d -> {
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
|
||||
fillEmpName(dataMap, d.getEmpId());
|
||||
bizDataMap.put("leave_" + d.getBizId(), dataMap);
|
||||
});
|
||||
break;
|
||||
case "travel":
|
||||
travelReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("travel_" + d.getBizId(), d));
|
||||
travelReqMapper.selectBatchIds(bizIds).forEach(d -> {
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
|
||||
fillEmpName(dataMap, d.getEmpId());
|
||||
bizDataMap.put("travel_" + d.getBizId(), dataMap);
|
||||
});
|
||||
break;
|
||||
case "seal":
|
||||
sealReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("seal_" + d.getBizId(), d));
|
||||
sealReqMapper.selectBatchIds(bizIds).forEach(d -> {
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
|
||||
fillEmpName(dataMap, d.getEmpId());
|
||||
bizDataMap.put("seal_" + d.getBizId(), dataMap);
|
||||
});
|
||||
break;
|
||||
case "reimburse":
|
||||
reimburseReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("reimburse_" + d.getBizId(), d));
|
||||
reimburseReqMapper.selectBatchIds(bizIds).forEach(d -> {
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
|
||||
fillEmpName(dataMap, d.getEmpId());
|
||||
bizDataMap.put("reimburse_" + d.getBizId(), dataMap);
|
||||
});
|
||||
break;
|
||||
case "appropriation":
|
||||
appropriationReqMapper.selectBatchIds(bizIds).forEach(d -> bizDataMap.put("appropriation_" + d.getBizId(), d));
|
||||
appropriationReqMapper.selectBatchIds(bizIds).forEach(d -> {
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(d, Map.class);
|
||||
fillEmpName(dataMap, d.getEmpId());
|
||||
bizDataMap.put("appropriation_" + d.getBizId(), dataMap);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -112,13 +136,20 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
String key = task.getBizType() + "_" + task.getBizId();
|
||||
Object data = bizDataMap.get(key);
|
||||
if (data != null) {
|
||||
// 将实体对象转换为 Map<String, Object>,方便前端使用
|
||||
Map<String, Object> dataMap = objectMapper.convertValue(data, Map.class);
|
||||
task.setBizData(dataMap);
|
||||
task.setBizData((Map<String, Object>) data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fillEmpName(Map<String, Object> dataMap, Long empId) {
|
||||
if (empId != null) {
|
||||
HrmEmployeeVo emp = employeeService.queryById(empId);
|
||||
if (emp != null && emp.getEmpName() != null) {
|
||||
dataMap.put("empName", emp.getEmpName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -155,12 +186,13 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
if (inst == null) {
|
||||
return false;
|
||||
}
|
||||
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
|
||||
// 无模板一次性审批(tplId=0 或 nodeId=0):直接结束流程
|
||||
if (inst.getTplId() != null && inst.getTplId() == 0L) {
|
||||
// 记录动作
|
||||
saveAction(taskId, inst.getInstId(), "approve", remark, actionUserId,task.getBizType(), task.getBizId());
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "approve", remark, operatorUserId);
|
||||
if (stampBo != null) {
|
||||
saveAction(taskId, inst.getInstId(), "stamp", "盖章", actionUserId,task.getBizType(), task.getBizId());
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "stamp", "盖章", operatorUserId);
|
||||
}
|
||||
task.setStatus("approved");
|
||||
baseMapper.updateById(task);
|
||||
@@ -172,7 +204,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
sealReqService.updateStatus(inst.getBizId(), "approved");
|
||||
if (stampBo != null) {
|
||||
// 盖章动作也写入流转历史
|
||||
saveAction(taskId, inst.getInstId(), "stamp", "盖章", actionUserId,task.getBizType(), task.getBizId());
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "stamp", "盖章", operatorUserId);
|
||||
sealReqService.stampWithJava(inst.getBizId(), stampBo);
|
||||
}
|
||||
}
|
||||
@@ -185,7 +217,7 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
return false;
|
||||
}
|
||||
// 记录动作
|
||||
saveAction(taskId, inst.getInstId(), "approve", remark, actionUserId,task.getBizType(),task.getBizId());
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "approve", remark, operatorUserId);
|
||||
// 完成当前任务
|
||||
task.setStatus("approved");
|
||||
baseMapper.updateById(task);
|
||||
@@ -269,7 +301,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
if (inst == null) {
|
||||
return false;
|
||||
}
|
||||
saveAction(taskId, inst.getInstId(), "reject", remark, actionUserId,task.getBizType(),task.getBizId());
|
||||
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "reject", remark, operatorUserId);
|
||||
task.setStatus("rejected");
|
||||
baseMapper.updateById(task);
|
||||
inst.setStatus("rejected");
|
||||
@@ -293,7 +326,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
if (inst == null) {
|
||||
return false;
|
||||
}
|
||||
saveAction(taskId, inst.getInstId(), "withdraw", remark, actionUserId, task.getBizType(), task.getBizId());
|
||||
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), task.getAssigneeUserId(), "withdraw", remark, operatorUserId);
|
||||
task.setStatus("withdraw");
|
||||
baseMapper.updateById(task);
|
||||
// 无模板一次性审批:撤回后业务回到 pending,并重新生成一个待办(仍然只允许一次审批)
|
||||
@@ -330,13 +364,13 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveAction(Long taskId, Long instId, String action, String remark, Long userId, String bizType, Long bizId) {
|
||||
private void saveAction(Long taskId, Long instId, String bizType, Long bizId, Long assigneeUserId, String action, String remark, Long actionUserId) {
|
||||
HrmFlowAction log = new HrmFlowAction();
|
||||
log.setTaskId(taskId);
|
||||
log.setInstId(instId);
|
||||
log.setAction(action);
|
||||
log.setRemark(remark);
|
||||
log.setActionUserId(userId);
|
||||
log.setActionUserId(actionUserId);
|
||||
log.setCreateTime(new Date());
|
||||
log.setBizType(bizType);
|
||||
log.setBizId(bizId);
|
||||
@@ -358,7 +392,8 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
return false;
|
||||
}
|
||||
// 记录动作
|
||||
saveAction(taskId, inst.getInstId(), "transfer", remark, actionUserId, task.getBizType(), task.getBizId());
|
||||
Long operatorUserId = actionUserId != null ? actionUserId : LoginHelper.getUserId();
|
||||
saveAction(taskId, inst.getInstId(), task.getBizType(), task.getBizId(), newAssigneeUserId, "transfer", remark, operatorUserId);
|
||||
|
||||
// 更新办理人
|
||||
HrmFlowTask u = new HrmFlowTask();
|
||||
@@ -369,7 +404,6 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
|
||||
@Override
|
||||
public HrmFlowTaskVo queryTodoByBiz(String bizType, Long bizId, Long assigneeUserId) {
|
||||
// 只取"待办 pending"的一条(理论上同一 biz 同一时刻最多一条待办)
|
||||
LambdaQueryWrapper<HrmFlowTask> lqw = Wrappers.<HrmFlowTask>lambdaQuery()
|
||||
.eq(bizType != null, HrmFlowTask::getBizType, bizType)
|
||||
.eq(bizId != null, HrmFlowTask::getBizId, bizId)
|
||||
@@ -378,12 +412,118 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
.orderByDesc(HrmFlowTask::getTaskId)
|
||||
.last("limit 1");
|
||||
HrmFlowTaskVo hrmFlowTaskVo = baseMapper.selectVoOne(lqw);
|
||||
if (hrmFlowTaskVo != null) {
|
||||
if (hrmFlowTaskVo != null && hrmFlowTaskVo.getAssigneeUserId() != null) {
|
||||
hrmFlowTaskVo.setAssigneeNickName(userService.selectNickNameById(hrmFlowTaskVo.getAssigneeUserId()));
|
||||
}
|
||||
return hrmFlowTaskVo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HrmFlowTaskDetailVo queryDetailByBiz(String bizType, Long bizId, Long assigneeUserId) {
|
||||
HrmFlowTaskDetailVo result = new HrmFlowTaskDetailVo();
|
||||
HrmFlowTaskVo currentTask = queryTodoByBiz(bizType, bizId, assigneeUserId);
|
||||
result.setCurrentTask(currentTask);
|
||||
|
||||
LambdaQueryWrapper<HrmFlowTask> historyQ = Wrappers.<HrmFlowTask>lambdaQuery()
|
||||
.eq(bizType != null, HrmFlowTask::getBizType, bizType)
|
||||
.eq(bizId != null, HrmFlowTask::getBizId, bizId)
|
||||
.orderByAsc(HrmFlowTask::getCreateTime);
|
||||
List<HrmFlowTaskVo> histories = baseMapper.selectVoList(historyQ);
|
||||
if (histories != null) {
|
||||
histories.forEach(task -> {
|
||||
if (task.getAssigneeUserId() != null) {
|
||||
task.setAssigneeNickName(userService.selectNickNameById(task.getAssigneeUserId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
result.setTaskHistory(histories == null ? Collections.emptyList() : histories);
|
||||
|
||||
HrmFlowInstance inst = instanceMapper.selectOne(Wrappers.<HrmFlowInstance>lambdaQuery()
|
||||
.eq(bizType != null, HrmFlowInstance::getBizType, bizType)
|
||||
.eq(bizId != null, HrmFlowInstance::getBizId, bizId)
|
||||
.orderByDesc(HrmFlowInstance::getInstId)
|
||||
.last("limit 1"));
|
||||
if (inst != null) {
|
||||
result.setFlowStatus(inst.getStatus());
|
||||
result.setCurrentNodeId(inst.getCurrentNodeId());
|
||||
HrmFlowNode node = inst.getCurrentNodeId() == null ? null : nodeMapper.selectById(inst.getCurrentNodeId());
|
||||
if (node != null) {
|
||||
result.setCurrentNodeName(node.getRemark());
|
||||
}
|
||||
result.setApproved(Boolean.valueOf("approved".equalsIgnoreCase(inst.getStatus())));
|
||||
}
|
||||
|
||||
result.setActionTimeline(buildActionTimeline(bizType, bizId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<HrmFlowActionTimelineVo> buildActionTimeline(String bizType, Long bizId) {
|
||||
LambdaQueryWrapper<HrmFlowAction> actionQ = Wrappers.<HrmFlowAction>lambdaQuery()
|
||||
.eq(bizType != null, HrmFlowAction::getBizType, bizType)
|
||||
.eq(bizId != null, HrmFlowAction::getBizId, bizId)
|
||||
.orderByAsc(HrmFlowAction::getCreateTime);
|
||||
List<HrmFlowAction> actions = actionMapper.selectList(actionQ);
|
||||
if (actions == null || actions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<Long, HrmFlowTask> taskMap = new HashMap<>();
|
||||
List<Long> taskIds = actions.stream().map(HrmFlowAction::getTaskId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (!taskIds.isEmpty()) {
|
||||
baseMapper.selectBatchIds(taskIds).forEach(task -> taskMap.put(task.getTaskId(), task));
|
||||
}
|
||||
|
||||
List<HrmFlowActionTimelineVo> timelines = new ArrayList<HrmFlowActionTimelineVo>();
|
||||
for (HrmFlowAction action : actions) {
|
||||
HrmFlowActionTimelineVo vo = new HrmFlowActionTimelineVo();
|
||||
vo.setActionId(action.getActionId());
|
||||
vo.setTaskId(action.getTaskId());
|
||||
vo.setInstId(action.getInstId());
|
||||
vo.setActionUserId(action.getActionUserId());
|
||||
vo.setActionUserName(action.getActionUserId() == null ? null : userService.selectNickNameById(action.getActionUserId()));
|
||||
vo.setAction(action.getAction());
|
||||
vo.setActionText(actionText(action.getAction()));
|
||||
vo.setRemark(action.getRemark());
|
||||
vo.setBizType(action.getBizType());
|
||||
vo.setBizId(action.getBizId());
|
||||
HrmFlowTask task = taskMap.get(action.getTaskId());
|
||||
if (task != null) {
|
||||
vo.setNodeId(task.getNodeId());
|
||||
vo.setTaskStatus(task.getStatus());
|
||||
HrmFlowNode node = task.getNodeId() == null ? null : nodeMapper.selectById(task.getNodeId());
|
||||
if (node != null) {
|
||||
vo.setNodeName(node.getRemark());
|
||||
}
|
||||
}
|
||||
vo.setCreateTime(action.getCreateTime());
|
||||
timelines.add(vo);
|
||||
}
|
||||
return timelines;
|
||||
}
|
||||
|
||||
private String actionText(String action) {
|
||||
if (action == null) {
|
||||
return "-";
|
||||
}
|
||||
String lower = action.toLowerCase();
|
||||
if ("approve".equals(lower)) {
|
||||
return "通过";
|
||||
}
|
||||
if ("reject".equals(lower)) {
|
||||
return "驳回";
|
||||
}
|
||||
if ("withdraw".equals(lower)) {
|
||||
return "撤回";
|
||||
}
|
||||
if ("transfer".equals(lower)) {
|
||||
return "转办";
|
||||
}
|
||||
if ("stamp".equals(lower)) {
|
||||
return "盖章";
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<HrmFlowTask> buildQueryWrapper(HrmFlowTaskBo bo) {
|
||||
LambdaQueryWrapper<HrmFlowTask> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(bo.getTaskId() != null, HrmFlowTask::getTaskId, bo.getTaskId());
|
||||
@@ -416,6 +556,11 @@ public class HrmFlowTaskServiceImpl implements IHrmFlowTaskService {
|
||||
|
||||
if (result.getRecords() != null && !result.getRecords().isEmpty()) {
|
||||
fillBizData(result.getRecords());
|
||||
result.getRecords().forEach(task -> {
|
||||
if (task.getAssigneeUserId() != null) {
|
||||
task.setAssigneeNickName(userService.selectNickNameById(task.getAssigneeUserId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return TableDataInfo.build(result);
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.ruoyi.hrm.service.IHrmSealReqService;
|
||||
import com.ruoyi.oss.core.OssClient;
|
||||
import com.ruoyi.oss.entity.UploadResult;
|
||||
import com.ruoyi.oss.factory.OssFactory;
|
||||
import com.ruoyi.system.domain.vo.SysOssVo;
|
||||
import com.ruoyi.system.mapper.SysOssMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -28,7 +30,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -48,10 +49,59 @@ public class HrmSealReqServiceImpl implements IHrmSealReqService {
|
||||
private final HrmSealReqMapper baseMapper;
|
||||
private final StampProperties stampProperties;
|
||||
private final IHrmFlowInstanceService flowInstanceService;
|
||||
private final SysOssMapper sysOssMapper;
|
||||
|
||||
@Override
|
||||
public HrmSealReqVo queryById(Long bizId) {
|
||||
return baseMapper.selectVoWithProjectById(bizId);
|
||||
HrmSealReqVo vo = baseMapper.selectVoWithProjectById(bizId);
|
||||
if (vo != null) {
|
||||
vo.setPdfPageTotal(queryPdfPageTotal(bizId));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer queryPdfPageTotal(Long bizId) {
|
||||
HrmSealReqVo vo = baseMapper.selectVoWithProjectById(bizId);
|
||||
String applyFileIds = vo != null ? vo.getApplyFileIds() : null;
|
||||
if (applyFileIds == null || applyFileIds.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
String firstFileId = applyFileIds.split(",")[0].trim();
|
||||
if (firstFileId.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
String fileUrl = resolveSealPdfUrl(firstFileId);
|
||||
if (fileUrl == null || fileUrl.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try (InputStream inputStream = getObject(fileUrl); PDDocument document = PDDocument.load(inputStream)) {
|
||||
return document.getNumberOfPages();
|
||||
} catch (Exception e) {
|
||||
log.warn("查询用印PDF页数失败 bizId={}, fileId={}, fileUrl={}", bizId, firstFileId, fileUrl, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveObjectKey(String fileRef) {
|
||||
if (fileRef == null) {
|
||||
return null;
|
||||
}
|
||||
String ref = fileRef.trim();
|
||||
if (ref.isEmpty()) {
|
||||
return ref;
|
||||
}
|
||||
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
||||
int idx = ref.indexOf("/files/");
|
||||
if (idx >= 0) {
|
||||
return ref.substring(idx + "/files/".length());
|
||||
}
|
||||
idx = ref.indexOf("/files%2F");
|
||||
if (idx >= 0) {
|
||||
return ref.substring(idx + "/files%2F".length());
|
||||
}
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,6 +319,17 @@ public class HrmSealReqServiceImpl implements IHrmSealReqService {
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSealPdfUrl(String ossIdText) {
|
||||
try {
|
||||
Long ossId = Long.valueOf(ossIdText);
|
||||
SysOssVo sysOss = sysOssMapper.selectVoById(ossId);
|
||||
return sysOss != null ? sysOss.getUrl() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("解析用印PDF文件地址失败 ossIdText={}", ossIdText, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getObject(String url) {
|
||||
OssClient storage = OssFactory.instance();
|
||||
return storage.getObjectContent(url);
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@@ -128,4 +129,36 @@ public class HrmTravelReqServiceImpl implements IHrmTravelReqService {
|
||||
private String defaultStatus(String status) {
|
||||
return status == null ? "draft" : status;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int earlyEnd(Long bizId) {
|
||||
HrmTravelReq travelReq = baseMapper.selectById(bizId);
|
||||
if (travelReq == null) {
|
||||
throw new RuntimeException("出差申请不存在");
|
||||
}
|
||||
|
||||
String status = travelReq.getStatus();
|
||||
if (!"approved".equals(status) && !"in_progress".equals(status)) {
|
||||
throw new RuntimeException("当前状态不能提前结束,只有已通过或进行中的出差才能提前结束");
|
||||
}
|
||||
|
||||
// 3. 检查是否已经提前结束过
|
||||
if (travelReq.getActualEndTime() != null) {
|
||||
throw new RuntimeException("该出差已经提前结束过了");
|
||||
}
|
||||
|
||||
// 4. 更新实际结束时间为当前时间
|
||||
travelReq.setActualEndTime(new Date());
|
||||
// 5. 可选:更新状态为已完成
|
||||
travelReq.setStatus("completed");
|
||||
|
||||
// 6. 执行更新
|
||||
int result = baseMapper.updateById(travelReq);
|
||||
if (result <= 0) {
|
||||
throw new RuntimeException("提前结束失败");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,13 @@
|
||||
<sql id="selectVo">
|
||||
SELECT
|
||||
e.emp_id, e.user_id, e.emp_no, e.emp_name, e.gender, e.mobile, e.email, e.id_no,
|
||||
e.hire_date, e.employment_type, e.status, e.dept_id, e.post_id, e.remark,
|
||||
e.hire_date, e.employment_type, e.status, su.dept_id, e.post_id, e.remark,
|
||||
e.create_by, e.create_time, e.update_by, e.update_time,
|
||||
d.dept_name,
|
||||
p.post_name
|
||||
FROM hrm_employee e
|
||||
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id AND d.del_flag = '0'
|
||||
left join sys_user su on su.user_id = e.user_id
|
||||
LEFT JOIN sys_dept d ON su.dept_id = d.dept_id AND d.del_flag = 0
|
||||
LEFT JOIN sys_post p ON e.post_id = p.post_id
|
||||
WHERE e.del_flag = 0
|
||||
</sql>
|
||||
|
||||
@@ -170,6 +170,8 @@ security:
|
||||
- /oa/attendanceRecord/**
|
||||
- /oa/oaWarehouse/**
|
||||
- /oa/oaWarehouseMaster/**
|
||||
# 高德逆地理(经纬度转城市等,供前端/H5 调用)
|
||||
- /oa/amap/**
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
@@ -318,7 +320,11 @@ flowable:
|
||||
# 关闭历史任务定时任务job
|
||||
async-history-executor-activate: false
|
||||
|
||||
|
||||
fad:
|
||||
amap:
|
||||
# 留作后端接口调用(服务 API)的 Key
|
||||
key: 978ae5bc551f57d172d3e397af5a6f67
|
||||
# 新增的前端 Web 端使用的 Key 和安全密钥
|
||||
webKey: 34bf20d1db5b183558b9bb85d6eed783
|
||||
securityKey: 6f9171724396deb5f8c42ef256b3cbc5
|
||||
|
||||
|
||||
@@ -2,13 +2,20 @@ package com.ruoyi.framework.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
private static final int CONNECT_TIMEOUT_MS = 5_000;
|
||||
private static final int READ_TIMEOUT_MS = 15_000;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
factory.setReadTimeout(READ_TIMEOUT_MS);
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,12 @@
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>fad-hrm</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ruoyi.oa.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
|
||||
import com.ruoyi.oa.service.IOaAmapGeocodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 高德地图:经纬度逆地理编码(城市名等)
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/oa/amap")
|
||||
public class OaAmapController extends BaseController {
|
||||
|
||||
private final IOaAmapGeocodeService oaAmapGeocodeService;
|
||||
|
||||
/**
|
||||
* 根据经纬度获取城市名称(高德逆地理编码)
|
||||
*/
|
||||
@GetMapping("/city")
|
||||
public R<AmapCityNameVo> cityByLocation(
|
||||
@NotNull(message = "经度不能为空")
|
||||
@DecimalMin(value = "-180.0", message = "经度范围无效")
|
||||
@DecimalMax(value = "180.0", message = "经度范围无效")
|
||||
@RequestParam Double longitude,
|
||||
@NotNull(message = "纬度不能为空")
|
||||
@DecimalMin(value = "-90.0", message = "纬度范围无效")
|
||||
@DecimalMax(value = "90.0", message = "纬度范围无效")
|
||||
@RequestParam Double latitude
|
||||
) {
|
||||
AmapCityNameVo vo = oaAmapGeocodeService.reverseGeocodeCity(longitude, latitude);
|
||||
return R.ok(vo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.oa.controller;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||
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.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.oa.domain.bo.OaCityBo;
|
||||
import com.ruoyi.oa.domain.vo.OaCityVo;
|
||||
import com.ruoyi.oa.service.IOaCityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/oa/city")
|
||||
@Validated
|
||||
public class OaCityController extends BaseController {
|
||||
|
||||
private final IOaCityService cityService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<OaCityVo> list(OaCityBo bo, PageQuery pageQuery) {
|
||||
return cityService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@GetMapping("/{cityId}")
|
||||
public R<OaCityVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long cityId) {
|
||||
return R.ok(cityService.queryById(cityId));
|
||||
}
|
||||
|
||||
@Log(title = "城市管理", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody OaCityBo bo) {
|
||||
return toAjax(cityService.insertByBo(bo));
|
||||
}
|
||||
|
||||
@Log(title = "城市管理", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody OaCityBo bo) {
|
||||
return toAjax(cityService.updateByBo(bo));
|
||||
}
|
||||
|
||||
@Log(title = "城市管理", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{cityIds}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] cityIds) {
|
||||
return toAjax(cityService.deleteWithValidByIds(Arrays.asList(cityIds), true));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
|
||||
@@ -51,6 +52,15 @@ public class OaProjectReportController extends BaseController {
|
||||
return iOaProjectReportService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@GetMapping("/travel-compare")
|
||||
public TableDataInfo<OaProjectTravelCompareVo> travelCompare(@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate start,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate end,
|
||||
@RequestParam(required = false) String nickName,
|
||||
@RequestParam(required = false) String workPlace,
|
||||
PageQuery pageQuery) {
|
||||
return iOaProjectReportService.getTravelCompareList(start, end, nickName, workPlace, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询项目报工列表
|
||||
*/
|
||||
|
||||
49
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaCity.java
Normal file
49
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaCity.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.oa.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 城市管理对象 oa_city
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("oa_city")
|
||||
public class OaCity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "city_id")
|
||||
private Long cityId;
|
||||
|
||||
/** 国家 */
|
||||
private String countryName;
|
||||
|
||||
/** 城市 */
|
||||
private String cityName;
|
||||
|
||||
/**
|
||||
* 城市编码
|
||||
*/
|
||||
private String cityCode;
|
||||
|
||||
/**
|
||||
* 所属省份
|
||||
*/
|
||||
private String provinceName;
|
||||
|
||||
|
||||
/** 状态 1正常 0禁用 */
|
||||
private Long status;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
}
|
||||
29
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaCityBo.java
Normal file
29
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaCityBo.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.oa.domain.bo;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 城市管理业务对象 oa_city
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class OaCityBo extends BaseEntity {
|
||||
|
||||
private Long cityId;
|
||||
|
||||
@NotBlank(message = "国家不能为空")
|
||||
private String countryName;
|
||||
|
||||
@NotBlank(message = "城市不能为空")
|
||||
private String cityName;
|
||||
|
||||
private Long status;
|
||||
private String cityCode;
|
||||
private String provinceName;
|
||||
private String remark;
|
||||
}
|
||||
@@ -207,6 +207,11 @@ public class SysOaProjectBo extends BaseEntity {
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 为 true 时在列表结果中附带各项目进度步骤统计(综合看板等,不参与 SQL 条件)
|
||||
*/
|
||||
private Boolean scheduleStats;
|
||||
|
||||
//是否置顶
|
||||
private Integer isTop;
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.oa.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 项目维度进度步骤汇总(综合看板左侧列表等)
|
||||
*/
|
||||
@Data
|
||||
public class ProjectScheduleStepStatsDto {
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Long totalNodes;
|
||||
|
||||
private Long completedNodes;
|
||||
|
||||
private Long pendingAcceptNodes;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 高德逆地理编码:城市名称(及可选行政区信息,便于展示/调试)
|
||||
*/
|
||||
@Data
|
||||
public class AmapCityNameVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 城市名(直辖市、省直辖等场景可能为省名或区名,与高德 addressComponent 一致) */
|
||||
private String cityName;
|
||||
|
||||
/** 省 */
|
||||
private String province;
|
||||
|
||||
/** 区/县 */
|
||||
private String district;
|
||||
}
|
||||
27
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaCityVo.java
Normal file
27
ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaCityVo.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 城市管理视图对象 oa_city
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class OaCityVo extends BaseEntity {
|
||||
|
||||
private Long cityId;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String cityName;
|
||||
|
||||
private Long status;
|
||||
|
||||
private String remark;
|
||||
/** 城市编码 */
|
||||
private String cityCode;
|
||||
/** 所属省份 */
|
||||
private String provinceName;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.oa.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 报工出差比对结果
|
||||
*/
|
||||
@Data
|
||||
public class OaProjectTravelCompareVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 用户昵称 */
|
||||
private String nickName;
|
||||
|
||||
/** 比对日期(一天) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate compareDate;
|
||||
|
||||
/** 是否出差 */
|
||||
private Long isTrip;
|
||||
|
||||
/** 是否出差(中文) */
|
||||
private String trip;
|
||||
|
||||
/** 报工地点 */
|
||||
private String workPlace;
|
||||
|
||||
/** 实际出差地点 */
|
||||
private String travelPlace;
|
||||
|
||||
/** 机器比对结果 */
|
||||
private String compareResult;
|
||||
|
||||
/** 是否通过 */
|
||||
private Boolean pass;
|
||||
|
||||
/** 出差记录ID */
|
||||
private Long travelBizId;
|
||||
|
||||
/** 报工ID */
|
||||
private Long reportId;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.convert.ExcelDictConvert;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
|
||||
@@ -19,7 +20,7 @@ import java.util.Date;
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class OaRequirementsVo {
|
||||
public class OaRequirementsVo extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.Date;
|
||||
|
||||
import com.alibaba.excel.annotation.format.DateTimeFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.alibaba.excel.annotation.ExcelIgnore;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
@@ -14,7 +15,6 @@ import com.ruoyi.system.domain.SysOss;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -284,4 +284,27 @@ public class SysOaProjectVo {
|
||||
private Long processCardCount;
|
||||
|
||||
private Long deliveryOrderCount;
|
||||
|
||||
/** 列表 SQL 聚合:任务/进度总览 */
|
||||
@ExcelIgnore
|
||||
private Long taskFinishCount;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long taskTotalCount;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long scheduleTotalCount;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long scheduleFinishCount;
|
||||
|
||||
/** 进度步骤汇总(列表请求 scheduleStats=true 时由服务层填充) */
|
||||
@ExcelIgnore
|
||||
private Long scheduleStepTotal;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long scheduleStepCompleted;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long scheduleStepPendingAccept;
|
||||
}
|
||||
|
||||
11
ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaCityMapper.java
Normal file
11
ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaCityMapper.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.oa.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.domain.OaCity;
|
||||
import com.ruoyi.oa.domain.vo.OaCityVo;
|
||||
|
||||
/**
|
||||
* 城市管理 Mapper
|
||||
*/
|
||||
public interface OaCityMapper extends BaseMapperPlus<OaCityMapper, OaCity, OaCityVo> {
|
||||
}
|
||||
@@ -52,5 +52,11 @@ public interface OaProjectReportMapper extends BaseMapperPlus<OaProjectReportMap
|
||||
|
||||
List<OaProjectReportVo> getClearList(@Param("start") LocalDate start, @Param("end") LocalDate end);
|
||||
|
||||
Page<OaProjectReportVo> selectTravelCompareReportPage(@Param("page") Page<OaProjectReportVo> page,
|
||||
@Param("start") LocalDate start,
|
||||
@Param("end") LocalDate end,
|
||||
@Param("nickName") String nickName,
|
||||
@Param("workPlace") String workPlace);
|
||||
|
||||
List<OaProjectReportVo> getSummaryData(@Param("start") LocalDate start, @Param("end") LocalDate end);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.oa.mapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.oa.domain.OaProjectScheduleStep;
|
||||
import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo;
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -44,4 +45,9 @@ public interface OaProjectScheduleStepMapper extends BaseMapperPlus<OaProjectSch
|
||||
int deleteByScheduleIds(@Param("scheduleIds") Collection<Long> scheduleIds);
|
||||
|
||||
Page<OaProjectScheduleStepVo> selectVoPageNew(Page<Object> build,@Param(Constants.WRAPPER) QueryWrapper<OaProjectScheduleStep> lqw);
|
||||
|
||||
/**
|
||||
* 按项目汇总进度步骤:总数、已完成(2)、待验收(1)
|
||||
*/
|
||||
List<ProjectScheduleStepStatsDto> selectStepStatsGroupByProjectId(@Param("projectIds") Collection<Long> projectIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.oa.service;
|
||||
|
||||
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
|
||||
|
||||
/**
|
||||
* 高德地图逆地理编码(经纬度 → 城市等)
|
||||
*/
|
||||
public interface IOaAmapGeocodeService {
|
||||
|
||||
/**
|
||||
* 根据经纬度解析城市名称等信息
|
||||
*
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
* @return 非 null;解析失败时 cityName 等可能为空
|
||||
*/
|
||||
AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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.OaCityBo;
|
||||
import com.ruoyi.oa.domain.vo.OaCityVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface IOaCityService {
|
||||
|
||||
OaCityVo queryById(Long cityId);
|
||||
|
||||
TableDataInfo<OaCityVo> queryPageList(OaCityBo bo, PageQuery pageQuery);
|
||||
|
||||
List<OaCityVo> queryList(OaCityBo bo);
|
||||
|
||||
Boolean insertByBo(OaCityBo bo);
|
||||
|
||||
Boolean updateByBo(OaCityBo bo);
|
||||
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -1,78 +1,48 @@
|
||||
package com.ruoyi.oa.service;
|
||||
|
||||
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
|
||||
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目报工Service接口
|
||||
*
|
||||
* @author hdka
|
||||
* @date 2025-06-16
|
||||
*/
|
||||
public interface IOaProjectReportService {
|
||||
|
||||
/**
|
||||
* 查询项目报工
|
||||
*/
|
||||
OaProjectReportVo queryById(Long reportId);
|
||||
|
||||
/**
|
||||
* 查询项目报工列表
|
||||
*/
|
||||
TableDataInfo<OaProjectReportVo> queryPageList(OaProjectReportBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询项目报工列表
|
||||
*/
|
||||
List<OaProjectReportVo> queryList(OaProjectReportBo bo);
|
||||
|
||||
/**
|
||||
* 新增项目报工
|
||||
*/
|
||||
Boolean insertByBo(OaProjectReportBo bo);
|
||||
|
||||
/**
|
||||
* 修改项目报工
|
||||
*/
|
||||
Boolean updateByBo(OaProjectReportBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除项目报工信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 报工数据看板
|
||||
* @return
|
||||
*/
|
||||
ProjectReportCardVo getCardData();
|
||||
|
||||
public List<ProjectReportTrendVo> getTrend(LocalDate start, LocalDate end);
|
||||
|
||||
public List<ProjectReportPieVo> getDistribution(LocalDate start, LocalDate end);
|
||||
|
||||
|
||||
List<OaProjectReportVo> getRankData(LocalDate start, LocalDate end);
|
||||
|
||||
List<OaProjectReportVo> getProjects(LocalDate start, LocalDate end);
|
||||
|
||||
List<OaProjectReportVo> clearList(LocalDate start, LocalDate end);
|
||||
|
||||
OaProjectReportVo queryById(Long reportId);
|
||||
|
||||
Boolean insertByBo(OaProjectReportBo bo);
|
||||
|
||||
Boolean updateByBo(OaProjectReportBo bo);
|
||||
|
||||
Boolean deleteWithValidByIds(List<Long> ids, Boolean isValid);
|
||||
|
||||
List<OaProjectReportVo> getRankData(LocalDate start, LocalDate end);
|
||||
|
||||
ProjectReportCardVo getCardData();
|
||||
|
||||
List<ProjectReportTrendVo> getTrend(LocalDate start, LocalDate end);
|
||||
|
||||
List<ProjectReportPieVo> getDistribution(LocalDate start, LocalDate end);
|
||||
|
||||
List<OaProjectReportVo> getProjects(LocalDate start, LocalDate end);
|
||||
|
||||
List<OaProjectReportVo> getSummaryData(LocalDate start, LocalDate end);
|
||||
|
||||
/**
|
||||
* 查询当前登录用户今日的报工记录
|
||||
*/
|
||||
OaProjectReportVo getTodayReportByCurrentUser();
|
||||
|
||||
Boolean insertReportSupplement(OaProjectReportBo bo);
|
||||
|
||||
TableDataInfo<OaProjectTravelCompareVo> getTravelCompareList(LocalDate start, LocalDate end, String nickName, String workPlace, PageQuery pageQuery);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.ruoyi.oa.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.oa.domain.vo.AmapCityNameVo;
|
||||
import com.ruoyi.oa.service.IOaAmapGeocodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* 高德逆地理编码:restapi.amap.com/v3/geocode/regeo
|
||||
* <p>
|
||||
* 配置项:{@code fad.amap.key}(与 {@code application.yml} 中一致)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OaAmapGeocodeServiceImpl implements IOaAmapGeocodeService {
|
||||
|
||||
private static final String AMAP_REGEO_URL = "https://restapi.amap.com/v3/geocode/regeo";
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${fad.amap.key:}")
|
||||
private String amapKey;
|
||||
|
||||
@Override
|
||||
public AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude) {
|
||||
AmapCityNameVo vo = new AmapCityNameVo();
|
||||
if (longitude == null || latitude == null) {
|
||||
return vo;
|
||||
}
|
||||
if (!StringUtils.hasText(amapKey)) {
|
||||
log.warn("fad.amap.key 未配置,无法调用高德逆地理编码");
|
||||
return vo;
|
||||
}
|
||||
|
||||
try {
|
||||
// 高德要求:location = 经度,纬度
|
||||
String location = longitude + "," + latitude;
|
||||
String url = UriComponentsBuilder.fromHttpUrl(AMAP_REGEO_URL)
|
||||
.queryParam("key", amapKey)
|
||||
.queryParam("location", location)
|
||||
.queryParam("extensions", "base")
|
||||
.queryParam("batch", "false")
|
||||
.queryParam("output", "JSON")
|
||||
.build(true)
|
||||
.toUriString();
|
||||
|
||||
String body = restTemplate.getForObject(url, String.class);
|
||||
if (!StringUtils.hasText(body)) {
|
||||
log.warn("高德逆地理编码响应为空, location={}", location);
|
||||
return vo;
|
||||
}
|
||||
JSONObject response = JSONObject.parseObject(body);
|
||||
if (response == null) {
|
||||
log.warn("高德逆地理编码 JSON 解析失败, location={}", location);
|
||||
return vo;
|
||||
}
|
||||
if (!"1".equals(response.getString("status"))) {
|
||||
String info = response.getString("info");
|
||||
log.warn("高德逆地理编码失败: status={}, info={}, infocode={}",
|
||||
response.getString("status"), info, response.getString("infocode"));
|
||||
return vo;
|
||||
}
|
||||
|
||||
JSONObject regeocode = response.getJSONObject("regeocode");
|
||||
if (regeocode == null) {
|
||||
return vo;
|
||||
}
|
||||
JSONObject addressComponent = regeocode.getJSONObject("addressComponent");
|
||||
if (addressComponent == null) {
|
||||
return vo;
|
||||
}
|
||||
|
||||
String province = nullToEmpty(addressComponent.getString("province"));
|
||||
String district = nullToEmpty(addressComponent.getString("district"));
|
||||
vo.setProvince(province);
|
||||
vo.setDistrict(district);
|
||||
|
||||
String cityName = resolveCityName(addressComponent, province);
|
||||
vo.setCityName(cityName);
|
||||
return vo;
|
||||
} catch (Exception e) {
|
||||
log.warn("高德逆地理编码异常, longitude={}, latitude={}, err={}",
|
||||
longitude, latitude, e.getMessage(), e);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 城市字段:普通城市为字符串;部分省直辖为 [];直辖市可能用省名表示
|
||||
*/
|
||||
private static String resolveCityName(JSONObject addressComponent, String province) {
|
||||
Object cityRaw = addressComponent.get("city");
|
||||
if (cityRaw instanceof JSONArray) {
|
||||
JSONArray arr = (JSONArray) cityRaw;
|
||||
if (!arr.isEmpty()) {
|
||||
return nullToEmpty(arr.getString(0));
|
||||
}
|
||||
return province;
|
||||
}
|
||||
String city = addressComponent.getString("city");
|
||||
if (StringUtils.hasText(city) && !"[]".equals(city)) {
|
||||
return city.trim();
|
||||
}
|
||||
return province;
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) {
|
||||
return s == null ? "" : s.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.oa.domain.OaCity;
|
||||
import com.ruoyi.oa.domain.bo.OaCityBo;
|
||||
import com.ruoyi.oa.domain.vo.OaCityVo;
|
||||
import com.ruoyi.oa.mapper.OaCityMapper;
|
||||
import com.ruoyi.oa.service.IOaCityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OaCityServiceImpl implements IOaCityService {
|
||||
|
||||
private final OaCityMapper baseMapper;
|
||||
|
||||
@Override
|
||||
public OaCityVo queryById(Long cityId) {
|
||||
return baseMapper.selectVoById(cityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaCityVo> queryPageList(OaCityBo bo, PageQuery pageQuery) {
|
||||
IPage<OaCityVo> page = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
|
||||
TableDataInfo<OaCityVo> tableDataInfo = new TableDataInfo<>(page.getRecords(), page.getTotal());
|
||||
return tableDataInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OaCityVo> queryList(OaCityBo bo) {
|
||||
return baseMapper.selectVoList(buildQueryWrapper(bo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean insertByBo(OaCityBo bo) {
|
||||
OaCity add = BeanUtil.toBean(bo, OaCity.class);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setCityId(add.getCityId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean updateByBo(OaCityBo bo) {
|
||||
OaCity update = BeanUtil.toBean(bo, OaCity.class);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<OaCity> buildQueryWrapper(OaCityBo bo) {
|
||||
LambdaQueryWrapper<OaCity> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getCountryName()), OaCity::getCountryName, bo.getCountryName());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getCityName()), OaCity::getCityName, bo.getCityName());
|
||||
lqw.eq(bo.getStatus() != null, OaCity::getStatus, bo.getStatus());
|
||||
lqw.orderByDesc(OaCity::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,35 @@
|
||||
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.conditions.query.QueryWrapper;
|
||||
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.helper.LoginHelper;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.hrm.domain.bo.HrmTravelReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmTravelReqVo;
|
||||
import com.ruoyi.hrm.service.IHrmTravelReqService;
|
||||
import com.ruoyi.oa.domain.OaProjectReport;
|
||||
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectTravelCompareVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportCardVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportPieVo;
|
||||
import com.ruoyi.oa.domain.vo.ProjectReportTrendVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.OaProjectReportBo;
|
||||
import com.ruoyi.oa.domain.vo.OaProjectReportVo;
|
||||
import com.ruoyi.oa.domain.OaProjectReport;
|
||||
import com.ruoyi.oa.mapper.OaProjectReportMapper;
|
||||
import com.ruoyi.oa.service.IOaProjectReportService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目报工Service业务层处理
|
||||
@@ -37,6 +42,7 @@ import java.util.*;
|
||||
public class OaProjectReportServiceImpl implements IOaProjectReportService {
|
||||
|
||||
private final OaProjectReportMapper baseMapper;
|
||||
private final IHrmTravelReqService hrmTravelReqService;
|
||||
|
||||
/**
|
||||
* 查询项目报工
|
||||
@@ -64,6 +70,77 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
|
||||
return baseMapper.selectAll(bo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<OaProjectTravelCompareVo> getTravelCompareList(LocalDate start, LocalDate end, String nickName, String workPlace, PageQuery pageQuery) {
|
||||
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() <= 0 ? 1 : pageQuery.getPageNum();
|
||||
int pageSize = pageQuery.getPageSize() == null || pageQuery.getPageSize() <= 0 ? 50 : pageQuery.getPageSize();
|
||||
Page<OaProjectReportVo> page = new Page<>(pageNum, pageSize);
|
||||
Page<OaProjectReportVo> reportPage = baseMapper.selectTravelCompareReportPage(page, start, end, nickName, workPlace);
|
||||
List<OaProjectReportVo> reportList = reportPage.getRecords();
|
||||
List<OaProjectTravelCompareVo> rows = new ArrayList<>();
|
||||
List<HrmTravelReqVo> travelList = hrmTravelReqService.queryList(new HrmTravelReqBo());
|
||||
|
||||
for (OaProjectReportVo report : reportList) {
|
||||
OaProjectTravelCompareVo vo = new OaProjectTravelCompareVo();
|
||||
vo.setNickName(report.getNickName());
|
||||
vo.setCompareDate(toLocalDate(report.getCreateTime()));
|
||||
vo.setWorkPlace(safeString(report.getWorkPlace()));
|
||||
vo.setReportId(report.getReportId());
|
||||
vo.setIsTrip(report.getIsTrip());
|
||||
vo.setTrip(report.getTrip());
|
||||
|
||||
if (report.getIsTrip() == null || report.getIsTrip() != 1) {
|
||||
vo.setTravelPlace("无出差记录");
|
||||
vo.setPass(false);
|
||||
vo.setCompareResult("该员工未出差");
|
||||
rows.add(vo);
|
||||
continue;
|
||||
}
|
||||
|
||||
HrmTravelReqVo matchedTravel = null;
|
||||
LocalDate reportDate = vo.getCompareDate();
|
||||
if (reportDate != null) {
|
||||
for (HrmTravelReqVo travel : travelList) {
|
||||
if (travel.getEmpId() == null || report.getUserId() == null || !travel.getEmpId().equals(report.getUserId())) {
|
||||
continue;
|
||||
}
|
||||
if (travel.getStartTime() == null) {
|
||||
continue;
|
||||
}
|
||||
LocalDate travelStart = toLocalDate(travel.getStartTime());
|
||||
LocalDate travelEnd = toLocalDate(travel.getActualEndTime() != null ? travel.getActualEndTime() : travel.getEndTime());
|
||||
if (travelStart == null || travelEnd == null) {
|
||||
continue;
|
||||
}
|
||||
if (!reportDate.isBefore(travelStart) && !reportDate.isAfter(travelEnd)) {
|
||||
matchedTravel = travel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedTravel == null) {
|
||||
vo.setTravelPlace("无出差记录");
|
||||
vo.setPass(false);
|
||||
vo.setCompareResult("该员工未出差");
|
||||
} else {
|
||||
String travelPlace = safeString(matchedTravel.getDestination());
|
||||
boolean pass = isLocationMatched(vo.getWorkPlace(), travelPlace);
|
||||
vo.setTravelPlace(travelPlace);
|
||||
vo.setPass(pass);
|
||||
vo.setCompareResult(pass ? "通过" : "异常");
|
||||
vo.setTravelBizId(matchedTravel.getBizId());
|
||||
}
|
||||
|
||||
rows.add(vo);
|
||||
}
|
||||
|
||||
TableDataInfo<OaProjectTravelCompareVo> table = TableDataInfo.build();
|
||||
table.setRows(rows);
|
||||
table.setTotal(reportPage.getTotal());
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
private QueryWrapper<OaProjectReport> buildQueryWrapper(OaProjectReportBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
@@ -110,6 +187,45 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
|
||||
return lqw;
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date date) {
|
||||
if (date == null) return null;
|
||||
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
private String safeString(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private boolean isLocationMatched(String a, String b) {
|
||||
if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
|
||||
return false;
|
||||
}
|
||||
String left = normalizeLocation(a);
|
||||
String right = normalizeLocation(b);
|
||||
return !left.isEmpty() && left.equals(right);
|
||||
}
|
||||
|
||||
private String normalizeLocation(String value) {
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() >= 2) {
|
||||
normalized = normalized.substring(0, 2);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private <T> TableDataInfo<T> toPage(List<T> list, PageQuery pageQuery) {
|
||||
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() <= 0 ? 1 : pageQuery.getPageNum();
|
||||
int pageSize = pageQuery.getPageSize() == null || pageQuery.getPageSize() <= 0 ? 50 : pageQuery.getPageSize();
|
||||
long total = list.size();
|
||||
int fromIndex = Math.min((pageNum - 1) * pageSize, list.size());
|
||||
int toIndex = Math.min(fromIndex + pageSize, list.size());
|
||||
List<T> records = list.subList(fromIndex, toIndex);
|
||||
TableDataInfo<T> tableDataInfo = TableDataInfo.build();
|
||||
tableDataInfo.setRows(records);
|
||||
tableDataInfo.setTotal(total);
|
||||
return tableDataInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增项目报工
|
||||
*/
|
||||
@@ -168,7 +284,7 @@ public class OaProjectReportServiceImpl implements IOaProjectReportService {
|
||||
* 批量删除项目报工
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
public Boolean deleteWithValidByIds(List<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
|
||||
|
||||
/** 步骤表公共筛选(与 oa_project_schedule 联表时须使用别名 opss) */
|
||||
private void appendCommonStepFilters (QueryWrapper<OaProjectScheduleStep> lqw, OaProjectScheduleStepBo bo, String alias) {
|
||||
lqw.eq(stepCol(alias, "del_flag"), 0);
|
||||
lqw.eq(bo.getScheduleId() != null, stepCol(alias, "schedule_id"), bo.getScheduleId());
|
||||
lqw.eq(bo.getStepOrder() != null, stepCol(alias, "step_order"), bo.getStepOrder());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getStepName()), stepCol(alias, "step_name"), bo.getStepName());
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.ruoyi.oa.domain.bo.OaProjectScheduleBo;
|
||||
import com.ruoyi.oa.domain.bo.SysOaWarehouseDetailBo;
|
||||
import com.ruoyi.oa.domain.dto.ProjectActivityDTO;
|
||||
import com.ruoyi.oa.domain.dto.ProjectDataDTO;
|
||||
import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto;
|
||||
import com.ruoyi.oa.domain.vo.*;
|
||||
import com.ruoyi.oa.service.CodeGeneratorService;
|
||||
import com.ruoyi.oa.service.IExchangeRateService;
|
||||
@@ -27,12 +28,14 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.SysOaProjectBo;
|
||||
import com.ruoyi.oa.domain.SysOaProject;
|
||||
import com.ruoyi.oa.mapper.OaProjectScheduleStepMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaProjectMapper;
|
||||
import com.ruoyi.oa.service.ISysOaProjectService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -53,6 +56,8 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
|
||||
|
||||
private final IOaProjectScheduleStepService oaProjectScheduleStepService;
|
||||
|
||||
private final OaProjectScheduleStepMapper oaProjectScheduleStepMapper;
|
||||
|
||||
@Autowired
|
||||
private CodeGeneratorService codeGeneratorService;
|
||||
@Autowired
|
||||
@@ -192,6 +197,31 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(bo.getScheduleStats()) && result.getRecords() != null && !result.getRecords().isEmpty()) {
|
||||
List<Long> projectIds = result.getRecords().stream()
|
||||
.map(SysOaProjectVo::getProjectId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!projectIds.isEmpty()) {
|
||||
List<ProjectScheduleStepStatsDto> statRows = oaProjectScheduleStepMapper.selectStepStatsGroupByProjectId(projectIds);
|
||||
Map<Long, ProjectScheduleStepStatsDto> statMap = statRows.stream()
|
||||
.collect(Collectors.toMap(ProjectScheduleStepStatsDto::getProjectId, Function.identity(), (a, b) -> a));
|
||||
for (SysOaProjectVo vo : result.getRecords()) {
|
||||
ProjectScheduleStepStatsDto s = statMap.get(vo.getProjectId());
|
||||
if (s != null) {
|
||||
vo.setScheduleStepTotal(s.getTotalNodes());
|
||||
vo.setScheduleStepCompleted(s.getCompletedNodes());
|
||||
vo.setScheduleStepPendingAccept(s.getPendingAcceptNodes());
|
||||
} else {
|
||||
vo.setScheduleStepTotal(0L);
|
||||
vo.setScheduleStepCompleted(0L);
|
||||
vo.setScheduleStepPendingAccept(0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
private QueryWrapper<SysOaProject> buildAliasPQueryWrapper(SysOaProjectBo bo) {
|
||||
@@ -221,6 +251,7 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
|
||||
}
|
||||
qw.eq(bo.getCustomerId() != null, "p.customer_id", bo.getCustomerId());
|
||||
qw.orderByDesc("p.is_top").orderByDesc("p.create_time");
|
||||
qw.groupBy("p.project_id");
|
||||
return qw;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
QueryWrapper<SysOaTask> lqw = Wrappers.query();
|
||||
lqw.eq("sot.del_flag", 0);
|
||||
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());
|
||||
lqw.eq(bo.getTaskId()!=null, "sot.task_id", bo.getTaskId());
|
||||
lqw.eq(bo.getCreateUserId()!=null, "sot.create_user_id", bo.getCreateUserId());
|
||||
lqw.eq(bo.getWorkerId()!=null, "sot.worker_id", bo.getWorkerId());
|
||||
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());
|
||||
|
||||
@@ -195,11 +195,54 @@
|
||||
left join sys_user su on su.user_id = opr.user_id
|
||||
left join sys_dept sd on su.dept_id = sd.dept_id
|
||||
WHERE opr.del_flag = 0
|
||||
AND DATE(opr.create_time)
|
||||
BETWEEN DATE_FORMAT(#{start}, '%Y-%m-%d')
|
||||
AND DATE_FORMAT(#{end}, '%Y-%m-%d')
|
||||
<if test="start != null">
|
||||
AND DATE(opr.create_time) >= DATE_FORMAT(#{start}, '%Y-%m-%d')
|
||||
</if>
|
||||
<if test="end != null">
|
||||
AND DATE(opr.create_time) <= DATE_FORMAT(#{end}, '%Y-%m-%d')
|
||||
</if>
|
||||
ORDER BY opr.create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectTravelCompareReportPage" resultType="com.ruoyi.oa.domain.vo.OaProjectReportVo">
|
||||
select opr.report_id,
|
||||
opr.user_id,
|
||||
opr.is_trip,
|
||||
opr.work_place,
|
||||
opr.project_id,
|
||||
opr.content,
|
||||
opr.create_time,
|
||||
opr.create_by,
|
||||
opr.update_time,
|
||||
opr.update_by,
|
||||
opr.del_flag,
|
||||
opr.remark,
|
||||
op.project_name,
|
||||
op.project_num,
|
||||
op.project_code,
|
||||
su.nick_name,
|
||||
opr.work_type,
|
||||
sd.dept_name
|
||||
from oa_project_report opr
|
||||
left join sys_oa_project op on opr.project_id = op.project_id
|
||||
left join sys_user su on su.user_id = opr.user_id
|
||||
left join sys_dept sd on su.dept_id = sd.dept_id
|
||||
<where>
|
||||
opr.del_flag = 0
|
||||
<if test="start != null">
|
||||
AND DATE(opr.create_time) >= DATE_FORMAT(#{start}, '%Y-%m-%d')
|
||||
</if>
|
||||
<if test="end != null">
|
||||
AND DATE(opr.create_time) <= DATE_FORMAT(#{end}, '%Y-%m-%d')
|
||||
</if>
|
||||
<if test="nickName != null and nickName != ''">
|
||||
AND su.nick_name LIKE CONCAT('%', #{nickName}, '%')
|
||||
</if>
|
||||
<if test="workPlace != null and workPlace != ''">
|
||||
AND opr.work_place LIKE CONCAT('%', #{workPlace}, '%')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY opr.create_time DESC
|
||||
LIMIT 20
|
||||
</select>
|
||||
|
||||
<select id="getSummaryData" resultType="com.ruoyi.oa.domain.vo.OaProjectReportVo">
|
||||
|
||||
@@ -282,5 +282,22 @@
|
||||
WHERE schedule_id = #{scheduleId}
|
||||
</select>
|
||||
|
||||
<select id="selectStepStatsGroupByProjectId"
|
||||
resultType="com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto">
|
||||
SELECT
|
||||
sch.project_id AS projectId,
|
||||
COUNT(step.track_id) AS totalNodes,
|
||||
IFNULL(SUM(CASE WHEN step.status = 2 THEN 1 ELSE 0 END), 0) AS completedNodes,
|
||||
IFNULL(SUM(CASE WHEN step.status = 1 THEN 1 ELSE 0 END), 0) AS pendingAcceptNodes
|
||||
FROM oa_project_schedule sch
|
||||
INNER JOIN oa_project_schedule_step step ON step.schedule_id = sch.schedule_id
|
||||
WHERE sch.del_flag = '0'
|
||||
AND step.del_flag = '0'
|
||||
AND sch.project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">
|
||||
#{pid}
|
||||
</foreach>
|
||||
GROUP BY sch.project_id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -310,9 +310,15 @@
|
||||
TIMESTAMPDIFF(DAY, NOW(), p.postpone_time) AS remainTime,
|
||||
p.customer_id AS customerId,
|
||||
c.name AS customerName,
|
||||
p.is_top AS isTop
|
||||
p.is_top AS isTop,
|
||||
COUNT(sot.task_id) AS taskTotalCount ,
|
||||
SUM(CASE WHEN sot.status = 1 THEN 1 ELSE 0 END) AS taskFinishCount,
|
||||
COUNT(ops.schedule_id) AS scheduleTotalCount,
|
||||
SUM(CASE WHEN ops.status = 2 THEN 1 ELSE 0 END) AS scheduleFinishCount
|
||||
FROM sys_oa_project p
|
||||
LEFT JOIN oa_customer c ON p.customer_id = c.customer_id
|
||||
left join sys_oa_task sot on sot.project_id = p.project_id and sot.del_flag = 0
|
||||
left join oa_project_schedule ops on ops.project_id = p.project_id and ops.del_flag = 0
|
||||
${ew.getCustomSqlSegment}
|
||||
</select>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"url": "https://gitee.com/KonBAI-Q/ruoyi-flowable-plus.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@babel/parser": "7.7.4",
|
||||
"@handsontable/vue": "^15.3.0",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<title>
|
||||
<%= webpackConfig.name %>
|
||||
</title>
|
||||
|
||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||
<style>
|
||||
html,
|
||||
@@ -18,7 +19,7 @@
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
.chromeframe {
|
||||
margin: 0.2em 0;
|
||||
|
||||
@@ -16,7 +16,13 @@ export function readCc(ccId) {
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 标记抄送为未读
|
||||
export function unreadCc(ccId) {
|
||||
return request({
|
||||
url: `/hrm/flow/cc/${ccId}/unread`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
// 手动抄送
|
||||
export function addCc(data) {
|
||||
return request({
|
||||
|
||||
@@ -254,9 +254,4 @@ export function listAssignTask (instId) {
|
||||
url: `/hrm/flow/instance/tasks/${instId}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
/**
|
||||
* 查询当前用户的审批历史
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
9
ruoyi-ui/src/api/hrm/myApply.js
Normal file
9
ruoyi-ui/src/api/hrm/myApply.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listMyApply(query) {
|
||||
return request({
|
||||
url: '/hrm/my-apply/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
@@ -46,3 +46,22 @@ export function allTravelReq(query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
export function earlyEndTravel(bizId) {
|
||||
return request({
|
||||
url: `/hrm/travel/earlyEnd/${bizId}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAmapKey() {
|
||||
return request({
|
||||
url: '/hrm/travel/amapKey',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
export function getAmapSecurityKey() {
|
||||
return request({
|
||||
url: '/hrm/travel/amapSecurityKey',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
15
ruoyi-ui/src/api/oa/amap.js
Normal file
15
ruoyi-ui/src/api/oa/amap.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 根据经纬度逆地理编码获取城市等信息(后端转发高德)
|
||||
* @param {number} longitude 经度
|
||||
* @param {number} latitude 纬度
|
||||
*/
|
||||
export function getCityByLocation (longitude, latitude) {
|
||||
return request({
|
||||
url: '/oa/amap/city',
|
||||
method: 'get',
|
||||
params: { longitude, latitude },
|
||||
timeout: 15000
|
||||
})
|
||||
}
|
||||
18
ruoyi-ui/src/api/oa/projectCompare.js
Normal file
18
ruoyi-ui/src/api/oa/projectCompare.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listTravelCompare (start, end, nickName, workPlace, pageNum, pageSize) {
|
||||
return request({
|
||||
url: '/oa/projectReport/travel-compare',
|
||||
method: 'get',
|
||||
params: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
...(nickName ? { nickName } : {}),
|
||||
...(workPlace ? { workPlace } : {}),
|
||||
pageNum,
|
||||
pageSize,
|
||||
orderByColumn: 'compareDate',
|
||||
isAsc: 'desc'
|
||||
}
|
||||
})
|
||||
}
|
||||
44
ruoyi-ui/src/api/system/city.js
Normal file
44
ruoyi-ui/src/api/system/city.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询城市列表
|
||||
export function listCity(query) {
|
||||
return request({
|
||||
url: '/oa/city/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询城市详细
|
||||
export function getCity(cityId) {
|
||||
return request({
|
||||
url: `/oa/city/${cityId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增城市
|
||||
export function addCity(data) {
|
||||
return request({
|
||||
url: '/oa/city',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改城市
|
||||
export function updateCity(data) {
|
||||
return request({
|
||||
url: '/oa/city',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除城市
|
||||
export function delCity(cityIds) {
|
||||
return request({
|
||||
url: `/oa/city/${cityIds}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
371
ruoyi-ui/src/components/AmapCitySelect/index.vue
Normal file
371
ruoyi-ui/src/components/AmapCitySelect/index.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="amap-city-select">
|
||||
<el-input
|
||||
v-model="cityName"
|
||||
:placeholder="placeholder"
|
||||
@focus="openMapDialog"
|
||||
clearable
|
||||
readonly
|
||||
>
|
||||
<i slot="prefix" class="el-icon-location" style="color: #409eff"></i>
|
||||
</el-input>
|
||||
|
||||
<el-dialog
|
||||
title="选择出差城市 / 地点"
|
||||
:visible.sync="dialogVisible"
|
||||
width="900px"
|
||||
:append-to-body="true"
|
||||
@opened="onDialogOpened"
|
||||
>
|
||||
<div class="map-selector">
|
||||
<div class="city-sidebar">
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入城市或具体地点搜索"
|
||||
size="small"
|
||||
prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="searchLocation"
|
||||
clearable
|
||||
@clear="searchKeyword = ''"
|
||||
>
|
||||
<el-button slot="append" @click="searchLocation">搜索</el-button>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 热门城市模块 -->
|
||||
<div class="hot-cities" v-if="!searchKeyword || searchResults.length === 0">
|
||||
<div class="section-title">热门城市</div>
|
||||
<div class="city-list">
|
||||
<span
|
||||
v-for="city in hotCities"
|
||||
:key="city"
|
||||
class="city-item"
|
||||
@click="selectHotCity(city)"
|
||||
>
|
||||
{{ city }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果模块 -->
|
||||
<div class="search-results" v-if="searchResults.length > 0">
|
||||
<div class="section-title">搜索结果</div>
|
||||
<div class="result-list">
|
||||
<div
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
@click="selectLocation(result)"
|
||||
>
|
||||
<i class="el-icon-location-outline"></i>
|
||||
<div class="result-info">
|
||||
<div class="poi-name">{{ result.name }}</div>
|
||||
<div class="poi-address">{{ result.address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<div id="amapContainer" class="amap-wrapper"></div>
|
||||
<div class="map-tip">
|
||||
<i class="el-icon-info"></i> 点击地图上的位置或搜索地点,自动识别归属城市
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<div class="selected-info" v-if="selectedCity">
|
||||
识别城市:<span class="selected-city">{{ selectedCity }}</span>
|
||||
<span v-if="selectedPoi" style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||||
( {{ selectedPoi }} )
|
||||
</span>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<div>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmCity" :disabled="!selectedCity">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AMapLoader from '@amap/amap-jsapi-loader'
|
||||
import { getAmapKey, getAmapSecurityKey } from '@/api/hrm/travel'
|
||||
|
||||
export default {
|
||||
name: 'AmapCitySelect',
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '请选择出差地点' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cityName: '',
|
||||
dialogVisible: false,
|
||||
searchKeyword: '',
|
||||
searchResults: [],
|
||||
selectedCity: '', // 最终表单需要的城市名
|
||||
selectedPoi: '', // 具体的地点名(仅用于展示)
|
||||
map: null,
|
||||
geocoder: null,
|
||||
placeSearch: null, // 新增地点搜索对象
|
||||
marker: null,
|
||||
AMap: null,
|
||||
hotCities: [
|
||||
'北京市', '上海市', '广州市', '深圳市',
|
||||
'杭州市', '南京市', '成都市', '武汉市',
|
||||
'重庆市', '天津市', '苏州市', '西安市'
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(val) { this.cityName = val || '' }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onDialogOpened() {
|
||||
const AMap = await this.loadAMap()
|
||||
if (!AMap) return
|
||||
this.$nextTick(() => {
|
||||
this.initMap()
|
||||
})
|
||||
},
|
||||
|
||||
async loadAMap() {
|
||||
if (this.AMap && window._AMapSecurityConfig) return this.AMap
|
||||
try {
|
||||
const [keyRes, securityRes] = await Promise.all([getAmapKey(), getAmapSecurityKey()])
|
||||
const amapkey = keyRes.data || keyRes.msg
|
||||
const securityKey = securityRes.data || securityRes.msg
|
||||
|
||||
if (!amapkey) throw new Error('未获取到高德地图 Key')
|
||||
|
||||
window._AMapSecurityConfig = { securityJsCode: securityKey }
|
||||
|
||||
this.AMap = await AMapLoader.load({
|
||||
key: amapkey,
|
||||
version: '2.0',
|
||||
// 加入 PlaceSearch 插件
|
||||
plugins: ['AMap.Geocoder', 'AMap.PlaceSearch']
|
||||
})
|
||||
return this.AMap
|
||||
} catch (error) {
|
||||
console.error('地图加载失败:', error)
|
||||
this.$message.error('地图加载失败,请重试')
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
initMap() {
|
||||
const container = document.getElementById('amapContainer')
|
||||
if (!container) {
|
||||
setTimeout(() => this.initMap(), 100)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.map) {
|
||||
this.map.destroy()
|
||||
this.map = null
|
||||
}
|
||||
|
||||
this.map = new this.AMap.Map('amapContainer', {
|
||||
zoom: 12,
|
||||
center: [116.397428, 39.90923], // 默认北京
|
||||
resizeEnable: true
|
||||
})
|
||||
|
||||
this.geocoder = new this.AMap.Geocoder()
|
||||
|
||||
// 初始化地点搜索插件
|
||||
this.placeSearch = new this.AMap.PlaceSearch({
|
||||
pageSize: 15, // 单页显示结果条数
|
||||
pageIndex: 1, // 页码
|
||||
autoFitView: false // 禁用自动调整视图,我们自己控制
|
||||
})
|
||||
|
||||
// 监听地图点击
|
||||
this.map.on('click', (e) => {
|
||||
const lng = e.lnglat.getLng()
|
||||
const lat = e.lnglat.getLat()
|
||||
this.selectedPoi = '地图选点'
|
||||
this.getCityByLngLat(lng, lat, true)
|
||||
})
|
||||
},
|
||||
|
||||
// 通过经纬度逆解析城市
|
||||
getCityByLngLat(lng, lat, setCenter = false) {
|
||||
this.geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.regeocode) {
|
||||
const addrComp = result.regeocode.addressComponent
|
||||
let city = addrComp.city
|
||||
// 直辖市的 city 可能是空的,取 province
|
||||
if (!city || city === '[]' || city.length === 0) {
|
||||
city = addrComp.province
|
||||
}
|
||||
this.selectedCity = city
|
||||
this.addMarker(lng, lat)
|
||||
if(setCenter) {
|
||||
this.map.setCenter([lng, lat])
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 在地图上打点
|
||||
addMarker(lng, lat) {
|
||||
if (this.marker) {
|
||||
this.marker.setMap(null)
|
||||
}
|
||||
this.marker = new this.AMap.Marker({
|
||||
position: [lng, lat],
|
||||
map: this.map,
|
||||
animation: 'AMAP_ANIMATION_DROP' // 加上掉落动画
|
||||
})
|
||||
},
|
||||
|
||||
openMapDialog() {
|
||||
this.dialogVisible = true
|
||||
this.selectedCity = this.value || ''
|
||||
this.selectedPoi = ''
|
||||
this.searchKeyword = ''
|
||||
this.searchResults = []
|
||||
},
|
||||
|
||||
// 搜索地点核心方法
|
||||
async searchLocation() {
|
||||
if (!this.searchKeyword.trim()) {
|
||||
this.searchResults = []
|
||||
return
|
||||
}
|
||||
|
||||
await this.loadAMap()
|
||||
|
||||
// 使用 PlaceSearch 搜索具体地点
|
||||
this.placeSearch.search(this.searchKeyword, (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
// 提取返回的 POI 列表
|
||||
const pois = result.poiList.pois
|
||||
this.searchResults = pois.map(poi => ({
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
address: poi.address && typeof poi.address === 'string' ? poi.address : poi.adname,
|
||||
location: poi.location, // 包含 lng/lat 的对象
|
||||
city: poi.cityname || poi.pname // 城市名
|
||||
}))
|
||||
} else {
|
||||
this.searchResults = []
|
||||
this.$message.info('未找到相关地点,请尝试更换关键词')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 选中左侧搜索结果列表的具体地点
|
||||
selectLocation(poi) {
|
||||
if (!poi.location) {
|
||||
this.$message.warning('该地点缺少坐标信息')
|
||||
return
|
||||
}
|
||||
|
||||
const lng = poi.location.lng
|
||||
const lat = poi.location.lat
|
||||
|
||||
// 设置地图中心点并放大(层级15看街道)
|
||||
this.map.setZoomAndCenter(15, [lng, lat])
|
||||
this.addMarker(lng, lat)
|
||||
|
||||
// 更新选择数据
|
||||
this.selectedCity = poi.city
|
||||
this.selectedPoi = poi.name
|
||||
this.$message.success(`已定位到:${poi.name}`)
|
||||
},
|
||||
|
||||
// 选中热门城市
|
||||
async selectHotCity(city) {
|
||||
await this.loadAMap()
|
||||
this.selectedCity = city
|
||||
this.selectedPoi = city
|
||||
|
||||
// 获取城市中心点坐标
|
||||
this.geocoder.getLocation(city, (status, result) => {
|
||||
if (status === 'complete' && result.geocodes.length) {
|
||||
const loc = result.geocodes[0].location
|
||||
this.map.setZoomAndCenter(11, [loc.lng, loc.lat])
|
||||
this.addMarker(loc.lng, loc.lat)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
confirmCity() {
|
||||
if (this.selectedCity) {
|
||||
this.cityName = this.selectedCity
|
||||
// 组件 v-model 抛出城市名
|
||||
this.$emit('input', this.selectedCity)
|
||||
// 如果你需要具体的地点名,可以额外抛出一个事件
|
||||
this.$emit('poi-change', { city: this.selectedCity, poi: this.selectedPoi })
|
||||
this.closeDialog()
|
||||
}
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.dialogVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.amap-city-select { width: 100%; }
|
||||
.map-selector { display: flex; gap: 20px; min-height: 450px;
|
||||
.city-sidebar { width: 300px; flex-shrink: 0; display: flex; flex-direction: column;
|
||||
.search-box { margin-bottom: 16px; }
|
||||
.section-title { font-size: 14px; font-weight: bold; color: #303133; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
|
||||
|
||||
.hot-cities .city-list { display: flex; flex-wrap: wrap; gap: 10px;
|
||||
.city-item { padding: 6px 14px; background: #f4f4f5; color: #606266; border-radius: 4px; font-size: 13px; cursor: pointer; transition: all 0.2s;
|
||||
&:hover { background: #409eff; color: white; }
|
||||
}
|
||||
}
|
||||
|
||||
/* 搜索结果列表样式升级 */
|
||||
.search-results {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.result-list {
|
||||
flex: 1; overflow-y: auto; padding-right: 5px;
|
||||
|
||||
/* 自定义滚动条 */
|
||||
&::-webkit-scrollbar { width: 4px; }
|
||||
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 4px; }
|
||||
|
||||
.result-item {
|
||||
padding: 12px 10px; cursor: pointer; display: flex; align-items: flex-start; gap: 10px; border-bottom: 1px solid #ebeef5; transition: background 0.2s;
|
||||
&:hover { background: #f0f7ff; }
|
||||
i { color: #409eff; margin-top: 3px; font-size: 16px; }
|
||||
.result-info {
|
||||
flex: 1; overflow: hidden;
|
||||
.poi-name { font-size: 14px; color: #303133; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.poi-address { font-size: 12px; color: #909399; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container { flex: 1; display: flex; flex-direction: column;
|
||||
.amap-wrapper { flex: 1; width: 100%; border: 1px solid #dcdfe6; border-radius: 6px; overflow: hidden; }
|
||||
.map-tip { margin-top: 10px; font-size: 12px; color: #909399; text-align: center; i { margin-right: 4px; color: #e6a23c; } }
|
||||
}
|
||||
}
|
||||
.dialog-footer { display: flex; justify-content: space-between; align-items: center;
|
||||
.selected-info { font-size: 14px; color: #606266; .selected-city { color: #409eff; font-weight: bold; font-size: 15px; } }
|
||||
}
|
||||
</style>
|
||||
@@ -182,6 +182,7 @@ export const constantRoutes = [
|
||||
];
|
||||
|
||||
export const dynamicRoutes = [
|
||||
|
||||
{
|
||||
path: "/system/user-auth",
|
||||
component: Layout,
|
||||
@@ -314,6 +315,7 @@ export const dynamicRoutes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
let routerPush = Router.prototype.push;
|
||||
|
||||
@@ -24,7 +24,7 @@ const state = {
|
||||
|
||||
projectQuery: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 15,
|
||||
keyword: ''
|
||||
},
|
||||
projectList: [],
|
||||
@@ -87,7 +87,8 @@ const actions = {
|
||||
const query = {
|
||||
pageNum: merged.pageNum,
|
||||
pageSize: merged.pageSize,
|
||||
keyword: (merged.keyword != null ? String(merged.keyword) : '').trim()
|
||||
keyword: (merged.keyword != null ? String(merged.keyword) : '').trim(),
|
||||
scheduleStats: true
|
||||
}
|
||||
commit('SET_PROJECT_QUERY', query)
|
||||
commit('SET_LOADING', true)
|
||||
|
||||
@@ -33,30 +33,6 @@ const permission = {
|
||||
return new Promise(resolve => {
|
||||
|
||||
getRouters().then(res => {
|
||||
|
||||
|
||||
const oaMenu = res.data.find(item => item.path === '/oa' || (item.meta && item.meta.title === '办公中心'));
|
||||
|
||||
if (oaMenu) {
|
||||
if (!oaMenu.children) oaMenu.children = [];
|
||||
|
||||
|
||||
const hasHistory = oaMenu.children.some(child => child.path === 'flowHistory');
|
||||
|
||||
if (!hasHistory) {
|
||||
oaMenu.children.push({
|
||||
name: 'HrmFlowHistory',
|
||||
path: 'flowHistory',
|
||||
hidden: false,
|
||||
component: 'hrm/flow/taskHistory',
|
||||
meta: {
|
||||
title: '审批历史',
|
||||
icon: 'date-range',
|
||||
noCache: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// ================= 新增拦截代码:将页面注入到办公中心结束 =================
|
||||
|
||||
const sdata = JSON.parse(JSON.stringify(res.data))
|
||||
|
||||
98
ruoyi-ui/src/utils/geolocationWorkPlace.js
Normal file
98
ruoyi-ui/src/utils/geolocationWorkPlace.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { getCityByLocation } from '@/api/oa/amap'
|
||||
|
||||
const GEO_OPTIONS = {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 20000,
|
||||
maximumAge: 120000
|
||||
}
|
||||
|
||||
export const EMPTY_GEOCODE = 'EMPTY_GEOCODE'
|
||||
|
||||
/**
|
||||
* 将 AmapCityNameVo 格式化为展示用工作地点字符串
|
||||
*/
|
||||
export function formatWorkPlaceFromAmap (vo) {
|
||||
if (!vo) {
|
||||
return ''
|
||||
}
|
||||
const province = (vo.province || '').trim()
|
||||
const cityName = (vo.cityName || '').trim()
|
||||
const district = (vo.district || '').trim()
|
||||
const parts = []
|
||||
if (province) {
|
||||
parts.push(province)
|
||||
}
|
||||
if (cityName && cityName !== province) {
|
||||
parts.push(cityName)
|
||||
}
|
||||
if (district) {
|
||||
parts.push(district)
|
||||
}
|
||||
const joined = parts.join(' ').trim()
|
||||
return joined || cityName || province || ''
|
||||
}
|
||||
|
||||
function getCurrentPositionAsync () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
||||
reject(new Error('BROWSER_UNSUPPORTED'))
|
||||
return
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
position => resolve(position),
|
||||
err => reject(err),
|
||||
GEO_OPTIONS
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器定位 + 后端逆地理 → 工作地点字符串
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function resolveWorkPlaceFromBrowser () {
|
||||
const position = await getCurrentPositionAsync()
|
||||
const { longitude, latitude } = position.coords
|
||||
const res = await getCityByLocation(longitude, latitude)
|
||||
const vo = res && res.data
|
||||
const text = formatWorkPlaceFromAmap(vo)
|
||||
if (!text) {
|
||||
const err = new Error(EMPTY_GEOCODE)
|
||||
console.warn('[workPlace] 逆地理结果为空', { longitude, latitude, vo })
|
||||
throw err
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 将定位/逆地理错误转换为用户可读文案
|
||||
*/
|
||||
export function geolocationUserMessage (err) {
|
||||
if (!err) {
|
||||
return '获取工作地点失败'
|
||||
}
|
||||
if (err.message === 'BROWSER_UNSUPPORTED') {
|
||||
return '当前浏览器不支持定位,请更换浏览器或使用 HTTPS 访问后重试'
|
||||
}
|
||||
if (err.message === EMPTY_GEOCODE) {
|
||||
return '无法根据当前位置解析城市,请稍后重试;若持续失败请联系管理员检查高德地图配置(fad.amap.key)'
|
||||
}
|
||||
const code = err.code
|
||||
if (code === 1) {
|
||||
return '您已拒绝定位权限,请在浏览器设置中允许本站点定位后点击「重新获取定位」'
|
||||
}
|
||||
if (code === 2) {
|
||||
return '暂时无法获取位置信息,请到信号较好处点击「重新获取定位」重试'
|
||||
}
|
||||
if (code === 3) {
|
||||
return '定位请求超时,请检查网络后点击「重新获取定位」重试'
|
||||
}
|
||||
const msg = err.message || ''
|
||||
if (msg.includes('timeout') || msg.includes('超时')) {
|
||||
return '接口请求超时,请稍后点击「重新获取定位」重试'
|
||||
}
|
||||
if (msg.includes('Network Error') || msg.includes('网络')) {
|
||||
return '网络异常,请检查连接后点击「重新获取定位」重试'
|
||||
}
|
||||
return '获取工作地点失败,请点击「重新获取定位」重试'
|
||||
}
|
||||
139
ruoyi-ui/src/utils/oaMenuNavigate.js
Normal file
139
ruoyi-ui/src/utils/oaMenuNavigate.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import store from '@/store'
|
||||
|
||||
function isHttpUrl (path) {
|
||||
return path && /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
function joinPaths (parentPath, segment) {
|
||||
if (segment == null || segment === '') {
|
||||
return parentPath || '/'
|
||||
}
|
||||
if (isHttpUrl(segment)) {
|
||||
return segment
|
||||
}
|
||||
if (segment.startsWith('/')) {
|
||||
return segment.replace(/\/+/g, '/')
|
||||
}
|
||||
const base = (parentPath || '').replace(/\/+$/, '')
|
||||
const rel = segment.replace(/^\//, '')
|
||||
if (!base) {
|
||||
return '/' + rel
|
||||
}
|
||||
return (base + '/' + rel).replace(/\/+/g, '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历侧边栏路由树,得到所有带 meta.title 的叶子及其完整 path(与菜单渲染路径一致)
|
||||
*/
|
||||
export function flattenSidebarLeaves (routes, parentPath = '') {
|
||||
const out = []
|
||||
if (!routes || !routes.length) {
|
||||
return out
|
||||
}
|
||||
for (const r of routes) {
|
||||
if (!r || r.hidden) {
|
||||
continue
|
||||
}
|
||||
const current = joinPaths(parentPath, r.path)
|
||||
if (r.children && r.children.length > 0) {
|
||||
out.push(...flattenSidebarLeaves(r.children, current))
|
||||
} else if (r.meta && r.meta.title) {
|
||||
out.push({ fullPath: current, title: r.meta.title, name: r.name })
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 按菜单名称(与 sys_menu.menu_name 一致)查找已注册的前端 path
|
||||
*/
|
||||
export function findMenuFullPathByTitles (titles) {
|
||||
const set = new Set((titles || []).filter(Boolean))
|
||||
if (!set.size) {
|
||||
return null
|
||||
}
|
||||
const routes = store.getters.sidebarRouters || []
|
||||
const leaves = flattenSidebarLeaves(routes)
|
||||
const hit = leaves.find((l) => set.has(l.title))
|
||||
return hit ? hit.fullPath : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前路由最后一级替换为另一段,用于「综合看板」与「任务」「进度」在同一父菜单下的场景
|
||||
*/
|
||||
export function siblingPathReplaceLast (currentPath, newLastSegment) {
|
||||
if (!currentPath || !newLastSegment) {
|
||||
return null
|
||||
}
|
||||
const normalized = String(currentPath).replace(/\/+$/, '')
|
||||
const idx = normalized.lastIndexOf('/')
|
||||
if (idx < 0) {
|
||||
return '/' + newLastSegment
|
||||
}
|
||||
return `${normalized.slice(0, idx)}/${newLastSegment}`.replace(/\/+/g, '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 在候选 path 中选第一个 resolve 后不是 404 页的地址
|
||||
*/
|
||||
export function pickExistingRoutePath (router, candidates) {
|
||||
for (const p of candidates) {
|
||||
if (!p) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const { route } = router.resolve({ path: p })
|
||||
if (!route || !route.matched || route.matched.length === 0) {
|
||||
continue
|
||||
}
|
||||
if (route.path === '/404' || (route.fullPath && route.fullPath.includes('/404'))) {
|
||||
continue
|
||||
}
|
||||
return p
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findLeafPathMatchingPath (predicate) {
|
||||
const routes = store.getters.sidebarRouters || []
|
||||
const leaves = flattenSidebarLeaves(routes)
|
||||
const hit = leaves.find((l) => predicate(l.fullPath))
|
||||
return hit ? hit.fullPath : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析「我的任务」对应前端 path(勿硬编码父级目录,避免项目中心与项目管理 path 不一致导致 404)
|
||||
*/
|
||||
export function resolveOaTaskCenterPath (vm) {
|
||||
const fromMenuTitle = findMenuFullPathByTitles(['我的任务', '任务管理'])
|
||||
const fromPathEndsTask = findLeafPathMatchingPath((p) => /\/task(\/|$)/i.test(p))
|
||||
const sib = siblingPathReplaceLast(vm.$route.path, 'task')
|
||||
const candidates = [fromMenuTitle, fromPathEndsTask, sib, '/project/task'].filter(Boolean)
|
||||
return pickExistingRoutePath(vm.$router, candidates) || candidates[candidates.length - 1]
|
||||
}
|
||||
|
||||
const PACE_MENU_TITLES = [
|
||||
'项目进度',
|
||||
'进度管理',
|
||||
'进度跟踪',
|
||||
'绑定进度',
|
||||
'进度中心'
|
||||
]
|
||||
|
||||
/**
|
||||
* 解析进度中心(pace 列表+抽屉)页面前端 path
|
||||
*/
|
||||
export function resolveOaPaceCenterPath (vm) {
|
||||
const fromMenu = findMenuFullPathByTitles(PACE_MENU_TITLES)
|
||||
const fromPathEndsPace = findLeafPathMatchingPath((p) => /\/pace$/i.test(p))
|
||||
const base = vm.$route.path
|
||||
const siblingSegs = ['pace', 'schedule', 'projectSchedule', 'project-schedule']
|
||||
const fromSiblings = siblingSegs
|
||||
.map((s) => siblingPathReplaceLast(base, s))
|
||||
.filter(Boolean)
|
||||
const candidates = [fromMenu, fromPathEndsPace, ...fromSiblings, '/project/pace'].filter(Boolean)
|
||||
return pickExistingRoutePath(vm.$router, candidates) || candidates[candidates.length - 1]
|
||||
}
|
||||
@@ -56,8 +56,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCc, readCc } from '@/api/hrm/cc';
|
||||
import applyTypeMinix from '@/views/hrm/minix/applyTypeMinix.js';
|
||||
import { listCc, readCc, unreadCc } from '@/api/hrm/cc';
|
||||
|
||||
export default {
|
||||
name: 'HrmFlowCc',
|
||||
@@ -114,6 +114,14 @@ export default {
|
||||
this.$modal.msgSuccess('已标记已读')
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
// 标记未读
|
||||
handleUnread(row) {
|
||||
unreadCc(row.ccId).then(() => {
|
||||
this.$modal.msgSuccess('已标记为未读')
|
||||
// 刷新当前列表(这条记录会移到未读列表)
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
handleRefresh () {
|
||||
this.getList()
|
||||
|
||||
323
ruoyi-ui/src/views/hrm/myApply.vue
Normal file
323
ruoyi-ui/src/views/hrm/myApply.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="my-apply-page">
|
||||
<section class="stats-strip">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">总申请</div>
|
||||
<div class="stat-value">{{ summary.total }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">审批中</div>
|
||||
<div class="stat-value warning">{{ summary.running }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">已通过</div>
|
||||
<div class="stat-value success">{{ summary.approved }}</div>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<section class="filters">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<div class="filter-row">
|
||||
<el-select v-model="query.bizType" placeholder="申请类型" clearable size="small" style="width: 130px" @change="handleFilterChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="请假" value="leave" />
|
||||
<el-option label="出差" value="travel" />
|
||||
<el-option label="用印" value="seal" />
|
||||
<el-option label="报销" value="reimburse" />
|
||||
<el-option label="拨款" value="appropriation" />
|
||||
</el-select>
|
||||
<el-select v-model="query.status" placeholder="状态" clearable size="small" style="width: 130px" @change="handleFilterChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="审批中" value="running" />
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
<el-option label="已撤销" value="revoked" />
|
||||
</el-select>
|
||||
<el-input v-model="query.keyword" placeholder="输入申请标题/备注关键词" size="small" clearable style="width: 260px" @keyup.enter.native="handleFilterChange" />
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleFilterChange">查询</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<el-card shadow="never" class="table-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span>申请列表</span>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe @row-dblclick="goDetail" empty-text="暂无申请记录">
|
||||
<el-table-column label="申请类型" min-width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.bizType)">{{ getTypeText(scope.row.bizType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请名称" min-width="240" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ getRowTitle(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发起人" min-width="140" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.nickName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusType(scope.row)" size="small">{{ statusText(scope.row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发起时间" prop="createTime" min-width="160">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="goDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="showEarlyEndButton(scope.row)"
|
||||
type="text"
|
||||
size="mini"
|
||||
style="color: #e6a23c"
|
||||
:loading="earlyEndLoadingId === scope.row.bizId"
|
||||
@click.stop="handleEarlyEnd(scope.row)"
|
||||
>
|
||||
提前结束
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:current-page="query.pageNum"
|
||||
:page-size="query.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listMyApply } from '@/api/hrm/myApply'
|
||||
import { earlyEndTravel } from '@/api/hrm/travel'
|
||||
import applyTypeMinix from '@/views/hrm/minix/applyTypeMinix.js'
|
||||
|
||||
export default {
|
||||
name: 'HrmMyApply',
|
||||
mixins: [applyTypeMinix],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
earlyEndLoadingId: null,
|
||||
list: [],
|
||||
total: 0,
|
||||
summary: {
|
||||
total: 0,
|
||||
running: 0,
|
||||
approved: 0
|
||||
},
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 50,
|
||||
bizType: '',
|
||||
status: '',
|
||||
keyword: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
getRowTitle (row) {
|
||||
return row.title || row.remark || '-'
|
||||
},
|
||||
isTravelCompleted (row) {
|
||||
if (!row || row.bizType !== 'travel') return false
|
||||
const endTime = row.endTime ? new Date(row.endTime).getTime() : 0
|
||||
const now = Date.now()
|
||||
return Boolean(row.actualEndTime) || (endTime && endTime <= now)
|
||||
},
|
||||
showEarlyEndButton (row) {
|
||||
if (!row || row.bizType !== 'travel') return false
|
||||
if (row.actualEndTime) return false
|
||||
const endTime = row.endTime ? new Date(row.endTime).getTime() : 0
|
||||
return row.status === 'approved' && endTime > Date.now()
|
||||
},
|
||||
statusText (row) {
|
||||
const status = row?.status
|
||||
if (row?.bizType === 'travel' && status === 'approved') {
|
||||
return this.isTravelCompleted(row) ? '已完成' : '进行中'
|
||||
}
|
||||
const map = { draft: '草稿', running: '审批中', pending: '审批中', approved: '已通过', rejected: '已驳回', revoked: '已撤销', finished: '已完成' }
|
||||
return map[status] || status || '-'
|
||||
},
|
||||
statusType (row) {
|
||||
const status = row?.status
|
||||
if (row?.bizType === 'travel' && status === 'approved') {
|
||||
return this.isTravelCompleted(row) ? 'success' : 'warning'
|
||||
}
|
||||
const map = { draft: 'info', running: 'warning', pending: 'warning', approved: 'success', rejected: 'danger', revoked: 'danger', finished: 'success' }
|
||||
return map[status] || 'info'
|
||||
},
|
||||
formatDate (val) {
|
||||
if (!val) return ''
|
||||
const d = new Date(val)
|
||||
const p = n => (n < 10 ? `0${n}` : n)
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
||||
},
|
||||
handleFilterChange () {
|
||||
this.query.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
resetQuery () {
|
||||
this.query = { pageNum: 1, pageSize: 10, bizType: '', status: '', keyword: '' }
|
||||
this.loadList()
|
||||
},
|
||||
async loadList () {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await listMyApply({
|
||||
pageNum: this.query.pageNum,
|
||||
pageSize: this.query.pageSize,
|
||||
bizType: this.query.bizType || undefined,
|
||||
status: this.query.status || undefined,
|
||||
keyword: this.query.keyword || undefined
|
||||
})
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.summary.total = res.total || 0
|
||||
this.summary.running = (res.rows || []).filter(i => ['running', 'pending'].includes(i.status) || (i.bizType === 'travel' && i.status === 'approved' && !this.isTravelCompleted(i))).length
|
||||
this.summary.approved = (res.rows || []).filter(i => i.status === 'approved' && !(i.bizType === 'travel' && !this.isTravelCompleted(i))).length
|
||||
} catch (err) {
|
||||
console.error('加载我的申请失败:', err)
|
||||
this.$message.error('加载我的申请失败')
|
||||
this.list = []
|
||||
this.total = 0
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSizeChange (val) {
|
||||
this.query.pageSize = val
|
||||
this.query.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
handleCurrentChange (val) {
|
||||
this.query.pageNum = val
|
||||
this.loadList()
|
||||
},
|
||||
async handleEarlyEnd (row) {
|
||||
this.$confirm('确认提前结束本次出差吗?结束后的实际时间将记录为当前时间。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
this.earlyEndLoadingId = row.bizId
|
||||
try {
|
||||
await earlyEndTravel(row.bizId)
|
||||
this.$message.success('提前结束成功')
|
||||
await this.loadList()
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '提前结束失败')
|
||||
} finally {
|
||||
this.earlyEndLoadingId = null
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
goDetail (row) {
|
||||
if (!row) return
|
||||
const routeMap = {
|
||||
leave: '/hrm/HrmLeaveDetail',
|
||||
travel: '/hrm/HrmTravelDetail',
|
||||
seal: '/hrm/HrmSealDetail',
|
||||
reimburse: '/hrm/HrmReimburseDetail',
|
||||
appropriation: '/hrm/HrmAppropriationDetail'
|
||||
}
|
||||
const path = routeMap[row.bizType]
|
||||
if (!path) {
|
||||
this.$message.warning('暂不支持该申请类型的详情页')
|
||||
return
|
||||
}
|
||||
this.$router.push({ path, query: { bizId: row.bizId } })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-apply-page {
|
||||
padding: 16px 20px 32px;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
|
||||
.stats-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid #e6e8ed;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin-top: 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.warning { color: #e6a23c; }
|
||||
.success { color: #67c23a; }
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
border: 1px solid #d7d9df;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stats-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -65,8 +65,8 @@
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="目的地" prop="destination">
|
||||
<el-input v-model="form.destination" placeholder="城市/地址/项目现场" />
|
||||
<div class="hint-text">请填写具体目的地,便于审批人判断出差必要性</div>
|
||||
<amap-city-select v-model="form.destination" placeholder="请选择出差城市" />
|
||||
<div class="hint-text">请通过地图或搜索选择具体城市,便于审批人判断出差必要性</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="block-title">出差说明</div>
|
||||
@@ -223,13 +223,15 @@ import { ccFlowTask, listFlowNode, listFlowTemplate } from '@/api/hrm/flow'
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
import UserMultiSelect from '@/components/UserSelect/multi.vue'
|
||||
import UserSelect from '@/components/UserSelect/single.vue'
|
||||
import AmapCitySelect from '@/components/AmapCitySelect/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'HrmTravelRequest',
|
||||
components: {
|
||||
UserSelect,
|
||||
FileUpload,
|
||||
UserMultiSelect
|
||||
UserMultiSelect,
|
||||
AmapCitySelect
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,39 @@
|
||||
<BizDetailContainer :bizId="currentBizId" bizType="travel" :preview="preview">
|
||||
<template slot-scope="{ detail }">
|
||||
<div>
|
||||
<!-- ===== 新增:提前结束按钮区域 ===== -->
|
||||
<div class="action-buttons" v-if="showEarlyEndButton(detail)">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
icon="el-icon-finished"
|
||||
:loading="earlyEndLoading"
|
||||
@click="handleEarlyEnd"
|
||||
>
|
||||
提前结束
|
||||
</el-button>
|
||||
<span class="hint-text">提前结束将把当前时间记录为实际结束时间</span>
|
||||
</div>
|
||||
<div v-if="detail.actualEndTime" class="early-end-info">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon>
|
||||
<template slot="default">
|
||||
该出差已于 {{ formatDate(detail.actualEndTime) }} 提前结束
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div v-else-if="isTravelCompleted(detail)" class="early-end-info">
|
||||
<el-alert
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon>
|
||||
<template slot="default">
|
||||
该出差已完成
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
<!-- 出差时间与行程 -->
|
||||
<div class="block-title">出差时间与行程</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
@@ -41,6 +74,7 @@
|
||||
<script>
|
||||
import FilePreview from "@/components/FilePreview/index.vue";
|
||||
import BizDetailContainer from '@/views/hrm/components/BizDetailContainer/index.vue';
|
||||
import { earlyEndTravel } from '@/api/hrm/travel'
|
||||
|
||||
export default {
|
||||
name: 'TravelDetail',
|
||||
@@ -53,7 +87,11 @@ export default {
|
||||
BizDetailContainer,
|
||||
FilePreview
|
||||
},
|
||||
name: 'HrmTravelDetail',
|
||||
data() {
|
||||
return {
|
||||
earlyEndLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentBizId () {
|
||||
return this.bizId || this.$route?.params?.bizId || this.$route?.query?.bizId || this.$route?.params?.id
|
||||
@@ -66,7 +104,44 @@ export default {
|
||||
const p = n => (n < 10 ? `0${n}` : n)
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
||||
},
|
||||
isTravelCompleted (detail) {
|
||||
if (!detail) return false
|
||||
const endTime = detail.endTime ? new Date(detail.endTime).getTime() : 0
|
||||
const now = Date.now()
|
||||
return Boolean(detail.actualEndTime) || (endTime && endTime <= now)
|
||||
},
|
||||
showEarlyEndButton(detail) {
|
||||
if (!detail) return false
|
||||
if (detail.actualEndTime) return false
|
||||
const status = detail.status
|
||||
const endTime = detail.endTime ? new Date(detail.endTime).getTime() : 0
|
||||
const now = Date.now()
|
||||
return status === 'approved' && endTime > now
|
||||
},
|
||||
|
||||
handleEarlyEnd() {
|
||||
this.$confirm('确认提前结束本次出差吗?结束后的实际时间将记录为当前时间。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
this.earlyEndLoading = true
|
||||
try {
|
||||
const bizId = this.currentBizId
|
||||
await earlyEndTravel(bizId)
|
||||
this.$message.success('提前结束成功')
|
||||
this.$emit('refresh')
|
||||
this.$forceUpdate()
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '提前结束失败')
|
||||
} finally {
|
||||
this.earlyEndLoading = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -375,4 +450,17 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.action-buttons {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #fdf6ec;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.early-end-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
204
ruoyi-ui/src/views/oa/city/index.vue
Normal file
204
ruoyi-ui/src/views/oa/city/index.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
|
||||
<el-form-item label="城市名称" prop="cityName">
|
||||
<el-input
|
||||
v-model="queryParams.cityName"
|
||||
placeholder="请输入城市名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</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="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格取消所有固定宽度,100%自适应铺满容器 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="cityList"
|
||||
@selection-change="handleSelectionChange"
|
||||
border stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" align="center" />
|
||||
<el-table-column label="序号" type="index" align="center" />
|
||||
<el-table-column prop="cityCode" label="城市编码" align="center" />
|
||||
<el-table-column prop="countryName" label="国家" align="center" />
|
||||
<el-table-column prop="cityName" label="城市名称" align="center" />
|
||||
<el-table-column prop="provinceName" label="所属省份" align="center" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.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="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="国家" prop="countryName">
|
||||
<el-input v-model="form.countryName" placeholder="请输入国家(如:中国)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="城市编码" prop="cityCode">
|
||||
<el-input v-model="form.cityCode" placeholder="请输入城市编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="城市名称" prop="cityName">
|
||||
<el-input v-model="form.cityName" placeholder="请输入城市名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属省份" prop="provinceName">
|
||||
<el-input v-model="form.provinceName" placeholder="请输入所属省份" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCity, getCity, delCity, addCity, updateCity } from "@/api/system/city";
|
||||
import Pagination from "@/components/Pagination";
|
||||
|
||||
export default {
|
||||
name: "City",
|
||||
components: { Pagination },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
cityList: [],
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 50, // 已改成每页50条
|
||||
cityName: null
|
||||
},
|
||||
ids: [],
|
||||
multiple: true,
|
||||
title: "",
|
||||
open: false,
|
||||
form: {},
|
||||
rules: {
|
||||
cityCode: [{ required: true, message: "城市编码不能为空", trigger: "blur" }],
|
||||
cityName: [{ required: true, message: "城市名称不能为空", trigger: "blur" }]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listCity(this.queryParams).then(response => {
|
||||
this.cityList = response.rows || [];
|
||||
this.total = response.total || 0;
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
resetQuery() {
|
||||
this.queryParams = { pageNum: 1, pageSize: 50, cityName: null };
|
||||
this.getList();
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.cityId);
|
||||
this.multiple = !selection.length;
|
||||
},
|
||||
handleAdd() {
|
||||
this.form = {};
|
||||
this.open = true;
|
||||
this.title = "添加城市";
|
||||
},
|
||||
handleUpdate(row) {
|
||||
getCity(row.cityId).then(response => {
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改城市";
|
||||
});
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.cityId) {
|
||||
updateCity(this.form).then(() => {
|
||||
this.$message.success("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
} else {
|
||||
addCity(this.form).then(() => {
|
||||
this.$message.success("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
const cityIds = row.cityId || this.ids.join(",");
|
||||
this.$confirm("确认删除选中的城市吗?", "提示", { type: "warning" }).then(() => {
|
||||
delCity(cityIds).then(() => {
|
||||
this.$message.success("删除成功");
|
||||
this.getList();
|
||||
});
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局弱化边框、阴影、降低层级 */
|
||||
.app-container {
|
||||
padding: 12px;
|
||||
}
|
||||
.el-table {
|
||||
box-shadow: none !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.el-table th, .el-table td {
|
||||
border-color: #f0f2f5 !important;
|
||||
}
|
||||
/* 按钮缩小、弱化样式 */
|
||||
.el-button--mini {
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* 降低弹窗层级柔和化 */
|
||||
.el-dialog {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="app-container dashboard2" v-loading="pageLoading">
|
||||
<div class="dashboard-shell">
|
||||
<div class="layout">
|
||||
<!-- 左侧 20%:项目列表 -->
|
||||
<div class="left">
|
||||
@@ -27,31 +28,33 @@
|
||||
class="project-item"
|
||||
:class="{ active: String(p.projectId) === String(currentProjectId) }"
|
||||
@click="handleSelectProject(p)"
|
||||
:title="p.projectName"
|
||||
>
|
||||
<div class="project-main">
|
||||
<div class="project-name text-ellipsis">{{ p.projectName }}</div>
|
||||
<div class="project-meta text-ellipsis">
|
||||
<span v-if="p.projectCode" class="code">{{ p.projectCode }}</span>
|
||||
<span class="time">{{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-stats">
|
||||
<span class="stat-item">任务 {{ p.taskFinishCount || 0 }}/{{ p.taskTotalCount || 0 }}</span>
|
||||
<span class="stat-item">进度 {{ p.scheduleFinishCount || 0 }}/{{ p.scheduleTotalCount || 0 }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="project-progress-row"
|
||||
:title="'当前进度:' + projectListScheduleLine(p)"
|
||||
>
|
||||
<span class="project-progress-label">当前进度:</span><span class="project-progress-value">{{ projectListScheduleLine(p) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!projectList || projectList.length === 0" class="left-empty">
|
||||
<el-empty :image-size="72" description="暂无项目"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="left-pager">
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:total="projectTotal"
|
||||
:current-page.sync="projectQuery.pageNum"
|
||||
:page-size.sync="projectQuery.pageSize"
|
||||
@current-change="getProjectList"
|
||||
/>
|
||||
</div>
|
||||
<!-- 分页组件 -->
|
||||
<pagination :total="projectTotal" :page.sync="projectQuery.pageNum" :limit.sync="projectQuery.pageSize" layout="prev,pager, next" pagerCount="3"
|
||||
@pagination="getProjectList" style="margin-top: 10px;" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧 80% -->
|
||||
@@ -99,7 +102,11 @@
|
||||
:row-config="{ isHover: true }"
|
||||
>
|
||||
<vxe-column field="projectCode" title="代号" width="72"></vxe-column>
|
||||
<vxe-column field="taskTitle" title="任务主题" min-width="96"></vxe-column>
|
||||
<vxe-column field="taskTitle" title="任务主题" min-width="96">
|
||||
<template #default="{ row }">
|
||||
<span class="dashboard-link" @click.stop="goToTaskCenter(row)">{{ row.taskTitle || '-' }}</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="scheduleProgress" title="对应进度" min-width="112">
|
||||
<template #default="{ row }">
|
||||
<div v-if="scheduleProgressUnlinked(row)" class="schedule-progress-wrap">
|
||||
@@ -108,7 +115,9 @@
|
||||
<div
|
||||
v-else
|
||||
class="schedule-progress-wrap schedule-progress-wrap--linked"
|
||||
:class="{ 'is-link': scheduleProgressClick(row) }"
|
||||
:title="scheduleProgressTitle(row)"
|
||||
@click.stop="goToPaceFromTaskRow(row)"
|
||||
>
|
||||
<el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag">
|
||||
{{ scheduleStepStatusLabel(row) }}
|
||||
@@ -160,7 +169,40 @@
|
||||
|
||||
<!-- 下:进度板块 — Tab1 进度明细表(默认);Tab2 原思维导图 + 图例 -->
|
||||
<div class="panel schedule-panel">
|
||||
<el-tabs v-model="scheduleViewTab" class="schedule-panel-tabs">
|
||||
<div v-if="currentProjectId" class="schedule-board-summary schedule-board-summary--pace-like">
|
||||
<el-row>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<div style="font-size: small;">
|
||||
<span style="color:#d0d0d0 ">项目名:</span>
|
||||
<span style="color: #409eff;">{{ projectDisplayName }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div style="font-size: small;">
|
||||
<span style="color:#d0d0d0 ">项目负责人:</span>
|
||||
<span>{{ projectManagerName }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div style="font-size: small;">
|
||||
<span style="color:#d0d0d0 ">当前进度:</span>
|
||||
<span>{{ dashboardScheduleSummary }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div style="font-size: small;">
|
||||
<span style="color:#d0d0d0 ">项目状态:</span>
|
||||
<span v-if="projectIsTop" style="color: #ff4d4f;">重点关注</span>
|
||||
<span v-else>一般项目</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-tabs v-model="scheduleViewTab" class="schedule-panel-tabs" @tab-click="onScheduleTabClick">
|
||||
<el-tab-pane label="进度明细" name="list">
|
||||
<!-- 进度数据:表格自然撑开高度,由 .progress-table-scroll 单独承担纵向/横向滚动 -->
|
||||
<div class="progress-table-pane" v-loading="xmindLoading">
|
||||
@@ -172,14 +214,18 @@
|
||||
<div v-else class="progress-table-scroll">
|
||||
<el-table
|
||||
:data="scheduleStepsForTable"
|
||||
size="mini"
|
||||
|
||||
border
|
||||
stripe
|
||||
class="progress-step-el-table"
|
||||
>
|
||||
<el-table-column prop="firstLevelNode" label="一级节点" min-width="112" show-overflow-tooltip />
|
||||
<el-table-column prop="secondLevelNode" label="二级节点" min-width="112" show-overflow-tooltip />
|
||||
<el-table-column prop="stepName" label="步骤名称" min-width="128" show-overflow-tooltip />
|
||||
<el-table-column prop="stepName" label="步骤名称" min-width="128" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<el-button type="text" size="mini" @click="goToPaceFromStepRow(row)">{{ row.stepName || '-' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="92" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag :type="stepStatusTagType(row)" size="mini">{{ stepStatusLabel(row) }}</el-tag>
|
||||
@@ -194,6 +240,12 @@
|
||||
<el-table-column label="负责人" min-width="96" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">{{ formatStepResponsible(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="{ row }">
|
||||
|
||||
<el-button size="text" @click="openStepById(row.trackId)" >详情 </el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,9 +258,10 @@
|
||||
<el-empty :image-size="80" description="暂无进度步骤数据"></el-empty>
|
||||
</div>
|
||||
<xmind
|
||||
ref="dashboardMindXmind"
|
||||
v-else-if="scheduleViewTab === 'mind'"
|
||||
:list="stepList"
|
||||
height="100%"
|
||||
height="1200px"
|
||||
dashboard-mode
|
||||
@refresh="onXmindRefresh"
|
||||
/>
|
||||
@@ -238,12 +291,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Xmind from '@/views/oa/project/pace/components/xmind.vue'
|
||||
|
||||
import { resolveOaPaceCenterPath, resolveOaTaskCenterPath } from '@/utils/oaMenuNavigate'
|
||||
export default {
|
||||
name: 'OaProjectDashboard2',
|
||||
components: { Xmind },
|
||||
@@ -265,8 +319,31 @@ export default {
|
||||
'projectTotal',
|
||||
'taskQuery',
|
||||
'taskList',
|
||||
'stepList'
|
||||
'stepList',
|
||||
'projectDetail'
|
||||
]),
|
||||
projectDisplayName () {
|
||||
const p = this.projectDetail
|
||||
return (p && p.projectName) ? String(p.projectName) : ''
|
||||
},
|
||||
projectManagerName () {
|
||||
const p = this.projectDetail
|
||||
return (p && p.functionary) ? String(p.functionary) : ''
|
||||
},
|
||||
projectIsTop () {
|
||||
const p = this.projectDetail
|
||||
if (!p) return false
|
||||
const top = p.isTop
|
||||
return top === true || top === 1 || top === '1'
|
||||
},
|
||||
/** 与 pace/step 中 scheduleSummary 文案规则一致 */
|
||||
dashboardScheduleSummary () {
|
||||
const list = this.stepList || []
|
||||
const totalCount = list.length
|
||||
const completedCount = list.filter((item) => item.status === 2).length
|
||||
const pendingCount = list.filter((item) => item.status === 1).length
|
||||
return `已完成(${completedCount})+ 待验收(${pendingCount}) / 总节点数(${totalCount})`
|
||||
},
|
||||
filteredTaskList () {
|
||||
const list = this.taskList || []
|
||||
const code = (this.taskQuery.projectCode || '').trim().toLowerCase()
|
||||
@@ -304,6 +381,18 @@ export default {
|
||||
this.getProjectList()
|
||||
},
|
||||
methods: {
|
||||
/** 进度导图 Tab 可见后触发 ECharts 重新测量 */
|
||||
onScheduleTabClick (tab) {
|
||||
if (!tab || tab.name !== 'mind') return
|
||||
this.$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const comp = this.$refs.dashboardMindXmind
|
||||
if (comp && typeof comp.scheduleResize === 'function') {
|
||||
comp.scheduleResize()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
async getProjectList () {
|
||||
this.projectLoading = true
|
||||
try {
|
||||
@@ -333,7 +422,28 @@ export default {
|
||||
} finally {
|
||||
this.pageLoading = false
|
||||
this.xmindLoading = false
|
||||
if (this.scheduleViewTab === 'mind') {
|
||||
this.$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const comp = this.$refs.dashboardMindXmind
|
||||
if (comp && typeof comp.scheduleResize === 'function') {
|
||||
comp.scheduleResize()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
openTaskById (taskId) {
|
||||
if (!taskId) return
|
||||
this.$router.push({ path: '/task/task/allocation', query: { taskId } })
|
||||
},
|
||||
|
||||
openStepById (trackId) {
|
||||
if (!trackId) return
|
||||
const projectId = this.currentProjectId
|
||||
this.$router.push({ path: '/step/files', query: { trackId, projectId } })
|
||||
},
|
||||
|
||||
resetTaskPage () {
|
||||
@@ -360,6 +470,16 @@ export default {
|
||||
} finally {
|
||||
this.pageLoading = false
|
||||
this.xmindLoading = false
|
||||
if (this.scheduleViewTab === 'mind') {
|
||||
this.$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const comp = this.$refs.dashboardMindXmind
|
||||
if (comp && typeof comp.scheduleResize === 'function') {
|
||||
comp.scheduleResize()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -411,6 +531,67 @@ export default {
|
||||
await this.refreshCurrent()
|
||||
},
|
||||
|
||||
goToTaskCenter (row) {
|
||||
if (!row || row.taskId == null || row.taskId === '') {
|
||||
return
|
||||
}
|
||||
const path = resolveOaTaskCenterPath(this)
|
||||
this.$router.push({ path, query: { taskId: String(row.taskId) } })
|
||||
},
|
||||
|
||||
resolveScheduleIdByTaskTrack (trackId) {
|
||||
if (trackId == null || trackId === '') {
|
||||
return null
|
||||
}
|
||||
const hit = (this.stepList || []).find((s) => String(s.trackId) === String(trackId))
|
||||
return hit && hit.scheduleId != null ? hit.scheduleId : null
|
||||
},
|
||||
|
||||
goToPaceFromTaskRow (row) {
|
||||
if (this.scheduleProgressUnlinked(row)) {
|
||||
this.$message.warning('该任务未关联进度')
|
||||
return
|
||||
}
|
||||
const scheduleId = this.resolveScheduleIdByTaskTrack(row.trackId)
|
||||
if (!scheduleId || !this.currentProjectId) {
|
||||
this.$message.warning('未找到对应进度主表')
|
||||
return
|
||||
}
|
||||
const path = resolveOaPaceCenterPath(this)
|
||||
this.$router.push({
|
||||
path,
|
||||
query: {
|
||||
projectId: String(this.currentProjectId),
|
||||
scheduleId: String(scheduleId),
|
||||
trackId: String(row.trackId),
|
||||
tabNode: row.tabNode != null ? String(row.tabNode) : '',
|
||||
firstLevelNode: row.firstLevelNode != null ? String(row.firstLevelNode) : ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goToPaceFromStepRow (row) {
|
||||
if (!row || row.scheduleId == null || row.scheduleId === '' || !this.currentProjectId) {
|
||||
this.$message.warning('缺少进度或项目信息')
|
||||
return
|
||||
}
|
||||
const query = {
|
||||
projectId: String(this.currentProjectId),
|
||||
scheduleId: String(row.scheduleId)
|
||||
}
|
||||
if (row.trackId != null && row.trackId !== '') {
|
||||
query.trackId = String(row.trackId)
|
||||
}
|
||||
if (row.tabNode) {
|
||||
query.tabNode = String(row.tabNode)
|
||||
}
|
||||
if (row.firstLevelNode) {
|
||||
query.firstLevelNode = String(row.firstLevelNode)
|
||||
}
|
||||
const path = resolveOaPaceCenterPath(this)
|
||||
this.$router.push({ path, query })
|
||||
},
|
||||
|
||||
noop () {},
|
||||
|
||||
/** 无 track_id 或无步骤名称 → 未关联(后端联表 oa_project_schedule_step,use_flag=1) */
|
||||
@@ -448,6 +629,10 @@ export default {
|
||||
return `${this.scheduleStepStatusLabel(row)} ${this.scheduleProgressPath(row)}`.trim()
|
||||
},
|
||||
|
||||
scheduleProgressClick (row) {
|
||||
return !!(row && row.trackId != null && String(row.trackId).trim() !== '')
|
||||
},
|
||||
|
||||
/** oa_project_schedule_step.status:与任务表「对应进度」标签一致 */
|
||||
stepStatusLabel (row) {
|
||||
return this.scheduleStepStatusLabel({ scheduleStatus: row && row.status })
|
||||
@@ -456,6 +641,14 @@ export default {
|
||||
return this.scheduleStepTagType({ scheduleStatus: row && row.status })
|
||||
},
|
||||
|
||||
/** 左侧列表:与进度中心一致的「当前进度」文案(数据来自 list scheduleStats) */
|
||||
projectListScheduleLine (p) {
|
||||
const total = Number(p && p.scheduleStepTotal != null ? p.scheduleStepTotal : 0)
|
||||
const done = Number(p && p.scheduleStepCompleted != null ? p.scheduleStepCompleted : 0)
|
||||
const pend = Number(p && p.scheduleStepPendingAccept != null ? p.scheduleStepPendingAccept : 0)
|
||||
return `已完成(${done})+ 待验收(${pend}) / 总节点数(${total})`
|
||||
},
|
||||
|
||||
formatDate (val) {
|
||||
if (!val) return '-'
|
||||
const s = String(val)
|
||||
@@ -545,13 +738,34 @@ export default {
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
min-height: 42px;
|
||||
min-height: 52px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
/* 单行展示:缩小字号 + 不换行,仍过长则省略号(悬停 title 看全文) */
|
||||
.project-progress-row {
|
||||
margin-top: 3px;
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.project-progress-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.project-progress-value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.project-item:hover {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
@@ -581,7 +795,9 @@ export default {
|
||||
margin-right: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.stat-item{
|
||||
margin-right:5px;
|
||||
}
|
||||
.left-pager {
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
@@ -663,7 +879,14 @@ export default {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.schedule-board-summary--pace-like {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 12px 8px;
|
||||
margin: -10px -10px 0;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.schedule-panel-tabs {
|
||||
@@ -726,7 +949,7 @@ export default {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height:300px;
|
||||
height:600px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -781,6 +1004,16 @@ export default {
|
||||
|
||||
.schedule-progress-wrap--linked {
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-link {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.schedule-progress-status-tag {
|
||||
@@ -810,7 +1043,7 @@ export default {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 320px;
|
||||
min-height: 1600px;
|
||||
}
|
||||
|
||||
.xmind-wrap :deep(.xmind-box--dashboard) {
|
||||
@@ -818,11 +1051,12 @@ export default {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 1600px;
|
||||
}
|
||||
|
||||
.xmind-wrap :deep(.xmind-container) {
|
||||
flex: 1;
|
||||
min-height: 300px !important;
|
||||
min-height: 1580px !important;
|
||||
}
|
||||
|
||||
.mind-legend-aside {
|
||||
|
||||
@@ -87,6 +87,15 @@ export default {
|
||||
tabNode: this.defaultTabNode,
|
||||
firstLevelNode: this.defaultFirstLevelNode
|
||||
});
|
||||
},
|
||||
/** 外部(路由深链等)同步选中进度类别与一级分类,并通知父级更新筛选 */
|
||||
setSelection (tabNode, firstLevelNode) {
|
||||
this.defaultTabNode = tabNode != null ? String(tabNode) : "";
|
||||
this.defaultFirstLevelNode = firstLevelNode != null ? String(firstLevelNode) : "";
|
||||
this.$emit("change", {
|
||||
tabNode: this.defaultTabNode,
|
||||
firstLevelNode: this.defaultFirstLevelNode
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,6 +107,11 @@ export default {
|
||||
type: Boolean | Number,
|
||||
default: false
|
||||
},
|
||||
/** 打开进度详情时由路由传入:定位左侧分类与表格筛选(tabNode / firstLevelNode / trackId) */
|
||||
initialStepFocus: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StepTable,
|
||||
@@ -289,8 +294,44 @@ export default {
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
initialStepFocus: {
|
||||
handler () {
|
||||
this.$nextTick(() => this.applyInitialStepFocus());
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyInitialStepFocus () {
|
||||
const hint = this.initialStepFocus;
|
||||
if (!hint || !this.projectScheduleStepList.length) {
|
||||
return;
|
||||
}
|
||||
let tabNode = hint.tabNode != null ? String(hint.tabNode) : "";
|
||||
let firstLevelNode = hint.firstLevelNode != null ? String(hint.firstLevelNode) : "";
|
||||
if ((!tabNode || !firstLevelNode) && hint.trackId != null && hint.trackId !== "") {
|
||||
const st = this.projectScheduleStepList.find(
|
||||
(s) => String(s.trackId) === String(hint.trackId)
|
||||
);
|
||||
if (st) {
|
||||
tabNode = st.tabNode != null ? String(st.tabNode) : tabNode;
|
||||
firstLevelNode = st.firstLevelNode != null ? String(st.firstLevelNode) : firstLevelNode;
|
||||
}
|
||||
}
|
||||
if (!tabNode) {
|
||||
return;
|
||||
}
|
||||
this.viewMode = "table";
|
||||
this.$nextTick(() => {
|
||||
const menu = this.$refs.menuSelectRef;
|
||||
if (menu && typeof menu.setSelection === "function") {
|
||||
menu.setSelection(tabNode, firstLevelNode);
|
||||
} else {
|
||||
this.defaultTabNode = tabNode;
|
||||
this.defaultFirstLevelNode = firstLevelNode;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 查询项目进度步骤跟踪列表 */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
@@ -298,6 +339,7 @@ export default {
|
||||
this.projectScheduleStepList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
this.$nextTick(() => this.applyInitialStepFocus());
|
||||
});
|
||||
},
|
||||
handleOverview () {
|
||||
|
||||
@@ -90,10 +90,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 容器高度,综合看板等场景可传 100% 以撑满父级 */
|
||||
/** 容器高度 */
|
||||
height: {
|
||||
type: String,
|
||||
default: '800px'
|
||||
default: '4800px'
|
||||
},
|
||||
/** 综合看板:紧凑、防重叠、三色状态、小圆点 */
|
||||
dashboardMode: {
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
return {
|
||||
width: '100%',
|
||||
height: this.height,
|
||||
minHeight: this.dashboardMode ? '300px' : '240px'
|
||||
minHeight: this.dashboardMode ? '1200px' : '240px'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -332,24 +332,24 @@ export default {
|
||||
type: 'tree',
|
||||
data: [treeData],
|
||||
/* 与折线图区域一致:留白、白底在容器上 */
|
||||
...(dm ? { left: '1%', right: '5%', top: '2%', bottom: '2%' } : {}),
|
||||
...(dm ? { left: '3%', right: '8%', top: '4%', bottom: '4%' } : {}),
|
||||
symbol: 'circle',
|
||||
...(dm ? {} : { symbolSize: 6 }),
|
||||
edgeShape: dm ? 'polyline' : 'curve',
|
||||
edgeForkPosition: dm ? '74%' : '50%',
|
||||
edgeForkPosition: dm ? '68%' : '50%',
|
||||
orient: 'LR',
|
||||
initialTreeDepth: 4,
|
||||
initialTreeDepth: 3,
|
||||
roam: true,
|
||||
scaleLimit: dm ? { min: 0.22, max: 5 } : undefined,
|
||||
expandAndCollapse: false,
|
||||
scaleLimit: dm ? { min: 0.3, max: 4 } : undefined,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: dm ? 11 : 12,
|
||||
fontWeight: 400,
|
||||
position: 'left',
|
||||
position: 'top',
|
||||
verticalAlign: 'middle',
|
||||
...(dm ? { align: 'right' } : {}),
|
||||
distance: 8,
|
||||
overflow: 'none',
|
||||
distance: 6,
|
||||
overflow: 'break',
|
||||
lineHeight: dm ? 15 : 14,
|
||||
color: dm ? '#606266' : undefined
|
||||
},
|
||||
@@ -358,46 +358,59 @@ export default {
|
||||
*/
|
||||
levels: dm
|
||||
? [
|
||||
{
|
||||
symbolSize: 18,
|
||||
itemStyle: { borderWidth: 2, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'top',
|
||||
distance: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
width: 140,
|
||||
overflow: 'break',
|
||||
lineHeight: 16,
|
||||
padding: [4, 8, 4, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 14,
|
||||
itemStyle: { borderWidth: 1.5, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'top',
|
||||
distance: 6,
|
||||
fontSize: 12,
|
||||
width: 180,
|
||||
overflow: 'break',
|
||||
lineHeight: 15,
|
||||
padding: [3, 6, 3, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 10,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff', shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.1)' },
|
||||
label: {
|
||||
position: 'top',
|
||||
verticalAlign: 'bottom',
|
||||
distance: 6,
|
||||
fontSize: 11,
|
||||
width: 180,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
padding: [3, 6, 3, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 6,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'left',
|
||||
distance: 8,
|
||||
fontSize: 11,
|
||||
width: 118,
|
||||
position: 'bottom',
|
||||
verticalAlign: 'top',
|
||||
distance: 4,
|
||||
fontSize: 10,
|
||||
width: 140,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
padding: [2, 6, 2, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 5,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff' },
|
||||
label: {
|
||||
position: 'left',
|
||||
distance: 10,
|
||||
fontSize: 11,
|
||||
width: 160,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
padding: [2, 8, 2, 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
symbolSize: 4,
|
||||
itemStyle: { borderWidth: 1, borderColor: '#fff', shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.1)' },
|
||||
label: {
|
||||
position: 'right',
|
||||
verticalAlign: 'middle',
|
||||
align: 'left',
|
||||
distance: 12,
|
||||
fontSize: 11,
|
||||
width: 232,
|
||||
overflow: 'break',
|
||||
lineHeight: 15,
|
||||
padding: [2, 8, 2, 8],
|
||||
formatter: (p) => this.wrapLabelText(p.name, 17)
|
||||
lineHeight: 13,
|
||||
padding: [2, 4, 2, 4]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -408,17 +421,17 @@ export default {
|
||||
{ symbolSize: 3 }
|
||||
],
|
||||
lineStyle: {
|
||||
width: dm ? 1 : 1.2,
|
||||
curveness: dm ? 0.1 : 0.3,
|
||||
color: dm ? '#e4e7ed' : '#ccc'
|
||||
width: 1,
|
||||
curveness: 0.5,
|
||||
color: '#c0c4cc'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'descendant',
|
||||
lineStyle: { width: 2, color: '#409eff' },
|
||||
itemStyle: dm ? { shadowBlur: 6, shadowColor: 'rgba(64,158,255,0.45)' } : undefined
|
||||
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(64,158,255,0.5)' }
|
||||
},
|
||||
expandAndCollapse: true,
|
||||
animationDuration: 280
|
||||
|
||||
animationDuration: 300
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -140,7 +140,8 @@
|
||||
<div style="padding:0 20px">
|
||||
<project-schedule-step :scheduleId="scheduleDetail.scheduleId" :master="scheduleDetail.functionary"
|
||||
:projectName="scheduleDetail.projectName" :projectStatus="scheduleDetail.projectStatus"
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId" />
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId"
|
||||
:initial-step-focus="scheduleStepFocusHint" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
@@ -153,7 +154,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { listProject } from "@/api/oa/project";
|
||||
import { addByProjectId, delProjectSchedule, listProjectSchedule, updateProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { addByProjectId, delProjectSchedule, getProjectSchedule, listProjectSchedule, updateProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect/index.vue";
|
||||
import UserSelect from "@/components/UserSelect/index.vue";
|
||||
@@ -194,12 +195,34 @@ export default {
|
||||
recentProjects: [],
|
||||
scheduleDetail: {},
|
||||
userList: [],
|
||||
postponeDrawer: false
|
||||
postponeDrawer: false,
|
||||
/** 综合看板等深链:打开抽屉后传给 step,用于选中进度类别/一级节点 */
|
||||
scheduleStepFocusHint: null
|
||||
};
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route.query': {
|
||||
handler (newQ, oldQ) {
|
||||
this.applyPaceRouteQueryBeforeFetch();
|
||||
const n = newQ || {};
|
||||
const o = oldQ || {};
|
||||
if (n.scheduleId != null && n.scheduleId !== '') {
|
||||
this.handleQuery();
|
||||
return;
|
||||
}
|
||||
const np = n.projectId != null ? String(n.projectId) : '';
|
||||
const op = o.projectId != null ? String(o.projectId) : '';
|
||||
if (np !== '' && np !== op) {
|
||||
this.handleQuery();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.currentUser = this.$store.state.user
|
||||
this.applyPaceRouteQueryBeforeFetch();
|
||||
this.getList();
|
||||
this.getProjectList();
|
||||
this.getAllUser();
|
||||
@@ -209,8 +232,58 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 关闭细节窗口
|
||||
applyPaceRouteQueryBeforeFetch () {
|
||||
const q = this.$route.query || {};
|
||||
if (q.projectId != null && q.projectId !== '') {
|
||||
this.queryParams.projectId = q.projectId;
|
||||
}
|
||||
},
|
||||
clearPaceDeepLinkQuery () {
|
||||
const q = { ...(this.$route.query || {}) };
|
||||
delete q.scheduleId;
|
||||
delete q.trackId;
|
||||
delete q.tabNode;
|
||||
delete q.firstLevelNode;
|
||||
if (Object.keys(q).length) {
|
||||
this.$router.replace({ path: this.$route.path, query: q });
|
||||
} else {
|
||||
this.$router.replace({ path: this.$route.path });
|
||||
}
|
||||
},
|
||||
async maybeOpenScheduleFromRoute () {
|
||||
const q = this.$route.query || {};
|
||||
if (!q.scheduleId) {
|
||||
return;
|
||||
}
|
||||
let row = this.scheduleList.find((s) => String(s.scheduleId) === String(q.scheduleId));
|
||||
if (!row) {
|
||||
try {
|
||||
const res = await getProjectSchedule(q.scheduleId);
|
||||
row = res.data;
|
||||
} catch (e) {
|
||||
this.$modal.msgError('未找到该进度或无权访问');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const hasFocus =
|
||||
(q.trackId != null && q.trackId !== '') ||
|
||||
(q.tabNode != null && q.tabNode !== '') ||
|
||||
(q.firstLevelNode != null && q.firstLevelNode !== '');
|
||||
this.scheduleStepFocusHint = hasFocus
|
||||
? {
|
||||
trackId: q.trackId != null ? String(q.trackId) : '',
|
||||
tabNode: q.tabNode != null ? String(q.tabNode) : '',
|
||||
firstLevelNode: q.firstLevelNode != null ? String(q.firstLevelNode) : ''
|
||||
}
|
||||
: null;
|
||||
this.getScheduleDetail(row);
|
||||
this.$nextTick(() => this.clearPaceDeepLinkQuery());
|
||||
},
|
||||
closeDetailShow (done) {
|
||||
this.scheduleStepFocusHint = null;
|
||||
this.getList();
|
||||
done()
|
||||
},
|
||||
@@ -245,14 +318,13 @@ export default {
|
||||
},
|
||||
getList () {
|
||||
this.loading = true
|
||||
|
||||
console.log(this.queryParams, this.searchTime)
|
||||
/* 日期搜索条件 */
|
||||
if (this.searchTime && this.searchTime.length) {
|
||||
this.queryParams.startTime = this.getDateStr(this.searchTime[0])
|
||||
this.queryParams.endTime = this.getDateStr(this.searchTime[1])
|
||||
}
|
||||
|
||||
this.queryParams.projectId = this.$route.query.projectId?this.$route.query.projectId:null
|
||||
this.queryParams.trackId = this.$route.query.trackId?this.$route.query.trackId:null
|
||||
listProjectSchedule(this.queryParams).then(res => {
|
||||
this.scheduleList = res.rows
|
||||
this.total = res.total
|
||||
@@ -273,6 +345,7 @@ export default {
|
||||
this.recentProjects = cache
|
||||
/* 3. 结束 loading */
|
||||
this.loading = false
|
||||
this.maybeOpenScheduleFromRoute()
|
||||
})
|
||||
},
|
||||
getProjectList () {
|
||||
@@ -293,6 +366,7 @@ export default {
|
||||
})
|
||||
},
|
||||
handleDetail (row) {
|
||||
this.scheduleStepFocusHint = null;
|
||||
// 把当前项目放到数组最前面,去重
|
||||
const list = [row, ...this.recentProjects.filter(p => p.projectId !== row.projectId)];
|
||||
// 只保留前 2 条
|
||||
|
||||
@@ -82,12 +82,42 @@
|
||||
<!-- 添加或修改项目报工对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
<!-- 今日报工提示 -->
|
||||
<el-alert v-if="hasTodayReport" title="今日已报工" type="warning" description="您今天已有报工记录,提交后将修改今日报工内容" show-icon
|
||||
<el-alert v-if="hasTodayReport" title="今日已报工" type="warning" description="您今天已有报工记录,提交后将修改今日报工内容" show-icon
|
||||
:closable="false" style="margin-bottom: 20px;">
|
||||
</el-alert>
|
||||
|
||||
<!-- 定位权限说明 -->
|
||||
<el-alert
|
||||
v-if="workPlaceLocateError && !form.reportId"
|
||||
title="定位获取失败"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;">
|
||||
<div slot="default">
|
||||
<p style="margin: 5px 0; font-weight: bold;">如果浏览器无法获取定位,请按以下步骤操作:</p>
|
||||
<ol style="margin: 5px 0; padding-left: 20px; line-height: 1.8;">
|
||||
<li>在浏览器地址栏输入:<code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">edge://flags/#unsafely-treat-insecure-origin-as-secure</code>(Edge) 或 <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">chrome://flags/#unsafely-treat-insecure-origin-as-secure</code>(Chrome)</li>
|
||||
<li>找到选项 <strong>"Insecure origins treated as secure"</strong></li>
|
||||
<li>输入您的站点地址:<code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">http://49.232.154.205</code>,然后设置为 <strong>Enabled</strong></li>
|
||||
<li>重启浏览器,再尝试点击「重新获取定位」按钮</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-alert>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="96px">
|
||||
<el-form-item label="工作地点" prop="workPlace">
|
||||
<el-input v-model="form.workPlace" placeholder="请输入工作地点" />
|
||||
<el-input
|
||||
v-model="form.workPlace"
|
||||
readonly
|
||||
:placeholder="workPlaceLoading ? '正在获取定位…' : '请点击「重新获取定位」获取工作地点 (不可手动输入)'"
|
||||
>
|
||||
<el-button
|
||||
slot="append"
|
||||
icon="el-icon-location-outline"
|
||||
:loading="workPlaceLoading"
|
||||
@click="refreshWorkPlace"
|
||||
>重新获取定位</el-button>
|
||||
</el-input>
|
||||
<div v-if="workPlaceLocateError" class="work-place-error">{{ workPlaceLocateError }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否出差" prop="isTrip">
|
||||
<el-radio-group v-model="form.isTrip">
|
||||
@@ -148,12 +178,19 @@ import { listDept } from "@/api/system/dept";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import ProjectReportDetail from "@/views/oa/project/report/components/ProjectReportDetail.vue";
|
||||
import {
|
||||
EMPTY_GEOCODE,
|
||||
geolocationUserMessage,
|
||||
resolveWorkPlaceFromBrowser
|
||||
} from "@/utils/geolocationWorkPlace";
|
||||
|
||||
export default {
|
||||
name: "ProjectReport",
|
||||
components: { ProjectReportDetail, ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
workPlaceLoading: false,
|
||||
workPlaceLocateError: "",
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
detailVisible: false,
|
||||
@@ -230,6 +267,50 @@ export default {
|
||||
this.getUserList();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {{ silent?: boolean, force?: boolean }} options
|
||||
*/
|
||||
async syncWorkPlaceFromGeolocation (options = {}) {
|
||||
const silent = !!options.silent;
|
||||
const force = !!options.force;
|
||||
if (!force && this.form && this.form.reportId) {
|
||||
return;
|
||||
}
|
||||
this.workPlaceLoading = true;
|
||||
this.workPlaceLocateError = "";
|
||||
try {
|
||||
const text = await resolveWorkPlaceFromBrowser();
|
||||
this.$set(this.form, "workPlace", text);
|
||||
if (!silent) {
|
||||
this.$modal.msgSuccess("已根据定位更新工作地点");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[projectReport] 工作地点定位失败", e);
|
||||
const msg = geolocationUserMessage(e);
|
||||
this.workPlaceLocateError = msg;
|
||||
this.$set(this.form, "workPlace", undefined);
|
||||
const isBrowserGeoError =
|
||||
e &&
|
||||
(e.code === 1 ||
|
||||
e.code === 2 ||
|
||||
e.code === 3 ||
|
||||
e.message === "BROWSER_UNSUPPORTED" ||
|
||||
e.message === EMPTY_GEOCODE);
|
||||
if (isBrowserGeoError) {
|
||||
this.$modal.msgWarning(msg);
|
||||
}
|
||||
} finally {
|
||||
this.workPlaceLoading = false;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.validateField("workPlace");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refreshWorkPlace () {
|
||||
this.syncWorkPlaceFromGeolocation({ silent: false, force: true });
|
||||
},
|
||||
/** 格式化日期为 yyyy-MM-dd 格式 */
|
||||
formatDate (date) {
|
||||
const year = date.getFullYear();
|
||||
@@ -258,11 +339,15 @@ export default {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "补录项目报工";
|
||||
this.$nextTick(() => {
|
||||
this.syncWorkPlaceFromGeolocation({ silent: true, force: false });
|
||||
});
|
||||
},
|
||||
|
||||
/** 检查今日报工 */
|
||||
/** 检查今日报工(须返回 Promise,供新增时 .finally 打开弹窗) */
|
||||
checkTodayReport () {
|
||||
getTodayProjectReport().then(response => {
|
||||
return getTodayProjectReport()
|
||||
.then(response => {
|
||||
if (response.data && response.data.reportId) {
|
||||
this.hasTodayReport = true;
|
||||
this.todayReportId = response.data.reportId;
|
||||
@@ -274,7 +359,8 @@ export default {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
}
|
||||
}).catch(() => {
|
||||
})
|
||||
.catch(() => {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
});
|
||||
@@ -313,6 +399,8 @@ export default {
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.workPlaceLoading = false;
|
||||
this.workPlaceLocateError = "";
|
||||
this.form = {
|
||||
reportId: undefined,
|
||||
handler: undefined,
|
||||
@@ -359,9 +447,15 @@ export default {
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.suppVisible = false;
|
||||
this.checkTodayReport();
|
||||
this.open = true;
|
||||
this.title = "添加项目报工";
|
||||
this.checkTodayReport().finally(() => {
|
||||
this.open = true;
|
||||
this.$nextTick(() => {
|
||||
if (!this.form.reportId) {
|
||||
this.syncWorkPlaceFromGeolocation({ silent: true, force: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
@@ -473,4 +567,11 @@ export default {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.work-place-error {
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
239
ruoyi-ui/src/views/oa/project/travelCompare.vue
Normal file
239
ruoyi-ui/src/views/oa/project/travelCompare.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="travel-compare-page">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">姓名</span>
|
||||
<el-input
|
||||
v-model="queryParams.nickName"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="请输入姓名"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">地点</span>
|
||||
<el-input
|
||||
v-model="queryParams.workPlace"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="请输入报工地点"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">时间</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="small"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="table-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span>报工审核</span>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe empty-text="暂无比对记录">
|
||||
<el-table-column label="姓名" prop="nickName" min-width="140" />
|
||||
<el-table-column label="日期" prop="compareDate" min-width="120">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.compareDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否出差" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.isTrip === 1 ? '是' : '否' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报工地点" prop="workPlace" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="实际出差地点" prop="travelPlace" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="机器比对结果" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
:class="[
|
||||
'result-tag',
|
||||
scope.row.compareResult === '通过' ? 'pass' : (scope.row.compareResult === '异常' ? 'fail' : 'neutral')
|
||||
]"
|
||||
>
|
||||
{{ scope.row.compareResult }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next, sizes, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listTravelCompare } from '@/api/oa/projectCompare'
|
||||
|
||||
export default {
|
||||
name: 'OaProjectTravelCompare',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 50,
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
nickName: '',
|
||||
workPlace: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
formatDateOnly (date) {
|
||||
const p = n => (n < 10 ? `0${n}` : `${n}`)
|
||||
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`
|
||||
},
|
||||
formatDate (val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
if (Number.isNaN(d.getTime())) return '-'
|
||||
return this.formatDateOnly(d)
|
||||
},
|
||||
handleQuery () {
|
||||
this.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
async loadList () {
|
||||
this.loading = true
|
||||
try {
|
||||
const [start, end] = this.dateRange || []
|
||||
const res = await listTravelCompare(
|
||||
start || null,
|
||||
end || null,
|
||||
this.queryParams.nickName,
|
||||
this.queryParams.workPlace,
|
||||
this.pageNum,
|
||||
this.pageSize
|
||||
)
|
||||
this.list = res?.rows || []
|
||||
this.total = res?.total || 0
|
||||
} catch (err) {
|
||||
console.error('加载报工审核失败:', err)
|
||||
this.$message.error('加载报工审核失败')
|
||||
this.list = []
|
||||
this.total = 0
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleCurrentChange (pageNum) {
|
||||
this.pageNum = pageNum
|
||||
this.loadList()
|
||||
},
|
||||
handleSizeChange (pageSize) {
|
||||
this.pageSize = pageSize
|
||||
this.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
resetQuery () {
|
||||
this.pageNum = 1
|
||||
this.pageSize = 50
|
||||
this.queryParams.nickName = ''
|
||||
this.queryParams.workPlace = ''
|
||||
this.dateRange = []
|
||||
this.loadList()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.travel-compare-page {
|
||||
padding: 16px 20px 32px;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
border: 1px solid #d7d9df;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
color: #2b2f36;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.result-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pass {
|
||||
background: #e8f7ee;
|
||||
color: #1f8b4c;
|
||||
}
|
||||
|
||||
.fail {
|
||||
background: #fdecec;
|
||||
color: #d93026;
|
||||
}
|
||||
|
||||
.neutral {
|
||||
background: #eef2f7;
|
||||
color: #5c6b7a;
|
||||
}
|
||||
</style>
|
||||
@@ -499,6 +499,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 转换数据为级联选择器格式
|
||||
convertToCascader (rows) {
|
||||
if (!rows || !rows.length) return [];
|
||||
@@ -614,13 +615,15 @@ export default {
|
||||
},
|
||||
/** 查询项目管理列表 */
|
||||
getList () {
|
||||
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
...this.queryParams,
|
||||
taskId:this.$route.query.taskId!=null?this.$route.query.taskId:null,
|
||||
beginTime: this.queryParams.searchTime[0] ? this.queryParams.searchTime[0] + ' 00:00:00' : undefined,
|
||||
finishTime: this.queryParams.searchTime[1] ? this.queryParams.searchTime[1] + ' 23:59:59' : undefined
|
||||
}
|
||||
console.log(payload, this.queryParams, 'payload');
|
||||
|
||||
listTask(payload).then(response => {
|
||||
this.taskList = response.rows;
|
||||
this.total = response.total;
|
||||
|
||||
24
sql/oa_city.sql
Normal file
24
sql/oa_city.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- 城市管理表 oa_city
|
||||
DROP TABLE IF EXISTS oa_city;
|
||||
CREATE TABLE oa_city (
|
||||
city_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '城市主键ID',
|
||||
country_name VARCHAR(64) NOT NULL COMMENT '国家',
|
||||
city_name VARCHAR(64) NOT NULL COMMENT '城市',
|
||||
status BIGINT DEFAULT 1 COMMENT '状态 1正常 0禁用',
|
||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
del_flag TINYINT DEFAULT 0 COMMENT '删除标志:0=正常,1=已删除',
|
||||
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (city_id),
|
||||
KEY idx_country_name (country_name),
|
||||
KEY idx_city_name (city_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市管理表';
|
||||
|
||||
INSERT INTO oa_city (country_name, city_name, status, remark) VALUES
|
||||
('中国', '北京', 1, '默认城市'),
|
||||
('中国', '上海', 1, '默认城市'),
|
||||
('中国', '广州', 1, '默认城市'),
|
||||
('中国', '深圳', 1, '默认城市'),
|
||||
('中国', '杭州', 1, '默认城市');
|
||||
Reference in New Issue
Block a user