瀏覽代碼

Update 完善实验模块

Yue 1 月之前
父節點
當前提交
28a5e7076e
共有 23 個文件被更改,包括 931 次插入34 次删除
  1. 2 1
      SERVER/ChickenFarmV3/.script/sql/new/init_y.sql
  2. 5 5
      SERVER/ChickenFarmV3/vb-admin/pom.xml
  3. 5 0
      SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/service/SampleService.java
  4. 1 0
      SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/java/cn/vber/common/translation/constant/TransConstant.java
  5. 28 0
      SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/java/cn/vber/common/translation/core/impl/SampleTranslationImpl.java
  6. 1 0
      SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  7. 1 1
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/ExperimentController.java
  8. 5 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Experiment.java
  9. 1 1
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentAuditBo.java
  10. 4 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentBo.java
  11. 9 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentSampleVo.java
  12. 6 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentVo.java
  13. 22 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentMapper.java
  14. 22 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleMapper.java
  15. 5 5
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/ExperimentServiceImpl.java
  16. 8 2
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/SampleServiceImpl.java
  17. 1 1
      UI/VB.VUE/src/components/modal-select/ModalSelect.vue
  18. 9 2
      UI/VB.VUE/src/components/modal-select/SampleSelect.vue
  19. 7 0
      UI/VB.VUE/src/stores/_notice.ts
  20. 141 0
      UI/VB.VUE/src/views/experiment/experiment/_detail.vue
  21. 465 0
      UI/VB.VUE/src/views/experiment/experiment/audit.vue
  22. 170 15
      UI/VB.VUE/src/views/experiment/experiment/index.vue
  23. 13 1
      UI/VB.VUE/src/views/experiment/sample/_sample.vue

+ 2 - 1
SERVER/ChickenFarmV3/.script/sql/new/init_y.sql

