From d143af7023cfd4a0ced6f0ecf04ae3b3a06fd1dc Mon Sep 17 00:00:00 2001
From: baoshiwei <baoshiwei@shlanbao.cn>
Date: 星期三, 15 四月 2026 13:11:28 +0800
Subject: [PATCH] feat(md): 添加称重盒子维护功能
---
ruoyi-plus-soybean/src/router/elegant/routes.ts | 9
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-calibrate.vue | 191 ++
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-config.vue | 272 ++++
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchConfigBo.java | 20
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/WeighingBoxMapper.xml | 158 ++
ruoyi-plus-soybean/src/service/api/md/weighing-box.ts | 113 +
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchCalibrateBo.java | 29
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/WeighingBoxServiceImpl.java | 453 ++++++
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/IWeighingBoxService.java | 121 +
RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java | 17
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-copy.vue | 126 +
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/CalibrationRecord.java | 80 +
ruoyi-plus-soybean/src/views/md/weighing-box/index.vue | 1322 +++++++++++++++++++
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/CalibrationRecordMapper.java | 40
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/ICalibrationRecordService.java | 40
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/CalibrationRecordServiceImpl.java | 40
ruoyi-plus-soybean/src/locales/langs/zh-cn.ts | 7
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/CalibrationRecordMapper.xml | 64
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-calibrate.vue | 210 +++
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/WeighingBoxBo.java | 28
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/controller/WeighingBoxController.java | 177 ++
ruoyi-plus-soybean/src/router/elegant/imports.ts | 1
ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-operate.vue | 338 ++++
ruoyi-plus-soybean/src/typings/elegant-router.d.ts | 2
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/WeighingBoxMapper.java | 76 +
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/WeighingBox.java | 88 +
ruoyi-plus-soybean/src/router/elegant/transform.ts | 1
27 files changed, 4,020 insertions(+), 3 deletions(-)
diff --git a/RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java b/RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
index 6c45085..e93d796 100755
--- a/RuoYi-Vue-Plus/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
+++ b/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);
+ };
+ }
+
/**
* 璁$畻涓や釜鏃ユ湡涔嬮棿鐨勬椂闂村樊锛屽苟浠ュぉ銆佸皬鏃跺拰鍒嗛挓鐨勬牸寮忚繑鍥�
*
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/controller/WeighingBoxController.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/controller/WeighingBoxController.java
new file mode 100644
index 0000000..8e2aa78
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/CalibrationRecord.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/CalibrationRecord.java
new file mode 100644
index 0000000..1430083
--- /dev/null
+++ b/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;
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/WeighingBox.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/WeighingBox.java
new file mode 100644
index 0000000..94ba5e5
--- /dev/null
+++ b/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姝e父 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;
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchCalibrateBo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchCalibrateBo.java
new file mode 100644
index 0000000..a680437
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchConfigBo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/BatchConfigBo.java
new file mode 100644
index 0000000..115c9b5
--- /dev/null
+++ b/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;
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/WeighingBoxBo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/domain/bo/WeighingBoxBo.java
new file mode 100644
index 0000000..9c8eb64
--- /dev/null
+++ b/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;
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/CalibrationRecordMapper.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/CalibrationRecordMapper.java
new file mode 100644
index 0000000..ff781bc
--- /dev/null
+++ b/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);
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/WeighingBoxMapper.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/mapper/WeighingBoxMapper.java
new file mode 100644
index 0000000..6ca79e9
--- /dev/null
+++ b/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);
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/ICalibrationRecordService.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/ICalibrationRecordService.java
new file mode 100644
index 0000000..52d7057
--- /dev/null
+++ b/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);
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/IWeighingBoxService.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/IWeighingBoxService.java
new file mode 100644
index 0000000..c6ee8b4
--- /dev/null
+++ b/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 婧愮洅瀛怚D
+ * @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);
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/CalibrationRecordServiceImpl.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/CalibrationRecordServiceImpl.java
new file mode 100644
index 0000000..ffe8994
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/WeighingBoxServiceImpl.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/md/service/impl/WeighingBoxServiceImpl.java
new file mode 100644
index 0000000..b8b001c
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/CalibrationRecordMapper.xml b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/CalibrationRecordMapper.xml
new file mode 100644
index 0000000..6e79c09
--- /dev/null
+++ b/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>
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/WeighingBoxMapper.xml b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/WeighingBoxMapper.xml
new file mode 100644
index 0000000..9665421
--- /dev/null
+++ b/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>
diff --git a/ruoyi-plus-soybean/src/locales/langs/zh-cn.ts b/ruoyi-plus-soybean/src/locales/langs/zh-cn.ts
index e6fb730..d22b9ef 100755
--- a/ruoyi-plus-soybean/src/locales/langs/zh-cn.ts
+++ b/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': '',
diff --git a/ruoyi-plus-soybean/src/router/elegant/imports.ts b/ruoyi-plus-soybean/src/router/elegant/imports.ts
index 56a0110..4639592 100755
--- a/ruoyi-plus-soybean/src/router/elegant/imports.ts
+++ b/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"),
diff --git a/ruoyi-plus-soybean/src/router/elegant/routes.ts b/ruoyi-plus-soybean/src/router/elegant/routes.ts
index 1a339b1..2fadf2c 100755
--- a/ruoyi-plus-soybean/src/router/elegant/routes.ts
+++ b/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'
+ }
}
]
},
diff --git a/ruoyi-plus-soybean/src/router/elegant/transform.ts b/ruoyi-plus-soybean/src/router/elegant/transform.ts
index edd1a94..4d6dcc9 100755
--- a/ruoyi-plus-soybean/src/router/elegant/transform.ts
+++ b/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",
diff --git a/ruoyi-plus-soybean/src/service/api/md/weighing-box.ts b/ruoyi-plus-soybean/src/service/api/md/weighing-box.ts
new file mode 100644
index 0000000..ac2f797
--- /dev/null
+++ b/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'
+ })
+};
diff --git a/ruoyi-plus-soybean/src/typings/elegant-router.d.ts b/ruoyi-plus-soybean/src/typings/elegant-router.d.ts
index f9e689d..42fe324 100755
--- a/ruoyi-plus-soybean/src/typings/elegant-router.d.ts
+++ b/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"
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/index.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/index.vue
new file mode 100644
index 0000000..99f7fd2
--- /dev/null
+++ b/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">姝e父</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: '姝e父',
+ 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>
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-calibrate.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-calibrate.vue
new file mode 100644
index 0000000..3237299
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-config.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-batch-config.vue
new file mode 100644
index 0000000..4884781
--- /dev/null
+++ b/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">瀛e害</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>
\ No newline at end of file
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-calibrate.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-calibrate.vue
new file mode 100644
index 0000000..b147d39
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-copy.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-copy.vue
new file mode 100644
index 0000000..ae7adca
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-operate.vue b/ruoyi-plus-soybean/src/views/md/weighing-box/modules/weighing-box-operate.vue
new file mode 100644
index 0000000..df2a654
--- /dev/null
+++ b/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鍙蜂骇绾柯疯川妫�鍙癆" />
+ </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>
\ No newline at end of file
--
Gitblit v1.9.3