广丰卷烟厂数采质量分析系统
baoshiwei
15 小时以前 d143af7023cfd4a0ced6f0ecf04ae3b3a06fd1dc
feat(md): 添加称重盒子维护功能

- 实现称重盒子的增删改查功能
- 添加单个和批量校准功能
- 实现批量配置校准周期功能
- 添加称重盒子复制功能
- 更新路由配置和类型定义
- 添加相关国际化配置
- 实现Java后端时间差计算工具方法
已修改6个文件
已添加21个文件
4023 ■■■■■ 文件已修改
RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/controller/WeighingBoxController.java 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/CalibrationRecord.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/WeighingBox.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchCalibrateBo.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchConfigBo.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/WeighingBoxBo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/CalibrationRecordMapper.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/WeighingBoxMapper.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/ICalibrationRecordService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/IWeighingBoxService.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/CalibrationRecordServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/WeighingBoxServiceImpl.java 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/CalibrationRecordMapper.xml 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/WeighingBoxMapper.xml 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/locales/langs/zh-cn.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/imports.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/routes.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/transform.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/service/api/md/weighing-box.ts 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/typings/elegant-router.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/index.vue 1322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-calibrate.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-config.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-calibrate.vue 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-copy.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-operate.vue 338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
@@ -200,6 +200,23 @@
        };
    }
    // èŽ·å–ä¸¤ä¸ªæ—¶é—´çš„å·®å€¼
    public static long getTimeDifference( Date nowDate,Date endDate, TimeUnit unit) {
        // è®¡ç®—时间差,单位为毫秒,取绝对值避免负数
        long diffInMillis = endDate.getTime() - nowDate.getTime();
        // æ ¹æ®ç›®æ ‡å•位转换时间差
        return switch (unit) {
            case DAYS -> diffInMillis / TimeUnit.DAYS.toMillis(1);
            case HOURS -> diffInMillis / TimeUnit.HOURS.toMillis(1);
            case MINUTES -> diffInMillis / TimeUnit.MINUTES.toMillis(1);
            case SECONDS -> diffInMillis / TimeUnit.SECONDS.toMillis(1);
            case MILLISECONDS -> diffInMillis;
            case MICROSECONDS -> TimeUnit.MILLISECONDS.toMicros(diffInMillis);
            case NANOSECONDS -> TimeUnit.MILLISECONDS.toNanos(diffInMillis);
        };
    }
    /**
     * è®¡ç®—两个日期之间的时间差,并以天、小时和分钟的格式返回
     *
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/controller/WeighingBoxController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
package org.dromara.qa.md.controller;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.qa.md.domain.bo.BatchCalibrateBo;
import org.dromara.qa.md.domain.bo.BatchConfigBo;
import org.dromara.qa.md.domain.WeighingBox;
import org.dromara.qa.md.service.IWeighingBoxService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
 * ç§°é‡ç›’子控制器
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/md/weighingBox")
public class WeighingBoxController extends BaseController {
    private final IWeighingBoxService weighingBoxService;
    /**
     * æŸ¥è¯¢ç§°é‡ç›’子列表
     */
    @SaCheckPermission("md:weighingBox:list")
    @GetMapping("/list")
    public TableDataInfo<WeighingBox> list(WeighingBox weighingBox, PageQuery pageQuery) {
        return weighingBoxService.queryPageList(weighingBox, pageQuery);
    }
    /**
     * èŽ·å–ç§°é‡ç›’å­è¯¦ç»†ä¿¡æ¯
     *
     * @param id ä¸»é”®
     */
    @SaCheckPermission("md:weighingBox:query")
    @GetMapping("/{id}")
    public R<WeighingBox> getInfo(@NotNull(message = "主键不能为空")
                                     @PathVariable Long id) {
        WeighingBox box = weighingBoxService.getById(id);
        if (box != null) {
            weighingBoxService.calculateCalibStatus(box);
        }
        return R.ok(box);
    }
    /**
     * æ–°å¢žç§°é‡ç›’子
     */
    @SaCheckPermission("md:weighingBox:add")
    @Log(title = "称重盒子", businessType = BusinessType.INSERT)
    @RepeatSubmit()
    @PostMapping()
    public R<Void> add(@Validated(AddGroup.class) @RequestBody WeighingBox weighingBox) {
        return toAjax(weighingBoxService.insertWeighingBox(weighingBox));
    }
    /**
     * ä¿®æ”¹ç§°é‡ç›’子
     */
    @SaCheckPermission("md:weighingBox:edit")
    @Log(title = "称重盒子", businessType = BusinessType.UPDATE)
    @RepeatSubmit()
    @PutMapping()
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody WeighingBox weighingBox) {
        return toAjax(weighingBoxService.updateWeighingBox(weighingBox));
    }
    /**
     * åˆ é™¤ç§°é‡ç›’子
     *
     * @param ids ä¸»é”®ä¸²
     */
    @SaCheckPermission("md:weighingBox:remove")
    @Log(title = "称重盒子", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public R<Void> remove(@NotEmpty(message = "主键不能为空")
                          @PathVariable Long[] ids) {
        return toAjax(weighingBoxService.deleteWeighingBoxByIds(ids));
    }
    /**
     * æ‰§è¡Œå•个校准
     */
    @SaCheckPermission("md:weighingBox:calibrate")
    @Log(title = "称重盒子校准", businessType = BusinessType.UPDATE)
    @PostMapping("/calibrate")
    public R<Void> calibrate(@RequestBody Map<String, Object> params) {
        try {
            Long boxId = Long.valueOf(params.get("boxId").toString());
            String calibDate = params.get("calibDate").toString();
            java.math.BigDecimal actualWeight = params.containsKey("actualWeight") && params.get("actualWeight") != null ? new java.math.BigDecimal(params.get("actualWeight").toString()) : null;
            String note = params.containsKey("note") ? params.get("note").toString() : null;
            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
            java.util.Date date = sdf.parse(calibDate);
            return toAjax(weighingBoxService.calibrate(boxId, date, actualWeight, note));
        } catch (Exception e) {
            return R.fail("日期格式错误");
        }
    }
    /**
     * æ‰¹é‡æ ¡å‡†
     */
    @SaCheckPermission("md:weighingBox:batchCalibrate")
    @Log(title = "称重盒子批量校准", businessType = BusinessType.UPDATE)
    @PostMapping("/batchCalibrate")
    public R<Map<String, Object>> batchCalibrate(@RequestBody BatchCalibrateBo batchCalibrateDTO) {
        Map<String, Object> result = weighingBoxService.batchCalibrate(batchCalibrateDTO);
        return R.ok(result);
    }
    /**
     * ç»Ÿä¸€é…ç½®æ ¡å‡†å‘¨æœŸ
     */
    @SaCheckPermission("md:weighingBox:batchConfig")
    @Log(title = "称重盒子统一配置", businessType = BusinessType.UPDATE)
    @PostMapping("/batchConfig")
    public R<Void> batchConfig(@RequestBody BatchConfigBo batchConfigDTO) {
        return toAjax(weighingBoxService.batchConfig(batchConfigDTO));
    }
    /**
     * æ‰¹é‡æ›´æ–°çŠ¶æ€
     */
    @SaCheckPermission("md:weighingBox:batchUpdateStatus")
    @Log(title = "称重盒子批量更新状态", businessType = BusinessType.UPDATE)
    @PostMapping("/batchUpdateStatus")
    public R<Void> batchUpdateStatus(@RequestBody Map<String, Object> params) {
        List<Long> boxIds = (List<Long>) params.get("boxIds");
        Integer activeStatus = (Integer) params.get("activeStatus");
        return toAjax(weighingBoxService.batchUpdateStatus(boxIds, activeStatus));
    }
    /**
     * å¤åˆ¶ç›’子
     */
    @Log(title = "称重盒子复制", businessType = BusinessType.INSERT)
    @PostMapping("/copy")
    public R<Map<String, Object>> copy(@RequestBody Map<String, Object> params) {
        try {
            Long sourceId = Long.valueOf(params.get("sourceId").toString());
            Integer count = Integer.valueOf(params.get("count").toString());
            Map<String, Object> result = weighingBoxService.copyBox(sourceId, count);
            return R.ok(result);
        } catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }
    /**
     * èŽ·å–æ ¡å‡†çŠ¶æ€ç»Ÿè®¡
     */
    @GetMapping("/statistics")
    public R<Map<String, Integer>> statistics() {
        Map<String, Integer> statistics = weighingBoxService.getStatistics();
        return R.ok(statistics);
    }
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/CalibrationRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package org.dromara.qa.md.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
 * é€šç”¨æ ¡å‡†è®°å½•表
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Data
@TableName("qm_calibration_record")
public class CalibrationRecord implements Serializable
{
    private static final long serialVersionUID = 1L;
    /** ä¸»é”®ID */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    /** æ ¡å‡†å¯¹è±¡ç±»åž‹(weighing_box/instrument) */
    private String targetType;
    /** æ ¡å‡†å¯¹è±¡ID */
    private Long targetId;
    /** å¯¹è±¡ç¼–号(冗余快照) */
    private String targetCode;
    /** å¯¹è±¡åç§°ï¼ˆå†—余快照) */
    private String targetName;
    /** æ ¡å‡†æ—¥æœŸ */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date calibDate;
    /** å½“时的校准周期(快照) */
    private Integer calibCycleDays;
    /** æ ‡å‡†é‡é‡ï¼ˆå¿«ç…§ï¼‰ */
    private BigDecimal standardWeight;
    /** æœ¬æ¬¡å®žæµ‹é‡é‡ */
    private BigDecimal actualWeight;
    /** åå·®å€¼ï¼ˆå®žæµ‹-标准) */
    private BigDecimal deviation;
    /** åå·®ç™¾åˆ†æ¯”(%) */
    private BigDecimal deviationPct;
    /** æ ¡å‡†å‰çš„上次校准日期 */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date prevCalibDate;
    /** æ ¡å‡†åŽè®¡ç®—的下次校准日期 */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date nextCalibDate;
    /** æ‰¹æ¬¡å·ï¼ˆæ‰¹é‡æ ¡å‡†æ—¶å¡«å……) */
    private String batchId;
    /** æ ¡å‡†å¤‡æ³¨ */
    private String note;
    /** æ“ä½œäºº */
    private String operator;
    /** è®°å½•创建时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/WeighingBox.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package org.dromara.qa.md.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
 * ç§°é‡ç›’子主表
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Data
@TableName("qm_weighing_box")
public class WeighingBox implements Serializable
{
    private static final long serialVersionUID = 1L;
    /** ä¸»é”®ID */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    /** ç›’子名称 */
    private String name;
    /** ç›’子编号(全局唯一) */
    private String code;
    /** æ ‡å‡†é‡é‡ */
    private BigDecimal weight;
    /** é‡é‡å•位(g/kg/mg) */
    private String unit;
    /** å­˜æ”¾ä½ç½® */
    private String location;
    /** æ ¡å‡†å‘¨æœŸï¼ˆå¤©ï¼‰ */
    private Integer calibCycleDays;
    /** æå‰æé†’天数 */
    private Integer remindDays;
    /** ä¸Šæ¬¡æ ¡å‡†æ—¥æœŸ */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date lastCalibDate;
    /** ä¸‹æ¬¡æ ¡å‡†æ—¥æœŸï¼ˆç³»ç»Ÿè‡ªåŠ¨è®¡ç®—ï¼‰ */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date nextCalibDate;
    /** å¯ç”¨çŠ¶æ€(1启用 0停用) */
    private Integer activeStatus;
    /** å¤‡æ³¨æè¿° */
    private String description;
    /** åˆ é™¤æ ‡è®°(0正常 1删除) */
    private Integer delFlag;
    /** åˆ›å»ºäºº */
    private String createBy;
    /** åˆ›å»ºæ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    /** æ›´æ–°äºº */
    private String updateBy;
    /** æ›´æ–°æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    // æ‰©å±•字段
    // æŽ’除掉
    @TableField(exist = false)
    private String calibStatus;
    @TableField(exist = false)
    private Integer calibDaysLeft;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchCalibrateBo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package org.dromara.qa.md.domain.bo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
 * æ‰¹é‡æ ¡å‡†DTO
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Data
public class BatchCalibrateBo
{
    private List<Long> boxIds;
    private Date calibDate;
    private String note;
    private List<CalibrateItem> items;
    @Data
    public static class CalibrateItem
    {
        private Long boxId;
        private BigDecimal actualWeight;
    }
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchConfigBo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package org.dromara.qa.md.domain.bo;
import lombok.Data;
import java.util.List;
/**
 * æ‰¹é‡é…ç½®DTO
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Data
public class BatchConfigBo
{
    private Integer calibCycleDays;
    private Integer remindDays;
    private String applyScope;
    private List<Long> boxIds;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/WeighingBoxBo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package org.dromara.qa.md.domain.bo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * ç§°é‡ç›’子DTO
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Data
public class WeighingBoxBo
{
    private Long id;
    private String name;
    private String code;
    private BigDecimal weight;
    private String unit;
    private String location;
    private Integer calibCycleDays;
    private Integer remindDays;
    private Date lastCalibDate;
    private Integer activeStatus;
    private String description;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/CalibrationRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package org.dromara.qa.md.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.qa.md.domain.CalibrationRecord;
import java.util.List;
/**
 * æ ¡å‡†è®°å½•Mapper接口
 *
 * @author ruoyi
 * @date 2026-04-09
 */
public interface CalibrationRecordMapper extends BaseMapper<CalibrationRecord>
{
    /**
     * æŸ¥è¯¢æ ¡å‡†è®°å½•列表
     *
     * @param record æ ¡å‡†è®°å½•
     * @return æ ¡å‡†è®°å½•集合
     */
    public List<CalibrationRecord> selectCalibrationRecordList(CalibrationRecord record);
    /**
     * æ ¹æ®ç›®æ ‡ID查询校准记录
     *
     * @param targetType ç›®æ ‡ç±»åž‹
     * @param targetId ç›®æ ‡ID
     * @return æ ¡å‡†è®°å½•集合
     */
    public List<CalibrationRecord> selectByTargetId(String targetType, Long targetId);
    /**
     * æ‰¹é‡æ’入校准记录
     *
     * @param records æ ¡å‡†è®°å½•列表
     * @return ç»“æžœ
     */
    public int batchInsert(List<CalibrationRecord> records);
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/WeighingBoxMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package org.dromara.qa.md.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.qa.md.domain.WeighingBox;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.List;
/**
 * ç§°é‡ç›’子Mapper接口
 *
 * @author ruoyi
 * @date 2026-04-09
 */
public interface WeighingBoxMapper extends BaseMapper<WeighingBox>
{
    /**
     * åˆ†é¡µæŸ¥è¯¢ç§°é‡ç›’子列表
     *
     * @param weighingBox æŸ¥è¯¢æ¡ä»¶
     * @param pageQuery åˆ†é¡µå‚æ•°
     * @return ç§°é‡ç›’子分页列表
     */
    public TableDataInfo<WeighingBox> selectPageList(WeighingBox weighingBox, PageQuery pageQuery);
    /**
     * æŸ¥è¯¢ç§°é‡ç›’子列表
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç§°é‡ç›’子集合
     */
    public List<WeighingBox> selectWeighingBoxList(WeighingBox weighingBox);
    /**
     * æŸ¥è¯¢å·²å¯ç”¨çš„称重盒子(用于提醒任务)
     *
     * @return ç§°é‡ç›’子集合
     */
    public List<WeighingBox> selectActiveWeighingBoxes();
    /**
     * æ‰¹é‡æ›´æ–°ç§°é‡ç›’子状态
     *
     * @param boxIds ç›’子ID列表
     * @param activeStatus çŠ¶æ€
     * @return ç»“æžœ
     */
    public int batchUpdateStatus(List<Long> boxIds, Integer activeStatus);
    /**
     * æ‰¹é‡æ›´æ–°æ ¡å‡†å‘¨æœŸ
     *
     * @param boxIds ç›’子ID列表
     * @param calibCycleDays æ ¡å‡†å‘¨æœŸ
     * @param remindDays æé†’天数
     * @return ç»“æžœ
     */
    public int batchUpdateCalibConfig(List<Long> boxIds, Integer calibCycleDays, Integer remindDays);
    /**
     * æŸ¥è¯¢ç§°é‡ç›’子列表(带校准状态过滤)
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç§°é‡ç›’子集合
     */
    public List<WeighingBox> selectWeighingBoxListWithCalibStatus(WeighingBox weighingBox);
    /**
     * æŸ¥è¯¢ç§°é‡ç›’子总数(带校准状态过滤)
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç§°é‡ç›’子总数
     */
    public long selectWeighingBoxCountWithCalibStatus(WeighingBox weighingBox);
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/ICalibrationRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package org.dromara.qa.md.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.qa.md.domain.CalibrationRecord;
import java.util.List;
/**
 * æ ¡å‡†è®°å½•服务接口
 *
 * @author ruoyi
 * @date 2026-04-09
 */
public interface ICalibrationRecordService extends IService<CalibrationRecord>
{
    /**
     * æŸ¥è¯¢æ ¡å‡†è®°å½•列表
     *
     * @param calibrationRecord æ ¡å‡†è®°å½•
     * @return æ ¡å‡†è®°å½•集合
     */
    public List<CalibrationRecord> selectCalibrationRecordList(CalibrationRecord calibrationRecord);
    /**
     * æ ¹æ®ç›®æ ‡ID查询校准记录
     *
     * @param targetType ç›®æ ‡ç±»åž‹
     * @param targetId ç›®æ ‡ID
     * @return æ ¡å‡†è®°å½•集合
     */
    public List<CalibrationRecord> selectByTargetId(String targetType, Long targetId);
    /**
     * æ‰¹é‡æ’入校准记录
     *
     * @param records æ ¡å‡†è®°å½•列表
     * @return ç»“æžœ
     */
    public int batchInsert(List<CalibrationRecord> records);
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/IWeighingBoxService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
package org.dromara.qa.md.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.qa.md.domain.bo.BatchCalibrateBo;
import org.dromara.qa.md.domain.bo.BatchConfigBo;
import org.dromara.qa.md.domain.WeighingBox;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.List;
import java.util.Map;
/**
 * ç§°é‡ç›’子服务接口
 *
 * @author ruoyi
 * @date 2026-04-09
 */
public interface IWeighingBoxService extends IService<WeighingBox>
{
    /**
     * åˆ†é¡µæŸ¥è¯¢ç§°é‡ç›’子列表
     *
     * @param weighingBox æŸ¥è¯¢æ¡ä»¶
     * @param pageQuery åˆ†é¡µå‚æ•°
     * @return ç§°é‡ç›’子分页列表
     */
    public TableDataInfo<WeighingBox> queryPageList(WeighingBox weighingBox, PageQuery pageQuery);
    /**
     * æŸ¥è¯¢ç§°é‡ç›’子列表
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç§°é‡ç›’子集合
     */
    public List<WeighingBox> selectWeighingBoxList(WeighingBox weighingBox);
    /**
     * æ–°å¢žç§°é‡ç›’子
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç»“æžœ
     */
    public int insertWeighingBox(WeighingBox weighingBox);
    /**
     * ä¿®æ”¹ç§°é‡ç›’子
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç»“æžœ
     */
    public int updateWeighingBox(WeighingBox weighingBox);
    /**
     * æ‰¹é‡åˆ é™¤ç§°é‡ç›’子
     *
     * @param boxIds éœ€è¦åˆ é™¤çš„称重盒子ID
     * @return ç»“æžœ
     */
    public int deleteWeighingBoxByIds(Long[] boxIds);
    /**
     * æ‰§è¡Œå•个校准
     *
     * @param boxId ç›’子ID
     * @param calibDate æ ¡å‡†æ—¥æœŸ
     * @param actualWeight å®žé™…重量
     * @param note å¤‡æ³¨
     * @return ç»“æžœ
     */
    public int calibrate(Long boxId, java.util.Date calibDate, java.math.BigDecimal actualWeight, String note);
    /**
     * æ‰¹é‡æ ¡å‡†
     *
     * @param batchCalibrateDTO æ‰¹é‡æ ¡å‡†å‚æ•°
     * @return ç»“æžœ
     */
    public Map<String, Object> batchCalibrate(BatchCalibrateBo batchCalibrateDTO);
    /**
     * ç»Ÿä¸€é…ç½®æ ¡å‡†å‘¨æœŸ
     *
     * @param batchConfigDTO æ‰¹é‡é…ç½®å‚æ•°
     * @return ç»“æžœ
     */
    public int batchConfig(BatchConfigBo batchConfigDTO);
    /**
     * æ‰¹é‡æ›´æ–°çŠ¶æ€
     *
     * @param boxIds ç›’子ID列表
     * @param activeStatus çŠ¶æ€
     * @return ç»“æžœ
     */
    public int batchUpdateStatus(List<Long> boxIds, Integer activeStatus);
    /**
     * å¤åˆ¶ç›’子
     *
     * @param sourceId æºç›’子ID
     * @param count å¤åˆ¶æ•°é‡
     * @return ç»“æžœ
     */
    public Map<String, Object> copyBox(Long sourceId, Integer count);
    /**
     * èŽ·å–æ ¡å‡†çŠ¶æ€ç»Ÿè®¡
     *
     * @return ç»Ÿè®¡ç»“æžœ
     */
    public Map<String, Integer> getStatistics();
    /**
     * è®¡ç®—校准状态
     *
     * @param weighingBox ç§°é‡ç›’子
     * @return ç§°é‡ç›’子(含状态信息)
     */
    public WeighingBox calculateCalibStatus(WeighingBox weighingBox);
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/CalibrationRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package org.dromara.qa.md.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.qa.md.domain.CalibrationRecord;
import org.dromara.qa.md.mapper.CalibrationRecordMapper;
import org.dromara.qa.md.service.ICalibrationRecordService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * æ ¡å‡†è®°å½•服务实现
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Service
public class CalibrationRecordServiceImpl extends ServiceImpl<CalibrationRecordMapper, CalibrationRecord> implements ICalibrationRecordService
{
    @Override
    public List<CalibrationRecord> selectCalibrationRecordList(CalibrationRecord calibrationRecord)
    {
        return baseMapper.selectCalibrationRecordList(calibrationRecord);
    }
    @Override
    public List<CalibrationRecord> selectByTargetId(String targetType, Long targetId)
    {
        CalibrationRecord record = new CalibrationRecord();
        record.setTargetType(targetType);
        record.setTargetId(targetId);
        return baseMapper.selectCalibrationRecordList(record);
    }
    @Override
    public int batchInsert(List<CalibrationRecord> records)
    {
        return baseMapper.batchInsert(records);
    }
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/WeighingBoxServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,453 @@
package org.dromara.qa.md.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.qa.md.domain.bo.BatchCalibrateBo;
import org.dromara.qa.md.domain.bo.BatchConfigBo;
import org.dromara.qa.md.domain.CalibrationRecord;
import org.dromara.qa.md.domain.WeighingBox;
import org.dromara.qa.md.mapper.WeighingBoxMapper;
import org.dromara.qa.md.service.ICalibrationRecordService;
import org.dromara.qa.md.service.IWeighingBoxService;
import org.dromara.qa.md.service.INotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * ç§°é‡ç›’子服务实现
 *
 * @author ruoyi
 * @date 2026-04-09
 */
@Service
public class WeighingBoxServiceImpl extends ServiceImpl<WeighingBoxMapper, WeighingBox> implements IWeighingBoxService
{
    @Autowired
    private WeighingBoxMapper weighingBoxMapper;
    @Autowired
    private ICalibrationRecordService calibrationRecordService;
    @Autowired
    private INotificationService notificationService;
    @Override
    public TableDataInfo<WeighingBox> queryPageList(WeighingBox weighingBox, PageQuery pageQuery) {
        // æ£€æŸ¥æ˜¯å¦éœ€è¦æŒ‰æ ¡å‡†çŠ¶æ€è¿‡æ»¤
        if (weighingBox.getCalibStatus() != null && !weighingBox.getCalibStatus().isEmpty()) {
            // ä½¿ç”¨å¸¦æ ¡å‡†çŠ¶æ€è¿‡æ»¤çš„æŸ¥è¯¢
            long total = weighingBoxMapper.selectWeighingBoxCountWithCalibStatus(weighingBox);
            List<WeighingBox> records = weighingBoxMapper.selectWeighingBoxListWithCalibStatus(weighingBox);
            // æ‰‹åŠ¨åˆ†é¡µ
            int pageNum = pageQuery.getPageNum();
            int pageSize = pageQuery.getPageSize();
            int start = (pageNum - 1) * pageSize;
            int end = Math.min(start + pageSize, records.size());
            List<WeighingBox> pageRecords = start < records.size() ? records.subList(start, end) : new ArrayList<>();
            // è®¡ç®—每个盒子的校准状态(保持与前端一致)
            for (WeighingBox box : pageRecords) {
                calculateCalibStatus(box);
            }
            // æž„建返回结果
            TableDataInfo<WeighingBox> tableDataInfo = new TableDataInfo<>();
            tableDataInfo.setRows(pageRecords);
            tableDataInfo.setTotal(total);
            tableDataInfo.setCode(200);
            tableDataInfo.setMsg("查询成功");
            return tableDataInfo;
        } else {
            // ä½¿ç”¨å¸¸è§„分页查询
            Page<WeighingBox> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
            // æž„建查询条件
            LambdaQueryWrapper<WeighingBox> queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
            queryWrapper.eq(WeighingBox::getDelFlag, 0);
            if (weighingBox.getName() != null && !weighingBox.getName().isEmpty()) {
                queryWrapper.like(WeighingBox::getName, weighingBox.getName())
                        .or().like(WeighingBox::getCode, weighingBox.getName())
                        .or().like(WeighingBox::getDescription, weighingBox.getName())
                        .or().like(WeighingBox::getLocation, weighingBox.getName());
            }
            if (weighingBox.getActiveStatus() != null) {
                queryWrapper.eq(WeighingBox::getActiveStatus, weighingBox.getActiveStatus());
            }
            queryWrapper.orderByDesc(WeighingBox::getCreateTime);
            // æ‰§è¡Œåˆ†é¡µæŸ¥è¯¢
            Page<WeighingBox> resultPage = baseMapper.selectPage(page, queryWrapper);
            // è®¡ç®—每个盒子的校准状态
            for (WeighingBox box : resultPage.getRecords()) {
                calculateCalibStatus(box);
            }
            return TableDataInfo.build(resultPage);
        }
    }
    @Override
    public List<WeighingBox> selectWeighingBoxList(WeighingBox weighingBox)
    {
        List<WeighingBox> list = weighingBoxMapper.selectWeighingBoxList(weighingBox);
        for (WeighingBox box : list)
        {
            calculateCalibStatus(box);
        }
        return list;
    }
    @Override
    public int insertWeighingBox(WeighingBox weighingBox)
    {
        // è®¡ç®—下次校准日期
        if (weighingBox.getLastCalibDate() != null && weighingBox.getCalibCycleDays() != null)
        {
            Date nextCalibDate = DateUtils.addDays(weighingBox.getLastCalibDate(), weighingBox.getCalibCycleDays());
            weighingBox.setNextCalibDate(nextCalibDate);
        }
        return baseMapper.insert(weighingBox);
    }
    @Override
    public int updateWeighingBox(WeighingBox weighingBox)
    {
        // è®¡ç®—下次校准日期
        if (weighingBox.getLastCalibDate() != null && weighingBox.getCalibCycleDays() != null)
        {
            Date nextCalibDate = DateUtils.addDays(weighingBox.getLastCalibDate(), weighingBox.getCalibCycleDays());
            weighingBox.setNextCalibDate(nextCalibDate);
        }
        return baseMapper.updateById(weighingBox);
    }
    @Override
    public int deleteWeighingBoxByIds(Long[] boxIds)
    {
        return baseMapper.deleteBatchIds(Arrays.asList(boxIds));
    }
    @Override
    @Transactional
    public int calibrate(Long boxId, Date calibDate, BigDecimal actualWeight, String note)
    {
        WeighingBox box = baseMapper.selectById(boxId);
        if (box == null)
        {
            return 0;
        }
        // ä¿å­˜æ ¡å‡†å‰çš„状态
        Date prevCalibDate = box.getLastCalibDate();
        // æ›´æ–°ç›’子信息
        box.setLastCalibDate(calibDate);
        box.setNextCalibDate(DateUtils.addDays(calibDate, box.getCalibCycleDays()));
        baseMapper.updateById(box);
        // è®¡ç®—偏差
        BigDecimal deviation = null;
        BigDecimal deviationPct = null;
        if (actualWeight != null)
        {
            deviation = actualWeight.subtract(box.getWeight());
            if (box.getWeight().compareTo(BigDecimal.ZERO) > 0)
            {
                deviationPct = deviation.divide(box.getWeight(), 3, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100));
            }
        }
        // åˆ›å»ºæ ¡å‡†è®°å½•
        CalibrationRecord record = new CalibrationRecord();
        record.setTargetType("weighing_box");
        record.setTargetId(boxId);
        record.setTargetCode(box.getCode());
        record.setTargetName(box.getName());
        record.setCalibDate(calibDate);
        record.setCalibCycleDays(box.getCalibCycleDays());
        record.setStandardWeight(box.getWeight());
        record.setActualWeight(actualWeight);
        record.setDeviation(deviation);
        record.setDeviationPct(deviationPct);
        record.setPrevCalibDate(prevCalibDate);
        record.setNextCalibDate(box.getNextCalibDate());
        record.setOperator("admin"); // å®žé™…应该从当前用户获取
        record.setNote(note);
        record.setCreateTime(new Date());
        calibrationRecordService.save(record);
        return 1;
    }
    @Override
    @Transactional
    public Map<String, Object> batchCalibrate(BatchCalibrateBo batchCalibrateDTO)
    {
        List<Long> boxIds = batchCalibrateDTO.getBoxIds();
        Date calibDate = batchCalibrateDTO.getCalibDate();
        String note = batchCalibrateDTO.getNote();
        List<BatchCalibrateBo.CalibrateItem> items = batchCalibrateDTO.getItems();
        Map<Long, BigDecimal> actualWeightMap = new HashMap<>();
        if (items != null)
        {
            for (BatchCalibrateBo.CalibrateItem item : items)
            {
                actualWeightMap.put(item.getBoxId(), item.getActualWeight());
            }
        }
        String batchId = UUID.randomUUID().toString();
        List<CalibrationRecord> records = new ArrayList<>();
        List<Map<String, Object>> results = new ArrayList<>();
        for (Long boxId : boxIds)
        {
            WeighingBox box = baseMapper.selectById(boxId);
            if (box == null)
            {
                continue;
            }
            // ä¿å­˜æ ¡å‡†å‰çš„状态
            Date prevCalibDate = box.getLastCalibDate();
            // æ›´æ–°ç›’子信息
            box.setLastCalibDate(calibDate);
            box.setNextCalibDate(DateUtils.addDays(calibDate, box.getCalibCycleDays()));
            baseMapper.updateById(box);
            // è®¡ç®—偏差
            BigDecimal actualWeight = actualWeightMap.get(boxId);
            BigDecimal deviation = null;
            BigDecimal deviationPct = null;
            if (actualWeight != null)
            {
                deviation = actualWeight.subtract(box.getWeight());
                if (box.getWeight().compareTo(BigDecimal.ZERO) > 0)
                {
                    deviationPct = deviation.divide(box.getWeight(), 3, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100));
                }
            }
            // åˆ›å»ºæ ¡å‡†è®°å½•
            CalibrationRecord record = new CalibrationRecord();
            record.setTargetType("weighing_box");
            record.setTargetId(boxId);
            record.setTargetCode(box.getCode());
            record.setTargetName(box.getName());
            record.setCalibDate(calibDate);
            record.setCalibCycleDays(box.getCalibCycleDays());
            record.setStandardWeight(box.getWeight());
            record.setActualWeight(actualWeight);
            record.setDeviation(deviation);
            record.setDeviationPct(deviationPct);
            record.setPrevCalibDate(prevCalibDate);
            record.setNextCalibDate(box.getNextCalibDate());
            record.setBatchId(batchId);
            record.setOperator("admin"); // å®žé™…应该从当前用户获取
            record.setNote(note);
            record.setCreateTime(new Date());
            records.add(record);
            // å‡†å¤‡è¿”回结果
            Map<String, Object> result = new HashMap<>();
            result.put("boxId", boxId);
            result.put("nextCalibDate", box.getNextCalibDate());
            results.add(result);
        }
        // æ‰¹é‡æ’入校准记录
        if (!records.isEmpty())
        {
            calibrationRecordService.batchInsert(records);
        }
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("successCount", boxIds.size());
        resultMap.put("failCount", 0);
        resultMap.put("results", results);
        return resultMap;
    }
    @Override
    @Transactional
    public int batchConfig(BatchConfigBo batchConfigDTO)
    {
        Integer calibCycleDays = batchConfigDTO.getCalibCycleDays();
        Integer remindDays = batchConfigDTO.getRemindDays();
        String applyScope = batchConfigDTO.getApplyScope();
        List<Long> boxIds = batchConfigDTO.getBoxIds();
        List<Long> targetBoxIds = new ArrayList<>();
        if ("all".equals(applyScope))
        {
            // æ‰€æœ‰å·²å¯ç”¨çš„盒子
            List<WeighingBox> boxes = weighingBoxMapper.selectActiveWeighingBoxes();
            for (WeighingBox box : boxes)
            {
                targetBoxIds.add(box.getId());
            }
        }
        else if ("selected".equals(applyScope) && boxIds != null)
        {
            targetBoxIds.addAll(boxIds);
        }
        if (targetBoxIds.isEmpty())
        {
            return 0;
        }
        // æ‰¹é‡æ›´æ–°æ ¡å‡†é…ç½®
        int result = weighingBoxMapper.batchUpdateCalibConfig(targetBoxIds, calibCycleDays, remindDays);
        // é‡æ–°è®¡ç®—下次校准日期
        for (Long boxId : targetBoxIds)
        {
            WeighingBox box = baseMapper.selectById(boxId);
            if (box.getLastCalibDate() != null)
            {
                box.setNextCalibDate(DateUtils.addDays(box.getLastCalibDate(), calibCycleDays));
                baseMapper.updateById(box);
            }
        }
        return result;
    }
    @Override
    public int batchUpdateStatus(List<Long> boxIds, Integer activeStatus)
    {
        return weighingBoxMapper.batchUpdateStatus(boxIds, activeStatus);
    }
    @Override
    public Map<String, Object> copyBox(Long sourceId, Integer count)
    {
        WeighingBox sourceBox = baseMapper.selectById(sourceId);
        if (sourceBox == null)
        {
            throw new RuntimeException("源盒子不存在");
        }
        Date nextCalibDate = null;
        List<Long> newIds = new ArrayList<>();
        List<String> newCodes = new ArrayList<>();
// è®¡ç®—下次校准日期
        if ( sourceBox.getCalibCycleDays() != null)
        {
            nextCalibDate = DateUtils.addDays(new Date(), sourceBox.getCalibCycleDays());
        }
        for (int i = 1; i <= count; i++)
        {
            WeighingBox newBox = new WeighingBox();
            newBox.setName(sourceBox.getName() + "-" + (char)('a' + i - 1));
            newBox.setCode(sourceBox.getCode() + "-" + (char)('a' + i - 1));
            newBox.setWeight(sourceBox.getWeight());
            newBox.setUnit(sourceBox.getUnit());
            newBox.setLocation(sourceBox.getLocation());
            newBox.setCalibCycleDays(sourceBox.getCalibCycleDays());
            newBox.setRemindDays(sourceBox.getRemindDays());
            newBox.setActiveStatus(sourceBox.getActiveStatus());
            newBox.setDescription(sourceBox.getDescription());
            newBox.setLastCalibDate(new Date());
            newBox.setNextCalibDate(nextCalibDate);
            newBox.setCreateBy("admin"); // å®žé™…应该从当前用户获取
            newBox.setCreateTime(new Date());
            baseMapper.insert(newBox);
            newIds.add(newBox.getId());
            newCodes.add(newBox.getCode());
        }
        Map<String, Object> result = new HashMap<>();
        result.put("ids", newIds);
        result.put("codes", newCodes);
        return result;
    }
    @Override
    public Map<String, Integer> getStatistics()
    {
        Map<String, Integer> statistics = new HashMap<>();
        statistics.put("total", Math.toIntExact(baseMapper.selectCount(null)));
        // å·²å¯ç”¨
        int activeCount = Math.toIntExact(baseMapper.selectCount(new LambdaQueryWrapper<WeighingBox>()
                .eq(WeighingBox::getActiveStatus, 1)
                .eq(WeighingBox::getDelFlag, 0)));
        statistics.put("active", activeCount);
        // å·²åœç”¨
        int inactiveCount = Math.toIntExact(baseMapper.selectCount(new LambdaQueryWrapper<WeighingBox>()
                .eq(WeighingBox::getActiveStatus, 0)
                .eq(WeighingBox::getDelFlag, 0)));
        statistics.put("inactive", inactiveCount);
        // æ ¡å‡†çŠ¶æ€ç»Ÿè®¡
        List<WeighingBox> boxes = baseMapper.selectList(new LambdaQueryWrapper<WeighingBox>()
            .eq(WeighingBox::getDelFlag, 0)
            .eq(WeighingBox::getActiveStatus, 1));
        int normal = 0, warning = 0, overdue = 0, unset = 0;
        for (WeighingBox box : boxes)
        {
            calculateCalibStatus(box);
            String status = box.getCalibStatus();
            if ("normal".equals(status)) normal++;
            else if ("warning".equals(status)) warning++;
            else if ("overdue".equals(status)) overdue++;
            else if ("unset".equals(status)) unset++;
        }
        statistics.put("normal", normal);
        statistics.put("warning", warning);
        statistics.put("overdue", overdue);
        statistics.put("unset", unset);
        return statistics;
    }
    @Override
    public WeighingBox calculateCalibStatus(WeighingBox weighingBox)
    {
        Date nextCalibDate = weighingBox.getNextCalibDate();
        if (nextCalibDate == null)
        {
            weighingBox.setCalibStatus("unset");
            weighingBox.setCalibDaysLeft(null);
            return weighingBox;
        }
        long daysLeft = DateUtils.getTimeDifference(new Date(), nextCalibDate, TimeUnit.DAYS);
        weighingBox.setCalibDaysLeft((int) daysLeft);
        if (daysLeft < 0)
        {
            weighingBox.setCalibStatus("overdue");
        }
        else if (daysLeft <= weighingBox.getRemindDays())
        {
            weighingBox.setCalibStatus("warning");
        }
        else
        {
            weighingBox.setCalibStatus("normal");
        }
        return weighingBox;
    }
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/CalibrationRecordMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.qa.md.mapper.CalibrationRecordMapper">
    <resultMap type="org.dromara.qa.md.domain.CalibrationRecord" id="CalibrationRecordResult">
        <id property="id" column="id"/>
        <result property="targetType" column="target_type"/>
        <result property="targetId" column="target_id"/>
        <result property="targetCode" column="target_code"/>
        <result property="targetName" column="target_name"/>
        <result property="calibDate" column="calib_date"/>
        <result property="calibCycleDays" column="calib_cycle_days"/>
        <result property="standardWeight" column="standard_weight"/>
        <result property="actualWeight" column="actual_weight"/>
        <result property="deviation" column="deviation"/>
        <result property="deviationPct" column="deviation_pct"/>
        <result property="prevCalibDate" column="prev_calib_date"/>
        <result property="nextCalibDate" column="next_calib_date"/>
        <result property="batchId" column="batch_id"/>
        <result property="note" column="note"/>
        <result property="operator" column="operator"/>
        <result property="createTime" column="create_time"/>
    </resultMap>
    <sql id="selectCalibrationRecordVo">
        select id, target_type, target_id, target_code, target_name, calib_date, calib_cycle_days, standard_weight, actual_weight, deviation, deviation_pct, prev_calib_date, next_calib_date, batch_id, note, operator, create_time from qm_calibration_record
    </sql>
    <select id="selectCalibrationRecordList" parameterType="org.dromara.qa.md.domain.CalibrationRecord" resultMap="CalibrationRecordResult">
        <include refid="selectCalibrationRecordVo"/>
        <where>
            <if test="targetType != null and targetType != ''">
                and target_type = #{targetType}
            </if>
            <if test="targetId != null">
                and target_id = #{targetId}
            </if>
            <if test="calibDate != null">
                and calib_date = #{calibDate}
            </if>
            <if test="batchId != null and batchId != ''">
                and batch_id = #{batchId}
            </if>
        </where>
        order by calib_date desc
    </select>
    <select id="selectByTargetId" resultMap="CalibrationRecordResult">
        <include refid="selectCalibrationRecordVo"/>
        where target_type = #{targetType} and target_id = #{targetId}
        order by calib_date desc
    </select>
    <insert id="batchInsert" parameterType="java.util.List">
        insert into qm_calibration_record (target_type, target_id, target_code, target_name, calib_date, calib_cycle_days, standard_weight, actual_weight, deviation, deviation_pct, prev_calib_date, next_calib_date, batch_id, note, operator, create_time)
        values
        <foreach collection="list" item="item" separator=",">
            (#{item.targetType}, #{item.targetId}, #{item.targetCode}, #{item.targetName}, #{item.calibDate}, #{item.calibCycleDays}, #{item.standardWeight}, #{item.actualWeight}, #{item.deviation}, #{item.deviationPct}, #{item.prevCalibDate}, #{item.nextCalibDate}, #{item.batchId}, #{item.note}, #{item.operator}, #{item.createTime})
        </foreach>
    </insert>
</mapper>
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/WeighingBoxMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.qa.md.mapper.WeighingBoxMapper">
    <resultMap type="org.dromara.qa.md.domain.WeighingBox" id="WeighingBoxResult">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="code" column="code"/>
        <result property="weight" column="weight"/>
        <result property="unit" column="unit"/>
        <result property="location" column="location"/>
        <result property="calibCycleDays" column="calib_cycle_days"/>
        <result property="remindDays" column="remind_days"/>
        <result property="lastCalibDate" column="last_calib_date"/>
        <result property="nextCalibDate" column="next_calib_date"/>
        <result property="activeStatus" column="active_status"/>
        <result property="description" column="description"/>
        <result property="delFlag" column="del_flag"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
    </resultMap>
    <sql id="selectWeighingBoxVo">
        select id, name, code, weight, unit, location, calib_cycle_days, remind_days, last_calib_date, next_calib_date, active_status, description, del_flag, create_by, create_time, update_by, update_time from qm_weighing_box
    </sql>
    <select id="selectPageList" resultType="org.dromara.qa.md.domain.WeighingBox">
        <include refid="selectWeighingBoxVo"/>
        <where>
            del_flag = '0'
            <if test="weighingBox.name != null and weighingBox.name != ''">
                and name like concat('%', #{weighingBox.name}, '%')
            </if>
            <if test="weighingBox.code != null and weighingBox.code != ''">
                and code like concat('%', #{weighingBox.code}, '%')
            </if>
            <if test="weighingBox.activeStatus != null">
                and active_status = #{weighingBox.activeStatus}
            </if>
        </where>
        order by create_time desc
    </select>
    <select id="selectWeighingBoxList" parameterType="org.dromara.qa.md.domain.WeighingBox" resultMap="WeighingBoxResult">
        <include refid="selectWeighingBoxVo"/>
        <where>
            <if test="delFlag != null">
                and del_flag = #{delFlag}
            </if>
            <if test="name != null and name != ''">
                and name like concat('%', #{name}, '%')
            </if>
            <if test="code != null and code != ''">
                and code like concat('%', #{code}, '%')
            </if>
            <if test="location != null and location != ''">
                and location like concat('%', #{location}, '%')
            </if>
            <if test="activeStatus != null">
                and active_status = #{activeStatus}
            </if>
        </where>
        order by create_time desc
    </select>
    <select id="selectActiveWeighingBoxes" resultMap="WeighingBoxResult">
        <include refid="selectWeighingBoxVo"/>
        where del_flag = 0 and active_status = 1
    </select>
    <update id="batchUpdateStatus">
        update qm_weighing_box set active_status = #{activeStatus} where id in
        <foreach collection="boxIds" item="id" open="(" separator="," close=")">
            ${id}
        </foreach>
    </update>
    <update id="batchUpdateCalibConfig">
        update qm_weighing_box set calib_cycle_days = #{calibCycleDays}, remind_days = #{remindDays} where id in
        <foreach collection="boxIds" item="id" open="(" separator="," close=")">
            ${id}
        </foreach>
    </update>
    <!-- å¸¦æ ¡å‡†çŠ¶æ€è¿‡æ»¤çš„æŸ¥è¯¢ -->
    <select id="selectWeighingBoxListWithCalibStatus" parameterType="org.dromara.qa.md.domain.WeighingBox" resultMap="WeighingBoxResult">
        select * from (
            select
                id, name, code, weight, unit, location, calib_cycle_days, remind_days,
                last_calib_date, next_calib_date, active_status, description,
                del_flag, create_by, create_time, update_by, update_time,
                case
                    when next_calib_date is null then 'unset'
                    when current_date > next_calib_date then 'overdue'
                    when current_date + interval '1 day' * remind_days >= next_calib_date then 'warning'
                    else 'normal'
                end as calib_status
            from qm_weighing_box
            where del_flag = 0
        ) as box
        where 1=1
        <if test="name != null and name != ''">
            and box.name like concat('%', #{name}, '%')
        </if>
        <if test="code != null and code != ''">
            and box.code like concat('%', #{code}, '%')
        </if>
        <if test="location != null and location != ''">
            and box.location like concat('%', #{location}, '%')
        </if>
        <if test="activeStatus != null">
            and box.active_status = #{activeStatus}
        </if>
        <if test="calibStatus != null and calibStatus != ''">
            and box.calib_status = #{calibStatus}
        </if>
        order by box.create_time desc
    </select>
    <!-- å¸¦æ ¡å‡†çŠ¶æ€è¿‡æ»¤çš„è®¡æ•° -->
    <select id="selectWeighingBoxCountWithCalibStatus" parameterType="org.dromara.qa.md.domain.WeighingBox" resultType="java.lang.Long">
        select count(*) from (
            select
        id, name, code, weight, unit, location, calib_cycle_days, remind_days,
        last_calib_date, next_calib_date, active_status, description,
        del_flag, create_by, create_time, update_by, update_time,
                case
                    when next_calib_date is null then 'unset'
                    when current_date > next_calib_date then 'overdue'
                    when current_date + interval '1 day' * remind_days >= next_calib_date then 'warning'
                    else 'normal'
                end as calib_status
            from qm_weighing_box
            where del_flag = 0
        ) as box
        where 1=1
        <if test="name != null and name != ''">
            and box.name like concat('%', #{name}, '%')
        </if>
        <if test="code != null and code != ''">
            and box.code like concat('%', #{code}, '%')
        </if>
        <if test="location != null and location != ''">
            and box.location like concat('%', #{location}, '%')
        </if>
        <if test="activeStatus != null">
            and box.active_status = #{activeStatus}
        </if>
        <if test="calibStatus != null and calibStatus != ''">
            and box.calib_status = #{calibStatus}
        </if>
    </select>
</mapper>
ruoyi-plus-soybean/src/locales/langs/zh-cn.ts
@@ -307,9 +307,10 @@
    'analy_store-silk': '',
    md: '',
    md_shift: '',
    qm: '',
    qm_batch: '',
    qm_std: '',
    qm: '基础数据管理',
    qm_batch: '批次管理',
    qm_std: '标准管理',
    qm_weighing_box: '称重盒子维护',
    report: '',
    report_demo: '',
    'report_silk-storage-output': '',
ruoyi-plus-soybean/src/router/elegant/imports.ts
@@ -34,6 +34,7 @@
  home: () => import("@/views/home/index.vue"),
  md_instrument: () => import("@/views/md/instrument/index.vue"),
  md_shift: () => import("@/views/md/shift/index.vue"),
  "md_weighing-box": () => import("@/views/md/weighing-box/index.vue"),
  monitor_cache: () => import("@/views/monitor/cache/index.vue"),
  monitor_logininfor: () => import("@/views/monitor/logininfor/index.vue"),
  monitor_online: () => import("@/views/monitor/online/index.vue"),
ruoyi-plus-soybean/src/router/elegant/routes.ts
@@ -206,6 +206,15 @@
          title: 'md_shift',
          i18nKey: 'route.md_shift'
        }
      },
      {
        name: 'md_weighing-box',
        path: '/md/weighing-box',
        component: 'view.md_weighing-box',
        meta: {
          title: 'md_weighing-box',
          i18nKey: 'route.md_weighing-box'
        }
      }
    ]
  },
ruoyi-plus-soybean/src/router/elegant/transform.ts
@@ -187,6 +187,7 @@
  "md": "/md",
  "md_instrument": "/md/instrument",
  "md_shift": "/md/shift",
  "md_weighing-box": "/md/weighing-box",
  "monitor": "/monitor",
  "monitor_cache": "/monitor/cache",
  "monitor_logininfor": "/monitor/logininfor",
ruoyi-plus-soybean/src/service/api/md/weighing-box.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
import { request } from '@/service/request';
/**
 * ç§°é‡ç›’子API
 */
export const weighingBoxApi = {
  /**
   * æŸ¥è¯¢ç§°é‡ç›’子列表
   */
  getList: (params: any) => request({
    url: '/md/weighingBox/list',
    method: 'get',
    params
  }),
  /**
   * èŽ·å–ç§°é‡ç›’å­è¯¦æƒ…
   */
  getInfo: (id: number) => request({
    url: `/md/weighingBox/${id}`,
    method: 'get'
  }),
  /**
   * æ–°å¢žç§°é‡ç›’子
   */
  add: (data: any) => request({
    url: '/md/weighingBox',
    method: 'post',
    data
  }),
  /**
   * ä¿®æ”¹ç§°é‡ç›’子
   */
  edit: (data: any) => request({
    url: '/md/weighingBox',
    method: 'put',
    data
  }),
  /**
   * åˆ é™¤ç§°é‡ç›’子
   */
  remove: (ids: number[]) => request({
    url: `/md/weighingBox/${ids.join(',')}`,
    method: 'delete'
  }),
  /**
   * æ‰§è¡Œå•个校准
   */
  calibrate: (data: {
    boxId: number;
    calibDate: string;
    actualWeight?: number;
    note?: string;
  }) => request({
    url: '/md/weighingBox/calibrate',
    method: 'post',
    data
  }),
  /**
   * æ‰¹é‡æ ¡å‡†
   */
  batchCalibrate: (data: any) => request({
    url: '/md/weighingBox/batchCalibrate',
    method: 'post',
    data
  }),
  /**
   * ç»Ÿä¸€é…ç½®æ ¡å‡†å‘¨æœŸ
   */
  batchConfig: (data: any) => request({
    url: '/md/weighingBox/batchConfig',
    method: 'post',
    data
  }),
  /**
   * æ‰¹é‡æ›´æ–°çŠ¶æ€
   */
  batchUpdateStatus: (data: {
    boxIds: number[];
    activeStatus: number;
  }) => request({
    url: '/md/weighingBox/batchUpdateStatus',
    method: 'post',
    data
  }),
  /**
   * å¤åˆ¶ç›’子
   */
  copy: (data: {
    sourceId: number;
    count: number;
  }) => request({
    url: '/md/weighingBox/copy',
    method: 'post',
    data
  }),
  /**
   * èŽ·å–æ ¡å‡†çŠ¶æ€ç»Ÿè®¡
   */
  getStatistics: () => request({
    url: '/md/weighingBox/statistics',
    method: 'get'
  })
};
ruoyi-plus-soybean/src/typings/elegant-router.d.ts
@@ -41,6 +41,7 @@
    "md": "/md";
    "md_instrument": "/md/instrument";
    "md_shift": "/md/shift";
    "md_weighing-box": "/md/weighing-box";
    "monitor": "/monitor";
    "monitor_cache": "/monitor/cache";
    "monitor_logininfor": "/monitor/logininfor";
@@ -161,6 +162,7 @@
    | "home"
    | "md_instrument"
    | "md_shift"
    | "md_weighing-box"
    | "monitor_cache"
    | "monitor_logininfor"
    | "monitor_online"
ruoyi-plus-soybean/src/views/md/weighing-box/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1322 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="page-header-left">
        <h1>称重盒子维护</h1>
      </div>
      <div class="header-actions">
        <n-button type="primary" @click="handleAdd">
          + æ–°å¢žç›’子
        </n-button>
      </div>
    </div>
    <!-- æœç´¢æ  -->
    <div class="search-bar">
        <n-input v-model:value="searchParams.name" placeholder="搜索名称、编号、位置..." style="width: 250px;" />
      <n-button type="primary" @click="handleSearch" style="width: 80px;">
        æœç´¢
      </n-button>
      <!-- å¿«æ·æ“ä½œæŒ‰é’® -->
      <div class="quick-actions">
        <!-- <n-button @click="handleBatchCalibrate"   >
          <span class="action-icon">🔧</span> æ‰¹é‡æ ¡å‡† <n-tag class="action-tag" size="small" type="error">{{ statistics.overdue || 0 }}</n-tag>
        </n-button> -->
         <n-button @click="handleBatchConfig('all')"  >
          <span class="action-icon">📐</span> æ‰¹é‡é…ç½®æ ¡å‡†å‘¨æœŸ
        </n-button>
       <!-- <n-button @click="handleBatchActivate"  >
          <span class="action-icon">✅</span> æ‰¹é‡å¯ç”¨
        </n-button>
        <n-button @click="handleBatchDeactivate" >
          <span class="action-icon">❌</span> æ‰¹é‡åœç”¨
        </n-button> -->
        <n-button @click="filterByCalibStatus('overdue')"  >
          <span class="action-icon">🚨</span> ä»…看逾期 <n-tag class="action-tag" size="small" type="error">{{ statistics.overdue || 0 }}</n-tag>
        </n-button>
        <n-button @click="filterByCalibStatus('warning')" >
          <span class="action-icon">⚠️</span> å³å°†åˆ°æœŸ <n-tag class="action-tag" size="small" type="warning">{{ statistics.warning || 0 }}</n-tag>
        </n-button>
        <!-- <n-button @click="handleCopy" >
          <span class="action-icon">📋</span> å¤åˆ¶ç›’子
        </n-button> -->
        <n-button @click="resetSearch" >
          <span class="action-icon">🔄</span> é‡ç½®ç­›é€‰
        </n-button>
      </div>
    </div>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-container">
      <div class="stat-card total" @click="filterByType('total')">
        <div class="stat-header">
          <div class="stat-icon">📦</div>
          <div class="stat-title">盒子总数</div>
        </div>
        <div class="stat-value">{{ statistics.total || 0 }}</div>
        <div class="stat-sub">已启用 {{ statistics.active || 0 }} Â· å·²åœç”¨ {{ statistics.inactive || 0 }}</div>
      </div>
      <div class="stat-card normal" @click="filterByCalibStatus('normal')">
        <div class="stat-header">
          <div class="stat-icon">✅</div>
          <div class="stat-title">正常</div>
        </div>
        <div class="stat-value">{{ statistics.normal || 0 }}</div>
        <div class="stat-sub">距离校准 > 7天</div>
      </div>
      <div class="stat-card warning" @click="filterByCalibStatus('warning')">
        <div class="stat-header">
          <div class="stat-icon">⚠️</div>
          <div class="stat-title">即将到期</div>
        </div>
        <div class="stat-value">{{ statistics.warning || 0 }}</div>
        <div class="stat-sub">7天内需校准</div>
      </div>
      <div class="stat-card overdue" @click="filterByCalibStatus('overdue')">
        <div class="stat-header">
          <div class="stat-icon">🚨</div>
          <div class="stat-title">已逾期</div>
        </div>
        <div class="stat-value">{{ statistics.overdue || 0 }}</div>
        <div class="stat-sub">校准已过期未完成</div>
      </div>
      <div class="stat-card inactive" @click="filterByActiveStatus(0)">
        <div class="stat-header">
          <div class="stat-icon">❌</div>
          <div class="stat-title">已停用</div>
        </div>
        <div class="stat-value">{{ statistics.inactive || 0 }}</div>
        <div class="stat-sub">暂不使用</div>
      </div>
    </div>
    <!-- æ‰¹é‡æ“ä½œæ  -->
    <div class="batch-bar" v-if="selectedRows.length > 0">
      <span class="batch-info">已选中 <strong>{{ selectedRows.length }}</strong> é¡¹</span>
      <div class="batch-actions">
        <n-button type="primary" size="small" tertiary  @click="handleBatchCalibrate">
         <span class="action-icon">🔧</span>  æ‰¹é‡æ ¡å‡†
        </n-button>
        <n-button type="success" size="small" tertiary  @click="handleBatchActivate">
         <span class="action-icon">✅</span>  æ‰¹é‡å¯ç”¨
        </n-button>
        <n-button type="tertiary" size="small" tertiary  @click="handleBatchDeactivate">
         <span class="action-icon">❌</span>  æ‰¹é‡åœç”¨
        </n-button>
        <n-button type="warning" size="small" tertiary  @click="handleBatchConfig('selected')">
         <span class="action-icon">📐</span>  æ‰¹é‡é…ç½®æ ¡å‡†å‘¨æœŸ
        </n-button>
        <n-button type="error" size="small" tertiary @click="handleDelete(undefined)">
         <span class="action-icon">🗑️</span>  æ‰¹é‡åˆ é™¤
        </n-button>
        <n-button size="small" @click="clearSelection">
          å–消选择
        </n-button>
      </div>
    </div>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <div class="table-container">
      <n-data-table
        :columns="columns"
        :data="data"
        :loading="loading"
        :pagination="mobilePagination"
        :row-key="(row) => row.id"
        :checked-row-keys="checkedRowKeys"
        @update:checked-row-keys="handleChecked"
        :bordered="true"
        :single-line="true"
      >
        <template #body-cell="{ column, row }">
          <template v-if="column.key === 'calibStatus'">
            <span class="calib-badge" :class="row.calibStatus">
              {{ getCalibIcon(row.calibStatus) }} {{ getCalibLabel(row.calibStatus) }}
              <span v-if="row.calibStatus === 'warning' || row.calibStatus === 'overdue'" class="days-left">
                {{ row.daysLeft ? `· å‰©ä½™${row.daysLeft}天` : '' }}
              </span>
            </span>
          </template>
          <template v-else-if="column.key === 'activeStatus'">
            <span class="status-tag active">
              å¯ç”¨
            </span>
          </template>
          <template v-else-if="column.key === 'calibCycleDays'">
            æ¯ {{ row.calibCycleDays }} å¤©
          </template>
          <template v-else-if="column.key === 'actions'">
            <div class="actions">
              <n-button size="small" circle @click="handleView(row)">
                <SvgIcon icon="lucide:eye" />
              </n-button>
              <n-button size="small" circle @click="handleCalibrate(row)">
                <SvgIcon icon="lucide:refresh-ccw" />
              </n-button>
              <n-button size="small" circle @click="handleEdit(row)">
                <SvgIcon icon="lucide:edit" />
              </n-button>
              <n-button size="small" circle type="error" @click="handleDelete([row.id])">
                <SvgIcon icon="lucide:trash-2" />
              </n-button>
            </div>
          </template>
        </template>
      </n-data-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗组件 -->
    <WeighingBoxOperate
      v-model:visible="drawerVisible"
      :operateType="operateType"
      :rowData="currentRow"
      @submitted="handleSubmitted"
    />
    <!-- æ ¡å‡†å¼¹çª—组件 -->
    <WeighingBoxCalibrate
      v-model:visible="calibrateVisible"
      :rowData="currentRow"
      @submitted="handleSubmitted"
    />
    <!-- æ‰¹é‡æ ¡å‡†å¼¹çª—组件 -->
    <WeighingBoxBatchCalibrate
      v-model:visible="batchCalibrateVisible"
      :selectedRows="selectedRows"
      @submitted="handleSubmitted"
    />
    <!-- ç»Ÿä¸€é…ç½®å¼¹çª—组件 -->
    <WeighingBoxBatchConfig
      v-model:visible="batchConfigVisible"
      :selectedRows="selectedRows"
      :type="applyScope"
      @submitted="handleSubmitted"
    />
    <!-- å¤åˆ¶å¼¹çª—组件 -->
    <WeighingBoxCopy
      v-model:visible="copyVisible"
      :rowData="currentRow"
      @submitted="handleSubmitted"
    />
  </div>
</template>
<script setup lang="tsx">
import { ref, reactive, computed, onMounted } from 'vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { NDivider, useDialog } from 'naive-ui';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
import WeighingBoxOperate from './modules/weighing-box-operate.vue';
import WeighingBoxCalibrate from './modules/weighing-box-calibrate.vue';
import WeighingBoxBatchCalibrate from './modules/weighing-box-batch-calibrate.vue';
import WeighingBoxBatchConfig from './modules/weighing-box-batch-config.vue';
import WeighingBoxCopy from './modules/weighing-box-copy.vue';
const message = useMessage();
const dialog = useDialog();
// æœç´¢å‚æ•°
const searchParams = reactive({
  pageNum: 1,
  pageSize: 10,
  name: '',
  code: '',
  activeStatus: undefined,
  calibStatus: undefined,
  location: undefined
});
// ç»Ÿè®¡æ•°æ®
const statistics = ref({
  total: 0,
  active: 0,
  inactive: 0,
  normal: 0,
  warning: 0,
  overdue: 0,
  unset: 0
});
// é€‰ä¸­çš„行
const checkedRowKeys = ref<number[]>([]);
const selectedRows = computed(() => {
  return data.value.filter(row => checkedRowKeys.value.includes(row.id));
});
// ä½¿ç”¨ useNaivePaginatedTable ç®¡ç†è¡¨æ ¼æ•°æ®
const { columns, data, getData, getDataByPage, loading, mobilePagination, scrollX } = useNaivePaginatedTable({
  api: () => weighingBoxApi.getList(searchParams),
  transform: response => defaultTransform(response),
  onPaginationParamsChange: params => {
    searchParams.pageNum = params.page;
    searchParams.pageSize = params.pageSize;
  },
  columns: () => [
    {
      type: 'selection',
      width: 40
    },
    {
      title: '编号',
      key: 'code',
      width: 120,
      sorter: true
    },
    {
      title: '名称',
      key: 'name',
      width: 150,
      sorter: true
    },
    {
      title: '重量',
      key: 'weight',
      width: 100,
      render: (row) => `${row.weight} ${row.unit || ''}`
    },
    {
      title: '位置',
      key: 'location',
      width: 150
    },
    {
      title: '校准周期',
      key: 'calibCycleDays',
      width: 120,
      sorter: true
    },
    {
      title: '下次校准',
      key: 'nextCalibDate',
      width: 120,
      sorter: true
    },
    {
      title: '校准状态',
      key: 'calibStatus',
      width: 150,
      render: (row) => {
        return (
          <span class={`calib-badge ${row.calibStatus}`}>
            {getCalibIcon(row.calibStatus)} {getCalibLabel(row.calibStatus)}
            {row.calibStatus === 'warning' || row.calibStatus === 'overdue' ?
              <span class="days-left">{row.daysLeft ? `· å‰©ä½™${row.daysLeft}天` : ''}</span> :
              ''
            }
          </span>
        );
      }
    },
    {
      title: '启用',
      key: 'activeStatus',
      width: 80,
      render: (row) => {
        const isActive = row.activeStatus === 1;
        return (
          <span class={`status-tag ${isActive ? 'active' : 'inactive'}`}>
            {isActive ? '启用' : '停用'}
          </span>
        );
      }
    },
    {
      title: '操作',
      key: 'actions',
      width: 130,
      fixed: 'right',
      render: (row) => {
        const divider = () => {
          return <NDivider vertical />;
        };
        const editBtn = () => {
          return (
            <ButtonIcon
              text
              type="primary"
              icon="material-symbols:drive-file-rename-outline-outline"
              tooltipContent="编辑"
              onClick={() => handleEdit(row)}
            />
          );
        };
        // æ ¡å‡†æŒ‰é’®
        const calibrateBtn = () => {
          return (
            <ButtonIcon
              text
              type="primary"
              icon="material-symbols:build-outline-rounded"
              tooltipContent="校准"
              onClick={() => handleCalibrate(row)}
            />
          );
        };
        // å¤åˆ¶æŒ‰é’®
        const copyBtn = () => {
          return (
            <ButtonIcon
              text
              type="primary"
              icon="material-symbols:copy-all-outline"
              tooltipContent="复制"
              onClick={() => handleCopy(row)}
            />
          );
        };
        // åˆ é™¤æŒ‰é’®
        const deleteBtn = () => {
          return (
            <ButtonIcon
              text
              type="error"
              icon="material-symbols:delete-outline"
              tooltipContent="删除"
              popconfirmContent="确定删除吗?"
              onPositiveClick={() => handleDelete([row.id])}
            />
          );
        };
        return (
          <div class="flex-center gap-8px">
            {editBtn()}
            {divider()}
            {calibrateBtn()}
            {divider()}
            {copyBtn()}
            {divider()}
            {deleteBtn()}
          </div>
        );
      }
    }
  ]
});
// å¼¹çª—相关状态
const drawerVisible = ref(false);
const calibrateVisible = ref(false);
const batchCalibrateVisible = ref(false);
const batchConfigVisible = ref(false);
const applyScope = ref('all');
const copyVisible = ref(false);
const operateType = ref('add');
const currentRow = ref({});
// èŽ·å–æ ¡å‡†çŠ¶æ€å›¾æ ‡
const getCalibIcon = (status) => {
  const icons = {
    normal: '✅',
    warning: '⚠️',
    overdue: '🚨',
    unset: 'ℹ️'
  };
  return icons[status] || 'ℹ️';
};
// èŽ·å–æ ¡å‡†çŠ¶æ€æ ‡ç­¾
const getCalibLabel = (status) => {
  const labels = {
    normal: '正常',
    warning: '即将到期',
    overdue: '已过期',
    unset: '未设置'
  };
  return labels[status] || '未设置';
};
// æŒ‰æ ¡å‡†çŠ¶æ€ç­›é€‰
const filterByCalibStatus = (status) => {
  searchParams.calibStatus = status;
  searchParams.activeStatus = undefined;
  handleSearch();
};
// æŒ‰å¯ç”¨çŠ¶æ€ç­›é€‰
const filterByActiveStatus = (status) => {
  searchParams.activeStatus = status;
  searchParams.calibStatus = undefined;
  handleSearch();
};
// æŒ‰ç±»åž‹ç­›é€‰
const filterByType = (type) => {
  if (type === 'total') {
    // æ˜¾ç¤ºæ‰€æœ‰ç›’子
    searchParams.calibStatus = undefined;
    searchParams.activeStatus = undefined;
  }
  handleSearch();
};
// æ¸…除选择
const clearSelection = () => {
  checkedRowKeys.value = [];
};
// å¯¼å‡º
const handleExport = () => {
  message.info('导出功能开发中');
};
// å¯¼å…¥
const handleImport = () => {
  message.info('导入功能开发中');
};
// æŸ¥çœ‹
const handleView = (row: any) => {
  message.info('查看功能开发中');
};
// åŠ è½½ç»Ÿè®¡æ•°æ®
const loadStatistics = async () => {
  try {
    const response = await weighingBoxApi.getStatistics();
    statistics.value = response.data || {};
  } catch (error) {
    console.error('加载统计数据失败', error);
  }
};
// æœç´¢
const handleSearch = () => {
  searchParams.pageNum = 1;
  getDataByPage();
};
// é‡ç½®æœç´¢
const resetSearch = () => {
  Object.keys(searchParams).forEach(key => {
    if (key !== 'pageNum' && key !== 'pageSize') {
      searchParams[key] = undefined;
    }
  });
  searchParams.pageNum = 1;
  getDataByPage();
};
// å¤„理选中
const handleChecked = (keys: number[]) => {
  checkedRowKeys.value = keys;
};
// æ–°å¢ž
const handleAdd = () => {
  operateType.value = 'add';
  currentRow.value = {};
  drawerVisible.value = true;
};
// ç¼–辑
const handleEdit = (row: any) => {
  operateType.value = 'edit';
  currentRow.value = row;
  drawerVisible.value = true;
};
// æ ¡å‡†
const handleCalibrate = (row: any) => {
  currentRow.value = row;
  calibrateVisible.value = true;
};
// æ‰¹é‡æ ¡å‡†
const handleBatchCalibrate = () => {
  if (selectedRows.value.length === 0) {
    message.warning('请选择要校准的盒子');
    return;
  }
  batchCalibrateVisible.value = true;
};
// æ‰¹é‡é…ç½®
const handleBatchConfig = (type: string) => {
  batchConfigVisible.value = true;
  applyScope.value = type;
};
// æ‰¹é‡å¯ç”¨
const handleBatchActivate = async () => {
  if (selectedRows.value.length === 0) {
    message.warning('请选择要启用的盒子');
    return;
  }
  dialog.warning({
    title: '批量启用确认',
    content: `确定要启用选中的 ${selectedRows.value.length} ä¸ªç›’子吗?`,
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        const response = await weighingBoxApi.batchUpdateStatus({
          boxIds: selectedRows.value.map(row => row.id),
          activeStatus: 1
        });
        const res = response.response.data;
        if (res.code === 200) {
          message.success('批量启用成功');
          getDataByPage();
          loadStatistics();
        } else {
          message.error(response.msg || '批量启用失败');
        }
      } catch (error) {
        message.error('批量启用失败');
      }
    }
  });
};
// æ‰¹é‡åœç”¨
const handleBatchDeactivate = async () => {
  if (selectedRows.value.length === 0) {
    message.warning('请选择要停用的盒子');
    return;
  }
  dialog.warning({
    title: '批量停用确认',
    content: `确定要停用选中的 ${selectedRows.value.length} ä¸ªç›’子吗?`,
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        const response = await weighingBoxApi.batchUpdateStatus({
          boxIds: selectedRows.value.map(row => row.id),
          activeStatus: 0
        });
        const res = response.response.data;
        if (res.code === 200) {
          message.success('批量停用成功');
          getDataByPage();
          loadStatistics();
        } else {
          message.error(res.msg || '批量停用失败');
        }
      } catch (error) {
        message.error('批量停用失败');
      }
    }
  });
};
// å¤åˆ¶
const handleCopy = (row: any) => {
  currentRow.value = row;
  copyVisible.value = true;
};
// åˆ é™¤
const handleDelete = async (ids?: number[]) => {
  const deleteIds = ids || selectedRows.value.map(row => row.id);
  console.log(deleteIds);
  if (deleteIds.length === 0) {
    message.warning('请选择要删除的盒子');
    return;
  }
  dialog.error({
    title: '批量删除确认',
    content: `确定要删除选中的 ${deleteIds.length} ä¸ªç›’子吗?此操作不可恢复。`,
    positiveText: '确定删除',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        const response = await weighingBoxApi.remove(deleteIds);
        console.log("删除响应:", response.response.data);
        const res = response.response.data;
        if (res.code === 200) {
          message.success('删除成功');
          getDataByPage();
          loadStatistics();
        } else {
          message.error(res.msg || '删除失败');
        }
      } catch (error) {
        message.error('删除失败');
      }
    }
  });
};
// æäº¤åŽå¤„理
const handleSubmitted = () => {
  getDataByPage();
  loadStatistics();
};
// åˆå§‹åŒ–
onMounted(() => {
  getData();
  loadStatistics();
});
</script>
<style scoped>
.app-container {
  padding: 20px;
}
/* é¡µé¢æ ‡é¢˜ */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 20px;
}
.page-header-left .breadcrumb {
  font-size: 14px;
  color: #666;
  margin-bottom: 8px;
}
.page-header-left h1 {
  font-size: 24px;
  font-weight: 600;
  color: #333;
  margin: 0;
}
.header-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}
/* æœç´¢æ  */
.search-bar {
  background: #fff;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
/* å¿«æ·æ“ä½œæŒ‰é’® */
.quick-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.action-icon {
  margin-right: 4px;
}
/* ç»Ÿè®¡å¡ç‰‡ */
.stats-container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 16px;
  margin-bottom: 20px;
}
.stat-card {
  background: #fff;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
}
.stat-card.total::before {
  background: #3b82f6;
}
.stat-card.normal::before {
  background: #10b981;
}
.stat-card.warning::before {
  background: #f59e0b;
}
.stat-card.overdue::before {
  background: #ef4444;
}
.stat-card.inactive::before {
  background: #6b7280;
}
.stat-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.stat-icon {
  font-size: 15px;
}
.stat-card .stat-title {
  font-size: 14px;
  color: #666;
  margin: 0;
}
.stat-card .stat-value {
  font-size: 28px;
  font-weight: 700;
  color: #333;
  margin-bottom: 4px;
}
.stat-card .stat-sub {
  font-size: 12px;
  color: #999;
}
.search-input {
  width: 250px;
}
/* æ‰¹é‡æ“ä½œæ  */
.batch-bar {
  background: #e6f7ff;
  border: 1px solid #91d5ff;
  border-radius: 8px;
  padding: 12px 16px;
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.batch-info {
  font-size: 14px;
  color: #1890ff;
  font-weight: 500;
}
.batch-actions {
  display: flex;
  gap: 8px;
}
/* è¡¨æ ¼å®¹å™¨ */
.table-container {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}
:deep(.n-data-table) {
  border-radius: 8px;
  overflow: hidden;
}
:deep(.n-data-table th) {
  background: #f8f9fa;
  font-size: 14px;
  font-weight: 600;
  color: #666;
  text-align: left;
  padding: 12px 16px;
  border-bottom: 1px solid #e5e7eb;
}
:deep(.n-data-table td) {
  padding: 12px 16px;
  font-size: 14px;
  color: #333;
  border-bottom: 1px solid #f0f0f0;
  vertical-align: middle;
}
:deep(.n-data-table tr:hover) {
  background: #fafbfc;
}
:deep(.n-data-table tr.n-data-table__row--selected) {
  background: #e6f7ff;
}
:deep(.n-data-table .n-data-table__pagination) {
  padding: 16px;
  border-top: 1px solid #f0f0f0;
}
/* çŠ¶æ€æ ‡ç­¾ */
.status-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}
.status-tag.active {
  background: #f6ffed;
  color: #52c41a;
  border: 1px solid #b7eb8f;
}
.status-tag.inactive {
  background: #f5f5f5;
  color: #999;
  border: 1px solid #d9d9d9;
}
/* æ ¡å‡†çŠ¶æ€æ ‡ç­¾ */
.calib-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}
.calib-badge.normal {
  background: #f6ffed;
  color: #52c41a;
  border: 1px solid #b7eb8f;
}
.calib-badge.warning {
  background: #fffbe6;
  color: #d48806;
  border: 1px solid #ffe58f;
}
.calib-badge.overdue {
  background: #fff2f0;
  color: #ff4d4f;
  border: 1px solid #ffccc7;
}
.calib-badge.unset {
  background: #f5f5f5;
  color: #999;
  border: 1px solid #d9d9d9;
}
.calib-badge .days-left {
  font-size: 11px;
  opacity: 0.8;
}
/* æ“ä½œæŒ‰é’® */
.actions {
  display: flex;
  gap: 4px;
}
.action-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid #e5e7eb;
  background: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s ease;
}
.action-btn:hover {
  background: #f5f5f5;
  border-color: #1890ff;
}
.action-btn.delete:hover {
  background: #fff2f0;
  border-color: #ff4d4f;
}
.action-btn .icon {
  font-size: 14px;
}
.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}
.gap-8px {
  gap: 8px;
}
/* å“åº”式调整 */
@media (max-width: 1200px) {
  .stats-container {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (max-width: 768px) {
  .stats-container {
    grid-template-columns: repeat(2, 1fr);
  }
  .search-bar {
    flex-direction: column;
    align-items: stretch;
  }
  .quick-actions {
    flex-direction: column;
    align-items: stretch;
  }
  .page-header {
    flex-direction: column;
    align-items: stretch;
  }
  .header-actions {
    margin-top: 12px;
  }
}
/* æ¨¡æ€æ¡†æ ·å¼ */
:deep(.n-modal-card) {
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
:deep(.n-modal-card .n-modal-card__header) {
  padding: 16px 20px;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
:deep(.n-modal-card .n-modal-card__header-title) {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
:deep(.n-modal-card .n-modal-card__body) {
  padding: 20px;
  overflow-y: auto;
  flex: 1;
}
:deep(.n-modal-card .n-modal-card__footer) {
  padding: 16px 20px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
/* è¡¨å•样式 */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.form-group {
  display: flex;
  flex-direction: column;
}
.form-group.full {
  grid-column: 1 / -1;
}
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
.form-label .required {
  color: #ff4d4f;
  margin-left: 4px;
}
.form-row {
  display: flex;
  gap: 10px;
  align-items: center;
}
.form-hint {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
}
.form-divider {
  grid-column: 1 / -1;
  border: none;
  border-top: 1px solid #e5e7eb;
  margin: 12px 0;
}
.form-section-title {
  grid-column: 1 / -1;
  font-size: 14px;
  font-weight: 600;
  color: #1890ff;
  margin: 12px 0 8px;
}
/* é¢„设模板 */
.preset-bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.preset-chip {
  padding: 6px 16px;
  border-radius: 20px;
  border: 1px solid #e5e7eb;
  font-size: 14px;
  color: #666;
  cursor: pointer;
  transition: all 0.2s ease;
  background: #fff;
}
.preset-chip:hover {
  border-color: #1890ff;
  color: #1890ff;
}
.preset-chip.active {
  background: #1890ff;
  color: #fff;
  border-color: #1890ff;
}
/* é…ç½®æ¨¡æ¿ */
.config-template-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-bottom: 20px;
}
.template-card {
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
  text-align: center;
  background: #fff;
}
.template-card:hover {
  border-color: #91d5ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
.template-card.selected {
  border-color: #1890ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
  background: #e6f7ff;
}
.template-card .t-icon {
  font-size: 24px;
  margin-bottom: 8px;
}
.template-card .t-name {
  font-size: 14px;
  font-weight: 600;
  color: #333;
  margin-bottom: 4px;
}
.template-card .t-desc {
  font-size: 12px;
  color: #666;
}
.template-card .t-custom-input {
  width: 80px;
  margin-top: 8px;
}
/* åº”用范围 */
.apply-target {
  margin-top: 16px;
}
.radio-group {
  display: flex;
  gap: 20px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.radio-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #333;
  cursor: pointer;
}
/* æ‰¹é‡æ ¡å‡†åˆ—表 */
.batch-cali-list {
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  margin: 12px 0;
}
.batch-cali-item {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid #f0f0f0;
  font-size: 14px;
}
.batch-cali-item:last-child {
  border-bottom: none;
}
.batch-cali-item .box-info {
  flex: 1;
}
.batch-cali-item .box-name {
  font-weight: 500;
  color: #333;
  margin-bottom: 4px;
}
.batch-cali-item .box-meta {
  font-size: 12px;
  color: #666;
}
.batch-cali-summary {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px 16px;
  margin-top: 12px;
  font-size: 14px;
  color: #666;
}
/* æ ¡å‡†ä¿¡æ¯ */
.calib-info {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 20px;
  border: 1px solid #e5e7eb;
}
.calib-info .info-row {
  display: flex;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 14px;
  border-bottom: 1px solid #e5e7eb;
}
.calib-info .info-row:last-child {
  border-bottom: none;
}
.calib-info .info-label {
  color: #666;
  font-weight: 500;
}
.calib-info .info-value {
  color: #333;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-hint {
  font-size: 12px;
  color: #999;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
/* å¼€å…³ */
.switch-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
}
.switch-label {
  font-size: 14px;
  color: #666;
}
.action-tag {
  font-size: 12px;
  font-weight: 500;
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 15px;
}
</style>
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-calibrate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
<template>
  <n-modal
    :show="visible"
    @update:show="(value) => emit('update:visible', value)"
    title="🔧 ä¸€é”®æ‰¹é‡æ ¡å‡†"
    preset="card"
    size="large"
    :style="bodyStyle"
  >
    <n-grid :cols="2" :x-gap="12" :y-gap="12">
      <n-form-item-gi label="校准日期" required>
        <n-date-picker v-model:value="form.calibDate" type="date" />
      </n-form-item-gi>
      <n-form-item-gi label="统一备注">
        <n-input v-model:value="form.note" placeholder="批量校准备注(可选)" />
      </n-form-item-gi>
    </n-grid>
    <div>
      <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
        <label class="form-label">选择的盒子</label>
        <n-checkbox v-model:checked="form.useStandardWeight">
          å…¨éƒ¨å¡«å…¥æ ‡å‡†é‡é‡
        </n-checkbox>
      </div>
      <div class="batch-cali-list">
        <div v-for="box in selectedRows" :key="box.id" class="batch-cali-item">
          <div class="box-info">
            <div class="box-name">{{ box.location }} Â· {{ box.name }} Â· {{ box.code }}</div>
          </div>
          <div style="display: flex; align-items: center;">
            <n-input
              v-model:value="box.actualWeight"
              type="number"
              placeholder="输入实际重量"
              style="width: 120px;"
            />
            <span style="margin-left: 8px; color: #666;">{{ box.unit || 'g' }}</span>
          </div>
        </div>
      </div>
      <div class="batch-cali-summary">
        <span>共 <strong>{{ selectedRows.length }}</strong> ä¸ªç›’子</span>
      </div>
    </div>
    <template #footer>
      <div class="modal-footer">
        <span class="modal-hint">校准后自动更新下次校准日期</span>
        <div class="modal-footer-right">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="success" @click="handleSubmit">✅ ç¡®è®¤å…¨éƒ¨æ ¡å‡†</n-button>
        </div>
      </div>
    </template>
  </n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  selectedRows: {
    type: Array,
    default: () => []
  }
});
const emit = defineEmits(['update:visible', 'submitted']);
const message = useMessage();
const form = reactive({
  calibDate: null,
  note: '',
  useStandardWeight: false
});
const bodyStyle = {
  width: '700px'
};
watch(() => props.visible, (newValue) => {
  if (newValue) {
    form.calibDate = new Date();
    form.note = '';
    form.useStandardWeight = false;
    // ä¸ºæ¯ä¸ªé€‰ä¸­çš„盒子添加 actualWeight å±žæ€§
    props.selectedRows.forEach(box => {
      box.actualWeight = '';
    });
  }
});
// ç›‘听 useStandardWeight å˜åŒ–
watch(() => form.useStandardWeight, (value) => {
  if (value) {
    props.selectedRows.forEach(box => {
      box.actualWeight = box.weight;
    });
  }
});
const handleCancel = () => {
  emit('update:visible', false);
};
const handleSubmit = async () => {
  try {
    const items = props.selectedRows.map(row => ({
      boxId: row.id,
      actualWeight: row.actualWeight
    }));
    const response = await weighingBoxApi.batchCalibrate({
      boxIds: props.selectedRows.map(row => row.id),
      calibDate: form.calibDate,
      note: form.note,
      items: items
    });
    const res = response.response.data;
    if (res.code === 200) {
      message.success('批量校准成功');
      emit('update:visible', false);
      emit('submitted');
    } else {
      message.error(res.msg || '批量校准失败');
    }
  } catch (error) {
    message.error('批量校准失败');
  }
};
</script>
<style scoped>
/* è¡¨å•样式 */
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
/* æ‰¹é‡æ ¡å‡†åˆ—表 */
.batch-cali-list {
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  margin: 12px 0;
}
.batch-cali-item {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid #f0f0f0;
  font-size: 14px;
}
.batch-cali-item:last-child {
  border-bottom: none;
}
.batch-cali-item .box-info {
  flex: 1;
}
.batch-cali-item .box-name {
  color: #333;
  margin-bottom: 4px;
}
.batch-cali-item .box-meta {
  font-size: 12px;
  color: #666;
}
.batch-cali-summary {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px 16px;
  margin-top: 12px;
  font-size: 14px;
  color: #666;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-hint {
  font-size: 12px;
  color: #999;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
</style>
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-config.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,272 @@
<template>
  <n-modal
    :show="visible"
    @update:show="(value) => emit('update:visible', value)"
    title="📐 ç»Ÿä¸€é…ç½®æ ¡å‡†å‘¨æœŸ"
    preset="card"
    size="medium"
    :style="bodyStyle"
  >
    <label class="form-label">选择周期模板</label>
    <div class="config-template-grid">
      <div class="template-card" :class="{ selected: selectedTemplate === 7 }" @click="selectTemplate(7)">
        <div class="t-icon">⚡</div>
        <div class="t-name">高频</div>
        <div class="t-desc">每 7 å¤©æ ¡å‡†</div>
      </div>
      <div class="template-card" :class="{ selected: selectedTemplate === 14 }" @click="selectTemplate(14)">
        <div class="t-icon">🔄</div>
        <div class="t-name">标准</div>
        <div class="t-desc">每 14 å¤©æ ¡å‡†</div>
      </div>
      <div class="template-card" :class="{ selected: selectedTemplate === 30 }" @click="selectTemplate(30)">
        <div class="t-icon">📅</div>
        <div class="t-name">常规</div>
        <div class="t-desc">每 30 å¤©æ ¡å‡†</div>
      </div>
      <div class="template-card" :class="{ selected: selectedTemplate === 90 }" @click="selectTemplate(90)">
        <div class="t-icon">📆</div>
        <div class="t-name">季度</div>
        <div class="t-desc">每 90 å¤©æ ¡å‡†</div>
      </div>
      <div class="template-card" :class="{ selected: selectedTemplate === 360 }" @click="selectTemplate(360)">
        <div class="t-icon">🗂️</div>
        <div class="t-name">年度</div>
        <div class="t-desc">每 360 å¤©æ ¡å‡†</div>
      </div>
      <div class="template-card" :class="{ selected: selectedTemplate === 'custom' }" @click="selectTemplate('custom')">
        <div class="t-icon">✏️</div>
        <div class="t-name">自定义</div>
        <n-input-number v-model:value="customDays" placeholder="天数" class="t-custom-input" />
      </div>
    </div>
    <div class="form-group" style="margin-top: 16px;">
      <label class="form-label">提前提醒天数</label>
      <div class="form-row">
        <n-input-number v-model:value="form.remindDays" style="max-width: 100px;" />
        <span class="form-unit">天(应用到所有选中盒子)</span>
      </div>
    </div>
    <div class="apply-target" style="margin-top: 16px;">
      <label class="form-label">应用范围</label>
      <div class="radio-group">
        <label class="radio-item" @click="selectApplyScope('all')">
          <n-radio :checked="form.applyScope === 'all'" name="applyScope" value="all" />
          å…¨éƒ¨å·²å¯ç”¨ç›’子
        </label>
        <label class="radio-item" @click="selectApplyScope('selected')">
          <n-radio :checked="form.applyScope === 'selected'" name="applyScope" value="selected" />
          ä»…选中项({{ selectedRows.length }}个)
        </label>
      </div>
    </div>
    <template #footer>
      <div class="modal-footer">
        <span class="modal-hint">将覆盖选中盒子的校准周期和提醒天数</span>
        <div class="modal-footer-right">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="primary" @click="handleSubmit">✅ åº”用配置</n-button>
        </div>
      </div>
    </template>
  </n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  selectedRows: {
    type: Array,
    default: () => []
  },
  type: {
    type: String,
    default: 'all'
  }
});
const emit = defineEmits(['update:visible', 'submitted']);
const message = useMessage();
const form = reactive({
  calibCycleDays: 30,
  remindDays: 7,
  applyScope: props.type
});
const customDays = ref(30);
const selectedTemplate = ref(30);
const bodyStyle = {
  width: '600px'
};
watch(() => props.visible, (newValue) => {
  if (newValue) {
    form.calibCycleDays = 30;
    form.remindDays = 7;
    form.applyScope = props.type;
    customDays.value = 30;
    selectedTemplate.value = 30;
  }
});
const selectTemplate = (days) => {
  selectedTemplate.value = days;
  if (days === 'custom') {
    form.calibCycleDays = customDays.value;
  } else {
    form.calibCycleDays = days;
  }
};
const selectApplyScope = (scope) => {
  form.applyScope = scope;
};
const handleCancel = () => {
  emit('update:visible', false);
};
const handleSubmit = async () => {
  try {
    const response = await weighingBoxApi.batchConfig({
      calibCycleDays: form.calibCycleDays,
      remindDays: form.remindDays,
      applyScope: form.applyScope,
      boxIds: props.selectedRows.map(row => row.id)
    });
    const res = response.response.data;
    if (res.code === 200) {
      message.success('批量配置成功');
      emit('update:visible', false);
      emit('submitted');
    } else {
      message.error(res.msg || '批量配置失败');
    }
  } catch (error) {
    message.error('批量配置失败');
  }
};
</script>
<style scoped>
/* è¡¨å•样式 */
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
.form-row {
  display: flex;
  gap: 10px;
  align-items: center;
}
.form-unit {
  font-size: 14px;
  color: #666;
}
/* é…ç½®æ¨¡æ¿ */
.config-template-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-bottom: 20px;
}
.template-card {
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
  text-align: center;
  background: #fff;
}
.template-card:hover {
  border-color: #91d5ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
.template-card.selected {
  border-color: #1890ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
  background: #e6f7ff;
}
.template-card .t-icon {
  font-size: 24px;
  margin-bottom: 8px;
}
.template-card .t-name {
  font-size: 14px;
  font-weight: 600;
  color: #333;
  margin-bottom: 4px;
}
.template-card .t-desc {
  font-size: 12px;
  color: #666;
}
.template-card .t-custom-input {
  margin-top: 8px;
}
/* åº”用范围 */
.apply-target {
  margin-top: 16px;
}
.radio-group {
  display: flex;
  gap: 20px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.radio-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #333;
  cursor: pointer;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-hint {
  font-size: 12px;
  color: #999;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
</style>
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-calibrate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
<template>
  <n-modal
    :show="visible"
    @update:show="(value) => emit('update:visible', value)"
    title="🔧 å®Œæˆæ ¡å‡†"
    preset="card"
    size="medium"
    :style="bodyStyle"
  >
    <div class="calib-info">
      <div class="info-row">
        <span class="info-label">盒子名称</span>
        <span class="info-value">{{ form.name }}</span>
      </div>
      <div class="info-row">
        <span class="info-label">盒子编号</span>
        <span class="info-value">{{ form.code }}</span>
      </div>
      <div class="info-row">
        <span class="info-label">所在位置</span>
        <span class="info-value">{{ form.location }}</span>
      </div>
    </div>
    <div class="form-group">
      <label class="form-label">本次校准日期 <span class="required">*</span></label>
      <n-date-picker v-model:value="form.calibDate" type="date" />
    </div>
    <div class="form-group">
      <label class="form-label">本次实测重量(可选)</label>
      <div class="form-row">
        <n-input-number v-model:value="form.actualWeight" placeholder="实测重量" />
        <span style="color: #666;">{{ props.rowData.unit || 'g' }}</span>
      </div>
    </div>
    <div class="form-group">
      <label class="form-label">校准备注</label>
      <n-input v-model:value="form.note" type="textarea" placeholder="校准结果、偏差说明等…" />
    </div>
    <template #footer>
      <div class="modal-footer">
        <div class="modal-footer-right">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="success" @click="handleSubmit">✅ ç¡®è®¤æ ¡å‡†å®Œæˆ</n-button>
        </div>
      </div>
    </template>
  </n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  rowData: {
    type: Object,
    default: () => ({})
  }
});
const emit = defineEmits(['update:visible', 'submitted']);
const message = useMessage();
const form = reactive({
  id: undefined,
  name: '',
  code: '',
  weight: '',
  calibDate: null,
  actualWeight: null,
  note: ''
});
const bodyStyle = {
  width: '500px'
};
watch(() => props.visible, (newValue) => {
  if (newValue && props.rowData) {
    form.id = props.rowData.id;
    form.name = props.rowData.name;
    form.code = props.rowData.code;
    form.weight = props.rowData.weight;
    form.calibDate = new Date();
    form.actualWeight = null;
    form.note = '';
  }
});
const handleCancel = () => {
  emit('update:visible', false);
};
const handleSubmit = async () => {
  try {
    const response = await weighingBoxApi.calibrate({
      boxId: form.id,
      calibDate: form.calibDate,
      actualWeight: form.actualWeight,
      note: form.note
    });
    const res = response.response.data;
    if (res.code === 200) {
      message.success('校准成功');
      emit('update:visible', false);
      emit('submitted');
    } else {
      message.error(res.msg || '校准失败');
    }
  } catch (error) {
    message.error('校准失败');
  }
};
</script>
<style scoped>
/* æ ¡å‡†ä¿¡æ¯ */
.calib-info {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 20px;
  border: 1px solid #e5e7eb;
}
.calib-info .info-row {
  display: flex;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 14px;
  border-bottom: 1px solid #e5e7eb;
}
.calib-info .info-row:last-child {
  border-bottom: none;
}
.calib-info .info-label {
  color: #666;
  font-weight: 500;
}
.calib-info .info-value {
  color: #333;
}
/* è¡¨å•样式 */
.form-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 16px;
}
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
.form-label .required {
  color: #ff4d4f;
  margin-left: 4px;
}
.form-row {
  display: flex;
  gap: 10px;
  align-items: center;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
</style>
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
<template>
  <n-modal
    :show="visible"
    @update:show="(value) => emit('update:visible', value)"
    title="📋 å¤åˆ¶ç§°é‡ç›’子"
    preset="card"
    size="medium"
    :style="bodyStyle"
  >
    <div class="form-group">
      <label class="form-label">源盒子</label>
      <n-input :value="form.sourceName" disabled />
    </div>
    <div class="form-group" style="margin-top: 12px;">
      <label class="form-label">复制数量</label>
      <n-input-number v-model:value="form.count" placeholder="请输入复制数量" min="1" max="20" style="max-width: 100px;" />
      <div class="form-hint">系统将自动递增编号后缀,如 WBOX-2024-001-a, -b, -c…</div>
    </div>
    <template #footer>
      <div class="modal-footer">
        <div class="modal-footer-right">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="primary" @click="handleSubmit">📋 ç¡®è®¤å¤åˆ¶</n-button>
        </div>
      </div>
    </template>
  </n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  rowData: {
    type: Object,
    default: () => ({})
  }
});
const emit = defineEmits(['update:visible', 'submitted']);
const message = useMessage();
const form = reactive({
  sourceId: undefined,
  sourceName: '',
  count: 1
});
const bodyStyle = {
  width: '400px'
};
watch(() => props.visible, (newValue) => {
  if (newValue && props.rowData) {
    form.sourceId = props.rowData.id;
    form.sourceName = props.rowData.name;
    form.count = 1;
  }
});
const handleCancel = () => {
  emit('update:visible', false);
};
const handleSubmit = async () => {
  try {
    const response = await weighingBoxApi.copy({
      sourceId: form.sourceId,
      count: form.count
    });
    const res = response.response.data;
    if (res.code === 200) {
      message.success('复制成功');
      emit('update:visible', false);
      emit('submitted');
    } else {
      message.error(res.msg || '复制失败');
    }
  } catch (error) {
    message.error('复制失败');
  }
};
</script>
<style scoped>
/* è¡¨å•样式 */
.form-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 16px;
}
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
.form-hint {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
</style>
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-operate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,338 @@
<template>
  <n-modal
    :show="visible"
    @update:show="(value) => emit('update:visible', value)"
    :title="operateType === 'add' ? '📦 æ–°å¢žç§°é‡ç›’子' : '📦 ç¼–辑称重盒子'"
    preset="card"
    size="medium"
    :style="bodyStyle"
    :bordered="false"
  >
    <n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
      <n-grid :cols="2" :x-gap="12" :y-gap="0">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <n-grid-item :span="2">
          <div class="form-section-title">📦 åŸºæœ¬ä¿¡æ¯</div>
        </n-grid-item>
        <n-form-item-gi path="name" label="盒子名称" required>
          <n-input v-model:value="form.name" placeholder="如:标准砝码盒 A-01" />
        </n-form-item-gi>
        <n-form-item-gi path="code" label="盒子编号" required>
          <n-input v-model:value="form.code" placeholder="如:WBOX-2024-001" />
        </n-form-item-gi>
        <n-form-item-gi path="weight" label="标准重量" required>
          <div class="form-row">
            <n-input-number v-model:value="form.weight" placeholder="500.00" />
            <n-input v-model:value="form.unit" placeholder="g" style="max-width: 80px;" />
          </div>
        </n-form-item-gi>
        <n-form-item-gi path="location" label="存放位置">
          <n-input v-model:value="form.location" placeholder="如:1号产线·质检台A" />
        </n-form-item-gi>
        <!-- æ ¡å‡†è®¾ç½® -->
        <n-grid-item :span="2">
          <hr class="form-divider">
          <div class="form-section-title">📐 æ ¡å‡†è®¾ç½®</div>
        </n-grid-item>
        <n-grid-item :span="2">
          <div class="form-group full">
            <label class="form-label">选择预设周期模板</label>
            <div class="preset-bar">
              <span class="preset-chip" @click="setPresetCycle(7)">每 7 å¤©</span>
              <span class="preset-chip" @click="setPresetCycle(14)">每 14 å¤©</span>
              <span class="preset-chip active" @click="setPresetCycle(30)">每 30 å¤©</span>
              <span class="preset-chip" @click="setPresetCycle(60)">每 60 å¤©</span>
              <span class="preset-chip" @click="setPresetCycle(90)">每 90 å¤©</span>
              <span class="preset-chip" @click="setPresetCycle(180)">每 180 å¤©</span>
            </div>
          </div>
        </n-grid-item>
        <n-form-item-gi path="calibCycleDays" label="校准周期" required>
          <div class="form-row">
            <n-input-number v-model:value="form.calibCycleDays" placeholder="30" />
            <span class="form-unit">天</span>
          </div>
        </n-form-item-gi>
        <n-form-item-gi path="remindDays" label="提前提醒天数">
          <div class="form-row">
            <n-input-number v-model:value="form.remindDays" placeholder="7" />
            <span class="form-unit">天</span>
          </div>
        </n-form-item-gi>
        <n-form-item-gi label="上次校准日期">
          <n-date-picker v-model:value="form.lastCalibDate" type="date" />
        </n-form-item-gi>
        <n-form-item-gi label="启用状态">
          <div class="switch-wrap">
            <n-switch v-model:value="form.activeStatus" />
            <span class="switch-label">{{ form.activeStatus ? '已启用' : '已停用' }}</span>
          </div>
        </n-form-item-gi>
        <!-- å¤‡æ³¨æè¿° -->
        <n-grid-item :span="2">
          <hr class="form-divider">
        </n-grid-item>
        <n-form-item-gi label="备注描述" :span="2">
          <n-input v-model:value="form.description" type="textarea" placeholder="可填写盒子规格说明、注意事项等…" />
        </n-form-item-gi>
      </n-grid>
    </n-form>
    <template #footer>
      <div class="modal-footer">
        <span class="modal-hint">💡 æŒ‰ Ctrl+Enter å¿«é€Ÿä¿å­˜</span>
        <div class="modal-footer-right">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="primary" @click="handleSubmit">保存</n-button>
        </div>
      </div>
    </template>
  </n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { weighingBoxApi } from '@/service/api/md/weighing-box';
import { useMessage } from 'naive-ui';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  operateType: {
    type: String,
    default: 'add'
  },
  rowData: {
    type: Object,
    default: () => ({})
  }
});
const emit = defineEmits(['update:visible', 'submitted']);
const message = useMessage();
const formRef = ref();
const form = reactive({
  id: undefined,
  name: '',
  code: '',
  weight: null,
  unit: 'g',
  location: '',
  calibCycleDays: 30,
  remindDays: 7,
  lastCalibDate: null,
  activeStatus: true,
  description: ''
});
const rules = {
  name: [{ required: true, message: '请输入盒子名称', trigger: 'blur' }],
  code: [{ required: true, message: '请输入盒子编号', trigger: 'blur' }],
  weight: [{ type: 'number', required: true, message: '请输入标准重量', trigger: 'blur' }],
  unit: [{ required: true, message: '请输入单位', trigger: 'blur' }],
  calibCycleDays: [{ type: 'number', required: true, message: '请输入校准周期', trigger: 'blur' }]
};
const bodyStyle = {
  width: '800px'
};
watch(() => props.visible, (newValue) => {
  if (newValue) {
    if (props.operateType === 'edit' && props.rowData) {
      console.log("编辑数据:", props.rowData);
      Object.assign(form, props.rowData);
      // å°†å¯ç”¨çŠ¶æ€è½¬æ¢ä¸ºæ•°å€¼
      form.activeStatus = form.activeStatus === 1;
      form.weight = Number(form.weight);
    } else {
      console.log("新增数据:", form);
      form.id = undefined;
      form.name = '';
      form.code = '';
      form.weight = null;
      form.unit = 'g';
      form.location = '';
      form.calibCycleDays = 30;
      form.remindDays = 7;
      form.activeStatus = true;
      form.description = '';
      form.lastCalibDate = null;
    }
  }
});
const setPresetCycle = (days) => {
  form.calibCycleDays = days;
};
const handleCancel = () => {
  emit('update:visible', false);
};
const handleSubmit = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    // å°†å¯ç”¨çŠ¶æ€è½¬æ¢ä¸ºæ•°å€¼
    form.activeStatus = form.activeStatus ? 1 : 0;
    let response;
    if (form.id) {
      response = await weighingBoxApi.edit(form);
    } else {
      response = await weighingBoxApi.add(form);
    }
    const res = response.response.data;
    if (res.code === 200) {
      message.success(form.id ? '修改成功' : '新增成功');
      emit('update:visible', false);
      emit('submitted');
    } else {
      message.error(res.msg || '操作失败');
    }
  } catch (error: any) {
    console.error('Form validation failed', error);
    if (error.message) {
      message.error(error.message);
    } else {
      message.error('操作失败');
    }
  }
};
</script>
<style scoped>
/* è¡¨å•样式 */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.form-group {
  display: flex;
  flex-direction: column;
}
.form-group.full {
  grid-column: 1 / -1;
}
.form-label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  margin-bottom: 8px;
}
.form-label .required {
  color: #ff4d4f;
  margin-left: 4px;
}
.form-row {
  display: flex;
  gap: 10px;
  align-items: center;
}
.form-hint {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
}
.form-divider {
  grid-column: 1 / -1;
  border: none;
  border-top: 1px solid #e5e7eb;
  margin: 12px 0;
}
.form-section-title {
  grid-column: 1 / -1;
  font-size: 14px;
  font-weight: 600;
  color: #1890ff;
  margin: 12px 0 8px;
}
/* é¢„设模板 */
.preset-bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.preset-chip {
  padding: 6px 16px;
  border-radius: 20px;
  border: 1px solid #e5e7eb;
  font-size: 14px;
  color: #666;
  cursor: pointer;
  transition: all 0.2s ease;
  background: #fff;
}
.preset-chip:hover {
  border-color: #1890ff;
  color: #1890ff;
}
.preset-chip.active {
  background: #1890ff;
  color: #fff;
  border-color: #1890ff;
}
/* æ¨¡æ€æ¡†åº•部 */
.modal-footer {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-hint {
  font-size: 12px;
  color: #999;
}
.modal-footer-right {
  display: flex;
  gap: 10px;
}
/* å¼€å…³ */
.switch-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
}
.switch-label {
  font-size: 14px;
  color: #666;
}
</style>