@@ -439,7 +439,8 @@ CREATE TABLE `e_experiment` (
     `review_manager` bigint DEFAULT NULL COMMENT '审核负责人',
     `raw_data_url` varchar(500) DEFAULT NULL COMMENT '实验原始数据URL(文件格式)',
     `report_url` varchar(500) DEFAULT NULL COMMENT '实验报告URL(文件格式)',
-    `experiment_status` tinyint DEFAULT NULL DEFAULT '1' COMMENT '实验状态(1:未开始 2:进行中 3:审核中 4:发回报告 5:已结束)',
+    `experiment_status` tinyint DEFAULT '1' COMMENT '实验状态(1:未开始 2:进行中 3:审核中 4:发回报告 5:已结束)',
+    `description` varchar(500) DEFAULT NULL COMMENT '实验状态(1:未开始 2:进行中 3:审核中 4:发回报告 5:已结束)',
     `create_org` bigint NOT NULL COMMENT '创建组织',
     `create_by` bigint NOT NULL COMMENT '创建人',
     `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

+ 5 - 5
SERVER/ChickenFarmV3/vb-admin/pom.xml

@@ -90,15 +90,15 @@
             <groupId>com.vap</groupId>
             <artifactId>vb-device</artifactId>
         </dependency>
-        <dependency>
-            <groupId>com.vap</groupId>
-            <artifactId>vb-experiment</artifactId>
-        </dependency>
+
         <dependency>
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-experiment</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 5 - 0
SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/service/SampleService.java

@@ -0,0 +1,5 @@
+package cn.vber.common.core.service;
+
+public interface SampleService {
+    String selectNameById(long id);
+}

+ 1 - 0
SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/java/cn/vber/common/translation/constant/TransConstant.java

@@ -37,6 +37,7 @@ public interface TransConstant {
     /**
      * 设备id转名称
      */
+    String SAMPLE_ID_TO_NAME = "sample_id_to_name";
     String DEVICE_ID_TO_NAME = "device_id_to_name";
 
     String FEED_ID_TO_NAME = "feed_id_to_name";

+ 28 - 0
SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/java/cn/vber/common/translation/core/impl/SampleTranslationImpl.java

@@ -0,0 +1,28 @@
+package cn.vber.common.translation.core.impl;
+
+import cn.vber.common.core.service.DeviceService;
+import cn.vber.common.core.service.SampleService;
+import cn.vber.common.translation.annotation.TranslationType;
+import cn.vber.common.translation.constant.TransConstant;
+import cn.vber.common.translation.core.TranslationInterface;
+import lombok.AllArgsConstructor;
+
+/**
+ * 字典翻译实现
+ *
+ * @author Iwb
+ */
+@AllArgsConstructor
+@TranslationType(type = TransConstant.SAMPLE_ID_TO_NAME)
+public class SampleTranslationImpl implements TranslationInterface<String> {
+
+    private final SampleService sampleService;
+
+    @Override
+    public String translation(Object key, String other) {
+        if (key instanceof Long id) {
+            return sampleService.selectNameById(id);
+        }
+        return null;
+    }
+}

+ 1 - 0
SERVER/ChickenFarmV3/vb-common/vb-common-translation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -17,6 +17,7 @@ cn.vber.common.translation.core.impl.VaccineSopNameTranslationImpl
 cn.vber.common.translation.core.impl.BatchSopNameTranslationImpl
 cn.vber.common.translation.core.impl.ChickenInfoTranslationImpl
 cn.vber.common.translation.core.impl.ChickenSopNameTranslationImpl
+cn.vber.common.translation.core.impl.SampleTranslationImpl
 cn.vber.common.translation.core.impl.DeviceTranslationImpl
 cn.vber.common.translation.core.impl.UnitTranslationImpl
 cn.vber.common.translation.core.impl.StoreHouseTranslationImpl

+ 1 - 1
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/ExperimentController.java

@@ -113,7 +113,7 @@ public class ExperimentController extends BaseController {
     @SaCheckPermission("experiment:experiment:edit")
     @Log(title = "实验管理", businessType = BusinessType.UPDATE)
     @PostMapping("/start/{id}")
-    public R<Void> start(@NotEmpty(message = "主键不能为空") @PathVariable Long id) {
+    public R<Void> start(@NotNull(message = "主键不能为空") @PathVariable Long id) {
         return toAjax(experimentService.start(id));
     }
 

+ 5 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Experiment.java

@@ -52,6 +52,11 @@ public class Experiment extends BaseEntity {
      */
     private String rawDataUrl;
 
+    /**
+     * 描述
+     */
+    private String description;
+
     /**
      * 实验报告
      */

+ 1 - 1
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentAuditBo.java

@@ -17,7 +17,7 @@ public class ExperimentAuditBo implements Serializable {
     /**
      * 是否通过
      */
-    private boolean isPass;
+    private boolean pass;
 
     /**
      * 拒绝理由

+ 4 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentBo.java

@@ -34,6 +34,10 @@ public class ExperimentBo extends BaseEntity {
      */
     @NotBlank(message = "实验名称不能为空", groups = {AddGroup.class, EditGroup.class})
     private String experimentName;
+    /**
+     * 描述
+     */
+    private String description;
 
     /**
      * 实验类型

+ 9 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentSampleVo.java

@@ -2,6 +2,8 @@ package cn.vber.experiment.domain.vo;
 
 import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
 import cn.idev.excel.annotation.ExcelProperty;
+import cn.vber.common.translation.annotation.Translation;
+import cn.vber.common.translation.constant.TransConstant;
 import cn.vber.experiment.domain.ExperimentSample;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
@@ -36,6 +38,13 @@ public class ExperimentSampleVo implements Serializable {
     @ExcelProperty(value = "样本ID")
     private Long sampleId;
 
+    /**
+     * 样本名称
+     */
+    @ExcelProperty(value = "样本名称")
+    @Translation(type = TransConstant.SAMPLE_ID_TO_NAME, mapper = "sampleId")
+    private String sampleName;
+
     /**
      * 实验ID
      */

+ 6 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentVo.java

@@ -76,6 +76,12 @@ public class ExperimentVo implements Serializable {
     @ExcelProperty(value = "实验原始数据")
     private String rawDataUrl;
 
+    /**
+     * 描述
+     */
+    @ExcelProperty(value = "描述")
+    private String description;
+
     /**
      * 实验报告
      */

+ 22 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentMapper.java

@@ -1,8 +1,14 @@
 package cn.vber.experiment.mapper;
 
+import cn.vber.common.mybatis.annotation.DataColumn;
+import cn.vber.common.mybatis.annotation.DataPermission;
 import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
 import cn.vber.experiment.domain.Experiment;
+import cn.vber.experiment.domain.Sample;
 import cn.vber.experiment.domain.vo.ExperimentVo;
+import cn.vber.experiment.domain.vo.SampleVo;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.stereotype.Repository;
 
 /**
@@ -14,4 +20,20 @@ import org.springframework.stereotype.Repository;
 @Repository
 public interface ExperimentMapper extends BaseMapperPlus<Experiment, ExperimentVo> {
 
+    /**
+     * 分页查询列表,并进行数据权限控制
+     *
+     * @param page         分页参数
+     * @param queryWrapper 查询条件
+     * @return 分页信息
+     */
+    @DataPermission({
+            @DataColumn(key = "orgName", value = "create_org"),
+            @DataColumn(key = "userName", value = "create_by")
+    })
+    default Page<ExperimentVo> selectPageList(Page<Experiment> page, Wrapper<Experiment> queryWrapper) {
+        return this.selectVoPage(page, queryWrapper);
+    }
+
+
 }

+ 22 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleMapper.java

@@ -1,12 +1,18 @@
 package cn.vber.experiment.mapper;
 
+import cn.vber.common.mybatis.annotation.DataColumn;
+import cn.vber.common.mybatis.annotation.DataPermission;
 import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
 import cn.vber.experiment.domain.ExperimentSample;
 import cn.vber.experiment.domain.Sample;
 import cn.vber.experiment.domain.vo.ExperimentSampleVo;
 import cn.vber.experiment.domain.vo.SampleVo;
 import cn.vber.system.domain.SysNoticeStatus;
+import cn.vber.system.domain.SysUser;
+import cn.vber.system.domain.vo.SysUserVo;
 import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.stereotype.Repository;
 
 /**
@@ -18,5 +24,21 @@ import org.springframework.stereotype.Repository;
 @Repository
 public interface SampleMapper extends BaseMapperPlus<Sample, SampleVo> {
 
+    /**
+     * 分页查询列表,并进行数据权限控制
+     *
+     * @param page         分页参数
+     * @param queryWrapper 查询条件
+     * @return 分页信息
+     */
+    @DataPermission({
+            @DataColumn(key = "orgName", value = "create_org"),
+            @DataColumn(key = "userName", value = "create_by")
+    })
+    default Page<SampleVo> selectPageList(Page<Sample> page, Wrapper<Sample> queryWrapper) {
+        return this.selectVoPage(page, queryWrapper);
+    }
+
+
 }
 

+ 5 - 5
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/ExperimentServiceImpl.java

@@ -72,7 +72,7 @@ public class ExperimentServiceImpl implements IExperimentService {
     @Override
     public TableDataInfo<ExperimentVo> queryPageList(ExperimentBo bo, PageQuery pageQuery) {
         LambdaQueryWrapper<Experiment> lqw = buildQueryWrapper(bo);
-        Page<ExperimentVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        Page<ExperimentVo> result = baseMapper.selectPageList(pageQuery.build(), lqw);
         return TableDataInfo.build(result);
     }
 
@@ -104,7 +104,7 @@ public class ExperimentServiceImpl implements IExperimentService {
     public Boolean insertByBo(ExperimentBo bo) {
         Experiment add = MapstructUtils.convert(bo, Experiment.class);
         validEntityBeforeSave(add);
-
+        add.setExperimentManager(LoginHelper.getUserId());
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
             bo.setId(add.getId());
@@ -124,7 +124,6 @@ public class ExperimentServiceImpl implements IExperimentService {
         validEntityBeforeSave(update);
         boolean flag = baseMapper.updateById(update) > 0;
         if (flag) {
-            experimentSampleMapper.delete(new LambdaUpdateWrapper<ExperimentSample>().eq(ExperimentSample::getExperimentId, update.getId()));
             insertExperimentSample(bo, update);
         }
         return flag;
@@ -140,11 +139,12 @@ public class ExperimentServiceImpl implements IExperimentService {
     /**
      * 批量插入实验样本关联
      */
-    private void insertExperimentSample(ExperimentBo bo, Experiment add) {
+    private void insertExperimentSample(ExperimentBo bo, Experiment entity) {
+        experimentSampleMapper.delete(new LambdaUpdateWrapper<ExperimentSample>().eq(ExperimentSample::getExperimentId, entity.getId()));
         if (bo.getSampleIds() != null && !bo.getSampleIds().isEmpty()) {
             List<ExperimentSample> sampleList = bo.getSampleIds().stream().map(id ->
                     new ExperimentSample()
-                            .setExperimentId(add.getId())
+                            .setExperimentId(entity.getId())
                             .setSampleId(id)
                             .setCreateBy(LoginHelper.getUserId())
                             .setCreateTime(DateUtils.getNowDate())

+ 8 - 2
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/SampleServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.vber.experiment.service.impl;
 
 import cn.vber.common.core.exception.ServiceException;
+import cn.vber.common.core.service.SampleService;
 import cn.vber.common.core.utils.DateUtils;
 import cn.vber.common.core.utils.MapstructUtils;
 import cn.vber.common.core.utils.StringUtils;
@@ -35,7 +36,7 @@ import java.util.Objects;
  */
 @RequiredArgsConstructor
 @Service
-public class SampleServiceImpl implements ISampleService {
+public class SampleServiceImpl implements ISampleService, SampleService {
 
     private final SampleMapper baseMapper;
     private final SampleFlowMapper flowMapper;
@@ -54,7 +55,7 @@ public class SampleServiceImpl implements ISampleService {
     @Override
     public TableDataInfo<SampleVo> queryPageList(SampleBo bo, PageQuery pageQuery) {
         LambdaQueryWrapper<Sample> lqw = buildQueryWrapper(bo);
-        Page<SampleVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        Page<SampleVo> result = baseMapper.selectPageList(pageQuery.build(), lqw);
         return TableDataInfo.build(result);
     }
 
@@ -191,4 +192,9 @@ public class SampleServiceImpl implements ISampleService {
         flowMapper.insert(sampleFlow);
         return sample;
     }
+
+    @Override
+    public String selectNameById(long id) {
+        return baseMapper.selectById(id).getSampleName();
+    }
 }

+ 1 - 1
UI/VB.VUE/src/components/modal-select/ModalSelect.vue

@@ -199,7 +199,7 @@ defineExpose({
 			@confirm="confirm"
 			append-to-body>
 			<template #body>
-				<transition-group name="vb-fade">
+				<transition-group name="vb-fade" v-if="showTag">
 					<el-card shadow="hover" class="mt-5 mb-2">
 						<el-tag
 							v-for="(data, key) in selectList"

+ 9 - 2
UI/VB.VUE/src/components/modal-select/SampleSelect.vue

@@ -14,7 +14,7 @@ const props = withDefaults(
 		saveAutoClose?: boolean
 	}>(),
 	{
-		showTag: true,
+		showTag: false,
 		multiple: true,
 		saveAutoClose: true
 	}
@@ -24,8 +24,8 @@ const emits = defineEmits<{
 	(e: "confirm", v: any[]): void
 }>()
 const selectRef = ref()
-
 const selectIds = ref(props.modelValue)
+
 const selectList = ref<any[]>([])
 
 const tableOpts = reactive({
@@ -108,6 +108,13 @@ function getSelectList() {
 		return selectList.value
 	}
 }
+
+watch(
+	() => props.modelValue,
+	(val) => {
+		selectIds.value = val
+	}
+)
 defineExpose({
 	open,
 	close,

+ 7 - 0
UI/VB.VUE/src/stores/_notice.ts

@@ -127,6 +127,13 @@ export const useNoticeStore = defineStore("noticeStore", () => {
 			case "d_c_reject":
 				router.push({ path: "/workOrder/myReceiveC" })
 				break
+			case "e_audit":
+				router.push({ path: "/experimentMg/experimentAudit" })
+				break
+				break
+			case "e_report":
+				router.push({ path: "/experimentMg/experiment" })
+				break
 		}
 	}
 	function readAll() {

+ 141 - 0
UI/VB.VUE/src/views/experiment/experiment/_detail.vue

@@ -0,0 +1,141 @@
+<script setup lang="ts">
+const detail = ref()
+const emits = defineEmits<{ (e: "update:modelValue", v: string): void }>()
+const modalRef = ref()
+function open(data) {
+	detail.value = data
+	modalRef.value.show()
+}
+
+const pdfPreviewRef = ref()
+function handlePreviewPdf(file: string) {
+	const fileId = file.split("$")[0],
+		fileName = file.split("$")[1]
+	pdfPreviewRef.value.open({
+		fileId,
+		fileName
+	})
+}
+function init() {
+	//
+}
+
+onMounted(init)
+
+defineExpose({
+	open
+})
+</script>
+<template>
+	<VbModal
+		v-model:modal="modalRef"
+		title="实验详情"
+		append-to-body
+		:confirm-btn="false"
+		:close-btn-class="'btn btn-danger'">
+		<template #body>
+			<div class="w-100 p-5">
+				<dl class="info">
+					<dt>实验名称:</dt>
+					<dd>{{ detail?.experimentName }}</dd>
+				</dl>
+				<dl class="info">
+					<dt>实验描述:</dt>
+					<dd>{{ detail?.description }}</dd>
+				</dl>
+				<el-row>
+					<el-col :span="12">
+						<dl class="info">
+							<dt>实验类型:</dt>
+							<dd>
+								<DictTag
+									type="experiment_type"
+									:value-is-number="1"
+									:value="detail?.experimentType"></DictTag>
+							</dd>
+						</dl>
+					</el-col>
+					<el-col :span="12">
+						<dl class="info">
+							<dt>实验状态:</dt>
+							<dd>
+								<DictTag
+									type="experiment_status"
+									:value-is-number="1"
+									:value="detail?.experimentStatus"></DictTag>
+							</dd>
+						</dl>
+					</el-col>
+				</el-row>
+				<el-row>
+					<el-col :span="12">
+						<dl class="info">
+							<dt>实验人:</dt>
+							<dd>{{ detail?.experimentManagerName }}</dd>
+						</dl>
+					</el-col>
+					<el-col :span="12">
+						<dl class="info">
+							<dt>审核人:</dt>
+							<dd>{{ detail?.reviewManagerName }}</dd>
+						</dl>
+					</el-col>
+				</el-row>
+
+				<dl class="info">
+					<dt>实验样本:</dt>
+					<dd>
+						{{ detail?.sampleList.map((item) => item.sampleName).join(",") }}
+					</dd>
+				</dl>
+
+				<dl class="info">
+					<dt>原始数据:</dt>
+					<dd>
+						<template v-if="detail?.rawDataUrl">
+							<template v-for="(file, key) in detail?.rawDataUrl.split(',')" :key="key">
+								<span
+									v-if="file.split('$').length > 1"
+									class="text-primary p-1"
+									@click="handlePreviewPdf(file)">
+									{{ file.split("$")[1] }}
+								</span>
+							</template>
+						</template>
+					</dd>
+				</dl>
+				<dl class="info">
+					<dt>实验报告:</dt>
+					<dd>
+						<template v-if="detail?.reportUrl">
+							<template v-for="(file, key) in detail?.reportUrl.split(',')" :key="key">
+								<span
+									v-if="file.split('$').length > 1"
+									class="text-primary p-1"
+									@click="handlePreviewPdf(file)">
+									{{ file.split("$")[1] }}
+								</span>
+							</template>
+						</template>
+					</dd>
+				</dl>
+			</div>
+		</template>
+	</VbModal>
+	<VbPdfPreview ref="pdfPreviewRef" />
+</template>
+
+<style lang="scss" scoped>
+.info {
+	display: flex;
+	margin-bottom: 10px;
+	dt {
+		width: 80px;
+		text-align: right;
+		margin-right: 10px;
+	}
+	dd {
+		flex: 1;
+	}
+}
+</style>

+ 465 - 0
UI/VB.VUE/src/views/experiment/experiment/audit.vue

@@ -0,0 +1,465 @@
+<script setup lang="ts" name="Experiment">
+import apis from "@a"
+import dayjs from "dayjs"
+import EDetail from "./_detail.vue"
+
+const experimentUsersOptions = ref([])
+const tableRef = ref()
+const modalRef = ref()
+const sampleSelectRef = ref()
+const opts = reactive({
+	columns: [
+		{ field: "id", name: "实验ID", width: 100, isSort: true, visible: false, tooltip: true },
+		{
+			field: "experimentName",
+			name: "实验名称",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{ field: "experimentType", name: "实验类型", visible: true, isSort: false, width: 200 },
+		{
+			field: "experimentManagerName",
+			name: "实验负责人",
+			visible: true,
+			isSort: false,
+			width: 120,
+			tooltip: true
+		},
+		{
+			field: "reviewManagerName",
+			name: "审核负责人",
+			visible: true,
+			isSort: false,
+			width: 120,
+			tooltip: true
+		},
+		{
+			field: "experimentStatus",
+			name: "实验状态",
+			visible: true,
+			isSort: false,
+			width: 100,
+			tooltip: true
+		},
+		{
+			field: "rawDataUrl",
+			name: "原始数据",
+			visible: true,
+			isSort: false,
+			width: 230,
+			tooltip: true
+		},
+		{ field: "reportUrl", name: "实验报告", visible: true, isSort: false, width: 230 },
+
+		{ field: "actions", name: `操作`, width: 150 }
+	] as any[],
+	queryParams: {
+		experimentName: undefined,
+		experimentType: undefined,
+		experimentStatus: 3
+	},
+	searchFormItems: [
+		{
+			field: "experimentName",
+			label: "实验名称",
+			class: "w-100",
+			required: false,
+			placeholder: "请输入实验名称",
+			component: "I",
+			listeners: {
+				keyup: (e: KeyboardEvent) => {
+					if (e.code == "Enter") {
+						handleQuery()
+					}
+				}
+			}
+		},
+		{
+			field: "experimentType",
+			label: "实验类型",
+			class: "w-100",
+			required: false,
+			component: "Dict",
+			props: {
+				placeholder: "请选择实验类型",
+				dictType: "experiment_type",
+				valueIsNumber: 1,
+				type: "select"
+			},
+			listeners: {
+				change: () => {
+					handleQuery()
+				}
+			}
+		}
+	] as any,
+	permission: "experiment:experiment",
+	handleBtns: [],
+	handleFuns: {
+		handleCreate,
+		handleUpdate: () => {
+			const row = tableRef.value.getSelected()
+			handleUpdate(row)
+		},
+		handleDelete: () => {
+			const rows = tableRef.value.getSelecteds()
+			handleDelete(rows)
+		}
+	},
+	customBtns: [],
+	tableListFun: apis.experiment.experimentApi.list,
+	getEntityFun: apis.experiment.experimentApi.get,
+	deleteEntityFun: apis.experiment.experimentApi.del,
+	exportUrl: apis.experiment.experimentApi.exportUrl,
+	exportName: "Experiment",
+	modalTitle: "实验管理",
+	formItems: [
+		{
+			field: "experimentName",
+			label: "实验名称",
+			class: "w-100",
+			required: true,
+			placeholder: "请输入实验名称",
+			component: "I"
+		},
+		{
+			field: "experimentType",
+			label: "实验类型",
+			class: "w-100",
+			required: true,
+			component: "Dict",
+			props: {
+				placeholder: "请选择实验类型",
+				dictType: "experiment_type",
+				type: "select",
+				valueIsNumber: 1
+			}
+		},
+		{
+			field: "sampleNames",
+			label: "实验样品",
+			class: "w-100",
+			required: true,
+			placeholder: "请选择实验样品",
+			data: () => [],
+			props: {
+				type: "select"
+			},
+			append: "icon",
+			appendClickFunc: () => {
+				sampleSelectRef.value.open()
+			}
+		},
+		{
+			field: "reviewManager",
+			label: "审核负责人",
+			class: "w-100",
+			required: true,
+			placeholder: "请选择审核负责人",
+			component: "VS",
+			data: () => experimentUsersOptions.value,
+			props: {
+				valueIsNumber: 1,
+				type: "select"
+			}
+		},
+		{
+			field: "description",
+			label: "实验描述",
+			class: "w-100",
+			required: false,
+			placeholder: "请输入实验描述",
+			component: "I",
+			props: {
+				type: "textarea"
+			}
+		}
+	] as any,
+	resetForm: () => {
+		form.value = emptyFormData.value
+	},
+	labelWidth: "80px",
+	emptyFormData: {
+		id: undefined,
+		experimentName: undefined,
+		experimentType: undefined,
+		sampleIds: undefined,
+		sampleNames: undefined,
+		reviewManager: undefined
+	}
+})
+const { queryParams, emptyFormData } = toRefs(opts)
+const form = ref<any>(emptyFormData.value)
+
+/** 搜索按钮操作 */
+function handleQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	addDateRange(query, query.dateRangeCreateTime)
+	addDateRange(query, query.dateRangeUpdateTime, "UpdateTime")
+	tableRef.value?.query(query)
+}
+
+/** 重置按钮操作 */
+function resetQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	query.dateRangeCreateTime = [] as any
+	addDateRange(query, query.dateRangeCreateTime)
+	query.dateRangeUpdateTime = [] as any
+	addDateRange(query, query.dateRangeUpdateTime, "UpdateTime")
+	//
+}
+
+function handleCreate() {
+	tableRef.value.defaultHandleFuns.handleCreate()
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row: any) {
+	// tableRef.value.defaultHandleFuns.handleUpdate("", row)
+	apis.experiment.experimentApi.get(row.id).then((res) => {
+		form.value = res.data
+		form.value.sampleIds = res.data.sampleList.map((item) => item.sampleId)
+		form.value.sampleNames = res.data.sampleList.map((item) => item.sampleName)
+		modalRef.value.show()
+	})
+}
+
+/** 删除按钮操作 */
+function handleDelete(rows: any[]) {
+	tableRef.value.defaultHandleFuns.handleDelete("", rows)
+}
+
+/** 提交按钮 */
+function submitForm() {
+	apis.experiment.experimentApi.addOrUpdate(form.value).then(() => {
+		handleQuery()
+	})
+}
+
+function onSampleSelectConfirm(data: any) {
+	console.log("data", data)
+	form.value.sampleIds = data.map((item) => item.id)
+	form.value.sampleNames = data.map((item) => item.sampleName)
+}
+
+function getExperimentUsersOptions() {
+	apis.experiment.experimentApi.getExperimentUsers().then((res) => {
+		experimentUsersOptions.value = res.data.map((item) => {
+			return {
+				label: `${item.nickname}(${item.username})`,
+				value: item.userId
+			}
+		})
+	})
+}
+
+function handleStart(row: any) {
+	message.confirm("确定要开始实验吗?", "开始实验").then(() => {
+		apis.experiment.experimentApi.start(row.id).then(() => {
+			handleQuery()
+		})
+	})
+}
+const reportModalRef = ref()
+const reportForm = ref({
+	id: undefined,
+	rawDataUrl: undefined,
+	reportUrl: undefined
+})
+const reportFormItems = [
+	{
+		field: "rawDataUrl",
+		label: "原始数据",
+		class: "w-100",
+		required: true,
+		placeholder: "请上传原始数据",
+		component: "Vu",
+		props: {
+			uploadType: "file",
+			uploadUrl: "resource/oss/upload/experiment",
+			fileType: ["pdf", "doc", "docx", "xls", "xlsx", "zip"],
+			fileSize: 10,
+			limit: 5
+		}
+	},
+	{
+		field: "reportUrl",
+		label: "实验报告",
+		class: "w-100",
+		required: true,
+		placeholder: "请上传实验报告",
+		component: "Vu",
+		props: {
+			uploadType: "file",
+			uploadUrl: "resource/oss/upload/experiment",
+			fileType: ["pdf", "doc", "docx", "xls", "xlsx", "zip"],
+			fileSize: 10,
+			limit: 5
+		}
+	}
+]
+function handleReport(row: any) {
+	reportForm.value = {
+		id: row.id,
+		rawDataUrl: undefined,
+		reportUrl: undefined
+	}
+	reportModalRef.value.show()
+}
+
+function submitReport() {
+	apis.experiment.experimentApi.submitExperiment(reportForm.value).then(() => {
+		handleQuery()
+	})
+}
+const pdfPreviewRef = ref()
+function handlePreviewPdf(file: string) {
+	const fileId = file.split("$")[0],
+		fileName = file.split("$")[1]
+	pdfPreviewRef.value.open({
+		fileId,
+		fileName
+	})
+}
+
+const detailRef = ref()
+function handleDetail(row: any) {
+	apis.experiment.experimentApi.get(row.id).then((res) => {
+		detailRef.value.open(res.data)
+	})
+}
+function handleAudit(row: any, isAudit: boolean) {
+	message
+		.confirm(
+			`确定要${isAudit ? "通过" : "拒绝"}审核,${isAudit ? "吗" : "发回重新报告"}?`,
+			`${isAudit ? "通过" : "拒绝"}审核`
+		)
+		.then(() => {
+			apis.experiment.experimentApi.auditExperiment({ id: row.id, pass: isAudit }).then(() => {
+				handleQuery()
+			})
+		})
+}
+function init() {
+	getExperimentUsersOptions()
+}
+onMounted(init)
+</script>
+<template>
+	<div class="app-container">
+		<VbDataTable
+			ref="tableRef"
+			keyField="id"
+			:columns="opts.columns"
+			:handle-perm="opts.permission"
+			:handle-btns="opts.handleBtns"
+			:handle-funs="opts.handleFuns"
+			:search-form-items="opts.searchFormItems"
+			:custom-btns="opts.customBtns"
+			:remote-fun="opts.tableListFun"
+			:get-entity-fun="opts.getEntityFun"
+			:delete-entity-fun="opts.deleteEntityFun"
+			:export-url="opts.exportUrl"
+			:export-name="opts.exportName"
+			:modal="modalRef"
+			:reset-form-fun="opts.resetForm"
+			v-model:form-data="form"
+			v-model:query-params="queryParams"
+			:check-multiple="true"
+			:reset-search-form-fun="resetQuery"
+			:custom-search-fun="handleQuery">
+			<template #experimentType="{ row }">
+				<DictTag type="experiment_type" :value-is-number="1" :value="row.experimentType"></DictTag>
+			</template>
+			<template #rawDataUrl="{ row }">
+				<template v-if="row.rawDataUrl">
+					<template v-for="(file, key) in row.rawDataUrl.split(',')" :key="key">
+						<span
+							v-if="file.split('$').length > 1"
+							class="text-primary p-1"
+							@click="handlePreviewPdf(file)">
+							{{ file.split("$")[1] }}
+						</span>
+					</template>
+				</template>
+				<template v-else>-</template>
+			</template>
+			<template #reportUrl="{ row }">
+				<template v-if="row.reportUrl">
+					<template v-for="(file, key) in row.reportUrl.split(',')" :key="key">
+						<span
+							v-if="file.split('$').length > 1"
+							class="text-primary p-1"
+							@click="handlePreviewPdf(file)">
+							{{ file.split("$")[1] }}
+						</span>
+					</template>
+				</template>
+				<template v-else>-</template>
+			</template>
+			<template #experimentStatus="{ row }">
+				<DictTag
+					type="experiment_status"
+					:value-is-number="1"
+					:value="row.experimentStatus"></DictTag>
+			</template>
+			<template #actions="{ row }">
+				<vb-tooltip content="实验详情" placement="top">
+					<el-button link type="primary" @click="handleDetail(row)">
+						<template #icon>
+							<VbIcon icon-name="book" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+				<vb-tooltip v-if="row.experimentStatus === 3" content="审核通过" placement="top">
+					<el-button
+						link
+						type="success"
+						@click="handleAudit(row, true)"
+						v-hasPermission="'experiment:experiment:edit'">
+						<template #icon>
+							<VbIcon icon-name="add-files" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+				<vb-tooltip v-if="row.experimentStatus === 3" content="发回报告" placement="top">
+					<el-button
+						link
+						type="danger"
+						@click="handleAudit(row, false)"
+						v-hasPermission="'experiment:experiment:edit'">
+						<template #icon>
+							<VbIcon icon-name="update-file" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+			</template>
+		</VbDataTable>
+		<VbModal
+			v-model:modal="modalRef"
+			:title="opts.modalTitle"
+			:form-data="form"
+			:form-items="opts.formItems"
+			:label-width="opts.labelWidth"
+			append-to-body
+			@confirm="submitForm"></VbModal>
+		<VbModal
+			v-model:modal="reportModalRef"
+			title="上传报告"
+			:form-data="reportForm"
+			:form-items="reportFormItems as any"
+			:label-width="opts.labelWidth"
+			append-to-body
+			@confirm="submitReport"></VbModal>
+		<SampleSelect
+			ref="sampleSelectRef"
+			v-model="form.sampleIds"
+			@confirm="onSampleSelectConfirm"></SampleSelect>
+
+		<EDetail ref="detailRef"></EDetail>
+
+		<VbPdfPreview ref="pdfPreviewRef" />
+	</div>
+</template>

+ 170 - 15
UI/VB.VUE/src/views/experiment/experiment/index.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts" name="Experiment">
 import apis from "@a"
 import dayjs from "dayjs"
+import EDetail from "./_detail.vue"
 
 const experimentUsersOptions = ref([])
 const tableRef = ref()
@@ -17,21 +18,21 @@ const opts = reactive({
 			width: "auto",
 			tooltip: true
 		},
-		{ field: "experimentType", name: "实验类型", visible: true, isSort: false, width: 100 },
+		{ field: "experimentType", name: "实验类型", visible: true, isSort: false, width: 200 },
 		{
-			field: "experimentManager",
+			field: "experimentManagerName",
 			name: "实验负责人",
 			visible: true,
 			isSort: false,
-			width: "auto",
+			width: 120,
 			tooltip: true
 		},
 		{
-			field: "reviewManager",
+			field: "reviewManagerName",
 			name: "审核负责人",
 			visible: true,
 			isSort: false,
-			width: "auto",
+			width: 120,
 			tooltip: true
 		},
 		{
@@ -39,7 +40,7 @@ const opts = reactive({
 			name: "实验状态",
 			visible: true,
 			isSort: false,
-			width: "auto",
+			width: 100,
 			tooltip: true
 		},
 		{
@@ -47,10 +48,10 @@ const opts = reactive({
 			name: "原始数据",
 			visible: true,
 			isSort: false,
-			width: "auto",
+			width: 230,
 			tooltip: true
 		},
-		{ field: "reportUrl", name: "实验报告", visible: true, isSort: false, width: 100 },
+		{ field: "reportUrl", name: "实验报告", visible: true, isSort: false, width: 230 },
 
 		{ field: "actions", name: `操作`, width: 150 }
 	] as any[],
@@ -186,7 +187,7 @@ const opts = reactive({
 			field: "description",
 			label: "实验描述",
 			class: "w-100",
-			required: true,
+			required: false,
 			placeholder: "请输入实验描述",
 			component: "I",
 			props: {
@@ -234,7 +235,13 @@ function handleCreate() {
 
 /** 修改按钮操作 */
 function handleUpdate(row: any) {
-	tableRef.value.defaultHandleFuns.handleUpdate("", row)
+	// tableRef.value.defaultHandleFuns.handleUpdate("", row)
+	apis.experiment.experimentApi.get(row.id).then((res) => {
+		form.value = res.data
+		form.value.sampleIds = res.data.sampleList.map((item) => item.sampleId)
+		form.value.sampleNames = res.data.sampleList.map((item) => item.sampleName)
+		modalRef.value.show()
+	})
 }
 
 /** 删除按钮操作 */
@@ -265,6 +272,82 @@ function getExperimentUsersOptions() {
 		})
 	})
 }
+
+function handleStart(row: any) {
+	message.confirm("确定要开始实验吗?", "开始实验").then(() => {
+		apis.experiment.experimentApi.start(row.id).then(() => {
+			handleQuery()
+		})
+	})
+}
+const reportModalRef = ref()
+const reportForm = ref({
+	id: undefined,
+	rawDataUrl: undefined,
+	reportUrl: undefined
+})
+const reportFormItems = [
+	{
+		field: "rawDataUrl",
+		label: "原始数据",
+		class: "w-100",
+		required: true,
+		placeholder: "请上传原始数据",
+		component: "Vu",
+		props: {
+			uploadType: "file",
+			uploadUrl: "resource/oss/upload/experiment",
+			fileType: ["pdf", "doc", "docx", "xls", "xlsx", "zip"],
+			fileSize: 10,
+			limit: 5
+		}
+	},
+	{
+		field: "reportUrl",
+		label: "实验报告",
+		class: "w-100",
+		required: true,
+		placeholder: "请上传实验报告",
+		component: "Vu",
+		props: {
+			uploadType: "file",
+			uploadUrl: "resource/oss/upload/experiment",
+			fileType: ["pdf", "doc", "docx", "xls", "xlsx", "zip"],
+			fileSize: 10,
+			limit: 5
+		}
+	}
+]
+function handleReport(row: any) {
+	reportForm.value = {
+		id: row.id,
+		rawDataUrl: undefined,
+		reportUrl: undefined
+	}
+	reportModalRef.value.show()
+}
+
+function submitReport() {
+	apis.experiment.experimentApi.submitExperiment(reportForm.value).then(() => {
+		handleQuery()
+	})
+}
+
+const detailRef = ref()
+function handleDetail(row: any) {
+	apis.experiment.experimentApi.get(row.id).then((res) => {
+		detailRef.value.open(res.data)
+	})
+}
+const pdfPreviewRef = ref()
+function handlePreviewPdf(file: string) {
+	const fileId = file.split("$")[0],
+		fileName = file.split("$")[1]
+	pdfPreviewRef.value.open({
+		fileId,
+		fileName
+	})
+}
 function init() {
 	getExperimentUsersOptions()
 }
@@ -296,12 +379,40 @@ onMounted(init)
 			<template #experimentType="{ row }">
 				<DictTag type="experiment_type" :value-is-number="1" :value="row.experimentType"></DictTag>
 			</template>
-			<template #reportUrl="{ row }"></template>
+			<template #rawDataUrl="{ row }">
+				<template v-if="row.rawDataUrl">
+					<template v-for="(file, key) in row.rawDataUrl.split(',')" :key="key">
+						<span
+							v-if="file.split('$').length > 1"
+							class="text-primary p-1"
+							@click="handlePreviewPdf(file)">
+							{{ file.split("$")[1] }}
+						</span>
+					</template>
+				</template>
+				<template v-else>-</template>
+			</template>
+			<template #reportUrl="{ row }">
+				<template v-if="row.reportUrl">
+					<template v-for="(file, key) in row.reportUrl.split(',')" :key="key">
+						<span
+							v-if="file.split('$').length > 1"
+							class="text-primary p-1"
+							@click="handlePreviewPdf(file)">
+							{{ file.split("$")[1] }}
+						</span>
+					</template>
+				</template>
+				<template v-else>-</template>
+			</template>
 			<template #experimentStatus="{ row }">
-				<DictTag type="experiment_status" :value-is-number="0" :value="row.reportUrl"></DictTag>
+				<DictTag
+					type="experiment_status"
+					:value-is-number="1"
+					:value="row.experimentStatus"></DictTag>
 			</template>
 			<template #actions="{ row }">
-				<vb-tooltip content="修改" placement="top">
+				<vb-tooltip v-if="row.experimentStatus === 1" content="修改" placement="top">
 					<el-button
 						link
 						type="primary"
@@ -312,7 +423,7 @@ onMounted(init)
 						</template>
 					</el-button>
 				</vb-tooltip>
-				<vb-tooltip content="删除" placement="top">
+				<vb-tooltip v-if="row.experimentStatus === 1" content="删除" placement="top">
 					<el-button
 						link
 						type="primary"
@@ -323,6 +434,37 @@ onMounted(init)
 						</template>
 					</el-button>
 				</vb-tooltip>
+				<vb-tooltip v-if="row.experimentStatus === 1" content="删除" placement="top">
+					<el-button link type="success" @click="handleStart(row)">
+						<template #icon>
+							<VbIcon icon-name="tablet-ok" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+				<vb-tooltip
+					v-if="row.experimentStatus === 2 || row.experimentStatus === 4"
+					content="提交报告"
+					placement="top">
+					<el-button
+						link
+						type="success"
+						@click="handleReport(row)"
+						v-hasPermission="'experiment:experiment:edit'">
+						<template #icon>
+							<VbIcon icon-name="add-files" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+				<vb-tooltip
+					v-if="row.experimentStatus === 3 || row.experimentStatus === 5"
+					content="实验详情"
+					placement="top">
+					<el-button link type="primary" @click="handleDetail(row)">
+						<template #icon>
+							<VbIcon icon-name="book" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
 			</template>
 		</VbDataTable>
 		<VbModal
@@ -333,7 +475,20 @@ onMounted(init)
 			:label-width="opts.labelWidth"
 			append-to-body
 			@confirm="submitForm"></VbModal>
+		<VbModal
+			v-model:modal="reportModalRef"
+			title="上传报告"
+			:form-data="reportForm"
+			:form-items="reportFormItems as any"
+			:label-width="opts.labelWidth"
+			append-to-body
+			@confirm="submitReport"></VbModal>
+		<SampleSelect
+			ref="sampleSelectRef"
+			v-model="form.sampleIds"
+			@confirm="onSampleSelectConfirm"></SampleSelect>
+		<EDetail ref="detailRef"></EDetail>
 
-		<SampleSelect ref="sampleSelectRef" @confirm="onSampleSelectConfirm"></SampleSelect>
+		<VbPdfPreview ref="pdfPreviewRef" />
 	</div>
 </template>

+ 13 - 1
UI/VB.VUE/src/views/experiment/sample/_sample.vue

@@ -571,10 +571,22 @@ function handleDownloadQr(id: string) {
 		<VbModal
 			v-model:modal="flowModalRef"
 			title="样品流转记录"
+			modal-dialog-style="max-width: 500px"
 			:confirm-btn="false"
 			:close-btn-class="'btn btn-danger'"
 			append-to-body>
-			<template #body></template>
+			<template #body>
+				<el-table :data="flowData" :show-header="false">
+					<!-- <el-table-column label="序号" type="index" width="60" align="center"></el-table-column> -->
+					<el-table-column label="序号" width="60" prop="id" align="center"></el-table-column>
+					<el-table-column label="操作人" prop="handlerName" align="center"></el-table-column>
+					<el-table-column label="操作时间" prop="handleTime" align="center">
+						<template #default="{ row }">
+							<span>{{ dayjs(row.handleTime).format("YYYY/MM/DD HH:mm:ss") }}</span>
+						</template>
+					</el-table-column>
+				</el-table>
+			</template>
 		</VbModal>
 	</div>
 </template>