推送项目重构代码
This commit is contained in:
@@ -55,6 +55,7 @@ public class SysUserController extends BaseController {
|
||||
private final ISysRoleService roleService;
|
||||
private final ISysPostService postService;
|
||||
private final ISysDeptService deptService;
|
||||
private final com.ruoyi.oa.im.ImSendService imSendService;
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
@@ -156,7 +157,12 @@ public class SysUserController extends BaseController {
|
||||
return R.fail("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
|
||||
}
|
||||
user.setPassword(BCrypt.hashpw(user.getPassword()));
|
||||
return toAjax(userService.insertUser(user));
|
||||
int rows = userService.insertUser(user);
|
||||
if (rows > 0 && StringUtils.isNotEmpty(user.getPhonenumber())) {
|
||||
imSendService.bindOrRegister(user.getUserId(), user.getPhonenumber(),
|
||||
StringUtils.isNotEmpty(user.getNickName()) ? user.getNickName() : user.getUserName());
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.system.service.ISysUserDashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户工作台布局
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/user/dashboard")
|
||||
public class SysUserDashboardController extends BaseController {
|
||||
|
||||
private final ISysUserDashboardService dashboardService;
|
||||
|
||||
/** 获取当前用户的工作台布局 */
|
||||
@GetMapping
|
||||
public R<Map<String, Object>> getLayout() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
Map<String, Object> data = new HashMap<>(2);
|
||||
data.put("layout", dashboardService.getLayout(userId));
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
/** 保存当前用户的工作台布局 */
|
||||
@PutMapping
|
||||
public R<Void> saveLayout(@RequestBody @NotNull LayoutBody body) {
|
||||
dashboardService.saveLayout(LoginHelper.getUserId(), body.getLayout());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/** 重置当前用户的工作台为默认布局 */
|
||||
@PostMapping("/reset")
|
||||
public R<Void> resetLayout() {
|
||||
dashboardService.resetLayout(LoginHelper.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/** 管理员:保存系统默认工作台布局(影响所有未自定义的用户) */
|
||||
@SaCheckRole("admin")
|
||||
@PutMapping("/default")
|
||||
public R<Void> saveDefault(@RequestBody @NotNull LayoutBody body) {
|
||||
dashboardService.saveDefaultLayout(body.getLayout());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
public static class LayoutBody {
|
||||
private String layout;
|
||||
public String getLayout() { return layout; }
|
||||
public void setLayout(String layout) { this.layout = layout; }
|
||||
}
|
||||
}
|
||||
@@ -174,3 +174,17 @@ sms:
|
||||
signName: 测试
|
||||
# 腾讯专用
|
||||
sdkAppId:
|
||||
|
||||
--- # OpenIM 集成
|
||||
openim:
|
||||
# OpenIM 核心 API 地址(admin token、发消息)
|
||||
api-url: http://49.232.154.205:10002
|
||||
# OpenIM chat 业务 API 地址(手机号注册/查询,可选)
|
||||
chat-url: http://49.232.154.205:10008
|
||||
# 与 OpenIM share.yml 中保持一致的 secret
|
||||
secret: openIM123
|
||||
# 管理员 userID(chat share.yml 中 adminUserID)
|
||||
admin-user-id: imAdmin
|
||||
# 发送系统消息时显示的发送者
|
||||
notification-sender: imAdmin
|
||||
enabled: true
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.oa.audit.mapper.OaWarehouseAuditLogMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 库房操作日志
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/oa/warehouseAudit")
|
||||
public class OaWarehouseAuditController extends BaseController {
|
||||
|
||||
private final OaWarehouseAuditLogMapper mapper;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<OaWarehouseAuditLog> list(OaWarehouseAuditLog query, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<OaWarehouseAuditLog> qw = Wrappers.lambdaQuery();
|
||||
if (query.getOpType() != null && !query.getOpType().isEmpty()) {
|
||||
qw.eq(OaWarehouseAuditLog::getOpType, query.getOpType());
|
||||
}
|
||||
if (query.getRefType() != null && !query.getRefType().isEmpty()) {
|
||||
qw.eq(OaWarehouseAuditLog::getRefType, query.getRefType());
|
||||
}
|
||||
if (query.getRefId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getRefId, query.getRefId());
|
||||
}
|
||||
if (query.getProjectId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getProjectId, query.getProjectId());
|
||||
}
|
||||
if (query.getWarehouseId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getWarehouseId, query.getWarehouseId());
|
||||
}
|
||||
if (query.getOpUserId() != null) {
|
||||
qw.eq(OaWarehouseAuditLog::getOpUserId, query.getOpUserId());
|
||||
}
|
||||
qw.orderByDesc(OaWarehouseAuditLog::getOpTime);
|
||||
return TableDataInfo.build(mapper.selectPage(pageQuery.build(), qw));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 库房统一操作日志 sys_oa_warehouse_audit_log
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_oa_warehouse_audit_log")
|
||||
public class OaWarehouseAuditLog {
|
||||
|
||||
@TableId(value = "log_id")
|
||||
private Long logId;
|
||||
|
||||
/** 操作类型,见 {@link OpType} */
|
||||
private String opType;
|
||||
|
||||
/** 关联实体类型:requirement/task/master/warehouse */
|
||||
private String refType;
|
||||
|
||||
/** 关联实体 ID */
|
||||
private Long refId;
|
||||
|
||||
private Long projectId;
|
||||
private Long warehouseId;
|
||||
|
||||
private String summary;
|
||||
private String beforeJson;
|
||||
private String afterJson;
|
||||
|
||||
private Long opUserId;
|
||||
private String opUserName;
|
||||
private Date opTime;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.oa.audit.mapper.OaWarehouseAuditLogMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 业务侧统一打点入口。
|
||||
* 所有调用异步执行,失败不影响主业务。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OaWarehouseAuditService {
|
||||
|
||||
private final OaWarehouseAuditLogMapper mapper;
|
||||
|
||||
/** 最常用:单参摘要 */
|
||||
@Async
|
||||
public void log(String opType, String refType, Long refId, String summary) {
|
||||
log(opType, refType, refId, null, null, summary, null, null);
|
||||
}
|
||||
|
||||
/** 完整版 */
|
||||
@Async
|
||||
public void log(String opType, String refType, Long refId,
|
||||
Long projectId, Long warehouseId,
|
||||
String summary, String beforeJson, String afterJson) {
|
||||
try {
|
||||
OaWarehouseAuditLog entity = new OaWarehouseAuditLog();
|
||||
entity.setOpType(opType);
|
||||
entity.setRefType(refType);
|
||||
entity.setRefId(refId);
|
||||
entity.setProjectId(projectId);
|
||||
entity.setWarehouseId(warehouseId);
|
||||
entity.setSummary(summary == null ? "" : summary);
|
||||
entity.setBeforeJson(beforeJson);
|
||||
entity.setAfterJson(afterJson);
|
||||
try {
|
||||
entity.setOpUserId(LoginHelper.getUserId());
|
||||
entity.setOpUserName(LoginHelper.getNickName());
|
||||
} catch (Exception ignore) {
|
||||
// 定时任务/系统调用下无登录态
|
||||
entity.setOpUserName("system");
|
||||
}
|
||||
mapper.insert(entity);
|
||||
} catch (Exception e) {
|
||||
log.warn("[audit] write failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
30
ruoyi-oa/src/main/java/com/ruoyi/oa/audit/OpType.java
Normal file
30
ruoyi-oa/src/main/java/com/ruoyi/oa/audit/OpType.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
/**
|
||||
* 库房操作类型枚举。值用 String,便于库里直接读懂。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public final class OpType {
|
||||
private OpType() {}
|
||||
|
||||
public static final String REQ_CREATE = "REQ_CREATE"; // 采购需求新建
|
||||
public static final String REQ_UPDATE = "REQ_UPDATE"; // 采购需求修改
|
||||
public static final String REQ_DONE = "REQ_DONE"; // 采购需求完成
|
||||
public static final String REQ_CANCEL = "REQ_CANCEL"; // 采购需求取消
|
||||
public static final String REQ_DELETE = "REQ_DELETE"; // 采购需求删除
|
||||
|
||||
public static final String TASK_CREATE = "TASK_CREATE"; // 车间采购创建
|
||||
public static final String TASK_DONE = "TASK_DONE"; // 车间采购完成
|
||||
public static final String TASK_CANCEL = "TASK_CANCEL"; // 车间采购取消
|
||||
|
||||
public static final String IN = "IN"; // 入库
|
||||
public static final String OUT = "OUT"; // 出库
|
||||
public static final String RETURN = "RETURN"; // 退库
|
||||
public static final String STOCK_ADJUST = "STOCK_ADJUST"; // 库存盘点/修正
|
||||
|
||||
public static final String REF_REQUIREMENT = "requirement";
|
||||
public static final String REF_TASK = "task";
|
||||
public static final String REF_MASTER = "master";
|
||||
public static final String REF_WAREHOUSE = "warehouse";
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseDetail;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseMaster;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseDetailMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMasterMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 查询某个采购需求关联的入库批次列表。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RequirementBatchService {
|
||||
|
||||
private final SysOaWarehouseMasterMapper masterMapper;
|
||||
private final SysOaWarehouseDetailMapper detailMapper;
|
||||
private final SysOaWarehouseMapper warehouseMapper;
|
||||
|
||||
public List<RequirementBatchVo> listByRequirement(Long requirementId) {
|
||||
// 1. 找所有 type=1 (入库) 且 requirement_id 匹配 的 master
|
||||
List<SysOaWarehouseMaster> masters = masterMapper.selectList(
|
||||
Wrappers.<SysOaWarehouseMaster>lambdaQuery()
|
||||
.eq(SysOaWarehouseMaster::getRequirementId, requirementId)
|
||||
.eq(SysOaWarehouseMaster::getType, 1)
|
||||
.eq(SysOaWarehouseMaster::getDelFlag, 0)
|
||||
.orderByDesc(SysOaWarehouseMaster::getSignTime));
|
||||
if (masters.isEmpty()) return Collections.emptyList();
|
||||
|
||||
// 2. 一次性查这些 master 的 detail
|
||||
List<Long> masterIds = masters.stream()
|
||||
.map(SysOaWarehouseMaster::getMasterId).collect(Collectors.toList());
|
||||
List<SysOaWarehouseDetail> details = detailMapper.selectList(
|
||||
Wrappers.<SysOaWarehouseDetail>lambdaQuery()
|
||||
.in(SysOaWarehouseDetail::getMasterId, masterIds)
|
||||
.eq(SysOaWarehouseDetail::getDelFlag, 0));
|
||||
Map<Long, List<SysOaWarehouseDetail>> detailsByMaster = details.stream()
|
||||
.collect(Collectors.groupingBy(SysOaWarehouseDetail::getMasterId));
|
||||
|
||||
// 3. 一次性查涉及的物料
|
||||
Set<Long> warehouseIds = details.stream()
|
||||
.map(SysOaWarehouseDetail::getWarehouseId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, SysOaWarehouse> whMap = warehouseIds.isEmpty()
|
||||
? Collections.emptyMap()
|
||||
: warehouseMapper.selectBatchIds(warehouseIds).stream()
|
||||
.collect(Collectors.toMap(SysOaWarehouse::getId, w -> w));
|
||||
|
||||
// 4. 组装
|
||||
List<RequirementBatchVo> result = new ArrayList<>();
|
||||
for (SysOaWarehouseMaster m : masters) {
|
||||
RequirementBatchVo vo = new RequirementBatchVo();
|
||||
vo.setMasterId(m.getMasterId());
|
||||
vo.setMasterNum(m.getMasterNum());
|
||||
vo.setSignTime(m.getSignTime());
|
||||
vo.setSignUser(m.getSignUser());
|
||||
|
||||
List<SysOaWarehouseDetail> ds = detailsByMaster.getOrDefault(m.getMasterId(), Collections.emptyList());
|
||||
long totalQty = 0L;
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
List<RequirementBatchVo.BatchDetailVo> dvos = new ArrayList<>();
|
||||
List<String> nameParts = new ArrayList<>();
|
||||
for (SysOaWarehouseDetail d : ds) {
|
||||
SysOaWarehouse w = whMap.get(d.getWarehouseId());
|
||||
long qty = d.getAmount() == null ? 0L : d.getAmount();
|
||||
totalQty += qty;
|
||||
BigDecimal unitPrice = d.getSignPrice() == null ? null : BigDecimal.valueOf(d.getSignPrice());
|
||||
if (unitPrice != null) {
|
||||
totalAmount = totalAmount.add(unitPrice.multiply(BigDecimal.valueOf(qty)));
|
||||
}
|
||||
RequirementBatchVo.BatchDetailVo dv = new RequirementBatchVo.BatchDetailVo();
|
||||
dv.setDetailId(d.getId());
|
||||
dv.setWarehouseId(d.getWarehouseId());
|
||||
dv.setAmount(d.getAmount());
|
||||
dv.setSignPrice(unitPrice);
|
||||
if (w != null) {
|
||||
dv.setWarehouseName(w.getName());
|
||||
dv.setModel(w.getModel());
|
||||
dv.setSpecifications(w.getSpecifications());
|
||||
dv.setUnit(w.getUnit());
|
||||
if (nameParts.size() < 3) nameParts.add(w.getName());
|
||||
}
|
||||
dvos.add(dv);
|
||||
}
|
||||
vo.setDetails(dvos);
|
||||
vo.setTotalQty(totalQty);
|
||||
vo.setTotalAmount(totalAmount);
|
||||
String summary = String.join("、", nameParts);
|
||||
if (dvos.size() > 3) summary += " 等 " + dvos.size() + " 项";
|
||||
vo.setSummary(summary);
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.oa.audit;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购需求关联的入库批次(一个需求多批到货)
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
public class RequirementBatchVo {
|
||||
|
||||
/** 入库单 ID */
|
||||
private Long masterId;
|
||||
/** 入库单编号 */
|
||||
private String masterNum;
|
||||
/** 入库时间 */
|
||||
private Date signTime;
|
||||
/** 入库人 */
|
||||
private String signUser;
|
||||
/** 本批次物料总件数(detail.amount 之和) */
|
||||
private Long totalQty;
|
||||
/** 本批次金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 物料概览(前几个名字拼接) */
|
||||
private String summary;
|
||||
/** 行明细 */
|
||||
private List<BatchDetailVo> details;
|
||||
|
||||
@Data
|
||||
public static class BatchDetailVo {
|
||||
private Long detailId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String model;
|
||||
private String specifications;
|
||||
private String unit;
|
||||
private Long amount;
|
||||
private BigDecimal signPrice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.oa.audit.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditLog;
|
||||
|
||||
/**
|
||||
* 审计日志 Mapper
|
||||
* 包路径包含 .mapper,被 @MapperScan("com.ruoyi.**.mapper") 自动扫描
|
||||
*/
|
||||
public interface OaWarehouseAuditLogMapper extends BaseMapperPlus<OaWarehouseAuditLogMapper, OaWarehouseAuditLog, OaWarehouseAuditLog> {
|
||||
}
|
||||
@@ -38,6 +38,17 @@ public class OaRequirementsController extends BaseController {
|
||||
|
||||
private final IOaRequirementsService iOaRequirementsService;
|
||||
|
||||
private final com.ruoyi.oa.audit.RequirementBatchService requirementBatchService;
|
||||
|
||||
/**
|
||||
* 查询采购需求关联的入库批次列表
|
||||
*/
|
||||
@GetMapping("/{requirementId}/batches")
|
||||
public R<java.util.List<com.ruoyi.oa.audit.RequirementBatchVo>> batches(
|
||||
@PathVariable Long requirementId) {
|
||||
return R.ok(requirementBatchService.listByRequirement(requirementId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询OA 需求列表
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_oa_warehouse_log")
|
||||
@TableName("_deprecated_sys_oa_warehouse_log")
|
||||
public class SysOaWarehouseLog extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@@ -89,5 +89,8 @@ public class OaRequirementsVo extends BaseEntity {
|
||||
private String ownerNickName;
|
||||
private String projectName;
|
||||
|
||||
/** 附件文件列表(已联查 sys_oss,每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
|
||||
private String accessoryFiles;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -91,4 +91,11 @@ public class SysOaWarehouseMasterVo {
|
||||
private Long requirementId;
|
||||
private String requirementName;
|
||||
|
||||
/** 物料种类数 */
|
||||
private Integer itemCount;
|
||||
/** 入库/出库总数量 */
|
||||
private Long totalQty;
|
||||
/** 物料概览(GROUP_CONCAT 名称) */
|
||||
private String itemsSummary;
|
||||
|
||||
}
|
||||
|
||||
26
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImBind.java
Normal file
26
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImBind.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* OA-OpenIM 用户绑定
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_user_im_bind")
|
||||
public class ImBind {
|
||||
|
||||
@TableId(value = "user_id")
|
||||
private Long userId;
|
||||
|
||||
private String phone;
|
||||
private String imUserId;
|
||||
private Integer bindStatus;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
88
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImSendService.java
Normal file
88
ruoyi-oa/src/main/java/com/ruoyi/oa/im/ImSendService.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import com.ruoyi.oa.im.mapper.ImBindMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 业务侧统一推送入口。
|
||||
* 业务代码只关心 OA userId,不用碰 OpenIM 概念。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImSendService {
|
||||
|
||||
private final ImBindMapper bindMapper;
|
||||
private final OpenImClient openImClient;
|
||||
|
||||
/**
|
||||
* 为 OA 用户绑定一个 IM 账号。若 IM 侧已有该手机号则跳过创建;否则先在 chat 注册。
|
||||
* 异步执行,新建/绑定失败不影响业务。
|
||||
*/
|
||||
@Async
|
||||
public void bindOrRegister(Long oaUserId, String phone, String nickname) {
|
||||
if (oaUserId == null || phone == null || phone.isEmpty()) { return; }
|
||||
try {
|
||||
ImBind exist = bindMapper.selectById(oaUserId);
|
||||
if (exist != null && exist.getImUserId() != null) {
|
||||
return;
|
||||
}
|
||||
String imUserId = openImClient.registerImUser(phone, nickname);
|
||||
if (imUserId == null) {
|
||||
log.info("[IM] register skipped for OA user {} phone {}", oaUserId, phone);
|
||||
return;
|
||||
}
|
||||
ImBind bind = new ImBind();
|
||||
bind.setUserId(oaUserId);
|
||||
bind.setPhone(phone);
|
||||
bind.setImUserId(imUserId);
|
||||
bind.setBindStatus(1);
|
||||
bindMapper.insert(bind);
|
||||
log.info("[IM] bound OA {} -> IM {}", oaUserId, imUserId);
|
||||
} catch (Exception e) {
|
||||
log.warn("[IM] bindOrRegister failed for OA {} : {}", oaUserId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给 OA 用户推送一条业务通知(自定义消息)。
|
||||
* 异步执行,失败不抛出(业务流程不应被推送阻塞)。
|
||||
*
|
||||
* @param oaUserId OA 用户ID (sys_user.user_id)
|
||||
* @param title 通知标题
|
||||
* @param description 通知内容(手机端通知栏展示)
|
||||
* @param bizType 业务类型,如 "task" / "project" / "salary"
|
||||
* @param bizId 业务主键,前端点通知后可据此跳转
|
||||
* @param route 前端跳转路径(可选)
|
||||
*/
|
||||
@Async
|
||||
public void sendToOaUser(Long oaUserId, String title, String description,
|
||||
String bizType, Object bizId, String route) {
|
||||
if (oaUserId == null) { return; }
|
||||
try {
|
||||
ImBind bind = bindMapper.selectById(oaUserId);
|
||||
if (bind == null || bind.getImUserId() == null
|
||||
|| (bind.getBindStatus() != null && bind.getBindStatus() == 0)) {
|
||||
log.debug("[IM] user {} not bound, skip push", oaUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("bizType", bizType);
|
||||
payload.put("bizId", bizId);
|
||||
if (route != null) { payload.put("route", route); }
|
||||
|
||||
openImClient.sendCustomToUser(bind.getImUserId(), title, description, payload);
|
||||
} catch (Exception e) {
|
||||
log.warn("[IM] push to OA user {} failed: {}", oaUserId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
220
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImClient.java
Normal file
220
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImClient.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* OpenIM 服务端调用封装
|
||||
* <p>
|
||||
* 1. 管理 admin token(带缓存与过期重取)<br>
|
||||
* 2. 发送自定义消息(OA 业务通知)
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OpenImClient {
|
||||
|
||||
/** 自定义消息 contentType,OpenIM 约定 110 以上为自定义 */
|
||||
public static final int CUSTOM_CONTENT_TYPE = 110;
|
||||
|
||||
/** 会话类型:单聊 */
|
||||
public static final int SESSION_SINGLE = 1;
|
||||
|
||||
private final OpenImProperties props;
|
||||
|
||||
/** 内存缓存的 admin token;过期前重取 */
|
||||
private volatile String adminToken;
|
||||
private volatile long adminTokenExpireAt = 0L;
|
||||
|
||||
/** chat 后端管理员 token 缓存 */
|
||||
private volatile String chatAdminToken;
|
||||
private volatile long chatAdminTokenExpireAt = 0L;
|
||||
|
||||
/** 获取 admin token,自动续期 */
|
||||
public String getAdminToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (adminToken != null && now < adminTokenExpireAt - 60_000L) {
|
||||
return adminToken;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (adminToken != null && now < adminTokenExpireAt - 60_000L) {
|
||||
return adminToken;
|
||||
}
|
||||
Map<String, Object> body = new HashMap<>(2);
|
||||
body.put("secret", props.getSecret());
|
||||
body.put("userID", props.getAdminUserId());
|
||||
|
||||
JSONObject resp = postJson(props.getApiUrl() + "/auth/get_admin_token", body, null);
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
throw new RuntimeException("get admin token failed: " + resp);
|
||||
}
|
||||
adminToken = data.getString("token");
|
||||
long expireSec = data.getLongValue("expireTimeSeconds");
|
||||
if (expireSec <= 0) { expireSec = 7 * 24 * 3600; }
|
||||
adminTokenExpireAt = now + expireSec * 1000L;
|
||||
log.info("[OpenIM] admin token refreshed, expires in {}s", expireSec);
|
||||
return adminToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送自定义业务消息到指定 IM userID(单聊,发送者为 admin)。
|
||||
*
|
||||
* @param recvImUserId 接收者 OpenIM userID
|
||||
* @param title 业务标题(如"任务分派")
|
||||
* @param description 简短描述(通知栏展示)
|
||||
* @param payload 额外业务字段(如 taskId、route 等)
|
||||
* @return true 发送成功
|
||||
*/
|
||||
public boolean sendCustomToUser(String recvImUserId, String title, String description,
|
||||
Map<String, Object> payload) {
|
||||
if (!props.isEnabled()) {
|
||||
log.debug("[OpenIM] disabled, skip send to {}", recvImUserId);
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", title);
|
||||
data.put("description", description);
|
||||
if (payload != null) { data.putAll(payload); }
|
||||
|
||||
Map<String, Object> customElem = new HashMap<>(3);
|
||||
customElem.put("data", JSON.toJSONString(data));
|
||||
customElem.put("description", description);
|
||||
customElem.put("extension", "");
|
||||
|
||||
Map<String, Object> content = new HashMap<>();
|
||||
content.put("customElem", customElem);
|
||||
|
||||
Map<String, Object> offlinePush = new HashMap<>();
|
||||
offlinePush.put("title", title);
|
||||
offlinePush.put("desc", description);
|
||||
offlinePush.put("ex", "");
|
||||
offlinePush.put("iOSPushSound", "default");
|
||||
offlinePush.put("iOSBadgeCount", true);
|
||||
|
||||
Map<String, Object> sendMsg = new HashMap<>();
|
||||
sendMsg.put("sendID", props.getNotificationSender());
|
||||
sendMsg.put("recvID", recvImUserId);
|
||||
sendMsg.put("senderNickname", "系统通知");
|
||||
sendMsg.put("senderPlatformID", 1);
|
||||
sendMsg.put("content", content);
|
||||
sendMsg.put("contentType", CUSTOM_CONTENT_TYPE);
|
||||
sendMsg.put("sessionType", SESSION_SINGLE);
|
||||
sendMsg.put("isOnlineOnly", false);
|
||||
sendMsg.put("notOfflinePush", false);
|
||||
sendMsg.put("offlinePushInfo", offlinePush);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("sendMsg", sendMsg);
|
||||
|
||||
JSONObject resp = postJson(props.getApiUrl() + "/msg/send_msg", body, getAdminToken());
|
||||
Integer errCode = resp.getInteger("errCode");
|
||||
if (errCode != null && errCode == 0) {
|
||||
return true;
|
||||
}
|
||||
log.warn("[OpenIM] send_msg failed: {}", resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** chat 后端 admin token */
|
||||
public String getChatAdminToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (chatAdminToken != null && now < chatAdminTokenExpireAt - 60_000L) {
|
||||
return chatAdminToken;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (chatAdminToken != null && now < chatAdminTokenExpireAt - 60_000L) {
|
||||
return chatAdminToken;
|
||||
}
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("account", props.getChatAdminAccount());
|
||||
body.put("password", md5(props.getChatAdminPassword()));
|
||||
JSONObject resp = postJson(props.getChatAdminUrl() + "/account/login", body, null);
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
if (data == null) { throw new RuntimeException("chat admin login failed: " + resp); }
|
||||
chatAdminToken = data.getString("adminToken");
|
||||
// chat admin token 默认 90 天,保守取 7 天续期
|
||||
chatAdminTokenExpireAt = now + 7L * 24 * 3600_000L;
|
||||
return chatAdminToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 chat 后端创建一个新 IM 账号(无需短信验证)。
|
||||
* 成功返回新 userID,已存在返回 null。
|
||||
*/
|
||||
public String registerImUser(String phoneNumber, String nickname) {
|
||||
if (!props.isEnabled()) { return null; }
|
||||
Map<String, Object> user = new HashMap<>();
|
||||
user.put("nickname", nickname == null ? phoneNumber : nickname);
|
||||
user.put("areaCode", props.getDefaultAreaCode());
|
||||
user.put("phoneNumber", phoneNumber);
|
||||
user.put("password", md5(props.getDefaultUserPassword()));
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("verifyCode", "666666");
|
||||
body.put("deviceID", "oa-backend");
|
||||
body.put("platform", 5);
|
||||
body.put("autoLogin", false);
|
||||
body.put("user", user);
|
||||
|
||||
JSONObject resp = postJson(props.getChatUrl() + "/account/register", body, getChatAdminToken());
|
||||
Integer errCode = resp.getInteger("errCode");
|
||||
if (errCode != null && errCode == 0) {
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
return data == null ? null : data.getString("userID");
|
||||
}
|
||||
if (errCode != null && errCode == 20004) {
|
||||
log.info("[OpenIM] phone {} already registered, skip", phoneNumber);
|
||||
return null;
|
||||
}
|
||||
log.warn("[OpenIM] register failed for phone {}: {}", phoneNumber, resp);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String md5(String text) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) { sb.append(String.format("%02x", b)); }
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("md5 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 内部:POST JSON */
|
||||
private JSONObject postJson(String url, Map<String, Object> body, String token) {
|
||||
HttpRequest req = HttpRequest.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("operationID", UUID.randomUUID().toString())
|
||||
.body(JSON.toJSONString(body))
|
||||
.timeout(8000);
|
||||
if (token != null) { req.header("token", token); }
|
||||
try (HttpResponse resp = req.execute()) {
|
||||
String text = resp.body();
|
||||
if (resp.getStatus() / 100 != 2) {
|
||||
log.warn("[OpenIM] http {} {} -> {}", resp.getStatus(), url, text);
|
||||
}
|
||||
return JSON.parseObject(text);
|
||||
} catch (Exception e) {
|
||||
log.error("[OpenIM] call {} failed", url, e);
|
||||
throw new RuntimeException("openim call failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImProperties.java
Normal file
49
ruoyi-oa/src/main/java/com/ruoyi/oa/im/OpenImProperties.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.oa.im;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* OpenIM 配置
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "openim")
|
||||
public class OpenImProperties {
|
||||
|
||||
/** OpenIM 核心 API 地址 */
|
||||
private String apiUrl = "http://49.232.154.205:10002";
|
||||
|
||||
/** OpenIM chat 业务 API 地址 */
|
||||
private String chatUrl = "http://49.232.154.205:10008";
|
||||
|
||||
/** OpenIM chat 管理 API 地址(注册新用户、admin login) */
|
||||
private String chatAdminUrl = "http://49.232.154.205:10009";
|
||||
|
||||
/** chat 后端 admin 账号 */
|
||||
private String chatAdminAccount = "chatAdmin";
|
||||
|
||||
/** chat 后端 admin 密码(md5 后传输;明文配置,登录时自动哈希) */
|
||||
private String chatAdminPassword = "chatAdmin";
|
||||
|
||||
/** 默认区号 */
|
||||
private String defaultAreaCode = "+86";
|
||||
|
||||
/** 新建 IM 用户的默认密码(明文,注册时自动 md5) */
|
||||
private String defaultUserPassword = "123456";
|
||||
|
||||
/** 与 OpenIM share.yml 一致的 secret */
|
||||
private String secret = "openIM123";
|
||||
|
||||
/** 管理员 userID */
|
||||
private String adminUserId = "imAdmin";
|
||||
|
||||
/** 系统通知发送者 userID */
|
||||
private String notificationSender = "imAdmin";
|
||||
|
||||
/** 总开关 */
|
||||
private boolean enabled = true;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.oa.im.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.oa.im.ImBind;
|
||||
|
||||
/**
|
||||
* OA-IM 绑定 Mapper
|
||||
* 包路径包含 .mapper,被 @MapperScan("com.ruoyi.**.mapper") 自动扫描
|
||||
*/
|
||||
public interface ImBindMapper extends BaseMapperPlus<ImBindMapper, ImBind, ImBind> {
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
|
||||
@@ -32,6 +34,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
|
||||
private final OaRequirementsMapper baseMapper;
|
||||
|
||||
private final OaWarehouseAuditService auditService;
|
||||
|
||||
/**
|
||||
* 查询OA 需求
|
||||
*/
|
||||
@@ -85,6 +89,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setRequirementId(add.getRequirementId());
|
||||
auditService.log(OpType.REQ_CREATE, OpType.REF_REQUIREMENT, add.getRequirementId(),
|
||||
"新建采购需求:" + add.getTitle());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -96,7 +102,22 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
public Boolean updateByBo(OaRequirementsBo bo) {
|
||||
OaRequirements update = BeanUtil.toBean(bo, OaRequirements.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean ok = baseMapper.updateById(update) > 0;
|
||||
if (ok) {
|
||||
String op = OpType.REQ_UPDATE;
|
||||
String summary = "修改采购需求:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
if (bo.getStatus() != null) {
|
||||
if (bo.getStatus() == 2) {
|
||||
op = OpType.REQ_DONE;
|
||||
summary = "采购需求完成:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
} else if (bo.getStatus() == 3) {
|
||||
op = OpType.REQ_CANCEL;
|
||||
summary = "采购需求取消:" + (update.getTitle() == null ? "" : update.getTitle());
|
||||
}
|
||||
}
|
||||
auditService.log(op, OpType.REF_REQUIREMENT, update.getRequirementId(), summary);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +135,12 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
boolean ok = baseMapper.deleteBatchIds(ids) > 0;
|
||||
if (ok) {
|
||||
for (Long id : ids) {
|
||||
auditService.log(OpType.REQ_DELETE, OpType.REF_REQUIREMENT, id, "删除采购需求");
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.ruoyi.oa.domain.vo.SysOaTaskVo;
|
||||
import com.ruoyi.oa.mapper.SysOaTaskMapper;
|
||||
import com.ruoyi.oa.service.IOaProjectOperationLogService;
|
||||
import com.ruoyi.oa.service.ISysOaTaskService;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -56,6 +57,8 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final ImSendService imSendService;
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
private IOaProjectOperationLogService operationLogService;
|
||||
@@ -217,6 +220,17 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (flag) {
|
||||
operationLogService.recordLog(add.getProjectId(), 3, add.getTaskId(),
|
||||
add.getTaskTitle(), 1, "新增任务: " + add.getTaskTitle(), null, null);
|
||||
// 推送给被分配的执行人(异步,失败不影响业务)
|
||||
if (!Objects.equals(workerId, LoginHelper.getUserId())) {
|
||||
imSendService.sendToOaUser(
|
||||
workerId,
|
||||
"新任务分派",
|
||||
"您有新任务:" + add.getTaskTitle(),
|
||||
"task",
|
||||
add.getTaskId(),
|
||||
"/task/task?taskId=" + add.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
// 判断是否为报工模式
|
||||
if (bo.getStatus()==1L){
|
||||
@@ -307,6 +321,17 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (task != null) {
|
||||
operationLogService.recordLog(task.getProjectId(), 3, bo.getTaskId(),
|
||||
task.getTaskTitle(), 6, "任务申请延期: " + task.getTaskTitle(), null, null);
|
||||
// 推送给任务创建人(审批人)
|
||||
if (task.getCreateUserId() != null
|
||||
&& !Objects.equals(task.getCreateUserId(), LoginHelper.getUserId())) {
|
||||
imSendService.sendToOaUser(
|
||||
task.getCreateUserId(),
|
||||
"任务申请延期",
|
||||
"任务【" + task.getTaskTitle() + "】申请延期,请审批",
|
||||
"task", task.getTaskId(),
|
||||
"/task/task?taskId=" + task.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
@@ -333,6 +358,16 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
if (ok) {
|
||||
operationLogService.recordLog(sysOaTask.getProjectId(), 3, sysOaTask.getTaskId(),
|
||||
sysOaTask.getTaskTitle(), 7, "任务延期审批通过: " + sysOaTask.getTaskTitle(), null, null);
|
||||
// 推送给执行人,告知延期已批准
|
||||
if (sysOaTask.getWorkerId() != null) {
|
||||
imSendService.sendToOaUser(
|
||||
sysOaTask.getWorkerId(),
|
||||
"任务延期已通过",
|
||||
"任务【" + sysOaTask.getTaskTitle() + "】延期审批已通过",
|
||||
"task", sysOaTask.getTaskId(),
|
||||
"/task/task?taskId=" + sysOaTask.getTaskId()
|
||||
);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseDetail;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import com.ruoyi.oa.domain.SysOaWarehouseLog;
|
||||
import com.ruoyi.oa.domain.bo.ReturnDetailBo;
|
||||
import com.ruoyi.oa.domain.bo.SysOaWarehouseBo;
|
||||
@@ -59,6 +61,9 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
@Autowired
|
||||
private SysOaWarehouseLogMapper warehouseLogMapper;
|
||||
|
||||
@Autowired
|
||||
private OaWarehouseAuditService auditService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询出库单管理
|
||||
@@ -117,6 +122,10 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
lqw.eq(bo.getType() != null, "sowm.type", bo.getType())
|
||||
.eq(bo.getProjectId()!= null, "sowm.project_id", bo.getProjectId())
|
||||
.eq(bo.getReturnType() != null, "sowm.return_type", bo.getReturnType())
|
||||
.eq(bo.getStatus() != null, "sowm.status", bo.getStatus())
|
||||
.eq(bo.getRequirementId() != null, "sowm.requirement_id", bo.getRequirementId())
|
||||
.like(bo.getMasterNum() != null && !bo.getMasterNum().isEmpty(), "sowm.master_num", bo.getMasterNum())
|
||||
.like(bo.getRemark() != null && !bo.getRemark().isEmpty(), "sowm.remark", bo.getRemark())
|
||||
// 其他过滤……
|
||||
.eq("sowm.del_flag", 0)
|
||||
.orderByDesc("sowm.create_time");
|
||||
@@ -154,6 +163,20 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
public Boolean insertByBo(SysOaWarehouseMasterBo bo) {
|
||||
SysOaWarehouseMaster add = BeanUtil.toBean(bo, SysOaWarehouseMaster.class);
|
||||
validEntityBeforeSave(add);
|
||||
|
||||
// 规则 1:出库(type=0)无项目 + 无备注 → 自动写入"办公耗材自动写入"
|
||||
if (add.getType() != null && add.getType() == 0
|
||||
&& add.getProjectId() == null
|
||||
&& (add.getRemark() == null || add.getRemark().trim().isEmpty())) {
|
||||
add.setRemark("办公耗材自动写入");
|
||||
}
|
||||
|
||||
// 规则 2:入库(type=1)必须强关联采购需求(requirement_id)
|
||||
if (add.getType() != null && add.getType() == 1
|
||||
&& add.getRequirementId() == null) {
|
||||
throw new RuntimeException("入库必须关联采购需求,请先选择需求");
|
||||
}
|
||||
|
||||
add.setReturnType(0); // 默认0
|
||||
add.setSignUser(LoginHelper.getNickName());
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
@@ -164,6 +187,16 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
sysOaWarehouseDetailBo.setType(bo.getType());
|
||||
warehouseDetailService.insertByBo(sysOaWarehouseDetailBo);
|
||||
}
|
||||
// 审计:根据 type 写不同操作类型
|
||||
String op = add.getType() == 0 ? OpType.OUT
|
||||
: add.getType() == 1 ? OpType.IN
|
||||
: OpType.RETURN;
|
||||
String typeLabel = add.getType() == 0 ? "出库" : add.getType() == 1 ? "入库" : "退库";
|
||||
auditService.log(op, OpType.REF_MASTER, add.getMasterId(),
|
||||
add.getProjectId(), null,
|
||||
typeLabel + "单 " + (add.getMasterNum() == null ? "" : add.getMasterNum())
|
||||
+ ",共 " + (bo.getWarehouseList() == null ? 0 : bo.getWarehouseList().size()) + " 项",
|
||||
null, null);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -211,13 +244,11 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
|
||||
SysOaWarehouse warehouse = warehouseMapper.selectById(detail.getWarehouseId());
|
||||
warehouse.setInventory(warehouse.getInventory() + dto.getReturnNum());
|
||||
warehouseMapper.updateById(warehouse);
|
||||
// 日志表插入记录
|
||||
SysOaWarehouseLog log = new SysOaWarehouseLog();
|
||||
log.setMasterId(detail.getMasterId());
|
||||
log.setWarehouseId(detail.getWarehouseId());
|
||||
log.setNum(Long.valueOf(dto.getReturnNum()));
|
||||
log.setRemark("退库操作");
|
||||
warehouseLogMapper.insert(log);
|
||||
// 统一审计日志
|
||||
auditService.log(OpType.RETURN, OpType.REF_MASTER, detail.getMasterId(),
|
||||
null, detail.getWarehouseId(),
|
||||
"退库 " + dto.getReturnNum() + " 件,物料 " + warehouse.getName(),
|
||||
null, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaWarehouseMasterMapper;
|
||||
import com.ruoyi.oa.service.ISysOaWarehouseDetailService;
|
||||
import com.ruoyi.oa.service.ISysOaWarehouseMasterService;
|
||||
import com.ruoyi.oa.audit.OaWarehouseAuditService;
|
||||
import com.ruoyi.oa.audit.OpType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -43,6 +45,8 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
|
||||
private final SysOaWarehouseTaskMapper baseMapper;
|
||||
|
||||
private final OaWarehouseAuditService auditService;
|
||||
|
||||
private final SysOaWarehouseMasterMapper masterMapper;
|
||||
|
||||
@Autowired
|
||||
@@ -100,6 +104,11 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setTaskId(add.getTaskId());
|
||||
auditService.log(OpType.TASK_CREATE, OpType.REF_TASK, add.getTaskId(),
|
||||
null, add.getWarehouseId(),
|
||||
"新建车间采购:" + (add.getName() == null ? "" : add.getName())
|
||||
+ " × " + (add.getTaskInventory() == null ? 0 : add.getTaskInventory()),
|
||||
null, null);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@@ -111,7 +120,19 @@ public class SysOaWarehouseTaskServiceImpl implements ISysOaWarehouseTaskService
|
||||
public Boolean updateByBo(SysOaWarehouseTaskBo bo) {
|
||||
SysOaWarehouseTask update = BeanUtil.toBean(bo, SysOaWarehouseTask.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean ok = baseMapper.updateById(update) > 0;
|
||||
if (ok && bo.getTaskStatus() != null) {
|
||||
String op;
|
||||
String label;
|
||||
if (bo.getTaskStatus() == 2) { op = OpType.TASK_DONE; label = "完成"; }
|
||||
else if (bo.getTaskStatus() == 3) { op = OpType.TASK_CANCEL; label = "取消"; }
|
||||
else { op = "TASK_UPDATE"; label = "修改"; }
|
||||
auditService.log(op, OpType.REF_TASK, update.getTaskId(),
|
||||
null, update.getWarehouseId(),
|
||||
label + "车间采购:" + (update.getName() == null ? "" : update.getName()),
|
||||
null, null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.oa.domain.OaSalaryMaster;
|
||||
import com.ruoyi.oa.domain.SysOaRemind;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import com.ruoyi.oa.mapper.OaSalaryMasterMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaRemindMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -19,6 +20,7 @@ public class OaSalaryRemindScheduler {
|
||||
|
||||
private final OaSalaryMasterMapper salaryMasterMapper;
|
||||
private final SysOaRemindMapper remindMapper;
|
||||
private final ImSendService imSendService;
|
||||
|
||||
// 老板用户ID
|
||||
private static final Long BOSS_USER_ID = 1859252208375152641L;
|
||||
@@ -60,6 +62,15 @@ public class OaSalaryRemindScheduler {
|
||||
remind.setRemark(master.getRemark());
|
||||
|
||||
remindMapper.insert(remind);
|
||||
|
||||
// 同时通过 IM 推送给老板
|
||||
imSendService.sendToOaUser(
|
||||
BOSS_USER_ID,
|
||||
"工资审批提醒",
|
||||
remind.getContent(),
|
||||
"salary", master.getMasterId(),
|
||||
"/finance/salary/list"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ruoyi.oa.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.oa.domain.SysOaProject;
|
||||
import com.ruoyi.oa.im.ImSendService;
|
||||
import com.ruoyi.oa.mapper.SysOaProjectMapper;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目到期提醒:每天 09:00 扫描 finishTime 在 [today, today+THRESHOLD_DAYS] 区间且未完结的项目,
|
||||
* 通过 OpenIM 推送给项目负责人。
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectExpiryRemindScheduler {
|
||||
|
||||
/** 提前多少天提醒 */
|
||||
private static final int THRESHOLD_DAYS = 3;
|
||||
|
||||
private final SysOaProjectMapper projectMapper;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ImSendService imSendService;
|
||||
|
||||
@Scheduled(cron = "0 0 9 * * ?")
|
||||
public void notifyExpiringProjects() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
|
||||
Date start = cal.getTime();
|
||||
cal.add(Calendar.DAY_OF_MONTH, THRESHOLD_DAYS + 1);
|
||||
Date end = cal.getTime();
|
||||
|
||||
LambdaQueryWrapper<SysOaProject> qw = Wrappers.lambdaQuery();
|
||||
qw.between(SysOaProject::getFinishTime, start, end);
|
||||
// 已结项的不推("3" 通常表示已完结,按你们的字典调整)
|
||||
qw.ne(SysOaProject::getProjectStatus, "3");
|
||||
List<SysOaProject> projects = projectMapper.selectList(qw);
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (SysOaProject p : projects) {
|
||||
String nickName = p.getFunctionary();
|
||||
if (nickName == null || nickName.isEmpty()) { continue; }
|
||||
|
||||
// functionary 存的是 nickName,反查 user_id
|
||||
SysUser user = userMapper.selectOne(
|
||||
Wrappers.<SysUser>lambdaQuery()
|
||||
.eq(SysUser::getNickName, nickName)
|
||||
.eq(SysUser::getDelFlag, "0")
|
||||
.last("limit 1")
|
||||
);
|
||||
if (user == null) {
|
||||
log.debug("[ProjectExpiry] nickName={} 未找到对应用户,跳过", nickName);
|
||||
continue;
|
||||
}
|
||||
|
||||
String title = "项目即将到期";
|
||||
String desc = String.format("项目【%s】将于 %s 到期,请及时跟进",
|
||||
p.getProjectName(), sdf.format(p.getFinishTime()));
|
||||
|
||||
imSendService.sendToOaUser(
|
||||
user.getUserId(), title, desc,
|
||||
"project", p.getProjectId(),
|
||||
"/oa/project?projectId=" + p.getProjectId()
|
||||
);
|
||||
}
|
||||
log.info("[ProjectExpiry] 推送 {} 个即将到期项目", projects.size());
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
r.*,
|
||||
u1.nick_name AS requester_nick_name,
|
||||
u2.nick_name AS owner_nick_name,
|
||||
p.project_name
|
||||
p.project_name,
|
||||
(
|
||||
SELECT GROUP_CONCAT(CONCAT_WS('|', o.oss_id, COALESCE(o.original_name, o.file_name), o.url)
|
||||
ORDER BY FIND_IN_SET(o.oss_id, r.accessory) SEPARATOR ',,')
|
||||
FROM sys_oss o
|
||||
WHERE r.accessory IS NOT NULL AND r.accessory <> ''
|
||||
AND FIND_IN_SET(o.oss_id, r.accessory)
|
||||
) AS accessory_files
|
||||
FROM oa_requirements r
|
||||
LEFT JOIN sys_user u1 ON r.requester_id = u1.user_id
|
||||
LEFT JOIN sys_user u2 ON r.owner_id = u2.user_id
|
||||
@@ -40,7 +47,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
r.*,
|
||||
u1.nick_name AS requester_nick_name,
|
||||
u2.nick_name AS owner_nick_name,
|
||||
p.project_name
|
||||
p.project_name,
|
||||
(
|
||||
SELECT GROUP_CONCAT(CONCAT_WS('|', o.oss_id, COALESCE(o.original_name, o.file_name), o.url)
|
||||
ORDER BY FIND_IN_SET(o.oss_id, r.accessory) SEPARATOR ',,')
|
||||
FROM sys_oss o
|
||||
WHERE r.accessory IS NOT NULL AND r.accessory <> ''
|
||||
AND FIND_IN_SET(o.oss_id, r.accessory)
|
||||
) AS accessory_files
|
||||
FROM oa_requirements r
|
||||
LEFT JOIN sys_user u1 ON r.requester_id = u1.user_id
|
||||
LEFT JOIN sys_user u2 ON r.owner_id = u2.user_id
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<result property="withdrawLock" column="withdraw_lock"/>
|
||||
<result property="requirementId" column="requirement_id"/>
|
||||
<result property="requirementName" column="requirementName"/>
|
||||
<result property="itemCount" column="item_count"/>
|
||||
<result property="totalQty" column="total_qty"/>
|
||||
<result property="itemsSummary" column="items_summary"/>
|
||||
|
||||
<collection property="warehouseList"
|
||||
column="master_id"
|
||||
@@ -55,12 +58,27 @@
|
||||
sowm.remark,
|
||||
sowm.status,
|
||||
sowm.is_like,
|
||||
sowm.requirement_id,
|
||||
${ew.sqlSelect},
|
||||
sop.project_name,
|
||||
req.title AS requirementName
|
||||
req.title AS requirementName,
|
||||
agg.item_count,
|
||||
agg.total_qty,
|
||||
agg.items_summary
|
||||
FROM sys_oa_warehouse_master sowm
|
||||
LEFT JOIN sys_oa_project sop ON sop.project_id = sowm.project_id
|
||||
LEFT JOIN oa_requirements req ON req.requirement_id = sowm.requirement_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
d.master_id,
|
||||
COUNT(*) AS item_count,
|
||||
IFNULL(SUM(d.amount), 0) AS total_qty,
|
||||
GROUP_CONCAT(w.name ORDER BY d.id SEPARATOR '、') AS items_summary
|
||||
FROM sys_oa_warehouse_detail d
|
||||
LEFT JOIN sys_oa_warehouse w ON w.id = d.warehouse_id
|
||||
WHERE d.del_flag = 0
|
||||
GROUP BY d.master_id
|
||||
) agg ON agg.master_id = sowm.master_id
|
||||
${ew.getCustomSqlSegment}
|
||||
</select>
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.system.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户工作台布局 sys_user_dashboard
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user_dashboard")
|
||||
public class SysUserDashboard extends BaseEntity {
|
||||
|
||||
@TableId(value = "user_id")
|
||||
private Long userId;
|
||||
|
||||
/** 布局 JSON: [{i,x,y,w,h,widgetKey,config}] */
|
||||
private String layoutJson;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ruoyi.system.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.system.domain.SysUserDashboard;
|
||||
|
||||
/**
|
||||
* 用户工作台布局 数据层
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface SysUserDashboardMapper extends BaseMapperPlus<SysUserDashboardMapper, SysUserDashboard, SysUserDashboard> {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
/**
|
||||
* 用户工作台布局 服务层
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface ISysUserDashboardService {
|
||||
|
||||
/** 读取指定用户布局;为空则返回默认布局 */
|
||||
String getLayout(Long userId);
|
||||
|
||||
/** 保存指定用户布局 */
|
||||
void saveLayout(Long userId, String layoutJson);
|
||||
|
||||
/** 重置为默认布局(删除用户自定义记录) */
|
||||
void resetLayout(Long userId);
|
||||
|
||||
/** 读取系统默认布局 */
|
||||
String getDefaultLayout();
|
||||
|
||||
/** 保存系统默认布局(管理员) */
|
||||
void saveDefaultLayout(String layoutJson);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import com.ruoyi.system.domain.SysConfig;
|
||||
import com.ruoyi.system.domain.SysUserDashboard;
|
||||
import com.ruoyi.system.mapper.SysUserDashboardMapper;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import com.ruoyi.system.service.ISysUserDashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 用户工作台布局 服务实现
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SysUserDashboardServiceImpl implements ISysUserDashboardService {
|
||||
|
||||
private static final String DEFAULT_LAYOUT_KEY = "sys.dashboard.defaultLayout";
|
||||
private static final String FALLBACK_LAYOUT = "[]";
|
||||
|
||||
private final SysUserDashboardMapper dashboardMapper;
|
||||
private final ISysConfigService configService;
|
||||
|
||||
@Override
|
||||
public String getLayout(Long userId) {
|
||||
SysUserDashboard entity = dashboardMapper.selectById(userId);
|
||||
if (entity != null && entity.getLayoutJson() != null && !entity.getLayoutJson().isEmpty()) {
|
||||
return entity.getLayoutJson();
|
||||
}
|
||||
return getDefaultLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveLayout(Long userId, String layoutJson) {
|
||||
SysUserDashboard exist = dashboardMapper.selectById(userId);
|
||||
if (exist == null) {
|
||||
SysUserDashboard entity = new SysUserDashboard();
|
||||
entity.setUserId(userId);
|
||||
entity.setLayoutJson(layoutJson);
|
||||
dashboardMapper.insert(entity);
|
||||
} else {
|
||||
exist.setLayoutJson(layoutJson);
|
||||
dashboardMapper.updateById(exist);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetLayout(Long userId) {
|
||||
dashboardMapper.deleteById(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultLayout() {
|
||||
String value = configService.selectConfigByKey(DEFAULT_LAYOUT_KEY);
|
||||
return (value == null || value.isEmpty()) ? FALLBACK_LAYOUT : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDefaultLayout(String layoutJson) {
|
||||
SysConfig config = new SysConfig();
|
||||
config.setConfigKey(DEFAULT_LAYOUT_KEY);
|
||||
config.setConfigValue(layoutJson);
|
||||
configService.updateConfig(config);
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@
|
||||
"vue-count-to": "1.0.13",
|
||||
"vue-cropper": "0.5.5",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-grid-layout": "^2.4.0",
|
||||
"vue-meta": "2.4.0",
|
||||
"vue-plugin-hiprint": "^0.0.60",
|
||||
"vue-print-nb": "^1.7.5",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询采购需求关联的入库批次
|
||||
export function getRequirementBatches(requirementId) {
|
||||
return request({
|
||||
url: `/oa/requirements/${requirementId}/batches`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询OA 需求列表
|
||||
export function listRequirements(query) {
|
||||
return request({
|
||||
|
||||
10
ruoyi-ui/src/api/oa/warehouse/auditLog.js
Normal file
10
ruoyi-ui/src/api/oa/warehouse/auditLog.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 库房操作日志(分页)
|
||||
export function listWarehouseAudit (query) {
|
||||
return request({
|
||||
url: '/oa/warehouseAudit/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
35
ruoyi-ui/src/api/system/dashboard.js
Normal file
35
ruoyi-ui/src/api/system/dashboard.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取当前用户工作台布局
|
||||
export function getDashboardLayout() {
|
||||
return request({
|
||||
url: '/system/user/dashboard',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存当前用户工作台布局
|
||||
export function saveDashboardLayout(layout) {
|
||||
return request({
|
||||
url: '/system/user/dashboard',
|
||||
method: 'put',
|
||||
data: { layout }
|
||||
})
|
||||
}
|
||||
|
||||
// 重置为默认布局
|
||||
export function resetDashboardLayout() {
|
||||
return request({
|
||||
url: '/system/user/dashboard/reset',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员:保存系统默认工作台
|
||||
export function saveDefaultDashboardLayout(layout) {
|
||||
return request({
|
||||
url: '/system/user/dashboard/default',
|
||||
method: 'put',
|
||||
data: { layout }
|
||||
})
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
|
||||
/* theme color */
|
||||
$--color-primary: #1890ff;
|
||||
$--color-success: #13ce66;
|
||||
$--color-warning: #ffba00;
|
||||
$--color-danger: #ff4949;
|
||||
$--color-primary: #1677ff;
|
||||
$--color-success: #00b42a;
|
||||
$--color-warning: #ff7d00;
|
||||
$--color-danger: #f53f3f;
|
||||
// $--color-info: #1E1E1E;
|
||||
|
||||
$--button-font-weight: 400;
|
||||
|
||||
@@ -8,10 +8,201 @@
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #f4f7f9;
|
||||
font-size: 12px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
font-family: "Inter", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
// ===== 全局紧凑化 =====
|
||||
// Element UI 默认字号 14px / 行高较松,整体收紧到 13px
|
||||
#app {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 表单 / 输入 / 按钮 紧凑
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.el-form-item__label {
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
/* 全局输入控件统一压低高度(含 input/select/cascader/date/range/autocomplete) */
|
||||
.el-input__inner,
|
||||
.el-textarea__inner,
|
||||
.el-select .el-input__inner,
|
||||
.el-cascader .el-input__inner,
|
||||
.el-autocomplete .el-input__inner,
|
||||
.el-date-editor .el-input__inner,
|
||||
.el-date-editor--daterange.el-range-editor,
|
||||
.el-date-editor--datetimerange.el-range-editor,
|
||||
.el-date-editor--monthrange.el-range-editor,
|
||||
.el-range-editor.el-input__inner {
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
.el-input--medium .el-input__inner { height: 30px !important; line-height: 30px !important; }
|
||||
.el-input--small .el-input__inner { height: 28px !important; line-height: 28px !important; }
|
||||
.el-input--mini .el-input__inner { height: 24px !important; line-height: 24px !important; font-size: 12px !important; }
|
||||
.el-input--mini.el-input,
|
||||
.el-input--mini .el-input__suffix,
|
||||
.el-input--mini .el-input__prefix { line-height: 24px !important; }
|
||||
.el-range-editor.el-input__inner { padding: 0 10px !important; }
|
||||
.el-range-editor .el-range-input { font-size: 12px !important; height: 100% !important; line-height: 1 !important; }
|
||||
.el-range-editor .el-range-separator { line-height: 26px !important; font-size: 12px !important; }
|
||||
.el-input__icon { line-height: 28px !important; }
|
||||
.el-input__suffix-inner .el-input__icon { line-height: 28px !important; }
|
||||
.el-form-item__content { line-height: 28px; }
|
||||
.el-button {
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.el-button--mini,
|
||||
.el-button--small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-button--medium {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
// 表格紧凑
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-table th.el-table__cell > .cell,
|
||||
.el-table td.el-table__cell > .cell {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
padding: 6px 0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.el-table td.el-table__cell {
|
||||
padding: 6px 0;
|
||||
}
|
||||
.el-table--mini th.el-table__cell,
|
||||
.el-table--mini td.el-table__cell {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
// 分页紧凑
|
||||
.el-pagination {
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-pagination .btn-prev,
|
||||
.el-pagination .btn-next,
|
||||
.el-pagination .el-pager li {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
// 卡片 / 容器 紧凑
|
||||
.el-card__header {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.el-dialog__header {
|
||||
padding: 14px 18px 10px;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 16px 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 10px 18px 14px;
|
||||
}
|
||||
|
||||
// 菜单收紧
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
font-size: 12px;
|
||||
height: 38px !important;
|
||||
line-height: 38px !important;
|
||||
}
|
||||
|
||||
// Tabs / Tags
|
||||
.el-tabs__item {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-tag {
|
||||
height: 22px;
|
||||
line-height: 20px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 描述列表 / 步骤
|
||||
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 复选框 / 单选
|
||||
.el-checkbox__label,
|
||||
.el-radio__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// Vben 风格:浅色侧边栏选中项
|
||||
#app .sidebar-container.theme-light {
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
border-radius: 6px;
|
||||
margin: 2px 8px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: #e6f4ff !important;
|
||||
color: #1677ff !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部导航 & 内容卡片化
|
||||
.app-main {
|
||||
background-color: #f4f7f9;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 21, 41, 0.04);
|
||||
}
|
||||
|
||||
// 圆角按钮/输入框
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -122,7 +313,7 @@ aside {
|
||||
|
||||
//main-container全局样式
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.components-container {
|
||||
@@ -180,4 +371,116 @@ aside {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 全局按钮缩小:默认按钮等比 small 化 */
|
||||
.el-button {
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.el-button--medium {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-button--small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-button--mini {
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-button [class*="el-icon-"] + span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.el-button-group > .el-button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 列表页搜索表单紧凑模式(加 class="compact-search" 即可生效) */
|
||||
.compact-search.el-form--inline {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.compact-search .el-form-item {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.compact-search .el-form-item__label {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #606266;
|
||||
line-height: 28px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.compact-search .el-form-item__content {
|
||||
line-height: 28px;
|
||||
}
|
||||
.compact-search .el-input--mini .el-input__inner,
|
||||
.compact-search .el-select .el-input__inner,
|
||||
.compact-search .el-date-editor .el-input__inner,
|
||||
.compact-search .el-range-editor .el-range-input {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.compact-search .el-range-editor.el-input__inner {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* compact-search 进一步收紧 */
|
||||
.compact-search .el-form-item__label {
|
||||
font-size: 11px !important;
|
||||
line-height: 26px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
.compact-search .el-form-item__content,
|
||||
.compact-search .el-input--mini,
|
||||
.compact-search .el-select,
|
||||
.compact-search .el-date-editor {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
.compact-search .el-input--mini .el-input__inner,
|
||||
.compact-search .el-select .el-input__inner,
|
||||
.compact-search .el-date-editor .el-input__inner,
|
||||
.compact-search .el-range-editor .el-range-input {
|
||||
height: 26px !important;
|
||||
line-height: 26px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
.compact-search .el-form-item {
|
||||
margin-bottom: 4px !important;
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
.compact-search .el-button--mini {
|
||||
padding: 5px 10px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
/* 紧凑型 tabs(class="compact-tabs") */
|
||||
.compact-tabs.el-tabs {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.compact-tabs .el-tabs__header {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.compact-tabs .el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
.compact-tabs .el-tabs__item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.compact-tabs .el-tabs__active-bar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* 加急行高亮(用于备注含 "急" 的未完成单据) */
|
||||
.el-table .row-urgent > td.el-table__cell {
|
||||
background: #fff1f0 !important;
|
||||
}
|
||||
.el-table .row-urgent:hover > td.el-table__cell {
|
||||
background: #ffd8d6 !important;
|
||||
}
|
||||
|
||||
@@ -304,8 +304,18 @@ h6 {
|
||||
}
|
||||
|
||||
.top-right-btn {
|
||||
position: relative;
|
||||
float: right;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
z-index: 3;
|
||||
float: none;
|
||||
}
|
||||
.app-container { position: relative; }
|
||||
.top-right-btn .el-button.is-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-input {
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||
box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||
-webkit-box-shadow: 1px 0 4px rgba(0, 21, 41, 0.08);
|
||||
box-shadow: 1px 0 4px rgba(0, 21, 41, 0.08);
|
||||
border-right: 1px solid #f0f0f0;
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
@@ -46,7 +47,7 @@
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +75,14 @@
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
font-size: 12px !important;
|
||||
height: 40px !important;
|
||||
line-height: 40px !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
@@ -91,10 +100,18 @@
|
||||
& .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .el-submenu .el-menu-item {
|
||||
min-width: $base-sidebar-width !important;
|
||||
font-size: 12px !important;
|
||||
height: 36px !important;
|
||||
line-height: 36px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
// 隐藏二级菜单及更深层级的图标
|
||||
.svg-icon {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .theme-dark .nest-menu .el-submenu>.el-submenu__title,
|
||||
|
||||
@@ -8,18 +8,18 @@ $tiffany: #4AB7BD;
|
||||
$yellow:#FEC171;
|
||||
$panGreen: #30B08F;
|
||||
|
||||
// 默认菜单主题风格
|
||||
$base-menu-color:#bfcbd9;
|
||||
$base-menu-color-active:#f4f4f5;
|
||||
$base-menu-background:#304156;
|
||||
$base-logo-title-color: #ffffff;
|
||||
// 默认菜单主题风格(Vben 风格浅色)
|
||||
$base-menu-color: rgba(0,0,0,.75);
|
||||
$base-menu-color-active: #1677ff;
|
||||
$base-menu-background: #ffffff;
|
||||
$base-logo-title-color: #1677ff;
|
||||
|
||||
$base-menu-light-color:rgba(0,0,0,.70);
|
||||
$base-menu-light-background:#ffffff;
|
||||
$base-logo-light-title-color: #001529;
|
||||
$base-menu-light-color: rgba(0,0,0,.75);
|
||||
$base-menu-light-background: #ffffff;
|
||||
$base-logo-light-title-color: #1677ff;
|
||||
|
||||
$base-sub-menu-background:#1f2d3d;
|
||||
$base-sub-menu-hover:#001528;
|
||||
$base-sub-menu-background: #ffffff;
|
||||
$base-sub-menu-hover: #f0f5ff;
|
||||
|
||||
// 自定义暗色菜单风格
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ $base-sub-menu-background:#000c17;
|
||||
$base-sub-menu-hover:#001528;
|
||||
*/
|
||||
|
||||
$base-sidebar-width: 200px;
|
||||
$base-sidebar-width: 150px;
|
||||
|
||||
// the :export directive is the magic sauce for webpack
|
||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
||||
|
||||
91
ruoyi-ui/src/components/Workbench/WidgetWrapper.vue
Normal file
91
ruoyi-ui/src/components/Workbench/WidgetWrapper.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="widget-wrapper" :class="{ editing: editing }">
|
||||
<div class="widget-header">
|
||||
<span class="widget-title">{{ title }}</span>
|
||||
<span v-if="editing" class="widget-remove" @click="$emit('remove')">
|
||||
<i class="el-icon-close"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="widget-body">
|
||||
<component :is="component" v-if="component" />
|
||||
<div v-else class="widget-missing">未知组件: {{ widgetKey }}</div>
|
||||
</div>
|
||||
<div v-if="editing" class="widget-mask"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWidget } from './widgets/registry'
|
||||
|
||||
export default {
|
||||
name: 'WidgetWrapper',
|
||||
props: {
|
||||
widgetKey: { type: String, required: true },
|
||||
editing: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
widgetMeta () {
|
||||
return getWidget(this.widgetKey)
|
||||
},
|
||||
title () {
|
||||
return this.widgetMeta ? this.widgetMeta.title : this.widgetKey
|
||||
},
|
||||
component () {
|
||||
return this.widgetMeta ? this.widgetMeta.component : null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.widget-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.widget-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fafafa;
|
||||
}
|
||||
.widget-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.widget-remove {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
&:hover { color: #f56c6c; }
|
||||
}
|
||||
.widget-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.widget-missing {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.widget-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(64, 158, 255, 0.04);
|
||||
pointer-events: none;
|
||||
}
|
||||
.editing {
|
||||
border: 1px dashed #409EFF;
|
||||
}
|
||||
</style>
|
||||
270
ruoyi-ui/src/components/Workbench/index.vue
Normal file
270
ruoyi-ui/src/components/Workbench/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="workbench" v-loading="loading">
|
||||
<!-- 编辑入口:不在编辑态时只显示一个浮动小图标,不抢占整行 -->
|
||||
<button
|
||||
v-if="!editing"
|
||||
class="workbench-edit-fab"
|
||||
title="编辑工作台"
|
||||
@click="enterEdit"
|
||||
>
|
||||
<i class="el-icon-edit"></i>
|
||||
</button>
|
||||
|
||||
<!-- 编辑态浮动工具条 -->
|
||||
<div v-if="editing" class="workbench-edit-bar">
|
||||
<el-button type="success" size="mini" icon="el-icon-check" @click="save">保存</el-button>
|
||||
<el-button size="mini" icon="el-icon-close" @click="cancel">取消</el-button>
|
||||
<el-button type="warning" size="mini" icon="el-icon-refresh-left" @click="reset">重置</el-button>
|
||||
<el-button v-if="isAdmin" type="danger" size="mini" plain icon="el-icon-upload2" @click="saveAsDefault">设为全局默认</el-button>
|
||||
<el-button type="primary" size="mini" plain icon="el-icon-plus" @click="pickerVisible = true">添加组件</el-button>
|
||||
</div>
|
||||
|
||||
<grid-layout
|
||||
v-if="layout.length"
|
||||
:layout.sync="layout"
|
||||
:col-num="12"
|
||||
:row-height="30"
|
||||
:is-draggable="editing"
|
||||
:is-resizable="editing"
|
||||
:vertical-compact="true"
|
||||
:use-css-transforms="true"
|
||||
:margin="[12, 12]"
|
||||
>
|
||||
<grid-item
|
||||
v-for="item in layout"
|
||||
:key="item.i"
|
||||
:x="item.x"
|
||||
:y="item.y"
|
||||
:w="item.w"
|
||||
:h="item.h"
|
||||
:i="item.i"
|
||||
:min-w="2"
|
||||
:min-h="2"
|
||||
>
|
||||
<widget-wrapper
|
||||
:widget-key="item.widgetKey"
|
||||
:editing="editing"
|
||||
@remove="removeItem(item.i)"
|
||||
/>
|
||||
</grid-item>
|
||||
</grid-layout>
|
||||
|
||||
<div v-else-if="!loading" class="workbench-empty">
|
||||
工作台为空,<el-button type="text" @click="enterEdit">点击编辑</el-button>添加组件
|
||||
</div>
|
||||
|
||||
<!-- 添加组件抽屉 -->
|
||||
<el-drawer
|
||||
title="添加组件"
|
||||
:visible.sync="pickerVisible"
|
||||
direction="rtl"
|
||||
size="320px"
|
||||
>
|
||||
<div class="picker-list">
|
||||
<div
|
||||
v-for="w in availableWidgets"
|
||||
:key="w.key"
|
||||
class="picker-item"
|
||||
@click="addWidget(w)"
|
||||
>
|
||||
<span>{{ w.title }}</span>
|
||||
<i class="el-icon-plus"></i>
|
||||
</div>
|
||||
<div v-if="!availableWidgets.length" class="picker-empty">所有组件已添加</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GridLayout, GridItem } from 'vue-grid-layout'
|
||||
import WidgetWrapper from './WidgetWrapper.vue'
|
||||
import { listWidgets, getWidget } from './widgets/registry'
|
||||
import {
|
||||
getDashboardLayout,
|
||||
saveDashboardLayout,
|
||||
resetDashboardLayout,
|
||||
saveDefaultDashboardLayout
|
||||
} from '@/api/system/dashboard'
|
||||
|
||||
export default {
|
||||
name: 'Workbench',
|
||||
components: { GridLayout, GridItem, WidgetWrapper },
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
editing: false,
|
||||
layout: [],
|
||||
backupLayout: [],
|
||||
pickerVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
availableWidgets () {
|
||||
const used = new Set(this.layout.map(i => i.widgetKey))
|
||||
return listWidgets().filter(w => !used.has(w.key))
|
||||
},
|
||||
isAdmin () {
|
||||
const roles = this.$store.getters.roles || []
|
||||
return roles.includes('admin')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchLayout()
|
||||
},
|
||||
methods: {
|
||||
fetchLayout () {
|
||||
this.loading = true
|
||||
getDashboardLayout().then(res => {
|
||||
const raw = res && res.data && res.data.layout
|
||||
this.layout = this.parseLayout(raw)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
parseLayout (raw) {
|
||||
if (!raw) return []
|
||||
try {
|
||||
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch (e) {
|
||||
console.warn('parse dashboard layout failed', e)
|
||||
return []
|
||||
}
|
||||
},
|
||||
enterEdit () {
|
||||
this.backupLayout = JSON.parse(JSON.stringify(this.layout))
|
||||
this.editing = true
|
||||
},
|
||||
cancel () {
|
||||
this.layout = this.backupLayout
|
||||
this.editing = false
|
||||
},
|
||||
save () {
|
||||
const payload = JSON.stringify(this.layout)
|
||||
this.loading = true
|
||||
saveDashboardLayout(payload).then(() => {
|
||||
this.$modal.msgSuccess('工作台已保存')
|
||||
this.editing = false
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
reset () {
|
||||
this.$modal.confirm('确定重置为默认布局?当前自定义将丢失').then(() => {
|
||||
this.loading = true
|
||||
return resetDashboardLayout()
|
||||
}).then(() => {
|
||||
this.editing = false
|
||||
this.fetchLayout()
|
||||
this.$modal.msgSuccess('已重置为默认布局')
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
removeItem (i) {
|
||||
this.layout = this.layout.filter(item => item.i !== i)
|
||||
},
|
||||
saveAsDefault () {
|
||||
this.$modal.confirm('确认将当前布局设为全局默认?所有未自定义过工作台的用户都会看到此布局').then(() => {
|
||||
const payload = JSON.stringify(this.layout)
|
||||
this.loading = true
|
||||
return saveDefaultDashboardLayout(payload)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已保存为全局默认布局')
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
addWidget (w) {
|
||||
const meta = getWidget(w.key)
|
||||
const size = (meta && meta.defaultSize) || { w: 6, h: 6 }
|
||||
const maxY = this.layout.reduce((m, it) => Math.max(m, it.y + it.h), 0)
|
||||
this.layout.push({
|
||||
i: w.key,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: size.w,
|
||||
h: size.h,
|
||||
widgetKey: w.key
|
||||
})
|
||||
this.pickerVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workbench {
|
||||
padding: 8px 12px 12px;
|
||||
min-height: calc(100vh - 68px);
|
||||
position: relative;
|
||||
}
|
||||
.workbench-edit-fab {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
color: #409EFF;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
transition: all .2s;
|
||||
opacity: 0.6;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.workbench-edit-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.workbench-empty {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
.picker-list {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.picker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
color: #409EFF;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
.picker-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
81
ruoyi-ui/src/components/Workbench/widgets/registry.js
Normal file
81
ruoyi-ui/src/components/Workbench/widgets/registry.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Widget 注册表:key -> { title, component, defaultSize }
|
||||
// 所有可放入工作台的组件统一在此登记。新增组件只需在此添加一项即可。
|
||||
// 栅格 col-num = 12,三列 ≈ w:4
|
||||
|
||||
import Announcements from '@/components/Announcements/index.vue'
|
||||
import MiniCalendar from '@/components/MiniCalendar/index.vue'
|
||||
import QuickEntry from '@/components/QuickEntry/index.vue'
|
||||
import {
|
||||
ExpressQuestionList,
|
||||
FeedbackList,
|
||||
FinancialCharts,
|
||||
MyTaskList,
|
||||
OwnerTaskList,
|
||||
ProjectManagement,
|
||||
RequirementList
|
||||
} from '@/components/HomeModules/index'
|
||||
|
||||
export const WIDGET_REGISTRY = {
|
||||
announcements: {
|
||||
title: '通知公告',
|
||||
component: Announcements,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
projectManagement: {
|
||||
title: '项目管理',
|
||||
component: ProjectManagement,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
ownerTaskList: {
|
||||
title: '分配我的任务',
|
||||
component: OwnerTaskList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
myTaskList: {
|
||||
title: '我发放的任务',
|
||||
component: MyTaskList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
financialCharts: {
|
||||
title: '财务图表',
|
||||
component: FinancialCharts,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
feedbackList: {
|
||||
title: '问题反馈',
|
||||
component: FeedbackList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
requirementList: {
|
||||
title: '需求下发',
|
||||
component: RequirementList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
expressQuestionList: {
|
||||
title: '快递问题',
|
||||
component: ExpressQuestionList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
miniCalendar: {
|
||||
title: '日程日历',
|
||||
component: MiniCalendar,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
quickEntry: {
|
||||
title: '快捷入口',
|
||||
component: QuickEntry,
|
||||
defaultSize: { w: 12, h: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
export function getWidget(key) {
|
||||
return WIDGET_REGISTRY[key]
|
||||
}
|
||||
|
||||
export function listWidgets() {
|
||||
return Object.keys(WIDGET_REGISTRY).map(key => ({
|
||||
key,
|
||||
title: WIDGET_REGISTRY[key].title,
|
||||
defaultSize: WIDGET_REGISTRY[key].defaultSize
|
||||
}))
|
||||
}
|
||||
@@ -28,8 +28,8 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
/* 40= navbar */
|
||||
min-height: calc(100vh - 40px);
|
||||
width: 100%;
|
||||
background-color: #f5f5f5;
|
||||
position: relative;
|
||||
@@ -37,17 +37,17 @@ export default {
|
||||
}
|
||||
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 50px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
/* 68 = navbar + tags-view = 40 + 28 */
|
||||
min-height: calc(100vh - 68px);
|
||||
}
|
||||
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 84px;
|
||||
padding-top: 68px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
@close="hiddenChat"
|
||||
/>
|
||||
</div> -->
|
||||
<AIChat class="right-menu-item" />
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
<el-tooltip content="用户" effect="dark" placement="bottom">
|
||||
<div class="right-menu-item hover-effect">
|
||||
@@ -279,14 +278,14 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
line-height: 36px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
@@ -315,7 +314,7 @@ export default {
|
||||
.right-menu {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
line-height: 40px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
@@ -350,9 +349,9 @@ export default {
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '福安德综合办公系统',
|
||||
title: '福安德办公',
|
||||
logo: logoImg
|
||||
}
|
||||
}
|
||||
@@ -55,21 +55,23 @@ export default {
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
background: #2b2f3a;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
& .sidebar-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
@@ -77,7 +79,7 @@ export default {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -231,7 +231,7 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
@@ -242,15 +242,15 @@ export default {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
border: 1px solid #d8dce5;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
margin-top: 3px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
|
||||
@@ -2,7 +2,7 @@ module.exports = {
|
||||
/**
|
||||
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
|
||||
*/
|
||||
sideTheme: 'theme-dark',
|
||||
sideTheme: 'theme-light',
|
||||
|
||||
/**
|
||||
* 是否系统布局配置
|
||||
@@ -22,7 +22,7 @@ module.exports = {
|
||||
/**
|
||||
* 是否固定头部
|
||||
*/
|
||||
fixedHeader: false,
|
||||
fixedHeader: true,
|
||||
|
||||
/**
|
||||
* 是否显示logo
|
||||
|
||||
@@ -5,7 +5,7 @@ const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dyn
|
||||
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
|
||||
const state = {
|
||||
title: '',
|
||||
theme: storageSetting.theme || '#409EFF',
|
||||
theme: storageSetting.theme || '#1677ff',
|
||||
sideTheme: storageSetting.sideTheme || sideTheme,
|
||||
showSettings: showSettings,
|
||||
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
||||
|
||||
@@ -1,159 +1,24 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<el-container>
|
||||
<el-main>
|
||||
<el-row style="flex:1;overflow:auto; margin:16px 0;">
|
||||
<QuickEntry />
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<!-- 左侧宫格区域 -->
|
||||
<el-col :span="20">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="通知公告">
|
||||
<Announcements />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="问题反馈">
|
||||
<FeedbackList />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="需求下发">
|
||||
<RequirementList />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="快递问题">
|
||||
<ExpressQuestionList />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<ProjectManagement />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="分配我的任务">
|
||||
<OwnerTaskList />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="我发放的任务">
|
||||
<MyTaskList />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<FinancialCharts />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<!-- 右侧整体日程区域 -->
|
||||
<el-col :span="4">
|
||||
<div class="panel panel-calendar" style="height:100%;display:flex;flex-direction:column;">
|
||||
<MiniCalendar :daysData="calendarDays" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<Workbench />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCache } from "@/api/monitor/cache";
|
||||
import { queryOwnList } from "@/api/oa/oaHoliday";
|
||||
import { getRemindList, updateRemind } from '@/api/oa/remind';
|
||||
import { getNotice, listNoticeLimit } from "@/api/system/notice";
|
||||
import Announcements from "@/components/Announcements/index.vue";
|
||||
import { ExpressQuestionList, FeedbackList, FinancialCharts, MyTaskList, OwnerTaskList, ProjectManagement, RequirementList } from '@/components/HomeModules/index';
|
||||
import Inventory from "@/components/Inventory/index.vue";
|
||||
import MiniCalendar from "@/components/MiniCalendar/index.vue";
|
||||
// import ProjectManagement from "@/components/ProjectManagement/index.vue";
|
||||
import QuickAccess from "@/components/QuickAccess/index.vue";
|
||||
import QuickEntry from "@/components/QuickEntry/index.vue";
|
||||
import { formatDate } from "@/utils";
|
||||
import { getRemindList, updateRemind } from '@/api/oa/remind'
|
||||
import Workbench from '@/components/Workbench/index.vue'
|
||||
|
||||
export default {
|
||||
name: "Index",
|
||||
components: {
|
||||
MiniCalendar,
|
||||
FinancialCharts,
|
||||
ProjectManagement,
|
||||
Announcements,
|
||||
Inventory,
|
||||
QuickAccess,
|
||||
QuickEntry,
|
||||
MyTaskList,
|
||||
OwnerTaskList,
|
||||
ExpressQuestionList,
|
||||
RequirementList,
|
||||
FeedbackList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
version: "0.8.3",
|
||||
commandstats: null,
|
||||
usedmemory: null,
|
||||
cache: [],
|
||||
finishedCount: 0,
|
||||
todoListCount: 0,
|
||||
ownCount: 0,
|
||||
noticeList: [],
|
||||
noticeTitle: "",
|
||||
noticeContent: "",
|
||||
drawer: false,
|
||||
dayParams: {
|
||||
pageSize: 999,
|
||||
pageNum: 1,
|
||||
holidayTime: new Date(),
|
||||
},
|
||||
calendarDays: [],
|
||||
};
|
||||
},
|
||||
name: 'Index',
|
||||
components: { Workbench },
|
||||
created () {
|
||||
this.getList();
|
||||
this.getListNotice();
|
||||
this.checkTaskRemind(); // 新增
|
||||
this.checkTaskRemind()
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true;
|
||||
getCache().then((response) => {
|
||||
this.cache = response.data;
|
||||
// this.$modal.closeLoading();
|
||||
});
|
||||
this.dayParams.holidayTime = formatDate(new Date());
|
||||
queryOwnList(this.dayParams).then((response) => {
|
||||
this.calendarDays = response;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getListNotice () {
|
||||
this.loading = true;
|
||||
listNoticeLimit().then((response) => {
|
||||
this.noticeList = response;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
toDrawer (nid) {
|
||||
this.drawer = true;
|
||||
getNotice(nid).then((res) => {
|
||||
this.noticeTitle = res.data.noticeTitle;
|
||||
this.noticeContent = res.data.noticeContent;
|
||||
});
|
||||
},
|
||||
goTarget (href) {
|
||||
this.$router.push({ path: href });
|
||||
},
|
||||
checkTaskRemind () {
|
||||
const targetUserId = this.$store.getters.id
|
||||
getRemindList({ pageNum: 1, pageSize: 20, targetUserId }).then(res => {
|
||||
const taskReminds = (res.data || []).filter(item => item.remindType === 'task');
|
||||
console.log(taskReminds, res)
|
||||
const taskReminds = (res.data || []).filter(item => item.remindType === 'task')
|
||||
taskReminds.forEach(remind => {
|
||||
const notifyInstance = this.$notify({
|
||||
title: '任务提醒',
|
||||
@@ -165,24 +30,21 @@ export default {
|
||||
dangerouslyUseHTMLString: true,
|
||||
duration: 0,
|
||||
showClose: true,
|
||||
customClass: 'remind-notify',
|
||||
onClose: () => { },
|
||||
onClick: () => { },
|
||||
});
|
||||
customClass: 'remind-notify'
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
const btn = document.getElementById(`remind-btn-${remind.remindId}`);
|
||||
const btn = document.getElementById(`remind-btn-${remind.remindId}`)
|
||||
if (btn) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
notifyInstance.close();
|
||||
this.$router.push({ path: '/task/task', query: { taskId: remind.detailId } });
|
||||
});
|
||||
e.stopPropagation()
|
||||
notifyInstance.close()
|
||||
this.$router.push({ path: '/task/task', query: { taskId: remind.detailId } })
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
// 专属于董事长的薪资待批
|
||||
const gmReminds = (res.data || []).filter(item => item.remindType === 'salary');
|
||||
const gmReminds = (res.data || []).filter(item => item.remindType === 'salary')
|
||||
gmReminds.forEach(remind => {
|
||||
const notifyInstance = this.$notify({
|
||||
title: '薪资待批',
|
||||
@@ -194,83 +56,35 @@ export default {
|
||||
dangerouslyUseHTMLString: true,
|
||||
duration: 0,
|
||||
showClose: true,
|
||||
customClass: 'remind-notify',
|
||||
onClose: () => { },
|
||||
onClick: () => { },
|
||||
});
|
||||
customClass: 'remind-notify'
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
const btn = document.getElementById(`remind-btn-${remind.remindId}`);
|
||||
const btn = document.getElementById(`remind-btn-${remind.remindId}`)
|
||||
if (btn) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
updateRemind({
|
||||
...remind,
|
||||
taskTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
|
||||
taskStatus: 1,
|
||||
taskStatus: 1
|
||||
})
|
||||
notifyInstance.close();
|
||||
this.$router.push({ path: '/finance/salary/list' });
|
||||
});
|
||||
notifyInstance.close()
|
||||
this.$router.push({ path: '/finance/salary/list' })
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.onboarding-homepage {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
.home {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.employee-form {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.flow-chart {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 栅格列样式 */
|
||||
.content-area {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 左侧功能区域 */
|
||||
.sidebar {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 响应式隐藏侧边栏 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.remind-notify {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 固定卡片高度 */
|
||||
.el-card {
|
||||
height: 430px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,21 +26,36 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="入库单编号" align="center" prop="masterNum" />
|
||||
<el-table-column label="操作时间" align="center" prop="signTime">
|
||||
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange" stripe size="small">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column label="入库时间" prop="signTime" width="100">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入库人" prop="signUser" width="80" />
|
||||
<el-table-column label="关联需求" min-width="160" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</span>
|
||||
<span v-if="scope.row.requirementName">{{ scope.row.requirementName }}</span>
|
||||
<span v-else style="color:#c0c4cc;">无关联</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" align="center" prop="signUser" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="关联项目" prop="projectName" min-width="160" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看详情
|
||||
</el-button>
|
||||
<span v-if="scope.row.projectName">{{ scope.row.projectName }}</span>
|
||||
<span v-else style="color:#c0c4cc;">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物料概览" prop="itemsSummary" min-width="220" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.itemsSummary">{{ scope.row.itemsSummary }}</span>
|
||||
<el-button v-else type="text" size="mini" @click="showDetail(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物料种类" prop="itemCount" width="80" align="right" />
|
||||
<el-table-column label="入库总数" prop="totalQty" width="80" align="right" />
|
||||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="100" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -58,7 +73,7 @@
|
||||
<i class="el-icon-s-order"></i>
|
||||
入库单
|
||||
</template>
|
||||
{{ detailData.masterId }}
|
||||
{{ detailData.masterNum }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
@@ -106,6 +121,20 @@
|
||||
<!-- 添加或修改仓库入库对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="采购需求" prop="requirementId" required>
|
||||
<el-select v-model="form.requirementId" filterable remote
|
||||
:remote-method="loadRequirementOptions" :loading="requirementLoading"
|
||||
placeholder="按需求标题搜索(必选)" style="width: 100%"
|
||||
@change="onRequirementChange">
|
||||
<el-option v-for="r in requirementOptions" :key="r.requirementId"
|
||||
:label="r.title" :value="r.requirementId">
|
||||
<span style="float: left">{{ r.title }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 12px">
|
||||
{{ r.projectName || '无项目' }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定项目">
|
||||
<el-radio-group v-model="projectFlag" :disabled="drawer">
|
||||
<el-radio :label="true">是</el-radio>
|
||||
@@ -216,6 +245,7 @@ import {
|
||||
listOaWarehouseMaster,
|
||||
updateOaWarehouseMaster
|
||||
} from "@/api/oa/warehouse/warehouseMaster";
|
||||
import { listRequirements } from "@/api/oa/requirement";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
|
||||
export default {
|
||||
@@ -270,6 +300,9 @@ export default {
|
||||
open: false,
|
||||
// 是否绑定项目
|
||||
projectFlag: false,
|
||||
// 采购需求下拉
|
||||
requirementOptions: [],
|
||||
requirementLoading: false,
|
||||
// 库存查询参数
|
||||
warehouseParams: {
|
||||
pageSize: 999,
|
||||
@@ -297,6 +330,9 @@ export default {
|
||||
warehouseId: [
|
||||
{ required: true, message: "入库对象id不能为空", trigger: "blur" }
|
||||
],
|
||||
requirementId: [
|
||||
{ required: true, message: "入库必须关联采购需求", trigger: "change" }
|
||||
],
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -304,6 +340,20 @@ export default {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
// 远程搜索采购需求
|
||||
loadRequirementOptions (keyword) {
|
||||
this.requirementLoading = true
|
||||
listRequirements({ pageNum: 1, pageSize: 20, title: keyword || undefined, status: 1 })
|
||||
.then(res => { this.requirementOptions = res.rows || [] })
|
||||
.finally(() => { this.requirementLoading = false })
|
||||
},
|
||||
onRequirementChange (id) {
|
||||
const r = this.requirementOptions.find(x => x.requirementId === id)
|
||||
if (r && r.projectId) {
|
||||
this.projectFlag = true
|
||||
this.form.projectId = r.projectId
|
||||
}
|
||||
},
|
||||
|
||||
// 添加新的一行
|
||||
addRow () {
|
||||
@@ -358,6 +408,7 @@ export default {
|
||||
reset () {
|
||||
this.form = {
|
||||
projectId: undefined,
|
||||
requirementId: undefined,
|
||||
warehouseList: [],
|
||||
};
|
||||
this.resetForm("form");
|
||||
@@ -382,6 +433,7 @@ export default {
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.loadRequirementOptions();
|
||||
if (this.drawer) {
|
||||
// 如果抽屉是打开的说明是从项目处进入的新增,从而加入projectId
|
||||
this.projectFlag = true;
|
||||
|
||||
102
ruoyi-ui/src/views/oa/oaOutWarehouse/detail.vue
Normal file
102
ruoyi-ui/src/views/oa/oaOutWarehouse/detail.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
|
||||
<el-form-item label="物料名" prop="warehouseName">
|
||||
<el-input v-model="queryParams.warehouseName" placeholder="名称模糊匹配" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
<project-select v-model="queryParams.projectId" style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="出库时间" prop="signTimeRange">
|
||||
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始" end-placeholder="结束"
|
||||
value-format="yyyy-MM-dd" />
|
||||
</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-table v-loading="loading" :data="list" stripe size="small">
|
||||
<el-table-column label="出库时间" prop="signTime" width="120">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物料" prop="warehouseName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="型号" prop="model" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="规格" prop="specifications" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="出库数量" prop="amount" width="90" align="right" />
|
||||
<el-table-column label="单位" prop="unit" width="60" />
|
||||
<el-table-column label="单价" prop="signPrice" width="90" align="right">
|
||||
<template slot-scope="scope">¥{{ Number(scope.row.signPrice || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
¥{{ ((scope.row.signPrice || 0) * (scope.row.amount || 0)).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目" prop="projectName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="出库单" prop="masterNum" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" prop="signUser" width="90" />
|
||||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOaWarehouseMaster } from '@/api/oa/warehouse/warehouseMaster'
|
||||
import request from '@/utils/request'
|
||||
import ProjectSelect from '@/components/fad-service/ProjectSelect'
|
||||
|
||||
export default {
|
||||
name: 'OutWarehouseDetail',
|
||||
components: { ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
searchTime: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
type: 0,
|
||||
warehouseName: undefined,
|
||||
projectId: undefined,
|
||||
signTimeStart: undefined,
|
||||
signTimeEnd: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
created () { this.getList() },
|
||||
watch: {
|
||||
searchTime (v) {
|
||||
this.queryParams.signTimeStart = v && v[0] || undefined
|
||||
this.queryParams.signTimeEnd = v && v[1] || undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true
|
||||
// 走 detail 列表接口,type=0 出库
|
||||
request({
|
||||
url: '/oa/oaOutWarehouse/list',
|
||||
method: 'get',
|
||||
params: this.queryParams
|
||||
}).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery () {
|
||||
this.searchTime = []
|
||||
this.queryParams = { pageNum: 1, pageSize: 20, type: 0 }
|
||||
this.getList()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,36 +1,31 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="操作时间" prop="signTime">
|
||||
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
|
||||
:default-time="['00:00:00', '23:59:59']">
|
||||
</el-date-picker>
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="62px" class="compact-search">
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
<project-select v-model="queryParams.projectId" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="出库单" prop="masterNum">
|
||||
<el-input v-model="queryParams.masterNum" placeholder="单号" clearable style="width: 140px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间" prop="signTime">
|
||||
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始" end-placeholder="结束"
|
||||
:default-time="['00:00:00', '23:59:59']" style="width: 230px" />
|
||||
</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-alert title="这个页面的退库按钮只是让他显示在退库管理中,并不会修改库存数量,在退库页面点击退库后才变更库存" type="warning" show-icon />
|
||||
|
||||
<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-form-item class="action-group">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
|
||||
@click="handleDeleteMaster">删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
@click="handleDeleteMaster">删除</el-button>
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
|
||||
v-hasPermi="['oa:oaOutWarehouse:export']">导出
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
v-hasPermi="['oa:oaOutWarehouse:export']">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
@@ -58,7 +53,7 @@
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh-right" @click="toRedo(scope.row)">撤销出库
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-back" @click="toReturn(scope.row)" title="将本条记录标记为需要退库">退库
|
||||
<el-button size="mini" type="text" icon="el-icon-back" @click="openReturnDialog(scope.row)" title="直接退库到库存">退库
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看详情
|
||||
</el-button>
|
||||
@@ -79,7 +74,7 @@
|
||||
<i class="el-icon-s-order"></i>
|
||||
出库单
|
||||
</template>
|
||||
{{ detailData.masterId }}
|
||||
{{ detailData.masterNum }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
@@ -115,18 +110,25 @@
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table v-loading="loading" :data="oaOutWarehouseList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table v-loading="loading" :data="oaOutWarehouseList">
|
||||
<el-table-column label="序号" align="center" type="index" width="60" />
|
||||
<el-table-column label="物料名" align="center" prop="warehouseName" />
|
||||
<el-table-column label="出库数量" align="center" prop="amount" />
|
||||
<el-table-column label="出库价格" align="center" prop="signPrice" />
|
||||
<el-table-column label="出库数量" align="center" prop="amount" width="90" />
|
||||
<el-table-column label="出库价格" align="center" prop="signPrice" width="100" />
|
||||
<el-table-column label="型号" align="center" prop="model" />
|
||||
<el-table-column label="规格" align="center" prop="specifications" />
|
||||
<el-table-column label="品牌" align="center" prop="brand" />
|
||||
<el-table-column v-if="returnMode" label="退库数量" align="center" width="130">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model.number="scope.row.returnAmount" type="number" size="mini" :min="0"
|
||||
:max="scope.row.amount" placeholder="0" @input="onReturnAmountInput($event, scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="detail = false">关闭</el-button>
|
||||
<el-button v-if="returnMode" type="primary" icon="el-icon-refresh-left"
|
||||
:disabled="!hasReturnItems()" @click="submitReturn">确认退库</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -240,7 +242,7 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
delOaOutWarehouse, updateReturnType
|
||||
delOaOutWarehouse, redoDetail, updateReturnType
|
||||
} from "@/api/oa/warehouse/oaOutWarehouse";
|
||||
import { listByMultiQuery, listOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
|
||||
import {
|
||||
@@ -259,6 +261,8 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// 退库模式(弹窗里显示数量输入和确认按钮)
|
||||
returnMode: false,
|
||||
// 细节数据
|
||||
detailData: {},
|
||||
// 抽屉
|
||||
@@ -312,7 +316,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 50,
|
||||
type: 0,
|
||||
},
|
||||
// 表单参数
|
||||
@@ -367,6 +371,37 @@ export default {
|
||||
.catch(() => { });
|
||||
},
|
||||
|
||||
// 就地打开退库弹窗,输入数量后直接调 redoDetail 扣明细 + 回库存
|
||||
openReturnDialog (row) {
|
||||
this.returnMode = true
|
||||
this.detailData = row
|
||||
this.oaOutWarehouseList = (row.warehouseList || []).map(it => ({ ...it, returnAmount: '' }))
|
||||
this.detail = true
|
||||
},
|
||||
onReturnAmountInput (val, row) {
|
||||
if (val === '') { row.returnAmount = ''; return }
|
||||
const n = Number(val)
|
||||
if (isNaN(n) || n < 0) { row.returnAmount = ''; return }
|
||||
if (n > row.amount) { row.returnAmount = row.amount; this.$message.warning('不能超过出库数量') }
|
||||
else row.returnAmount = Math.floor(n)
|
||||
},
|
||||
hasReturnItems () {
|
||||
return this.oaOutWarehouseList.some(it => Number(it.returnAmount) > 0)
|
||||
},
|
||||
submitReturn () {
|
||||
const items = this.oaOutWarehouseList.filter(it => Number(it.returnAmount) > 0)
|
||||
if (!items.length) return
|
||||
const summary = items.map(it => `${it.warehouseName}×${it.returnAmount}`).join('、')
|
||||
this.$confirm(`确认退库:${summary} ?`, '退库确认', { type: 'warning' }).then(() => {
|
||||
const payload = items.map(it => ({ detailId: it.id, returnNum: Number(it.returnAmount) }))
|
||||
redoDetail(payload).then(() => {
|
||||
this.$modal.msgSuccess('退库成功')
|
||||
this.detail = false
|
||||
this.returnMode = false
|
||||
this.getList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
toReturn (row) {
|
||||
this.$confirm("确定要对此出库单进行退货操作吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -726,3 +761,20 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 搜索 / 重置 之后再隔一段空白接 新增/删除/导出 */
|
||||
::v-deep .action-group {
|
||||
margin-left: 16px;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: #e4e7ed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="68px" class="compact-search">
|
||||
<el-form-item label="操作时间" prop="signTime">
|
||||
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
|
||||
:default-time="['00:00:00', '23:59:59']">
|
||||
@@ -12,13 +13,9 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-document" size="mini" @click="viewReturnLog">退库日志
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<el-button type="text" icon="el-icon-document" size="mini" class="view-log-link"
|
||||
@click="viewReturnLog">查看退库日志</el-button>
|
||||
|
||||
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
@@ -70,7 +67,7 @@
|
||||
<i class="el-icon-s-order"></i>
|
||||
出库单
|
||||
</template>
|
||||
{{ detailData.masterId }}
|
||||
{{ detailData.masterNum }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
@@ -172,7 +169,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 50,
|
||||
type: 0,
|
||||
returnType: 1
|
||||
}
|
||||
@@ -382,3 +379,15 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.view-log-link {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 110px;
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
z-index: 3;
|
||||
&:hover { color: #409eff; }
|
||||
}
|
||||
</style>
|
||||
|
||||
101
ruoyi-ui/src/views/oa/oaWarehouse/auditLog.vue
Normal file
101
ruoyi-ui/src/views/oa/oaWarehouse/auditLog.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
|
||||
<el-form-item label="操作类型" prop="opType">
|
||||
<el-select v-model="queryParams.opType" placeholder="全部" clearable style="width: 160px">
|
||||
<el-option v-for="t in opTypes" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="实体类型" prop="refType">
|
||||
<el-select v-model="queryParams.refType" placeholder="全部" clearable style="width: 140px">
|
||||
<el-option label="采购需求" value="requirement" />
|
||||
<el-option label="车间采购" value="task" />
|
||||
<el-option label="出入库单" value="master" />
|
||||
<el-option label="物料" value="warehouse" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="实体ID" prop="refId">
|
||||
<el-input v-model="queryParams.refId" placeholder="可选" clearable style="width: 160px" />
|
||||
</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-table v-loading="loading" :data="list" stripe size="small">
|
||||
<el-table-column label="时间" prop="opTime" width="160">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.opTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" prop="opUserName" width="100" />
|
||||
<el-table-column label="类型" prop="opType" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="tagType(scope.row.opType)" size="mini">{{ tagLabel(scope.row.opType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="摘要" prop="summary" min-width="320" show-overflow-tooltip />
|
||||
<el-table-column label="关联" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.refType">{{ refLabel(scope.row.refType) }} #{{ scope.row.refId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listWarehouseAudit } from '@/api/oa/warehouse/auditLog'
|
||||
|
||||
const TAG_MAP = {
|
||||
REQ_CREATE: { type: 'success', label: '需求新建' },
|
||||
REQ_UPDATE: { type: 'info', label: '需求修改' },
|
||||
REQ_DONE: { type: 'success', label: '需求完成' },
|
||||
REQ_CANCEL: { type: 'warning', label: '需求取消' },
|
||||
REQ_DELETE: { type: 'danger', label: '需求删除' },
|
||||
TASK_CREATE: { type: 'success', label: '采购创建' },
|
||||
TASK_DONE: { type: 'success', label: '采购完成' },
|
||||
TASK_CANCEL: { type: 'warning', label: '采购取消' },
|
||||
TASK_UPDATE: { type: 'info', label: '采购修改' },
|
||||
IN: { type: 'success', label: '入库' },
|
||||
OUT: { type: 'warning', label: '出库' },
|
||||
RETURN: { type: 'info', label: '退库' },
|
||||
STOCK_ADJUST:{ type: 'danger', label: '库存修正' }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'WarehouseAuditLog',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
queryParams: { pageNum: 1, pageSize: 20, opType: undefined, refType: undefined, refId: undefined },
|
||||
opTypes: Object.keys(TAG_MAP).map(k => ({ value: k, label: TAG_MAP[k].label }))
|
||||
}
|
||||
},
|
||||
created () { this.getList() },
|
||||
methods: {
|
||||
getList () {
|
||||
this.loading = true
|
||||
listWarehouseAudit(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery () {
|
||||
this.queryParams = { pageNum: 1, pageSize: 20, opType: undefined, refType: undefined, refId: undefined }
|
||||
this.getList()
|
||||
},
|
||||
tagType (op) { return (TAG_MAP[op] || {}).type || '' },
|
||||
tagLabel (op) { return (TAG_MAP[op] || {}).label || op },
|
||||
refLabel (ref) {
|
||||
return { requirement: '采购需求', task: '车间采购', master: '出入库单', warehouse: '物料' }[ref] || ref
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<el-dialog title="新建采购单" :visible.sync="visibleProxy" width="1080px" append-to-body
|
||||
:close-on-click-modal="false" custom-class="add-purchase-dialog" @close="handleClose">
|
||||
|
||||
<!-- 选中清单 sticky strip(任何步骤都看得到) -->
|
||||
<div class="selected-strip" v-if="selected.length">
|
||||
<span class="strip-label">已选 {{ selected.length }} 项:</span>
|
||||
<el-tag v-for="(it, idx) in selected" :key="it._key" type="info" size="small"
|
||||
closable disable-transitions @close="removeItem(idx)" class="strip-tag">
|
||||
{{ tagText(it) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else class="selected-strip empty">还没选任何物料</div>
|
||||
|
||||
<!-- 步骤指示 -->
|
||||
<el-steps :active="step" simple class="compact-steps" finish-status="success">
|
||||
<el-step title="选物料" icon="el-icon-search" />
|
||||
<el-step title="设数量备注" icon="el-icon-edit-outline" />
|
||||
<el-step title="确认提交" icon="el-icon-check" />
|
||||
</el-steps>
|
||||
|
||||
<!-- Step 1: 选物料 + 右侧推荐 -->
|
||||
<div v-if="step === 0" class="step1-wrap">
|
||||
<div class="step1-main">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
||||
<el-input v-model="keyword" placeholder="搜索 物料名 / 型号 / 品牌" clearable
|
||||
size="mini" prefix-icon="el-icon-search" style="flex:1"
|
||||
@input="onSearch" @clear="onSearch" />
|
||||
<el-button size="mini" type="success" plain icon="el-icon-plus"
|
||||
@click="newMatVisible = true">新增物料</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 新增物料子弹窗 -->
|
||||
<el-dialog title="新增物料" :visible.sync="newMatVisible" width="520px" append-to-body
|
||||
:close-on-click-modal="false">
|
||||
<el-form ref="newMatForm" :model="newMat" :rules="newMatRules" size="mini" label-width="80px">
|
||||
<el-form-item label="物料名" prop="name">
|
||||
<el-input v-model="newMat.name" placeholder="必填,如:RVV 电源线" />
|
||||
</el-form-item>
|
||||
<el-form-item label="型号" prop="model">
|
||||
<el-input v-model="newMat.model" placeholder="如:RVV-2×1.5" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌" prop="brand">
|
||||
<el-input v-model="newMat.brand" placeholder="如:远东" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格" prop="specifications">
|
||||
<el-input v-model="newMat.specifications" placeholder="如:100m/卷" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="newMat.unit" placeholder="如:卷/个/m" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预警阈值" prop="threshold">
|
||||
<el-input-number v-model="newMat.threshold" :min="0" controls-position="right"
|
||||
style="width: 140px" />
|
||||
<span style="color:#909399; margin-left:8px;">库存低于此值会提醒补货</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="本次采购" prop="taskInventory">
|
||||
<el-input-number v-model="newMat.taskInventory" :min="1" controls-position="right"
|
||||
style="width: 140px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button size="mini" @click="newMatVisible = false">取消</el-button>
|
||||
<el-button size="mini" type="primary" :loading="newMatSubmitting"
|
||||
@click="submitNewMaterial">保存并加入采购单</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-table v-loading="loading" :data="list" size="mini" stripe
|
||||
:row-class-name="rowDangerClass" max-height="320">
|
||||
<template slot="empty">
|
||||
<div style="padding: 24px 0; text-align: center;">
|
||||
<div style="color:#909399; margin-bottom: 12px;">
|
||||
<i class="el-icon-search" style="font-size: 28px;"></i>
|
||||
<div style="margin-top: 6px;">没有找到匹配的物料</div>
|
||||
<div v-if="keyword" style="font-size: 11px; margin-top: 4px;">
|
||||
库里没有「{{ keyword }}」,直接新增?
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="success" size="mini" icon="el-icon-plus"
|
||||
@click="openNewMatFromSearch">新增物料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column label="物料" prop="name" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="型号" prop="model" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="品牌" prop="brand" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="规格" prop="specifications" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="库存" prop="inventory" width="64" align="right" />
|
||||
<el-table-column label="阈值" prop="threshold" width="64" align="right" />
|
||||
<el-table-column label="操作" width="56" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button v-if="!isSelected(s.row)" type="text" class="add-mini-btn"
|
||||
icon="el-icon-circle-plus-outline" @click="addItem(s.row)">加入</el-button>
|
||||
<i v-else class="el-icon-check" style="color:#67c23a; font-size:14px;" title="已加" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize" :page-sizes="[20, 50, 100]" @pagination="loadList" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧 推荐(低于阈值 Top 20,按库存与阈值差倒序) -->
|
||||
<div class="step1-side">
|
||||
<div class="side-header">
|
||||
<i class="el-icon-magic-stick" style="color:#e6a23c;"></i>
|
||||
<span>推荐补货({{ recommend.length }})</span>
|
||||
</div>
|
||||
<div class="side-list" v-loading="recommendLoading">
|
||||
<div v-for="r in recommend" :key="r.id" class="rec-item" :class="{ disabled: recIsSelected(r) }"
|
||||
@click="recIsSelected(r) ? null : addRecommend(r)">
|
||||
<div class="rec-text" :title="recommendText(r)">
|
||||
{{ recommendText(r) }}
|
||||
</div>
|
||||
<div class="rec-meta">
|
||||
<span class="rec-stock">库存 {{ r.inventory || 0 }} / 阈值 {{ r.threshold || 0 }}</span>
|
||||
<i v-if="recIsSelected(r)" class="el-icon-check" style="color:#67c23a"></i>
|
||||
<i v-else class="el-icon-circle-plus-outline" style="color:#409eff"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!recommendLoading && !recommend.length" class="rec-empty">
|
||||
暂无低于阈值的物料
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: 设数量备注 -->
|
||||
<div v-if="step === 1">
|
||||
<el-alert v-if="!selected.length" type="warning" :closable="false"
|
||||
title="请先在步骤 1 选物料" />
|
||||
<el-table v-else :data="selected" size="mini" stripe max-height="380">
|
||||
<el-table-column label="物料" min-width="200">
|
||||
<template slot-scope="s">{{ tagText(s.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" prop="unit" width="60" />
|
||||
<el-table-column label="采购数量" width="120" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.taskInventory" :min="1" size="mini" controls-position="right"
|
||||
style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="截止日期" width="160" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-date-picker v-model="s.row.endTime" type="date" size="mini" value-format="yyyy-MM-dd"
|
||||
placeholder="选日期" style="width: 140px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单项备注" min-width="160">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.remark" size="mini" placeholder="可不填" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="50" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" style="color:#f56c6c" @click="removeItem(s.$index)">×</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 确认 -->
|
||||
<div v-if="step === 2">
|
||||
<el-form size="mini" label-width="80px">
|
||||
<el-form-item label="关联需求">
|
||||
<el-select v-model="form.requirementId" placeholder="可选" filterable clearable style="width: 100%">
|
||||
<el-option v-for="r in requirementOptions" :key="r.requirementId" :label="r.title"
|
||||
:value="r.requirementId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单据备注">
|
||||
<el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="例如:急、月度补货" />
|
||||
<div style="color:#909399; font-size:11px;">提示:备注里写"急"会被识别为加急单</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="border:1px solid #ebeef5; border-radius:4px; padding:6px 10px; max-height:180px; overflow:auto;">
|
||||
<div v-for="(it, i) in selected" :key="it._key"
|
||||
style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed #f0f0f0;">
|
||||
<span>{{ i + 1 }}. {{ tagText(it) }}</span>
|
||||
<span style="color:#606266;">× {{ it.taskInventory }} {{ it.unit || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="mini" @click="visibleProxy = false">取消</el-button>
|
||||
<el-button size="mini" v-if="step > 0" @click="step--">上一步</el-button>
|
||||
<el-button size="mini" type="primary" v-if="step < 2"
|
||||
:disabled="step === 0 && !selected.length" @click="step++">下一步</el-button>
|
||||
<el-button size="mini" type="success" v-if="step === 2"
|
||||
:loading="submitting" @click="submit">提交采购单</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addOaWarehouse, listNotThreshold, listThreshold } from '@/api/oa/warehouse/oaWarehouse'
|
||||
import { addOaWarehouseTaskBatch } from '@/api/oa/warehouse/warehouseTask'
|
||||
import { listRequirements } from '@/api/oa/requirement'
|
||||
|
||||
export default {
|
||||
name: 'AddPurchaseDialog',
|
||||
props: { visible: Boolean },
|
||||
data () {
|
||||
return {
|
||||
step: 0,
|
||||
loading: false,
|
||||
submitting: false,
|
||||
keyword: '',
|
||||
queryParams: { pageNum: 1, pageSize: 20, name: '' },
|
||||
list: [],
|
||||
total: 0,
|
||||
// 右侧推荐
|
||||
recommend: [],
|
||||
recommendLoading: false,
|
||||
selected: [],
|
||||
form: { requirementId: undefined, remark: '' },
|
||||
requirementOptions: [],
|
||||
// 新增物料子弹窗
|
||||
newMatVisible: false,
|
||||
newMatSubmitting: false,
|
||||
newMat: { name: '', model: '', brand: '', specifications: '', unit: '',
|
||||
threshold: 5, taskInventory: 1 },
|
||||
newMatRules: {
|
||||
name: [{ required: true, message: '物料名必填', trigger: 'blur' }],
|
||||
taskInventory: [{ required: true, message: '采购数量必填', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleProxy: {
|
||||
get () { return this.visible },
|
||||
set (v) { this.$emit('update:visible', v) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (v) {
|
||||
if (v) {
|
||||
this.step = 0
|
||||
this.selected = []
|
||||
this.form = { requirementId: undefined, remark: '' }
|
||||
this.keyword = ''
|
||||
this.queryParams = { pageNum: 1, pageSize: 20, name: '' }
|
||||
this.loadList()
|
||||
this.loadRecommend()
|
||||
this.loadRequirements()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 列表为空时点新增物料:把搜索词预填到物料名
|
||||
openNewMatFromSearch () {
|
||||
if (this.keyword) this.newMat.name = this.keyword
|
||||
this.newMatVisible = true
|
||||
},
|
||||
submitNewMaterial () {
|
||||
this.$refs.newMatForm.validate(ok => {
|
||||
if (!ok) return
|
||||
this.newMatSubmitting = true
|
||||
const matPayload = {
|
||||
name: this.newMat.name,
|
||||
model: this.newMat.model,
|
||||
brand: this.newMat.brand,
|
||||
specifications: this.newMat.specifications,
|
||||
unit: this.newMat.unit,
|
||||
threshold: this.newMat.threshold,
|
||||
inventory: 0,
|
||||
price: 0
|
||||
}
|
||||
addOaWarehouse(matPayload).then(res => {
|
||||
const newId = (res && res.data && (res.data.id || res.data.warehouseId)) || res.id || res.warehouseId
|
||||
this.selected.push({
|
||||
_key: 'new_' + Date.now(),
|
||||
warehouseId: newId,
|
||||
name: this.newMat.name,
|
||||
model: this.newMat.model,
|
||||
brand: this.newMat.brand,
|
||||
specifications: this.newMat.specifications,
|
||||
unit: this.newMat.unit,
|
||||
taskInventory: this.newMat.taskInventory,
|
||||
endTime: '',
|
||||
remark: '新增物料'
|
||||
})
|
||||
this.$modal.msgSuccess('物料已建档并加入采购单')
|
||||
this.newMatVisible = false
|
||||
this.newMat = { name: '', model: '', brand: '', specifications: '', unit: '',
|
||||
threshold: 5, taskInventory: 1 }
|
||||
this.loadList()
|
||||
}).finally(() => { this.newMatSubmitting = false })
|
||||
})
|
||||
},
|
||||
tagText (it) {
|
||||
return [it.name, it.model, it.brand, it.specifications].filter(Boolean).join(' · ')
|
||||
},
|
||||
onSearch () {
|
||||
this.queryParams.name = this.keyword || ''
|
||||
this.queryParams.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
loadList () {
|
||||
this.loading = true
|
||||
// 统一查所有物料;低于阈值的行会被 rowDangerClass 自动染红
|
||||
listNotThreshold(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
loadRecommend () {
|
||||
this.recommendLoading = true
|
||||
listThreshold({ pageNum: 1, pageSize: 20 }).then(res => {
|
||||
this.recommend = res.rows || []
|
||||
}).finally(() => { this.recommendLoading = false })
|
||||
},
|
||||
recommendText (r) {
|
||||
return [r.name, r.model, r.brand, r.specifications].filter(Boolean).join('-')
|
||||
},
|
||||
recIsSelected (r) {
|
||||
return this.selected.some(s => s.warehouseId === r.id)
|
||||
},
|
||||
addRecommend (r) {
|
||||
this.selected.push({
|
||||
_key: r.id + '_' + Date.now(),
|
||||
warehouseId: r.id,
|
||||
name: r.name,
|
||||
model: r.model,
|
||||
brand: r.brand,
|
||||
specifications: r.specifications,
|
||||
unit: r.unit,
|
||||
taskInventory: Math.max(1, (r.threshold || 1) - (r.inventory || 0)),
|
||||
endTime: '',
|
||||
remark: ''
|
||||
})
|
||||
},
|
||||
loadRequirements () {
|
||||
listRequirements({ pageNum: 1, pageSize: 200, status: 1 }).then(res => {
|
||||
this.requirementOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
isSelected (row) {
|
||||
return this.selected.some(s => s.warehouseId === row.id)
|
||||
},
|
||||
addItem (row) {
|
||||
const item = {
|
||||
_key: row.id + '_' + Date.now(),
|
||||
warehouseId: row.id,
|
||||
name: row.name,
|
||||
model: row.model,
|
||||
brand: row.brand,
|
||||
specifications: row.specifications,
|
||||
unit: row.unit,
|
||||
taskInventory: Math.max(1, (row.threshold || 1) - (row.inventory || 0)),
|
||||
endTime: '',
|
||||
remark: ''
|
||||
}
|
||||
this.selected.push(item)
|
||||
},
|
||||
removeItem (idx) {
|
||||
this.selected.splice(idx, 1)
|
||||
},
|
||||
rowDangerClass ({ row }) {
|
||||
return (row.inventory != null && row.threshold != null && row.inventory < row.threshold)
|
||||
? 'row-urgent' : ''
|
||||
},
|
||||
submit () {
|
||||
if (!this.selected.length) {
|
||||
this.$modal.msgWarning('请先选择物料')
|
||||
return
|
||||
}
|
||||
// 把单据层 remark 透传到每一项;后端按需消化
|
||||
const payload = this.selected.map(s => ({
|
||||
warehouseId: s.warehouseId,
|
||||
name: s.name,
|
||||
model: s.model,
|
||||
brand: s.brand,
|
||||
specifications: s.specifications,
|
||||
unit: s.unit,
|
||||
taskInventory: s.taskInventory,
|
||||
endTime: s.endTime,
|
||||
remark: s.remark || this.form.remark || '',
|
||||
requirementId: this.form.requirementId
|
||||
}))
|
||||
this.submitting = true
|
||||
addOaWarehouseTaskBatch(payload).then(() => {
|
||||
this.$modal.msgSuccess('采购单已创建')
|
||||
this.visibleProxy = false
|
||||
this.$emit('saved')
|
||||
}).finally(() => { this.submitting = false })
|
||||
},
|
||||
handleClose () { this.step = 0 }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-purchase-dialog ::v-deep .el-dialog__body { padding: 12px 18px; }
|
||||
.selected-strip {
|
||||
background: #f4f7fa;
|
||||
border: 1px dashed #d8dce5;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 6px;
|
||||
min-height: 32px;
|
||||
font-size: 12px;
|
||||
&.empty { color: #909399; }
|
||||
}
|
||||
.strip-label { color: #606266; margin-right: 4px; }
|
||||
.strip-tag { margin-right: 0 !important; }
|
||||
.compact-steps { margin-bottom: 10px; }
|
||||
.compact-steps ::v-deep .el-step__title { font-size: 12px; line-height: 28px; }
|
||||
.compact-steps ::v-deep .is-simple .el-step__head { font-size: 13px; }
|
||||
.add-mini-btn.el-button {
|
||||
padding: 0 !important;
|
||||
font-size: 11px !important;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
i { font-size: 12px; margin-right: 2px; }
|
||||
}
|
||||
/* Step1 左右布局 */
|
||||
.step1-wrap { display: flex; gap: 12px; }
|
||||
.step1-main { flex: 1; min-width: 0; }
|
||||
.step1-side {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 460px;
|
||||
background: #fffefb;
|
||||
}
|
||||
.side-header {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
background: #fdf6ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.side-list { flex: 1; overflow-y: auto; padding: 4px; }
|
||||
.rec-item {
|
||||
padding: 6px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
transition: all .15s;
|
||||
border: 1px solid transparent;
|
||||
&:hover { background: #ecf5ff; border-color: #d9ecff; }
|
||||
&.disabled { opacity: .55; cursor: default; background: #f4f4f5; }
|
||||
}
|
||||
.rec-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #303133;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.rec-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #909399;
|
||||
font-size: 10px;
|
||||
}
|
||||
.rec-stock { color: #f56c6c; }
|
||||
.rec-empty { padding: 24px 0; text-align: center; color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="68px" class="compact-search">
|
||||
<el-form-item label="型号" prop="model">
|
||||
<el-input
|
||||
v-model="queryParams.model"
|
||||
@@ -87,16 +88,17 @@
|
||||
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
|
||||
plain
|
||||
icon="el-icon-upload"
|
||||
type="primary"
|
||||
icon="el-icon-shopping-cart-2"
|
||||
size="mini"
|
||||
@click="addWarehouseTask"
|
||||
>添加采购计划</el-button>
|
||||
@click="addDialogVisible = true"
|
||||
>新建采购单</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<add-purchase-dialog :visible.sync="addDialogVisible" @saved="getList" />
|
||||
|
||||
<el-table v-loading="loading" :data="oaWarehouseList" @selection-change="handleSelectionChange" :row-class-name="tableRowClassName">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index"/>
|
||||
@@ -227,10 +229,13 @@
|
||||
import { delOaWarehouse, getOaWarehouse, listOaWarehouse, updateOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
|
||||
import { addOaWarehouseMasterToIn } from "@/api/oa/warehouse/warehouseMaster";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import AddPurchaseDialog from "./components/AddPurchaseDialog.vue";
|
||||
export default {
|
||||
name: "OaWarehouse",
|
||||
components: { AddPurchaseDialog },
|
||||
data() {
|
||||
return {
|
||||
addDialogVisible: false,
|
||||
// 用户导入参数
|
||||
upload: {
|
||||
// 是否显示弹出层(用户导入)
|
||||
@@ -270,7 +275,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 50,
|
||||
inventory: undefined,
|
||||
model: undefined,
|
||||
unit: undefined,
|
||||
|
||||
@@ -1,107 +1,154 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<!-- 状态筛选 tabs:全部 / 加急 / 未完成 / 已完成 -->
|
||||
<el-tabs v-model="statusFilter" @tab-click="onStatusTabChange" class="compact-tabs">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane name="urgent">
|
||||
<span slot="label" style="color:#f56c6c;">
|
||||
<i class="el-icon-warning-outline"></i> 加急 ({{ stat.urgent }})
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`未完成 (${stat.undone})`" name="undone" />
|
||||
<el-tab-pane :label="`已完成 (${stat.done})`" name="done" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="68px" class="compact-search">
|
||||
<el-form-item label="关联需求" prop="requirementId">
|
||||
<el-select v-model="queryParams.requirementId" placeholder="选择需求" filterable clearable style="width: 200px">
|
||||
<el-option v-for="item in requirementList" :key="item.requirementId" :label="item.title"
|
||||
:value="item.requirementId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间" prop="signTime">
|
||||
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
|
||||
:default-time="['00:00:00', '23:59:59']">
|
||||
</el-date-picker>
|
||||
:default-time="['00:00:00', '23:59:59']" />
|
||||
</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="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
|
||||
v-hasPermi="['oa:oaOutWarehouse:remove']">删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" class="add-purchase-btn"
|
||||
@click="addDialogVisible = true">新建采购单</el-button>
|
||||
<add-purchase-dialog :visible.sync="addDialogVisible" @saved="getList" />
|
||||
|
||||
<el-table v-loading="loading" :data="TaskList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="采购单编号" align="center" prop="masterNum">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.masterNum" size="mini" placeholder="请输入采购单编号"
|
||||
@blur="updateMasterRemark(scope.row)" />
|
||||
<el-table v-loading="loading" :data="TaskList" @selection-change="handleSelectionChange" stripe size="small"
|
||||
row-key="masterId" :expand-row-keys="expandedKeys"
|
||||
:row-class-name="rowClassName" @expand-change="onRowExpand">
|
||||
<el-table-column type="expand" width="36">
|
||||
<template slot-scope="props">
|
||||
<div style="padding: 10px 24px; background:#fafafa;">
|
||||
<!-- 顶部:模式 + 批量入库 -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<span style="font-weight:600;">物料明细({{ (itemsMap[props.row.masterId] || []).length }} 项)</span>
|
||||
<el-radio-group v-model="mode" size="mini" v-if="props.row.status === 0">
|
||||
<el-radio-button label="single">单个操作</el-radio-button>
|
||||
<el-radio-button label="batch">批量操作</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="mode === 'batch' && props.row.status === 0"
|
||||
style="display:flex; align-items:center; gap:6px;">
|
||||
<el-select v-model="batchStatus" size="mini" placeholder="批量设置状态" style="width:140px">
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :value="s.value" :label="s.label" />
|
||||
</el-select>
|
||||
<el-button size="mini" type="success" @click="submitComplete">执行入库</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="itemsLoading[props.row.masterId]"
|
||||
:data="itemsMap[props.row.masterId] || []" size="mini" stripe ref="warehouseTable">
|
||||
<el-table-column v-if="mode === 'batch' && props.row.status === 0"
|
||||
type="selection" width="44" align="center" />
|
||||
<el-table-column label="物料名" prop="name" min-width="120" />
|
||||
<el-table-column label="截止" prop="endTime" width="120" align="center">
|
||||
<template slot-scope="s">
|
||||
<template v-if="s.row.endTime != null && s.row.taskStatus !== 2">
|
||||
<span v-if="dayDiff(s.row.endTime) > 3">{{ parseTime(s.row.endTime, '{y}-{m}-{d}') }}</span>
|
||||
<el-tag v-else-if="dayDiff(s.row.endTime) > 0" type="warning" size="mini" effect="plain">剩{{ dayDiff(s.row.endTime) }}天</el-tag>
|
||||
<el-tag v-else-if="dayDiff(s.row.endTime) === 0" type="danger" size="mini" effect="plain">今日</el-tag>
|
||||
<el-tag v-else type="danger" size="mini" effect="plain">逾{{ Math.abs(dayDiff(s.row.endTime)) }}天</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="taskInventory" width="64" align="right" />
|
||||
<el-table-column label="单位" prop="unit" width="56" />
|
||||
<el-table-column label="单价" width="110" align="right">
|
||||
<template slot-scope="s">
|
||||
<el-input v-if="s.row.taskStatus !== 2 && props.row.status === 0"
|
||||
v-model="s.row.price" size="mini" type="number" placeholder="¥" />
|
||||
<span v-else>¥{{ Number(s.row.price || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="型号" prop="model" min-width="100" />
|
||||
<el-table-column label="规格" prop="specifications" min-width="100" />
|
||||
<el-table-column label="品牌" prop="brand" min-width="80" />
|
||||
<el-table-column label="备注" prop="remark" min-width="140">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.remark" size="mini" placeholder="备注"
|
||||
:disabled="s.row.taskStatus === 2 || props.row.status === 1"
|
||||
@blur="updateRemark(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="taskStatus" width="110" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-tag v-if="s.row.taskStatus === 2" type="success" size="mini">完成</el-tag>
|
||||
<el-select v-else-if="mode === 'single' && props.row.status === 0"
|
||||
v-model="s.row.taskStatus" size="mini" placeholder="状态"
|
||||
@change="handleUpdateTask(s.row)">
|
||||
<el-option v-for="opt in filteredStatusOptions(s.row)" :key="opt.value"
|
||||
:value="opt.value" :label="opt.label" />
|
||||
</el-select>
|
||||
<el-tag v-else size="mini">{{ statusLabel(s.row.taskStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="60">
|
||||
<template slot-scope="s">
|
||||
<el-button v-if="s.row.taskStatus !== 2 && props.row.status === 0"
|
||||
size="mini" type="text" style="color:#f56c6c"
|
||||
@click="handleBatchDelete(s.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'warning'">{{
|
||||
scope.row.status === 1 ? "完成" : "未完成"
|
||||
}}
|
||||
</el-tag>
|
||||
</template>
|
||||
<el-table-column type="selection" width="44" align="center" />
|
||||
<el-table-column label="操作时间" prop="signTime" width="100">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.signTime, "{y}-{m}-{d}") }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近截止日期" align="center" prop="nearestEndTime">
|
||||
<template slot-scope="scope" v-if="scope.row.nearestEndTime != null && scope.row.status !== 1">
|
||||
<template v-if="dayDiff(scope.row.nearestEndTime) > 3">
|
||||
<!-- 超过 3 天,正常显示 -->
|
||||
<span>{{ parseTime(scope.row.nearestEndTime, '{y}-{m}-{d}') }}</span>
|
||||
|
||||
</template><template v-else-if="dayDiff(scope.row.nearestEndTime) > 0">
|
||||
<!-- 未来 1–3 天 -->
|
||||
<el-tag type="warning" effect="plain">
|
||||
剩余{{ dayDiff(scope.row.nearestEndTime) }}天
|
||||
</el-tag>
|
||||
|
||||
</template><template v-else-if="dayDiff(scope.row.nearestEndTime) === 0">
|
||||
<!-- 今天到期 -->
|
||||
<el-tag type="danger" effect="plain">
|
||||
<i class="el-icon-warning-outline"></i> 今日过期!
|
||||
</el-tag>
|
||||
|
||||
</template><template v-else>
|
||||
<!-- 已过期 -->
|
||||
<el-tag type="danger" effect="plain">
|
||||
逾期{{ Math.abs(dayDiff(scope.row.endTime)) }}天
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作时间" align="center" prop="signTime">
|
||||
<el-table-column label="操作人" prop="signUser" width="90" />
|
||||
<el-table-column label="关联需求" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.signTime, "{y}-{m}-{d}") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" align="center" prop="signUser" />
|
||||
<el-table-column label="备注" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.remark" size="mini" type="textarea" placeholder="请输入备注"
|
||||
@blur="updateMasterRemark(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="需求编号" align="center" prop="requirementNum">
|
||||
<template slot-scope="scope">
|
||||
<el-select v-model="scope.row.requirementId" placeholder="请选择需求编号" filterable clearable
|
||||
@change="updateMasterRemark(scope.row)">
|
||||
<el-select v-model="scope.row.requirementId" placeholder="选择需求" filterable clearable size="mini"
|
||||
style="width: 100%" @change="updateMasterRemark(scope.row)">
|
||||
<el-option v-for="item in requirementList" :key="item.requirementId" :label="item.title"
|
||||
:value="item.requirementId" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="状态" prop="status" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看
|
||||
</el-button>
|
||||
|
||||
<el-button size="mini" type="text" icon="el-icon-finished" v-if="scope.row.status === 0"
|
||||
@click="handleIn(scope.row)">执行入库
|
||||
</el-button>
|
||||
|
||||
<el-button size="mini" type="text" icon="el-icon-check" v-if="scope.row.status === 0"
|
||||
@click="handComplete(scope.row)">完成
|
||||
</el-button>
|
||||
|
||||
<el-button size="mini" type="text" icon="el-icon-download" @click="handleExport(scope.row)">导出
|
||||
</el-button>
|
||||
|
||||
<el-button size="mini" type="text" icon="el-icon-remove" @click="handleDelete(scope.row)">删除
|
||||
</el-button>
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'warning'" size="mini">
|
||||
{{ scope.row.status === 1 ? "已完成" : "未完成" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.remark" size="mini" placeholder="点击编辑"
|
||||
@blur="updateMasterRemark(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" v-if="scope.row.status === 0"
|
||||
@click="expandRow(scope.row)">执行入库</el-button>
|
||||
<el-button size="mini" type="text" v-if="scope.row.status === 0"
|
||||
@click="handComplete(scope.row)">完成</el-button>
|
||||
<el-button size="mini" type="text" @click="handleExport(scope.row)">导出</el-button>
|
||||
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -109,126 +156,12 @@
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<el-drawer size="70%" :title="parseTime(searchItem.signTime) + '-采购单'" :visible.sync="drawer">
|
||||
<el-table v-loading="loading" :data="warehouseTaskList">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="物料名" align="center" prop="name" />
|
||||
<el-table-column label="采购数量" align="center" prop="taskInventory" />
|
||||
<el-table-column label="单位" align="center" prop="unit" />
|
||||
<el-table-column label="品牌" align="center" prop="brand" />
|
||||
<el-table-column label="型号" align="center" prop="model" />
|
||||
<el-table-column label="规格" align="center" prop="specifications" />
|
||||
<el-table-column label="操作" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" icon="el-icon-edit" @click="handleRemoveTask(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer size="70%" :title="parseTime(searchItem.signTime, '{y}_{m}_{d}') + '_采购单'"
|
||||
:visible.sync="completeDrawer">
|
||||
<!-- 工具栏 -->
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:10px">
|
||||
<!-- 模式切换 -->
|
||||
<el-radio-group v-model="mode" size="mini">
|
||||
<el-radio-button label="single">单个入库</el-radio-button>
|
||||
<el-radio-button label="batch">批量操作</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="warehouseTaskList" ref="warehouseTable">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" type="index" />
|
||||
<el-table-column label="物料名" align="center" prop="name" />
|
||||
<el-table-column label="截止日期" align="center" prop="endTime">
|
||||
<template slot-scope="scope" v-if="scope.row.endTime != null">
|
||||
<template v-if="dayDiff(scope.row.endTime) > 3">
|
||||
<!-- 超过 3 天,正常显示 -->
|
||||
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
|
||||
|
||||
</template><template v-else-if="dayDiff(scope.row.endTime) > 0">
|
||||
<!-- 未来 1–3 天 -->
|
||||
<el-tag type="warning" effect="plain">
|
||||
剩余{{ dayDiff(scope.row.endTime) }}天
|
||||
</el-tag>
|
||||
|
||||
</template><template v-else-if="dayDiff(scope.row.endTime) === 0">
|
||||
<!-- 今天到期 -->
|
||||
<el-tag type="danger" effect="plain">
|
||||
<i class="el-icon-warning-outline"></i> 今日过期!
|
||||
</el-tag>
|
||||
|
||||
</template><template v-else>
|
||||
<!-- 已过期 -->
|
||||
<el-tag type="danger" effect="plain">
|
||||
逾期{{ Math.abs(dayDiff(scope.row.endTime)) }}天
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="采购数量" align="center" prop="taskInventory" />
|
||||
<el-table-column label="采购价格" align="center" prop="price">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.price" v-if="scope.row.taskStatus !== 2" type="number"></el-input>
|
||||
<span v-else>
|
||||
已经操作入库,录入价格请前往入库明细查看
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="品牌" align="center" prop="brand" />
|
||||
<el-table-column label="规格" align="center" prop="specifications" />
|
||||
<el-table-column label="备注" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.remark" size="mini" placeholder="请输入备注" :disabled="scope.row.taskStatus === 2"
|
||||
@blur="updateRemark(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- ⇩⇩⇩ ③ 操作列(单行按钮) ⇩⇩⇩ -->
|
||||
<!-- ★ 新增状态列 ★ -->
|
||||
<el-table-column label="状态" prop="taskStatus" width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<!-- 单个模式:可编辑 -->
|
||||
<el-tag type="success" v-if="scope.row.taskStatus === 2">完成</el-tag>
|
||||
<!-- 单个模式下可编辑,动态过滤选项 -->
|
||||
<el-select v-else-if="mode === 'single'" v-model="scope.row.taskStatus" size="mini" placeholder="状态"
|
||||
@change="handleUpdateTask(scope.row)">
|
||||
<el-option v-for="s in filteredStatusOptions(scope.row)" :key="s.value" :value="s.value"
|
||||
:label="s.label" />
|
||||
</el-select>
|
||||
|
||||
<!-- 批量模式:只显示 -->
|
||||
<span v-else>{{ statusLabel(scope.row.taskStatus) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
|
||||
<el-button size="mini" type="danger" v-if="scope.row.taskStatus !== 2"
|
||||
@click="handleBatchDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display: flex; justify-content: flex-end; margin: 20px">
|
||||
<!-- 批量模式下才显示 -->
|
||||
<div v-if="mode === 'batch'">
|
||||
<el-select v-model="batchStatus" size="mini" placeholder="选择批量状态">
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :value="s.value" :label="s.label" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button @click="submitComplete" v-if="mode === 'batch'" size="mini" type="success">执行入库</el-button>
|
||||
<el-button @click="completeDrawer = false" size="mini">关闭</el-button>
|
||||
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listRequirements } from "@/api/oa/requirement";
|
||||
import AddPurchaseDialog from "./components/AddPurchaseDialog.vue";
|
||||
import {
|
||||
addOaWarehouseMaster,
|
||||
delOaWarehouseMaster, listOaWarehouseMaster,
|
||||
@@ -246,8 +179,19 @@ import {
|
||||
|
||||
export default {
|
||||
name: "OaOutWarehouse",
|
||||
components: { AddPurchaseDialog },
|
||||
data () {
|
||||
return {
|
||||
// 顶部状态筛选
|
||||
statusFilter: 'all',
|
||||
stat: { undone: 0, done: 0, urgent: 0 },
|
||||
// 物料明细缓存(按 masterId)
|
||||
itemsMap: {},
|
||||
itemsLoading: {},
|
||||
// 当前展开的行(单展开)
|
||||
expandedKeys: [],
|
||||
// 新建采购单 dialog
|
||||
addDialogVisible: false,
|
||||
completeDrawer: false,
|
||||
mode: 'single',
|
||||
batchStatus: null, // 批量入库时选择的状态
|
||||
@@ -484,10 +428,84 @@ export default {
|
||||
this.queryParams.endTime = '';
|
||||
}
|
||||
listOaWarehouseMaster(this.queryParams).then((res) => {
|
||||
this.TaskList = res.rows;
|
||||
this.total = res.total;
|
||||
this.TaskList = res.rows || [];
|
||||
this.total = res.total || 0;
|
||||
this.loading = false;
|
||||
// 重置已展开行的缓存
|
||||
this.itemsMap = {};
|
||||
});
|
||||
this.refreshStat();
|
||||
},
|
||||
// 加急判定(备注里含「急」)
|
||||
isUrgent (remark) {
|
||||
return !!remark && remark.indexOf('急') !== -1
|
||||
},
|
||||
// 加急且未完成 → 行红底
|
||||
rowClassName ({ row }) {
|
||||
if (row.status !== 1 && this.isUrgent(row.remark)) return 'row-urgent'
|
||||
return ''
|
||||
},
|
||||
// 拉总览数量(不分页快速 head 计数)
|
||||
refreshStat () {
|
||||
const base = { ...this.queryParams, pageNum: 1, pageSize: 1, status: undefined, remark: undefined };
|
||||
Promise.all([
|
||||
listOaWarehouseMaster({ ...base, status: 0 }),
|
||||
listOaWarehouseMaster({ ...base, status: 1 }),
|
||||
listOaWarehouseMaster({ ...base, remark: '急' })
|
||||
]).then(([u, d, urg]) => {
|
||||
this.stat.undone = u.total || 0
|
||||
this.stat.done = d.total || 0
|
||||
this.stat.urgent = urg.total || 0
|
||||
})
|
||||
},
|
||||
// tab 切换
|
||||
onStatusTabChange () {
|
||||
// 重置过滤
|
||||
this.queryParams.status = undefined
|
||||
this.queryParams.remark = undefined
|
||||
if (this.statusFilter === 'undone') {
|
||||
this.queryParams.status = 0
|
||||
} else if (this.statusFilter === 'done') {
|
||||
this.queryParams.status = 1
|
||||
} else if (this.statusFilter === 'urgent') {
|
||||
this.queryParams.remark = '急'
|
||||
}
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
// 行展开:懒加载物料明细 + 同步 warehouseTaskList(兼容老方法)
|
||||
onRowExpand (row, expanded) {
|
||||
// 单展开:只保留当前
|
||||
if (expanded && expanded.length) {
|
||||
this.expandedKeys = [row.masterId]
|
||||
} else {
|
||||
this.expandedKeys = this.expandedKeys.filter(k => k !== row.masterId)
|
||||
}
|
||||
if (!expanded || !expanded.length) return
|
||||
const id = row.masterId
|
||||
this.currentMasterId = id
|
||||
if (this.itemsMap[id]) {
|
||||
this.warehouseTaskList = this.itemsMap[id]
|
||||
return
|
||||
}
|
||||
this.$set(this.itemsLoading, id, true)
|
||||
getOaWarehouseTaskByMasterId(id).then(res => {
|
||||
const list = res.data || res.rows || []
|
||||
this.$set(this.itemsMap, id, list)
|
||||
this.warehouseTaskList = list
|
||||
}).finally(() => {
|
||||
this.$set(this.itemsLoading, id, false)
|
||||
})
|
||||
},
|
||||
// 程序化展开(用于"执行入库"按钮)
|
||||
expandRow (row) {
|
||||
if (this.expandedKeys.includes(row.masterId)) {
|
||||
this.expandedKeys = this.expandedKeys.filter(k => k !== row.masterId)
|
||||
return
|
||||
}
|
||||
this.expandedKeys = [row.masterId]
|
||||
// 触发数据加载
|
||||
this.onRowExpand(row, [row.masterId])
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
@@ -639,3 +657,18 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-purchase-btn.el-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 110px; /* 让出 right-toolbar 的位置 */
|
||||
z-index: 3;
|
||||
height: 24px;
|
||||
line-height: 22px;
|
||||
padding: 0 8px !important;
|
||||
font-size: 11px !important;
|
||||
border-radius: 4px;
|
||||
i { font-size: 11px; margin-right: 2px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="68px" class="compact-search">
|
||||
<el-form-item label="需求标题" prop="title">
|
||||
<el-input v-model="queryParams.title" placeholder="请输入需求标题" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
@@ -40,41 +41,60 @@
|
||||
</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="success" plain icon="el-icon-edit" size="mini" :disabled="single"
|
||||
@click="handleUpdate">修改</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-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button :type="onlyOwnerMe ? 'primary' : 'default'" icon="el-icon-user" size="mini"
|
||||
@click="toggleOnlyOwnerMe">只看发布给我的</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button :type="onlyRequesterMe ? 'primary' : 'default'" icon="el-icon-user-solid" size="mini"
|
||||
@click="toggleOnlyRequesterMe">只看我发布的</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
<div class="req-toolbar">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
<span class="req-toolbar-sep"></span>
|
||||
<el-button :type="onlyOwnerMe ? 'primary' : ''" icon="el-icon-user" size="mini" @click="toggleOnlyOwnerMe">只看发布给我的</el-button>
|
||||
<el-button :type="onlyRequesterMe ? 'primary' : ''" icon="el-icon-user-solid" size="mini" @click="toggleOnlyRequesterMe">只看我发布的</el-button>
|
||||
</div>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<!-- 新增提示组件 -->
|
||||
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange"
|
||||
@expand-change="onExpandChange">
|
||||
<el-table-column type="expand" width="36">
|
||||
<template slot-scope="props">
|
||||
<div style="padding: 8px 24px; background:#fafafa;">
|
||||
<div style="font-weight:600; margin-bottom:6px;">
|
||||
已入库批次({{ (batchMap[props.row.requirementId] || []).length }} 批)
|
||||
</div>
|
||||
<el-table v-loading="batchLoading[props.row.requirementId]"
|
||||
:data="batchMap[props.row.requirementId] || []" size="mini" stripe>
|
||||
<el-table-column label="入库时间" prop="signTime" width="160">
|
||||
<template slot-scope="s">{{ parseTime(s.row.signTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入库单" prop="masterNum" width="160" />
|
||||
<el-table-column label="入库人" prop="signUser" width="100" />
|
||||
<el-table-column label="物料概览" prop="summary" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="总数量" prop="totalQty" width="80" align="right" />
|
||||
<el-table-column label="总金额" width="110" align="right">
|
||||
<template slot-scope="s">¥{{ Number(s.row.totalAmount || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="(batchMap[props.row.requirementId] || []).length === 0
|
||||
&& !batchLoading[props.row.requirementId]"
|
||||
style="text-align:center; color:#909399; padding:12px 0;">
|
||||
暂无入库批次
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="需求标题" align="center" prop="title" />
|
||||
<el-table-column label="需求方" align="center" prop="requesterNickName" />
|
||||
<el-table-column label="负责人" align="center" prop="ownerNickName" />
|
||||
<el-table-column label="关联项目" align="center" prop="projectName" />
|
||||
<el-table-column label="需求描述" align="center" prop="description" />
|
||||
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
|
||||
title="点击复制">{{ row.description }}</span>
|
||||
<span v-else style="color:#c0c4cc;">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||
@@ -105,9 +125,18 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" align="center" prop="accessory" max-width="180">
|
||||
<el-table-column label="附件" align="center" prop="accessoryFiles" width="180">
|
||||
<template slot-scope="{ row }">
|
||||
<file-preview v-model="row.accessory" :simple="true" />
|
||||
<template v-if="parseAccessoryFiles(row.accessoryFiles).length">
|
||||
<el-tooltip v-for="f in parseAccessoryFiles(row.accessoryFiles)" :key="f.ossId"
|
||||
:content="f.name" placement="top" effect="dark">
|
||||
<a class="accessory-link" :href="f.url || 'javascript:void(0)'"
|
||||
target="_blank" @click="!f.url && downloadOss(f.ossId)">
|
||||
<i class="el-icon-paperclip"></i>{{ f.name }}
|
||||
</a>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
@@ -189,7 +218,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import FilePreview from '@/components/FilePreview';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
@@ -200,6 +229,9 @@ export default {
|
||||
components: { FileUpload, FilePreview, ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
// 入库批次(按 requirementId 缓存)
|
||||
batchMap: {},
|
||||
batchLoading: {},
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
@@ -225,7 +257,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 50,
|
||||
title: undefined,
|
||||
requesterId: undefined,
|
||||
ownerId: undefined,
|
||||
@@ -270,6 +302,55 @@ export default {
|
||||
this.getUsers();
|
||||
},
|
||||
methods: {
|
||||
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
||||
parseAccessoryFiles (raw) {
|
||||
if (!raw) return []
|
||||
return String(raw).split(',,').map(s => {
|
||||
const [ossId, name, url] = s.split('|')
|
||||
return { ossId, name: name || '附件', url: url || '' }
|
||||
}).filter(f => f.ossId)
|
||||
},
|
||||
copyText (text) {
|
||||
if (!text) return
|
||||
const fallback = () => {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try { document.execCommand('copy'); this.$modal.msgSuccess('复制成功') }
|
||||
catch (e) { this.$modal.msgError('复制失败,请手动选中') }
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => this.$modal.msgSuccess('复制成功'))
|
||||
.catch(fallback)
|
||||
} else {
|
||||
fallback()
|
||||
}
|
||||
},
|
||||
downloadOss (ossId) {
|
||||
if (this.$download && this.$download.oss) {
|
||||
this.$download.oss(ossId)
|
||||
} else {
|
||||
const file = this.ossFileMap[ossId]
|
||||
if (file && file.url) window.open(file.url, '_blank')
|
||||
}
|
||||
},
|
||||
// 展开行:加载该需求的入库批次
|
||||
onExpandChange (row, expanded) {
|
||||
if (!expanded || !expanded.length) return
|
||||
const id = row.requirementId
|
||||
if (this.batchMap[id]) return // 已有缓存
|
||||
this.$set(this.batchLoading, id, true)
|
||||
getRequirementBatches(id).then(res => {
|
||||
this.$set(this.batchMap, id, res.data || [])
|
||||
}).finally(() => {
|
||||
this.$set(this.batchLoading, id, false)
|
||||
})
|
||||
},
|
||||
async onStatusChange (row, newVal) {
|
||||
row._updating = true
|
||||
// 如果后端需要字符串,可改为 String(newVal)
|
||||
@@ -485,3 +566,43 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.req-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 6px;
|
||||
::v-deep .el-button--mini {
|
||||
padding: 4px 10px !important;
|
||||
font-size: 11px !important;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.req-toolbar-sep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #e4e7ed;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.copyable-text {
|
||||
cursor: pointer;
|
||||
&:hover { color: #409eff; text-decoration: underline; }
|
||||
}
|
||||
.accessory-link {
|
||||
display: inline-block;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
color: #409eff;
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
i { margin-right: 2px; }
|
||||
&:hover { text-decoration: underline; }
|
||||
& + .accessory-link { border-left: 1px solid #ebeef5; }
|
||||
}
|
||||
</style>
|
||||
|
||||
22
sql/sys_user_dashboard.sql
Normal file
22
sql/sys_user_dashboard.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- ----------------------------
|
||||
-- 用户工作台个性化布局表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_dashboard`;
|
||||
CREATE TABLE `sys_user_dashboard` (
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
|
||||
`layout_json` longtext NOT NULL COMMENT '布局 JSON: [{i,x,y,w,h,widgetKey,config}]',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`user_id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户工作台布局';
|
||||
|
||||
-- ----------------------------
|
||||
-- 默认工作台布局(存到 sys_config,前端在用户没有自定义布局时回退到此)
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`)
|
||||
SELECT (SELECT IFNULL(MAX(config_id), 0) + 1 FROM `sys_config` AS t),
|
||||
'默认工作台布局', 'sys.dashboard.defaultLayout',
|
||||
'[{"i":"announcements","x":0,"y":0,"w":6,"h":8,"widgetKey":"announcements"},{"i":"projectManagement","x":6,"y":0,"w":6,"h":8,"widgetKey":"projectManagement"},{"i":"ownerTaskList","x":0,"y":8,"w":6,"h":8,"widgetKey":"ownerTaskList"},{"i":"financialCharts","x":6,"y":8,"w":6,"h":8,"widgetKey":"financialCharts"},{"i":"miniCalendar","x":0,"y":16,"w":4,"h":8,"widgetKey":"miniCalendar"},{"i":"quickEntry","x":4,"y":16,"w":8,"h":4,"widgetKey":"quickEntry"}]',
|
||||
'Y', 'admin', NOW(), '工作台默认布局,用户首次进入或重置时使用'
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `config_key` = 'sys.dashboard.defaultLayout');
|
||||
4
sql/sys_user_dashboard_default_3col.sql
Normal file
4
sql/sys_user_dashboard_default_3col.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 把默认工作台改成三列布局(w:4 占 1/3)
|
||||
UPDATE `sys_config`
|
||||
SET `config_value` = '[{"i":"announcements","x":0,"y":0,"w":4,"h":8,"widgetKey":"announcements"},{"i":"projectManagement","x":4,"y":0,"w":4,"h":8,"widgetKey":"projectManagement"},{"i":"ownerTaskList","x":8,"y":0,"w":4,"h":8,"widgetKey":"ownerTaskList"},{"i":"financialCharts","x":0,"y":8,"w":4,"h":8,"widgetKey":"financialCharts"},{"i":"feedbackList","x":4,"y":8,"w":4,"h":8,"widgetKey":"feedbackList"},{"i":"miniCalendar","x":8,"y":8,"w":4,"h":8,"widgetKey":"miniCalendar"},{"i":"quickEntry","x":0,"y":16,"w":12,"h":4,"widgetKey":"quickEntry"}]'
|
||||
WHERE `config_key` = 'sys.dashboard.defaultLayout';
|
||||
74
sql/sys_user_im_bind.sql
Normal file
74
sql/sys_user_im_bind.sql
Normal file
@@ -0,0 +1,74 @@
|
||||
-- ----------------------------
|
||||
-- OA 用户 ↔ OpenIM 用户 绑定表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_im_bind`;
|
||||
CREATE TABLE `sys_user_im_bind` (
|
||||
`user_id` bigint(20) NOT NULL COMMENT 'OA 用户ID (sys_user.user_id)',
|
||||
`phone` varchar(20) NOT NULL COMMENT '手机号(绑定时记录的)',
|
||||
`im_user_id` varchar(64) NOT NULL COMMENT 'OpenIM userID(chat 生成的字符串)',
|
||||
`bind_status` tinyint(1) DEFAULT 1 COMMENT '1=正常 0=禁用',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`user_id`) USING BTREE,
|
||||
KEY `idx_phone` (`phone`),
|
||||
KEY `idx_im_user_id` (`im_user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA-OpenIM 用户绑定';
|
||||
|
||||
-- ----------------------------
|
||||
-- 一次性回填:从 IM 侧 mongo 导出的 phone→userID 映射表
|
||||
-- 与 sys_user.phonenumber 关联,命中即绑定
|
||||
-- ----------------------------
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_im_phone_map`;
|
||||
CREATE TEMPORARY TABLE `tmp_im_phone_map` (
|
||||
`phone` varchar(20) NOT NULL,
|
||||
`im_user_id` varchar(64) NOT NULL,
|
||||
PRIMARY KEY (`phone`)
|
||||
);
|
||||
|
||||
INSERT INTO `tmp_im_phone_map` (`phone`, `im_user_id`) VALUES
|
||||
('15690169553','5546292033'),
|
||||
('19932047759','2756281826'),
|
||||
('15075462410','2553281144'),
|
||||
('18915343363','5026155910'),
|
||||
('18324818443','2877251793'),
|
||||
('15383265119','8330802909'),
|
||||
('18940843511','4349480400'),
|
||||
('17714433303','2999550914'),
|
||||
('13390839803','2181055021'),
|
||||
('18803235354','7658449575'),
|
||||
('13700303346','1663065057'),
|
||||
('17783455912','2152870750'),
|
||||
('17829506825','3477330474'),
|
||||
('15591903178','8576556261'),
|
||||
('15109286032','8251618343'),
|
||||
('15615500565','1050079788'),
|
||||
('13630266330','5347384098'),
|
||||
('15956900268','2642329050'),
|
||||
('18728390370','7870875282'),
|
||||
('18976589764','5165937678'),
|
||||
('15092755532','4952160908'),
|
||||
('15380239551','2739756387'),
|
||||
('18354752253','3394331602'),
|
||||
('17692304283','4265596684'),
|
||||
('13572086005','3585749899'),
|
||||
('15081674433','1492306574'),
|
||||
('13861602746','8736597942'),
|
||||
('19711921991','3259846354'),
|
||||
('15330091963','1537622221');
|
||||
|
||||
INSERT INTO `sys_user_im_bind` (`user_id`, `phone`, `im_user_id`)
|
||||
SELECT u.user_id, u.phonenumber, m.im_user_id
|
||||
FROM `sys_user` u
|
||||
JOIN `tmp_im_phone_map` m ON m.phone = u.phonenumber
|
||||
WHERE u.del_flag = '0'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`phone` = VALUES(`phone`),
|
||||
`im_user_id` = VALUES(`im_user_id`);
|
||||
|
||||
-- 校验结果
|
||||
SELECT COUNT(*) AS bound_count FROM `sys_user_im_bind`;
|
||||
SELECT u.user_id, u.user_name, u.phonenumber, b.im_user_id
|
||||
FROM `sys_user` u
|
||||
LEFT JOIN `sys_user_im_bind` b ON b.user_id = u.user_id
|
||||
WHERE u.del_flag = '0' AND u.phonenumber IS NOT NULL AND u.phonenumber <> ''
|
||||
ORDER BY b.im_user_id IS NULL DESC;
|
||||
58
sql/warehouse_phase0_audit_log.sql
Normal file
58
sql/warehouse_phase0_audit_log.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- ==========================================================
|
||||
-- 库房重构 Phase 0:建审计日志表 + 修正历史状态
|
||||
-- 全部为新增/数据修正,零影响现有功能
|
||||
-- ==========================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 1. 旧 log 表留底(结构残缺,只有 20 行)
|
||||
-- ----------------------------------------------------------
|
||||
RENAME TABLE `sys_oa_warehouse_log` TO `_deprecated_sys_oa_warehouse_log`;
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 2. 统一审计日志表
|
||||
-- ----------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `sys_oa_warehouse_audit_log`;
|
||||
CREATE TABLE `sys_oa_warehouse_audit_log` (
|
||||
`log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`op_type` varchar(32) NOT NULL COMMENT '操作类型:REQ_CREATE/REQ_DONE/REQ_CANCEL/TASK_CREATE/TASK_DONE/IN/OUT/RETURN/STOCK_ADJUST 等',
|
||||
`ref_type` varchar(32) DEFAULT NULL COMMENT '关联实体类型:requirement/task/master/warehouse',
|
||||
`ref_id` bigint DEFAULT NULL COMMENT '关联实体ID',
|
||||
`project_id` bigint DEFAULT NULL COMMENT '所属项目ID(可选,便于按项目筛日志)',
|
||||
`warehouse_id` bigint DEFAULT NULL COMMENT '关联物料ID(可选,便于按物料筛日志)',
|
||||
`summary` varchar(500) NOT NULL COMMENT '一句话摘要(前端列表直接展示)',
|
||||
`before_json` text COMMENT '变更前快照JSON(可选)',
|
||||
`after_json` text COMMENT '变更后快照JSON(可选)',
|
||||
`op_user_id` bigint DEFAULT NULL COMMENT '操作人ID',
|
||||
`op_user_name` varchar(64) DEFAULT NULL COMMENT '操作人昵称(冗余便于展示)',
|
||||
`op_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`log_id`),
|
||||
KEY `idx_op_type` (`op_type`),
|
||||
KEY `idx_ref` (`ref_type`, `ref_id`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_warehouse_id` (`warehouse_id`),
|
||||
KEY `idx_op_time` (`op_time`),
|
||||
KEY `idx_op_user_id` (`op_user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库房统一操作日志';
|
||||
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 3. 修正历史退库单状态(去审批化前的尾巴)
|
||||
-- type=2 且 status=0 的 8 条历史单子,本意是"已批准的退库",统一改成 status=1
|
||||
-- ----------------------------------------------------------
|
||||
UPDATE `sys_oa_warehouse_master`
|
||||
SET `status` = 1,
|
||||
`update_time`= NOW(),
|
||||
`update_by` = 'system_migration'
|
||||
WHERE `type` = 2
|
||||
AND `status` = 0
|
||||
AND `del_flag` = 0;
|
||||
SELECT ROW_COUNT() AS type2_status0_fixed;
|
||||
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 4. 给 oa_requirements 补可读注释(之前没注释也没乱码,但 status 含义不明)
|
||||
-- ----------------------------------------------------------
|
||||
ALTER TABLE `oa_requirements`
|
||||
MODIFY COLUMN `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '完成情况(0=未采购 1=采购中 2=完成 3=取消)',
|
||||
COMMENT = '采购需求(项目大件/特殊件,对应"采购需求"菜单)';
|
||||
69
sql/warehouse_phase0_menu.sql
Normal file
69
sql/warehouse_phase0_menu.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- ==========================================================
|
||||
-- 库房菜单调整(无权限控制版)
|
||||
-- ==========================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 1. 复活并修正"采购需求"菜单
|
||||
-- ----------------------------------------------------------
|
||||
UPDATE sys_menu
|
||||
SET menu_name = '采购需求',
|
||||
path = 'requirement',
|
||||
component = 'oa/task/requirement/index',
|
||||
perms = NULL,
|
||||
visible = '0',
|
||||
status = '0',
|
||||
order_num = 30,
|
||||
icon = 'tool'
|
||||
WHERE menu_id = 1968929776643162114;
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 2. 新增"出库明细"菜单
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO sys_menu
|
||||
(menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES
|
||||
(2059900000000000001, '出库明细', 1852933620281430018, 65,
|
||||
'outDetail', 'oa/oaOutWarehouse/detail', 'C', '0', '0',
|
||||
NULL, 'detail', 'admin', NOW());
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 3. 新增"操作日志"菜单
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO sys_menu
|
||||
(menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES
|
||||
(2059900000000000002, '操作日志', 1852933620281430018, 90,
|
||||
'auditLog', 'oa/oaWarehouse/auditLog', 'C', '0', '0',
|
||||
NULL, 'log', 'admin', NOW());
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 4. 隐藏过时/测试菜单(visible='1' = 隐藏)
|
||||
-- ----------------------------------------------------------
|
||||
UPDATE sys_menu SET visible = '1'
|
||||
WHERE menu_id IN (
|
||||
1920003824857874433, -- 测试页面
|
||||
1938799329876361217, -- 退库日志
|
||||
1906233436477644801 -- 采购计划
|
||||
);
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 5. 重排库房一级菜单顺序
|
||||
-- ----------------------------------------------------------
|
||||
UPDATE sys_menu SET order_num = 10 WHERE menu_id = 1925143921102114818; -- 智慧库房
|
||||
UPDATE sys_menu SET order_num = 20 WHERE menu_id = 1852933963887202305; -- 库存管理
|
||||
UPDATE sys_menu SET order_num = 30 WHERE menu_id = 1968929776643162114; -- 采购需求
|
||||
UPDATE sys_menu SET order_num = 40 WHERE menu_id = 1906249355421573121; -- 车间采购
|
||||
UPDATE sys_menu SET order_num = 50 WHERE menu_id = 1902604787711459329; -- 入库明细
|
||||
UPDATE sys_menu SET order_num = 60 WHERE menu_id = 1853343196679536641; -- 仓库出库
|
||||
UPDATE sys_menu SET order_num = 65 WHERE menu_id = 2059900000000000001; -- 出库明细
|
||||
UPDATE sys_menu SET order_num = 70 WHERE menu_id = 1938787785255927809; -- 退库管理
|
||||
UPDATE sys_menu SET order_num = 90 WHERE menu_id = 2059900000000000002; -- 操作日志
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 6. 校验
|
||||
-- ----------------------------------------------------------
|
||||
SELECT menu_id, menu_name, path, component, visible, order_num
|
||||
FROM sys_menu
|
||||
WHERE parent_id = 1852933620281430018
|
||||
AND status = '0'
|
||||
ORDER BY order_num;
|
||||
159
sql/warehouse_simplify_migration.sql
Normal file
159
sql/warehouse_simplify_migration.sql
Normal file
@@ -0,0 +1,159 @@
|
||||
-- ==========================================================
|
||||
-- 库房模块简化迁移
|
||||
-- 目的:删除冗余表、修复乱码注释、补关键索引
|
||||
-- 注意:所有 ALTER 在 MySQL 8 上为 in-place / instant,业务低峰执行
|
||||
-- ==========================================================
|
||||
-- 强烈建议执行前做一次备份:
|
||||
-- mysqldump -uroot -p fad_oa_dev \
|
||||
-- sys_oa_warehouse sys_oa_warehouse_master sys_oa_warehouse_detail \
|
||||
-- sys_oa_warehouse_task sys_oa_warehouse_log sys_oa_warehouse_request \
|
||||
-- > warehouse_backup_$(date +%Y%m%d).sql
|
||||
-- ==========================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 1. 删除完全未使用的表
|
||||
-- ----------------------------------------------------------
|
||||
-- request 表 0 行,前端没接入,确认废弃
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 2. 修复字段注释(数据库中是 ? 乱码,重新设置为可读中文)
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
-- 2.1 sys_oa_warehouse 物料档案 / 实时库存
|
||||
ALTER TABLE `sys_oa_warehouse`
|
||||
MODIFY COLUMN `id` bigint NOT NULL AUTO_INCREMENT COMMENT '物料ID',
|
||||
MODIFY COLUMN `inventory` bigint DEFAULT NULL COMMENT '当前库存数量',
|
||||
MODIFY COLUMN `model` varchar(50) DEFAULT NULL COMMENT '型号',
|
||||
MODIFY COLUMN `price` decimal(18,2) DEFAULT '0.00' COMMENT '加权平均单价',
|
||||
MODIFY COLUMN `unit` varchar(5) DEFAULT NULL COMMENT '单位',
|
||||
MODIFY COLUMN `name` varchar(50) DEFAULT NULL COMMENT '物料名称',
|
||||
MODIFY COLUMN `brand` varchar(200) DEFAULT NULL COMMENT '品牌',
|
||||
MODIFY COLUMN `specifications` varchar(125) DEFAULT NULL COMMENT '规格',
|
||||
MODIFY COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
MODIFY COLUMN `create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
MODIFY COLUMN `create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
MODIFY COLUMN `update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
MODIFY COLUMN `update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
MODIFY COLUMN `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0=正常 2=删除)',
|
||||
MODIFY COLUMN `threshold` bigint NOT NULL DEFAULT '5' COMMENT '库存预警阈值',
|
||||
COMMENT = '物料档案/实时库存';
|
||||
|
||||
-- 2.2 sys_oa_warehouse_master 出入库单据头
|
||||
ALTER TABLE `sys_oa_warehouse_master`
|
||||
MODIFY COLUMN `master_id` bigint NOT NULL AUTO_INCREMENT COMMENT '单据ID',
|
||||
MODIFY COLUMN `master_num` varchar(296) DEFAULT NULL COMMENT '单据编号',
|
||||
MODIFY COLUMN `type` int DEFAULT NULL COMMENT '类型(0=出库 1=入库 2=归还)',
|
||||
MODIFY COLUMN `project_id` bigint DEFAULT NULL COMMENT '关联项目ID',
|
||||
MODIFY COLUMN `requirement_id` bigint DEFAULT NULL COMMENT '关联需求ID',
|
||||
MODIFY COLUMN `sign_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '签收/操作时间',
|
||||
MODIFY COLUMN `sign_user` varchar(20) DEFAULT NULL COMMENT '签收人',
|
||||
MODIFY COLUMN `is_like` int DEFAULT '0' COMMENT '是否收藏(0=否 1=是)',
|
||||
MODIFY COLUMN `status` int DEFAULT '0' COMMENT '单据状态',
|
||||
MODIFY COLUMN `return_type` int DEFAULT '0' COMMENT '归还类型(0=否 1=部分归还)',
|
||||
MODIFY COLUMN `withdraw_lock` tinyint NOT NULL DEFAULT '0' COMMENT '撤回锁定(1=锁定)',
|
||||
MODIFY COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
MODIFY COLUMN `del_flag` tinyint NOT NULL DEFAULT '0' COMMENT '删除标志',
|
||||
COMMENT = '出入库单据头';
|
||||
|
||||
-- 2.3 sys_oa_warehouse_detail 出入库单据行
|
||||
ALTER TABLE `sys_oa_warehouse_detail`
|
||||
MODIFY COLUMN `id` bigint NOT NULL AUTO_INCREMENT COMMENT '明细ID',
|
||||
MODIFY COLUMN `master_id` bigint DEFAULT NULL COMMENT '所属单据ID(sys_oa_warehouse_master)',
|
||||
MODIFY COLUMN `warehouse_id` bigint NOT NULL COMMENT '物料ID(sys_oa_warehouse)',
|
||||
MODIFY COLUMN `amount` bigint DEFAULT NULL COMMENT '本次出入库数量',
|
||||
MODIFY COLUMN `sign_price` decimal(18,2) DEFAULT '0.00' COMMENT '签收单价',
|
||||
MODIFY COLUMN `project_id` bigint DEFAULT NULL COMMENT '所属项目ID',
|
||||
MODIFY COLUMN `father_id` bigint DEFAULT NULL COMMENT '父任务ID(task 派生明细使用)',
|
||||
MODIFY COLUMN `remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
MODIFY COLUMN `del_flag` tinyint(1) DEFAULT '0' COMMENT '删除标志',
|
||||
COMMENT = '出入库单据行';
|
||||
|
||||
-- 2.4 sys_oa_warehouse_task 库房任务(采购/补货)
|
||||
ALTER TABLE `sys_oa_warehouse_task`
|
||||
MODIFY COLUMN `task_id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID',
|
||||
MODIFY COLUMN `master_id` bigint DEFAULT NULL COMMENT '完成后写回的单据ID',
|
||||
MODIFY COLUMN `warehouse_id` bigint DEFAULT NULL COMMENT '关联物料ID(新采购可空)',
|
||||
MODIFY COLUMN `name` varchar(50) DEFAULT NULL COMMENT '物料名称(快照)',
|
||||
MODIFY COLUMN `model` varchar(20) DEFAULT NULL COMMENT '型号',
|
||||
MODIFY COLUMN `brand` varchar(20) DEFAULT NULL COMMENT '品牌',
|
||||
MODIFY COLUMN `specifications` varchar(40) DEFAULT NULL COMMENT '规格',
|
||||
MODIFY COLUMN `unit` varchar(50) DEFAULT NULL COMMENT '单位',
|
||||
MODIFY COLUMN `task_inventory` int DEFAULT NULL COMMENT '任务数量(采购量/补货量)',
|
||||
MODIFY COLUMN `task_status` int DEFAULT '0' COMMENT '任务状态(0=待处理 1=进行中 2=完成 3=取消)',
|
||||
MODIFY COLUMN `end_time` datetime DEFAULT NULL COMMENT '期望完成时间',
|
||||
MODIFY COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
MODIFY COLUMN `del_flag` int DEFAULT '0' COMMENT '删除标志',
|
||||
COMMENT = '库房任务(采购/补货)';
|
||||
|
||||
-- 2.5 sys_oa_warehouse_log 库房操作日志
|
||||
ALTER TABLE `sys_oa_warehouse_log`
|
||||
MODIFY COLUMN `log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
MODIFY COLUMN `master_id` bigint DEFAULT NULL COMMENT '关联单据ID',
|
||||
MODIFY COLUMN `warehouse_id` bigint DEFAULT NULL COMMENT '关联物料ID',
|
||||
MODIFY COLUMN `num` int DEFAULT NULL COMMENT '变更数量',
|
||||
MODIFY COLUMN `del_flag` int NOT NULL DEFAULT '0' COMMENT '删除标志',
|
||||
COMMENT = '库房操作日志';
|
||||
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 3. 补关键索引(之前所有表只有主键,查询慢的根因)
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
-- master 单据头:按项目、状态、类型查
|
||||
ALTER TABLE `sys_oa_warehouse_master`
|
||||
ADD INDEX `idx_project_id` (`project_id`),
|
||||
ADD INDEX `idx_type_status` (`type`, `status`),
|
||||
ADD INDEX `idx_sign_time` (`sign_time`);
|
||||
|
||||
-- detail 单据行:按单据、物料、项目查(最高频)
|
||||
ALTER TABLE `sys_oa_warehouse_detail`
|
||||
ADD INDEX `idx_master_id` (`master_id`),
|
||||
ADD INDEX `idx_warehouse_id` (`warehouse_id`),
|
||||
ADD INDEX `idx_project_id` (`project_id`);
|
||||
|
||||
-- warehouse 物料:按名称、型号搜
|
||||
ALTER TABLE `sys_oa_warehouse`
|
||||
ADD INDEX `idx_name` (`name`),
|
||||
ADD INDEX `idx_model` (`model`),
|
||||
ADD INDEX `idx_del_flag` (`del_flag`);
|
||||
|
||||
-- task 任务:按状态、物料、单据
|
||||
ALTER TABLE `sys_oa_warehouse_task`
|
||||
ADD INDEX `idx_task_status` (`task_status`),
|
||||
ADD INDEX `idx_warehouse_id` (`warehouse_id`),
|
||||
ADD INDEX `idx_master_id` (`master_id`);
|
||||
|
||||
-- log:按单据、物料查
|
||||
ALTER TABLE `sys_oa_warehouse_log`
|
||||
ADD INDEX `idx_master_id` (`master_id`),
|
||||
ADD INDEX `idx_warehouse_id` (`warehouse_id`);
|
||||
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 4. 数据健康检查(运行后看看)
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
-- 4.1 库存为负数的物料(数据错乱)
|
||||
SELECT id, name, model, inventory FROM sys_oa_warehouse
|
||||
WHERE del_flag = 0 AND inventory < 0;
|
||||
|
||||
-- 4.2 detail 指向已删除/不存在物料的孤儿行
|
||||
SELECT d.id, d.master_id, d.warehouse_id
|
||||
FROM sys_oa_warehouse_detail d
|
||||
LEFT JOIN sys_oa_warehouse w ON w.id = d.warehouse_id
|
||||
WHERE w.id IS NULL AND d.del_flag = 0
|
||||
LIMIT 50;
|
||||
|
||||
-- 4.3 detail 指向已删除/不存在单据的孤儿行
|
||||
SELECT d.id, d.master_id
|
||||
FROM sys_oa_warehouse_detail d
|
||||
LEFT JOIN sys_oa_warehouse_master m ON m.master_id = d.master_id
|
||||
WHERE m.master_id IS NULL AND d.del_flag = 0
|
||||
LIMIT 50;
|
||||
|
||||
-- 4.4 task 中超过 90 天还在进行中的(疑似遗留)
|
||||
SELECT task_id, name, task_status, create_time
|
||||
FROM sys_oa_warehouse_task
|
||||
WHERE task_status IN (0, 1)
|
||||
AND create_time < DATE_SUB(NOW(), INTERVAL 90 DAY)
|
||||
AND del_flag = 0;
|
||||
Reference in New Issue
Block a user