From 5740eb93928f3cb8e17c3fa5f082f42072ebbc31 Mon Sep 17 00:00:00 2001
From: zhuguifei <312353457@qq.com>
Date: 星期四, 12 三月 2026 13:03:17 +0800
Subject: [PATCH] feat: 新增基础数据维护-判定规程维护界面和后端接口

---
 ruoyi-plus-soybean/src/views/qm/std/index.vue                                                            |  229 ++++++++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/IQmStdService.java         |   68 ++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/impl/QmStdServiceImpl.java |  139 +++++
 ruoyi-plus-soybean/src/views/qm/std/modules/std-operate-drawer.vue                                       |  179 ++++++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/mapper/QmStdMapper.java            |   15 
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/bo/QmStdBo.java             |   97 +++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/qa/qm/QmStdMapper.xml                    |    6 
 ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue                              |  282 ++++++++++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/QmStd.java                  |   97 +++
 ruoyi-plus-soybean/src/service/api/qm/std.ts                                                             |   35 +
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/vo/QmStdVo.java             |  120 ++++
 ruoyi-plus-soybean/src/typings/api/qm.std.api.d.ts                                                       |   85 +++
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/controller/QmStdController.java    |  105 ++++
 ruoyi-plus-soybean/src/views/qm/std/modules/std-search.vue                                               |   96 +++
 14 files changed, 1,553 insertions(+), 0 deletions(-)

diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/controller/QmStdController.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/controller/QmStdController.java
new file mode 100644
index 0000000..1343e1c
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/controller/QmStdController.java
@@ -0,0 +1,105 @@
+package org.dromara.qa.qm.controller;
+
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.dromara.qa.qm.domain.bo.QmStdBo;
+import org.dromara.qa.qm.domain.vo.QmStdVo;
+import org.dromara.qa.qm.service.IQmStdService;
+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.excel.utils.ExcelUtil;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * 鍒ゅ畾瑙勭▼
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/qm/std")
+public class QmStdController extends BaseController {
+
+    private final IQmStdService qmStdService;
+
+    /**
+     * 鏌ヨ鍒ゅ畾瑙勭▼鍒楄〃
+     */
+    @SaCheckPermission("qm:std:list")
+    @GetMapping("/list")
+    public TableDataInfo<QmStdVo> list(QmStdBo bo, PageQuery pageQuery) {
+        return qmStdService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 瀵煎嚭鍒ゅ畾瑙勭▼鍒楄〃
+     */
+    @SaCheckPermission("qm:std:export")
+    @Log(title = "鍒ゅ畾瑙勭▼", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(QmStdBo bo, HttpServletResponse response) {
+        List<QmStdVo> list = qmStdService.queryList(bo);
+        ExcelUtil.exportExcel(list, "鍒ゅ畾瑙勭▼", QmStdVo.class, response);
+    }
+
+    /**
+     * 鑾峰彇鍒ゅ畾瑙勭▼璇︾粏淇℃伅
+     *
+     * @param id 涓婚敭
+     */
+    @SaCheckPermission("qm:std:query")
+    @GetMapping("/{id}")
+    public R<QmStdVo> getInfo(@NotNull(message = "涓婚敭涓嶈兘涓虹┖")
+                                     @PathVariable String id) {
+        return R.ok(qmStdService.queryById(id));
+    }
+
+    /**
+     * 鏂板鍒ゅ畾瑙勭▼
+     */
+    @SaCheckPermission("qm:std:add")
+    @Log(title = "鍒ゅ畾瑙勭▼", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody QmStdBo bo) {
+        return toAjax(qmStdService.insertByBo(bo));
+    }
+
+    /**
+     * 淇敼鍒ゅ畾瑙勭▼
+     */
+    @SaCheckPermission("qm:std:edit")
+    @Log(title = "鍒ゅ畾瑙勭▼", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody QmStdBo bo) {
+        return toAjax(qmStdService.updateByBo(bo));
+    }
+
+    /**
+     * 鍒犻櫎鍒ゅ畾瑙勭▼
+     *
+     * @param ids 涓婚敭涓�
+     */
+    @SaCheckPermission("qm:std:remove")
+    @Log(title = "鍒ゅ畾瑙勭▼", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "涓婚敭涓嶈兘涓虹┖")
+                          @PathVariable String[] ids) {
+        return toAjax(qmStdService.deleteWithValidByIds(List.of(ids), true));
+    }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/QmStd.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/QmStd.java
new file mode 100644
index 0000000..e542fd3
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/QmStd.java
@@ -0,0 +1,97 @@
+package org.dromara.qa.qm.domain;
+
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.io.Serial;
+
+/**
+ * 鍒ゅ畾瑙勭▼瀵硅薄 qm_std
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+@Data
+@TableName("qm_std")
+public class QmStd {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 缂栫爜
+     */
+    @TableId(value = "id")
+    private String id;
+
+    /**
+     * 瑙勭▼浠g爜
+     */
+    private String stdCode;
+
+    /**
+     * 瑙勭▼鍚嶇О
+     */
+    private String stdName;
+
+    /**
+     * 鍒涘缓鏃堕棿
+     */
+    private Date cdate;
+
+    /**
+     * 鐗堟湰鍙�
+     */
+    private Long ver;
+
+    /**
+     * 鍚敤鏍囪瘑
+     */
+    private Long enable;
+
+    /**
+     * 妫�楠岄」鐩甤ode
+     */
+    private String itemCod;
+
+    /**
+     * 妫�楠岄」鐩悕绉�
+     */
+    private String itemNam;
+
+    /**
+     * 鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�
+     */
+    private Long typ;
+
+    /**
+     * 澶囨敞
+     */
+    private String stdDes;
+
+    /**
+     * 妫�楠岀骇鍒�
+     */
+    private Long checkLevel;
+
+    /**
+     * 绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡
+     */
+    private Long category;
+
+    /**
+     * $column.columnComment
+     */
+    private String matfltype;
+
+    /**
+     * $column.columnComment
+     */
+    private String matflname;
+
+
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/bo/QmStdBo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/bo/QmStdBo.java
new file mode 100644
index 0000000..455f462
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/bo/QmStdBo.java
@@ -0,0 +1,97 @@
+package org.dromara.qa.qm.domain.bo;
+
+import org.dromara.qa.qm.domain.QmStd;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import jakarta.validation.constraints.*;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 鍒ゅ畾瑙勭▼涓氬姟瀵硅薄 qm_std
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = QmStd.class, reverseConvertGenerate = false)
+public class QmStdBo extends BaseEntity {
+
+    /**
+     * 缂栫爜
+     */
+    @NotBlank(message = "缂栫爜涓嶈兘涓虹┖", groups = { EditGroup.class })
+    private String id;
+
+    /**
+     * 瑙勭▼浠g爜
+     */
+    private String stdCode;
+
+    /**
+     * 瑙勭▼鍚嶇О
+     */
+    private String stdName;
+
+    /**
+     * 鍒涘缓鏃堕棿
+     */
+    private Date cdate;
+
+    /**
+     * 鐗堟湰鍙�
+     */
+    private Long ver;
+
+    /**
+     * 鍚敤鏍囪瘑
+     */
+    private Long enable;
+
+    /**
+     * 妫�楠岄」鐩甤ode
+     */
+    private String itemCod;
+
+    /**
+     * 妫�楠岄」鐩悕绉�
+     */
+    private String itemNam;
+
+    /**
+     * 鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�
+     */
+    private Long typ;
+
+    /**
+     * 澶囨敞
+     */
+    private String stdDes;
+
+    /**
+     * 妫�楠岀骇鍒�
+     */
+    private Long checkLevel;
+
+    /**
+     * 绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡
+     */
+    private Long category;
+
+    /**
+     * $column.columnComment
+     */
+    private String matfltype;
+
+    /**
+     * $column.columnComment
+     */
+    private String matflname;
+
+
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/vo/QmStdVo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/vo/QmStdVo.java
new file mode 100644
index 0000000..dc70c62
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/domain/vo/QmStdVo.java
@@ -0,0 +1,120 @@
+package org.dromara.qa.qm.domain.vo;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.dromara.qa.qm.domain.QmStd;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+
+/**
+ * 鍒ゅ畾瑙勭▼瑙嗗浘瀵硅薄 qm_std
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = QmStd.class)
+public class QmStdVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 缂栫爜
+     */
+    @ExcelProperty(value = "缂栫爜")
+    private String id;
+
+    /**
+     * 瑙勭▼浠g爜
+     */
+    @ExcelProperty(value = "瑙勭▼浠g爜")
+    private String stdCode;
+
+    /**
+     * 瑙勭▼鍚嶇О
+     */
+    @ExcelProperty(value = "瑙勭▼鍚嶇О")
+    private String stdName;
+
+    /**
+     * 鍒涘缓鏃堕棿
+     */
+    @ExcelProperty(value = "鍒涘缓鏃堕棿")
+    private Date cdate;
+
+    /**
+     * 鐗堟湰鍙�
+     */
+    @ExcelProperty(value = "鐗堟湰鍙�")
+    private Long ver;
+
+    /**
+     * 鍚敤鏍囪瘑
+     */
+    @ExcelProperty(value = "鍚敤鏍囪瘑")
+    private Long enable;
+
+    /**
+     * 妫�楠岄」鐩甤ode
+     */
+    @ExcelProperty(value = "妫�楠岄」鐩甤ode")
+    private String itemCod;
+
+    /**
+     * 妫�楠岄」鐩悕绉�
+     */
+    @ExcelProperty(value = "妫�楠岄」鐩悕绉�")
+    private String itemNam;
+
+    /**
+     * 鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�
+     */
+    @ExcelProperty(value = "鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�")
+    private Long typ;
+
+    /**
+     * 澶囨敞
+     */
+    @ExcelProperty(value = "澶囨敞")
+    private String stdDes;
+
+    /**
+     * 妫�楠岀骇鍒�
+     */
+    @ExcelProperty(value = "妫�楠岀骇鍒�")
+    private Long checkLevel;
+
+    /**
+     * 绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡
+     */
+    @ExcelProperty(value = "绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡")
+    private Long category;
+
+    /**
+     * $column.columnComment
+     */
+    @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
+    private String matfltype;
+
+    /**
+     * $column.columnComment
+     */
+    @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
+    private String matflname;
+
+
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/mapper/QmStdMapper.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/mapper/QmStdMapper.java
new file mode 100644
index 0000000..ba24c78
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/mapper/QmStdMapper.java
@@ -0,0 +1,15 @@
+package org.dromara.qa.qm.mapper;
+
+import org.dromara.qa.qm.domain.QmStd;
+import org.dromara.qa.qm.domain.vo.QmStdVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * 鍒ゅ畾瑙勭▼Mapper鎺ュ彛
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+public interface QmStdMapper extends BaseMapperPlus<QmStd, QmStdVo> {
+
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/IQmStdService.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/IQmStdService.java
new file mode 100644
index 0000000..18498cb
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/IQmStdService.java
@@ -0,0 +1,68 @@
+package org.dromara.qa.qm.service;
+
+import org.dromara.qa.qm.domain.vo.QmStdVo;
+import org.dromara.qa.qm.domain.bo.QmStdBo;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍒ゅ畾瑙勭▼Service鎺ュ彛
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+public interface IQmStdService {
+
+    /**
+     * 鏌ヨ鍒ゅ畾瑙勭▼
+     *
+     * @param id 涓婚敭
+     * @return 鍒ゅ畾瑙勭▼
+     */
+    QmStdVo queryById(String id);
+
+    /**
+     * 鍒嗛〉鏌ヨ鍒ゅ畾瑙勭▼鍒楄〃
+     *
+     * @param bo        鏌ヨ鏉′欢
+     * @param pageQuery 鍒嗛〉鍙傛暟
+     * @return 鍒ゅ畾瑙勭▼鍒嗛〉鍒楄〃
+     */
+    TableDataInfo<QmStdVo> queryPageList(QmStdBo bo, PageQuery pageQuery);
+
+    /**
+     * 鏌ヨ绗﹀悎鏉′欢鐨勫垽瀹氳绋嬪垪琛�
+     *
+     * @param bo 鏌ヨ鏉′欢
+     * @return 鍒ゅ畾瑙勭▼鍒楄〃
+     */
+    List<QmStdVo> queryList(QmStdBo bo);
+
+    /**
+     * 鏂板鍒ゅ畾瑙勭▼
+     *
+     * @param bo 鍒ゅ畾瑙勭▼
+     * @return 鏄惁鏂板鎴愬姛
+     */
+    Boolean insertByBo(QmStdBo bo);
+
+    /**
+     * 淇敼鍒ゅ畾瑙勭▼
+     *
+     * @param bo 鍒ゅ畾瑙勭▼
+     * @return 鏄惁淇敼鎴愬姛
+     */
+    Boolean updateByBo(QmStdBo bo);
+
+    /**
+     * 鏍¢獙骞舵壒閲忓垹闄ゅ垽瀹氳绋嬩俊鎭�
+     *
+     * @param ids     寰呭垹闄ょ殑涓婚敭闆嗗悎
+     * @param isValid 鏄惁杩涜鏈夋晥鎬ф牎楠�
+     * @return 鏄惁鍒犻櫎鎴愬姛
+     */
+    Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/impl/QmStdServiceImpl.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/impl/QmStdServiceImpl.java
new file mode 100644
index 0000000..db6d78f
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/qm/service/impl/QmStdServiceImpl.java
@@ -0,0 +1,139 @@
+package org.dromara.qa.qm.service.impl;
+
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.dromara.qa.qm.domain.bo.QmStdBo;
+import org.dromara.qa.qm.domain.vo.QmStdVo;
+import org.dromara.qa.qm.domain.QmStd;
+import org.dromara.qa.qm.mapper.QmStdMapper;
+import org.dromara.qa.qm.service.IQmStdService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Collection;
+
+/**
+ * 鍒ゅ畾瑙勭▼Service涓氬姟灞傚鐞�
+ *
+ * @author zhuguifei
+ * @date 2026-03-11
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class QmStdServiceImpl implements IQmStdService {
+
+    private final QmStdMapper baseMapper;
+
+    /**
+     * 鏌ヨ鍒ゅ畾瑙勭▼
+     *
+     * @param id 涓婚敭
+     * @return 鍒ゅ畾瑙勭▼
+     */
+    @Override
+    public QmStdVo queryById(String id){
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 鍒嗛〉鏌ヨ鍒ゅ畾瑙勭▼鍒楄〃
+     *
+     * @param bo        鏌ヨ鏉′欢
+     * @param pageQuery 鍒嗛〉鍙傛暟
+     * @return 鍒ゅ畾瑙勭▼鍒嗛〉鍒楄〃
+     */
+    @Override
+    public TableDataInfo<QmStdVo> queryPageList(QmStdBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<QmStd> lqw = buildQueryWrapper(bo);
+        Page<QmStdVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 鏌ヨ绗﹀悎鏉′欢鐨勫垽瀹氳绋嬪垪琛�
+     *
+     * @param bo 鏌ヨ鏉′欢
+     * @return 鍒ゅ畾瑙勭▼鍒楄〃
+     */
+    @Override
+    public List<QmStdVo> queryList(QmStdBo bo) {
+        LambdaQueryWrapper<QmStd> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<QmStd> buildQueryWrapper(QmStdBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<QmStd> lqw = Wrappers.lambdaQuery();
+        lqw.orderByAsc(QmStd::getId);
+        lqw.like(StringUtils.isNotBlank(bo.getStdCode()), QmStd::getStdCode, bo.getStdCode());
+        lqw.like(StringUtils.isNotBlank(bo.getStdName()), QmStd::getStdName, bo.getStdName());
+        lqw.eq(bo.getTyp() != null, QmStd::getTyp, bo.getTyp());
+        lqw.eq(StringUtils.isNotBlank(bo.getStdDes()), QmStd::getStdDes, bo.getStdDes());
+        lqw.eq(bo.getCheckLevel() != null, QmStd::getCheckLevel, bo.getCheckLevel());
+        lqw.eq(bo.getCategory() != null, QmStd::getCategory, bo.getCategory());
+        lqw.eq(StringUtils.isNotBlank(bo.getMatfltype()), QmStd::getMatfltype, bo.getMatfltype());
+        lqw.like(StringUtils.isNotBlank(bo.getMatflname()), QmStd::getMatflname, bo.getMatflname());
+        return lqw;
+    }
+
+    /**
+     * 鏂板鍒ゅ畾瑙勭▼
+     *
+     * @param bo 鍒ゅ畾瑙勭▼
+     * @return 鏄惁鏂板鎴愬姛
+     */
+    @Override
+    public Boolean insertByBo(QmStdBo bo) {
+        QmStd add = MapstructUtils.convert(bo, QmStd.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 淇敼鍒ゅ畾瑙勭▼
+     *
+     * @param bo 鍒ゅ畾瑙勭▼
+     * @return 鏄惁淇敼鎴愬姛
+     */
+    @Override
+    public Boolean updateByBo(QmStdBo bo) {
+        QmStd update = MapstructUtils.convert(bo, QmStd.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 淇濆瓨鍓嶇殑鏁版嵁鏍¢獙
+     */
+    private void validEntityBeforeSave(QmStd entity){
+        //TODO 鍋氫竴浜涙暟鎹牎楠�,濡傚敮涓�绾︽潫
+    }
+
+    /**
+     * 鏍¢獙骞舵壒閲忓垹闄ゅ垽瀹氳绋嬩俊鎭�
+     *
+     * @param ids     寰呭垹闄ょ殑涓婚敭闆嗗悎
+     * @param isValid 鏄惁杩涜鏈夋晥鎬ф牎楠�
+     * @return 鏄惁鍒犻櫎鎴愬姛
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
+        if(isValid){
+            //TODO 鍋氫竴浜涗笟鍔′笂鐨勬牎楠�,鍒ゆ柇鏄惁闇�瑕佹牎楠�
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+}
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/qa/qm/QmStdMapper.xml b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/qa/qm/QmStdMapper.xml
new file mode 100644
index 0000000..22bbec6
--- /dev/null
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/resources/mapper/qa/qm/QmStdMapper.xml
@@ -0,0 +1,6 @@
+<?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.qm.mapper.QmStdMapper">
+</mapper>
diff --git a/ruoyi-plus-soybean/src/service/api/qm/std.ts b/ruoyi-plus-soybean/src/service/api/qm/std.ts
new file mode 100644
index 0000000..e2de6de
--- /dev/null
+++ b/ruoyi-plus-soybean/src/service/api/qm/std.ts
@@ -0,0 +1,35 @@
+import { request } from '@/service/request';
+
+/** 鑾峰彇鍒ゅ畾瑙勭▼鍒楄〃 */
+export function fetchGetStdList (params?: Api.Qm.StdSearchParams) {
+    return request<Api.Qm.StdList>({
+        url: '/qm/std/list',
+        method: 'get',
+        params
+    });
+}
+/** 鏂板鍒ゅ畾瑙勭▼ */
+export function fetchCreateStd (data: Api.Qm.StdOperateParams) {
+    return request<boolean>({
+        url: '/qm/std',
+        method: 'post',
+        data
+    });
+}
+
+/** 淇敼鍒ゅ畾瑙勭▼ */
+export function fetchUpdateStd (data: Api.Qm.StdOperateParams) {
+    return request<boolean>({
+        url: '/qm/std',
+        method: 'put',
+        data
+    });
+}
+
+/** 鎵归噺鍒犻櫎鍒ゅ畾瑙勭▼ */
+export function fetchBatchDeleteStd (ids: CommonType.IdType[]) {
+    return request<boolean>({
+        url: `/qm/std/${ids.join(',')}`,
+        method: 'delete'
+    });
+}
diff --git a/ruoyi-plus-soybean/src/typings/api/qm.std.api.d.ts b/ruoyi-plus-soybean/src/typings/api/qm.std.api.d.ts
new file mode 100644
index 0000000..a043dfb
--- /dev/null
+++ b/ruoyi-plus-soybean/src/typings/api/qm.std.api.d.ts
@@ -0,0 +1,85 @@
+/**
+ * Namespace Api
+ *
+ * All backend api type
+ */
+declare namespace Api {
+    /**
+     * namespace Qm
+     *
+     * backend api module: "Qm"
+     */
+    namespace Qm {
+        /** std */
+        type Std = Common.CommonRecord<{
+            /** 缂栫爜 */
+                id: CommonType.IdType; 
+            /** 瑙勭▼浠g爜 */
+                stdCode: string; 
+            /** 瑙勭▼鍚嶇О */
+                stdName: string; 
+            /** 鍒涘缓鏃堕棿 */
+                cdate: string; 
+            /** 鐗堟湰鍙� */
+                ver: number; 
+            /** 鍚敤鏍囪瘑 */
+                enable: number; 
+            /** 妫�楠岄」鐩甤ode */
+                itemCod: string; 
+            /** 妫�楠岄」鐩悕绉� */
+                itemNam: string; 
+            /** 鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹� */
+                typ: number; 
+            /** 澶囨敞 */
+                stdDes: string; 
+            /** 妫�楠岀骇鍒� */
+                checkLevel: number; 
+            /** 绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡 */
+                category: number; 
+            /** $column.columnComment */
+                matfltype: string; 
+            /** $column.columnComment */
+                matflname: string; 
+        }>;
+
+        /** std search params */
+        type StdSearchParams = CommonType.RecordNullable<
+            Pick<
+                Api.Qm.Std,
+                        | 'stdCode'
+                        | 'stdName'
+                        | 'typ'
+                        | 'stdDes'
+                        | 'checkLevel'
+                        | 'category'
+                        | 'matfltype'
+                        | 'matflname'
+            > &
+            Api.Common.CommonSearchParams
+        >;
+
+        /** std operate params */
+        type StdOperateParams = CommonType.RecordNullable<
+            Pick<
+                Api.Qm.Std,
+                        | 'id'
+                        | 'stdCode'
+                        | 'stdName'
+                        | 'cdate'
+                        | 'ver'
+                        | 'enable'
+                        | 'itemCod'
+                        | 'itemNam'
+                        | 'typ'
+                        | 'stdDes'
+                        | 'checkLevel'
+                        | 'category'
+                        | 'matfltype'
+                        | 'matflname'
+            >
+        >;
+
+        /** std list */
+        type StdList = Api.Common.PaginatingQueryRecord<Std>;
+    }
+}
diff --git a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue
new file mode 100644
index 0000000..4021450
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue
@@ -0,0 +1,282 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import { useEcharts } from '@/hooks/common/echarts';
+
+defineOptions({
+  name: 'StoreSilkSankey'
+});
+
+interface StoreSilkDetailVo {
+  fsNum?: string;
+  siloNum?: string;
+  equNo?: string;
+  output?: number | null;
+}
+
+interface Props {
+  rollerRecordList?: StoreSilkDetailVo[];
+  packerRecordList?: StoreSilkDetailVo[];
+  rows?: any[];
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  rollerRecordList: () => [],
+  packerRecordList: () => [],
+  rows: () => []
+});
+
+type SankeyType = 'roller' | 'packer';
+
+const sankeyType = ref<SankeyType>('roller');
+
+function normalizeNumber(val: unknown) {
+  const v = Number(val);
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function calcRollerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 50;
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function calcPackerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 10 / 250;
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function formatTime(val: unknown) {
+  if (!val) return '';
+  const s = String(val);
+  return s.length > 19 ? s.slice(0, 19) : s;
+}
+
+function buildChartData(list: StoreSilkDetailVo[], type: SankeyType) {
+  const nodeSet = new Set<string>();
+  const linkMap = new Map<string, { source: string; target: string; value: number }>();
+  const nodeInValue = new Map<string, number>();
+  const nodeOutValue = new Map<string, number>();
+  const nodeExtra = new Map<string, any>();
+  const rows = Array.isArray(props.rows) ? props.rows : [];
+
+  function addLink(source: string, target: string, value: number) {
+    if (value <= 0) return;
+    nodeSet.add(source);
+    nodeSet.add(target);
+    nodeInValue.set(target, (nodeInValue.get(target) ?? 0) + value);
+    nodeOutValue.set(source, (nodeOutValue.get(source) ?? 0) + value);
+    const key = `${source}__->__${target}`;
+    const existed = linkMap.get(key);
+    if (existed) {
+      existed.value += value;
+      return;
+    }
+    linkMap.set(key, { source, target, value });
+  }
+
+  for (const item of list) {
+    const val = normalizeNumber(item.output);
+    if (val === null) continue;
+    if (val <= 0) continue;
+
+    const fsBase = `鍠備笣鏈� ${item.fsNum || '-'}`;
+    const siloBase = `鍌ㄤ笣鏌� ${item.siloNum || '-'}`;
+    const equ = item.equNo ? `${item.equNo}#${type === 'roller' ? '鍗锋帴鏈�' : '鍖呰鏈�'}` : type === 'roller' ? '鍗锋帴鏈�' : '鍖呰鏈�';
+    const target = equ;
+    const fs = fsBase;
+    const silo = siloBase;
+
+    addLink(fs, silo, val);
+    addLink(silo, target, val);
+
+    const parentRow = rows.find((r: any) => {
+      const dList = type === 'roller' ? r?.rollerDetailList : r?.packerDetailList;
+      if (!Array.isArray(dList)) return false;
+      return dList.some((d: any) => d?.fsNum === item.fsNum && d?.siloNum === item.siloNum && d?.equNo === item.equNo);
+    });
+
+    if (parentRow) {
+      const materialname = String(parentRow.materialname ?? '');
+      const batchcode = String(parentRow.batchcode ?? parentRow.batchCode ?? '');
+      const jobinput = Number(parentRow.jobinput);
+      const distimebegin = formatTime(parentRow.distimebegin);
+      const distimeend = formatTime(parentRow.distimeend);
+
+      nodeExtra.set(fs, {
+        kind: 'fs',
+        displayName: fsBase,
+        meta: { materialname, batchcode, jobinput }
+      });
+      nodeExtra.set(silo, {
+        kind: 'silo',
+        displayName: siloBase,
+        meta: { distimebegin, distimeend }
+      });
+    }
+  }
+
+  const nodes = Array.from(nodeSet).map(name => {
+    const inV = nodeInValue.get(name) ?? 0;
+    const outV = nodeOutValue.get(name) ?? 0;
+    const value = inV > 0 ? inV : outV;
+    return { name, value, ...(nodeExtra.get(name) ?? {}) };
+  });
+  const links = Array.from(linkMap.values());
+
+  return { nodes, links };
+}
+
+const activeList = computed(() => {
+  return sankeyType.value === 'roller' ? props.rollerRecordList : props.packerRecordList;
+});
+
+watch(
+  () => [props.rollerRecordList, props.packerRecordList],
+  () => {
+    const rollerLen = Array.isArray(props.rollerRecordList) ? props.rollerRecordList.length : 0;
+    const packerLen = Array.isArray(props.packerRecordList) ? props.packerRecordList.length : 0;
+
+    if (sankeyType.value === 'roller' && rollerLen === 0 && packerLen > 0) {
+      sankeyType.value = 'packer';
+      return;
+    }
+
+    if (sankeyType.value === 'packer' && packerLen === 0 && rollerLen > 0) {
+      sankeyType.value = 'roller';
+    }
+  },
+  { deep: true, immediate: true }
+);
+
+const hasData = computed(() => {
+  return Boolean(activeList.value && activeList.value.length > 0);
+});
+
+const { domRef, updateOptions } = useEcharts(() => {
+  const { nodes, links } = buildChartData(activeList.value, sankeyType.value);
+
+  return {
+    tooltip: {
+      trigger: 'item'
+    },
+    series: [
+      {
+        type: 'sankey',
+        data: nodes,
+        links,
+        nodeAlign: 'justify',
+        layoutIterations: 32,
+        emphasis: {
+          focus: 'adjacency'
+        },
+        lineStyle: {
+          color: 'source',
+          curveness: 0.5,
+          opacity: 0.35
+        },
+        label: {
+          position: 'right',
+          formatter: (params: any) => {
+            const name = String(params?.data?.name ?? '');
+            const value = params?.data?.value;
+            const kind = params?.data?.kind;
+            const displayName = String(params?.data?.displayName ?? name);
+            const meta = params?.data?.meta;
+
+            if (kind === 'fs') {
+              const materialLine = meta?.materialname ? `鐗屽彿锛�${meta.materialname}` : '';
+              const batchLine = meta?.batchcode ? `鎵规锛�${meta.batchcode}` : '';
+              const jobLine =
+                Number.isFinite(Number(meta?.jobinput)) ? `鎶曟枡閲嶉噺锛�${Number(meta.jobinput).toFixed(2)}kg` : '';
+              const lines: string[] = [displayName];
+              lines.push('');
+              if (materialLine) lines.push(materialLine);
+              if (batchLine) lines.push(batchLine);
+              if (jobLine) {
+                lines.push('');
+                lines.push(jobLine);
+              }
+              return lines.join('\n');
+            }
+
+            if (kind === 'silo') {
+              const beginLine = meta?.distimebegin ? `寮�濮嬶細${meta.distimebegin}` : '';
+              const endLine = meta?.distimeend ? `缁撴潫锛�${meta.distimeend}` : '';
+              const valLine =
+                typeof value === 'number' && Number.isFinite(value) ? `浜ч噺锛�${value.toFixed(2)}绠盽 : '';
+              const lines: string[] = [displayName];
+              lines.push('');
+              if (beginLine) lines.push(beginLine);
+              if (endLine) lines.push(endLine);
+              if (valLine) {
+                lines.push('');
+                lines.push(valLine);
+              }
+              return lines.join('\n');
+            }
+            const showValue =
+              name.startsWith('鍌ㄤ笣鏌� ') ||
+              name.endsWith('#鍗锋帴鏈�') ||
+              name.endsWith('#鍖呰鏈�') ||
+              name === '鍗锋帴鏈�' ||
+              name === '鍖呰鏈�';
+
+            if (showValue && typeof value === 'number' && Number.isFinite(value)) {
+              return `${displayName}\n${value.toFixed(2)}绠盽;
+            }
+            return displayName;
+        }
+      }
+      },
+    ]
+  };
+});
+
+watch(
+  () => [props.rollerRecordList, props.packerRecordList, sankeyType.value],
+  () => {
+    updateOptions(opts => {
+      const { nodes, links } = buildChartData(activeList.value, sankeyType.value);
+      opts.series[0].data = nodes as any;
+      (opts.series[0] as any).links = links;
+      return opts;
+    });
+  },
+  { deep: true, immediate: true }
+);
+</script>
+
+<template>
+  <div class="h-full flex-col-stretch">
+    <div class="pb-8px">
+      <NSpace :size="8">
+        <NButton
+          size="small"
+          ghost
+          :type="sankeyType === 'roller' ? 'primary' : 'default'"
+          @click="sankeyType = 'roller'"
+        >
+          鍗锋帴
+        </NButton>
+        <NButton
+          size="small"
+          ghost
+          :type="sankeyType === 'packer' ? 'primary' : 'default'"
+          @click="sankeyType = 'packer'"
+        >
+          鍖呰
+        </NButton>
+      </NSpace>
+    </div>
+    <div v-if="!hasData" class="flex-center flex-1 text-gray-400">
+      鏆傛棤{{ sankeyType === 'roller' ? '鍗锋帴' : '鍖呰' }}妗戝熀鍥炬暟鎹�
+    </div>
+    <div v-else ref="domRef" class="flex-1 min-h-360px overflow-hidden"></div>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/ruoyi-plus-soybean/src/views/qm/std/index.vue b/ruoyi-plus-soybean/src/views/qm/std/index.vue
new file mode 100644
index 0000000..f4f6b03
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/qm/std/index.vue
@@ -0,0 +1,229 @@
+<script setup lang="tsx">
+import { ref } from 'vue';
+import { NDivider } from 'naive-ui';
+import dayjs from 'dayjs';
+import { fetchBatchDeleteStd, fetchGetStdList } from '@/service/api/qm/std';
+import { useAppStore } from '@/store/modules/app';
+import { useAuth } from '@/hooks/business/auth';
+import { useDownload } from '@/hooks/business/download';
+import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
+import { $t } from '@/locales';
+import ButtonIcon from '@/components/custom/button-icon.vue';
+import StdOperateDrawer from './modules/std-operate-drawer.vue';
+import StdSearch from './modules/std-search.vue';
+
+defineOptions({
+  name: 'StdList'
+});
+
+
+const appStore = useAppStore();
+const { download } = useDownload();
+const { hasAuth } = useAuth();
+
+const searchParams = ref<Api.Qm.StdSearchParams>({
+  pageNum: 1,
+  pageSize: 10,
+  stdCode: null,
+  stdName: null,
+  typ: null,
+  stdDes: null,
+  checkLevel: null,
+  category: 0,
+  matfltype: null,
+  matflname: null,
+  params: {}
+});
+
+const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
+  useNaivePaginatedTable({
+  api: () => fetchGetStdList(searchParams.value),
+  transform: response => defaultTransform(response),
+  onPaginationParamsChange: params => {
+    searchParams.value.pageNum = params.page;
+    searchParams.value.pageSize = params.pageSize;
+  },
+  columns: () => [
+    {
+      type: 'selection',
+      align: 'center',
+      width: 48
+    },
+    {
+      key: 'index',
+      title: $t('common.index'),
+      align: 'center',
+      width: 64,
+      render: (_, index) => index + 1
+    },
+
+    {
+      key: 'stdCode',
+      title: '瑙勭▼浠g爜',
+      align: 'center',
+      width: 200
+    },
+    {
+      key: 'stdName',
+      title: '瑙勭▼鍚嶇О',
+      align: 'center',
+      width: 300
+    },
+    {
+      key: 'category',
+      title: '鐗╂枡绫诲瀷',
+      align: 'center',
+      minWidth: 120,
+      render: row => {
+        const v = String(row?.category ?? '');
+        return v === '0' ? '鎴愬搧' : v === '1' ? '杈呮枡' : v;
+      }
+    },
+    {
+      key: 'cdate',
+      title: '鍒涘缓鏃堕棿',
+      align: 'center',
+      minWidth: 120,
+      render: row => (row.cdate ? dayjs(row.cdate).format('YYYY-MM-DD') : '')
+    },
+    {
+      key: 'ver',
+      title: '鐗堟湰鍙�',
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'enable',
+      title: '鍚敤鏍囪瘑',
+      align: 'center',
+      minWidth: 120,
+      render: row => {
+        const v = String(row?.enable ?? '');
+        return v === '0' ? '鍋滅敤' : v === '1' ? '鍚敤' : v;
+      }
+    },
+    {
+      key: 'operate',
+      title: $t('common.operate'),
+      align: 'center',
+      fixed: 'right',
+      width: 130,
+      render: row => {
+        const divider = () => {
+          if (!hasAuth('qm:std:edit') || !hasAuth('qm:std:remove')) {
+            return null;
+          }
+          return <NDivider vertical />;
+        };
+
+        const editBtn = () => {
+          if (!hasAuth('qm:std:edit')) {
+            return null;
+          }
+          return (
+            <ButtonIcon
+              text
+              type="primary"
+              icon="material-symbols:drive-file-rename-outline-outline"
+              tooltipContent={$t('common.edit')}
+              onClick={() => edit(row.id)}
+            />
+          );
+        };
+
+        const deleteBtn = () => {
+          if (!hasAuth('qm:std:remove')) {
+            return null;
+          }
+          return (
+            <ButtonIcon
+              text
+              type="error"
+              icon="material-symbols:delete-outline"
+              tooltipContent={$t('common.delete')}
+              popconfirmContent={$t('common.confirmDelete')}
+              onPositiveClick={() => handleDelete(row.id)}
+            />
+          );
+        };
+
+        return (
+          <div class="flex-center gap-8px">
+            {editBtn()}
+            {divider()}
+            {deleteBtn()}
+          </div>
+        );
+      }
+    }
+  ]
+});
+
+const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
+  useTableOperate(data, 'id', getData);
+
+async function handleBatchDelete() {
+  // request
+  const { error } = await fetchBatchDeleteStd(checkedRowKeys.value);
+  if (error) return;
+  onBatchDeleted();
+}
+
+async function handleDelete(id: CommonType.IdType) {
+  // request
+  const { error } = await fetchBatchDeleteStd([id]);
+  if (error) return;
+  onDeleted();
+}
+
+function edit(id: CommonType.IdType) {
+  handleEdit(id);
+}
+
+function handleExport() {
+  download('/qm/std/export', searchParams.value, `鍒ゅ畾瑙勭▼_${new Date().getTime()}.xlsx`);
+}
+</script>
+
+<template>
+  <div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
+    <StdSearch v-model:model="searchParams" @search="getDataByPage" />
+    <NCard title="鍒ゅ畾瑙勭▼鍒楄〃" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
+      <template #header-extra>
+        <TableHeaderOperation
+          v-model:columns="columnChecks"
+          :disabled-delete="checkedRowKeys.length === 0"
+          :loading="loading"
+          :show-add="hasAuth('qm:std:add')"
+          :show-delete="hasAuth('qm:std:remove')"
+          :show-export="hasAuth('qm:std:export')"
+          @add="handleAdd"
+          @delete="handleBatchDelete"
+          @export="handleExport"
+          @refresh="getData"
+        />
+      </template>
+      <NDataTable
+        v-model:checked-row-keys="checkedRowKeys"
+        :columns="columns"
+        :data="data"
+        size="small"
+        :flex-height="!appStore.isMobile"
+        :scroll-x="scrollX"
+        :loading="loading"
+        remote
+        :row-key="row => row.id"
+        :pagination="mobilePagination"
+        class="sm:h-full"
+      />
+      <StdOperateDrawer
+        v-model:visible="drawerVisible"
+        :operate-type="operateType"
+        :row-data="editingData"
+        @submitted="getDataByPage"
+      />
+    </NCard>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/ruoyi-plus-soybean/src/views/qm/std/modules/std-operate-drawer.vue b/ruoyi-plus-soybean/src/views/qm/std/modules/std-operate-drawer.vue
new file mode 100644
index 0000000..6316192
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/qm/std/modules/std-operate-drawer.vue
@@ -0,0 +1,179 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import { jsonClone } from '@sa/utils';
+import { fetchCreateStd, fetchUpdateStd } from '@/service/api/qm/std';
+import { useFormRules, useNaiveForm } from '@/hooks/common/form';
+import { $t } from '@/locales';
+
+defineOptions({
+  name: 'StdOperateDrawer'
+});
+
+interface Props {
+  /** the type of operation */
+  operateType: NaiveUI.TableOperateType;
+  /** the edit row data */
+  rowData?: Api.Qm.Std | null;
+}
+
+const props = defineProps<Props>();
+
+interface Emits {
+  (e: 'submitted'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const visible = defineModel<boolean>('visible', {
+  default: false
+});
+
+const { formRef, validate, restoreValidation } = useNaiveForm();
+const { createRequiredRule } = useFormRules();
+
+const title = computed(() => {
+  const titles: Record<NaiveUI.TableOperateType, string> = {
+    add: '鏂板鍒ゅ畾瑙勭▼',
+    edit: '缂栬緫鍒ゅ畾瑙勭▼'
+  };
+  return titles[props.operateType];
+});
+
+type Model = Api.Qm.StdOperateParams;
+
+const model = ref<Model>(createDefaultModel());
+
+function createDefaultModel(): Model {
+  return {
+      id: '',
+      stdCode: '',
+      stdName: '',
+      cdate: null,
+      ver: null,
+      enable: null,
+      itemCod: '',
+      itemNam: '',
+      typ: null,
+      stdDes: '',
+      checkLevel: null,
+      category: null,
+      matfltype: '',
+      matflname: ''
+  };
+}
+
+type RuleKey = Extract<
+  keyof Model,
+  | 'id'
+>;
+
+const rules: Record<RuleKey, App.Global.FormRule> = {
+  id: createRequiredRule('缂栫爜涓嶈兘涓虹┖'),
+};
+
+function handleUpdateModelWhenEdit() {
+  model.value = createDefaultModel();
+
+  if (props.operateType === 'edit' && props.rowData) {
+    Object.assign(model.value, jsonClone(props.rowData));
+  }
+}
+
+function closeDrawer() {
+  visible.value = false;
+}
+
+async function handleSubmit() {
+  await validate();
+
+  const { id, stdCode, stdName, cdate, ver, enable, itemCod, itemNam, typ, stdDes, checkLevel, category, matfltype, matflname } = model.value;
+
+  // request
+  if (props.operateType === 'add') {
+    const { error } = await fetchCreateStd({ stdCode, stdName, cdate, ver, enable, itemCod, itemNam, typ, stdDes, checkLevel, category, matfltype, matflname });
+    if (error) return;
+  }
+
+  if (props.operateType === 'edit') {
+    const { error } = await fetchUpdateStd({ id, stdCode, stdName, cdate, ver, enable, itemCod, itemNam, typ, stdDes, checkLevel, category, matfltype, matflname });
+    if (error) return;
+  }
+
+  window.$message?.success($t('common.updateSuccess'));
+  closeDrawer();
+  emit('submitted');
+}
+
+watch(visible, () => {
+  if (visible.value) {
+    handleUpdateModelWhenEdit();
+    restoreValidation();
+  }
+});
+</script>
+
+<template>
+  <NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
+    <NDrawerContent :title="title" :native-scrollbar="false" closable>
+      <NForm ref="formRef" :model="model" :rules="rules">
+        <NFormItem label="瑙勭▼浠g爜" path="stdCode">
+          <NInput v-model:value="model.stdCode" placeholder="璇疯緭鍏ヨ绋嬩唬鐮�" />
+        </NFormItem>
+        <NFormItem label="瑙勭▼鍚嶇О" path="stdName">
+          <NInput v-model:value="model.stdName" placeholder="璇疯緭鍏ヨ绋嬪悕绉�" />
+        </NFormItem>
+        <NFormItem label="鍒涘缓鏃堕棿" path="cdate">
+          <NDatePicker
+            v-model:formatted-value="model.cdate"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            clearable
+          />
+        </NFormItem>
+        <NFormItem label="鐗堟湰鍙�" path="ver">
+          <NInput v-model:value="model.ver" placeholder="璇疯緭鍏ョ増鏈彿" />
+        </NFormItem>
+        <NFormItem label="鍚敤鏍囪瘑" path="enable">
+          <NInput v-model:value="model.enable" placeholder="璇疯緭鍏ュ惎鐢ㄦ爣璇�" />
+        </NFormItem>
+        <NFormItem label="妫�楠岄」鐩甤ode" path="itemCod">
+          <NInput v-model:value="model.itemCod" placeholder="璇疯緭鍏ユ楠岄」鐩甤ode" />
+        </NFormItem>
+        <NFormItem label="妫�楠岄」鐩悕绉�" path="itemNam">
+          <NInput v-model:value="model.itemNam" placeholder="璇疯緭鍏ユ楠岄」鐩悕绉�" />
+        </NFormItem>
+        <NFormItem label="鍒ゅ畾绫诲瀷  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�" path="typ">
+          <NInput v-model:value="model.typ" placeholder="璇疯緭鍏ュ垽瀹氱被鍨�  0-鎵嬪姩鍒ゅ畾,1-涓婁笅闄愬垽瀹�,2-骞冲潎鍊煎垽瀹�,3-SD鍊煎垽瀹�,4-CV鍊煎垽瀹�,5-瓒呮爣鏁板垽瀹�" />
+        </NFormItem>
+        <NFormItem label="澶囨敞" path="stdDes">
+          <NInput v-model:value="model.stdDes" placeholder="璇疯緭鍏ュ娉�" />
+        </NFormItem>
+        <NFormItem label="妫�楠岀骇鍒�" path="checkLevel">
+          <NInput v-model:value="model.checkLevel" placeholder="璇疯緭鍏ユ楠岀骇鍒�" />
+        </NFormItem>
+        <NFormItem label="绫诲瀷  锛� 0-鎴愬搧  1-杈呮枡" path="category">
+          <NInput v-model:value="model.category" placeholder="璇疯緭鍏ョ被鍨�  锛� 0-鎴愬搧  1-杈呮枡" />
+        </NFormItem>
+        <NFormItem label="$column.columnComment" path="matfltype">
+          <NSelect
+            v-model:value="model.matfltype"
+            placeholder="璇烽�夋嫨$column.columnComment"
+            :options="[{ value: '0', label: '璇烽�夋嫨瀛楀吀鐢熸垚' }]"
+            clearable
+          />
+        </NFormItem>
+        <NFormItem label="$column.columnComment" path="matflname">
+          <NInput v-model:value="model.matflname" placeholder="璇疯緭鍏�$column.columnComment" />
+        </NFormItem>
+      </NForm>
+      <template #footer>
+        <NSpace :size="16">
+          <NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
+          <NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
+        </NSpace>
+      </template>
+    </NDrawerContent>
+  </NDrawer>
+</template>
+
+<style scoped></style>
diff --git a/ruoyi-plus-soybean/src/views/qm/std/modules/std-search.vue b/ruoyi-plus-soybean/src/views/qm/std/modules/std-search.vue
new file mode 100644
index 0000000..cdd3bac
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/qm/std/modules/std-search.vue
@@ -0,0 +1,96 @@
+<script setup lang="ts">
+import { toRaw } from 'vue';
+import { jsonClone } from '@sa/utils';
+import { useNaiveForm } from '@/hooks/common/form';
+import { $t } from '@/locales';
+
+defineOptions({
+  name: 'StdSearch'
+});
+
+interface Emits {
+  (e: 'search'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const { formRef, validate, restoreValidation } = useNaiveForm();
+
+const model = defineModel<Api.Qm.StdSearchParams>('model', { required: true });
+
+const defaultModel = jsonClone(toRaw(model.value));
+
+// 鍒ゅ畾绫诲瀷閫夐」锛�0-鎵嬪姩鍒ゅ畾 1-涓婁笅闄愬垽瀹� 2-骞冲潎鍊煎垽瀹� 3-SD鍊煎垽瀹� 4-CV鍊煎垽瀹� 5-瓒呮爣鏁板垽瀹�
+const typOptions = [
+  { label: '鎵嬪姩鍒ゅ畾', value: 0 },
+  { label: '涓婁笅闄愬垽瀹�', value: 1 },
+  { label: '骞冲潎鍊煎垽瀹�', value: 2 },
+  { label: 'SD鍊煎垽瀹�', value: 3 },
+  { label: 'CV鍊煎垽瀹�', value: 4 },
+  { label: '瓒呮爣鏁板垽瀹�', value: 5 }
+];
+
+// 鐗╂枡绫诲瀷閫夐」锛�0-鎴愬搧 1-杈呮枡
+const categoryOptions = [
+  { label: '鎴愬搧', value: 0 },
+  { label: '杈呮枡', value: 1 }
+];
+
+function resetModel() {
+  Object.assign(model.value, defaultModel);
+}
+
+async function reset() {
+  await restoreValidation();
+  resetModel();
+  emit('search');
+}
+
+async function search() {
+  await validate();
+  emit('search');
+}
+</script>
+
+<template>
+  <NCard :bordered="false" size="small" class="card-wrapper">
+    <NCollapse>
+      <NCollapseItem :title="$t('common.search')" name="qm-std-search">
+        <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
+          <NGrid responsive="screen" item-responsive>
+            <NFormItemGi span="24 s:12 m:6" label="瑙勭▼浠g爜" label-width="auto" path="stdCode" class="pr-24px">
+              <NInput v-model:value="model.stdCode" placeholder="璇疯緭鍏ヨ绋嬩唬鐮�" />
+            </NFormItemGi>
+            <NFormItemGi span="24 s:12 m:6" label="瑙勭▼鍚嶇О" label-width="auto" path="stdName" class="pr-24px">
+              <NInput v-model:value="model.stdName" placeholder="璇疯緭鍏ヨ绋嬪悕绉�" />
+            </NFormItemGi>
+            <NFormItemGi span="24 s:12 m:6" label="鍒ゅ畾绫诲瀷" label-width="auto" path="typ" class="pr-24px">
+              <NSelect v-model:value="model.typ" :options="typOptions" placeholder="璇烽�夋嫨鍒ゅ畾绫诲瀷" clearable />
+            </NFormItemGi>
+            <NFormItemGi span="24 s:12 m:6" label="鐗╂枡绫诲瀷" label-width="auto" path="category" class="pr-24px">
+              <NSelect v-model:value="model.category" :options="categoryOptions" placeholder="璇烽�夋嫨鐗╂枡绫诲瀷" clearable />
+            </NFormItemGi>
+            <NFormItemGi :show-feedback="false" span="24" class="pr-24px">
+              <NSpace class="w-full" justify="end">
+                <NButton @click="reset">
+                  <template #icon>
+                    <icon-ic-round-refresh class="text-icon" />
+                  </template>
+                  {{ $t('common.reset') }}
+                </NButton>
+                <NButton type="primary" ghost @click="search">
+                  <template #icon>
+                    <icon-ic-round-search class="text-icon" />
+                  </template>
+                  {{ $t('common.search') }}
+                </NButton>
+              </NSpace>
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+      </NCollapseItem>
+    </NCollapse>
+  </NCard>
+</template>
+
+<style scoped></style>

--
Gitblit v1.9.3