董国庆
2025-05-14 cf4789baf6ab84d30b9e4626f37346ee0d8baadb
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/leshan-laboratory
20个文件已添加
4个文件已修改
5686 ■■■■■ 已修改文件
culture/src/assets/public/require.png 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/assets/public/selectType.png 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/Table/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/confirm-storage-dialog/index.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/router/index.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/add.vue 778 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/addProgenitor.vue 801 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/components/AddSublevelForm.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/components/AddSublevelPlan.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/components/ParentForm.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/components/PlanForm.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/index.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddAncestor.vue 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddSublevelForm.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/PlanForm.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/add.vue 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/confirm-storage-dialog.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/inoculation-slope-record-dialog.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/preserve-strain-record-dialog.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/separation-record-dialog.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/chief-cell/index.vue 465 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/add.vue 370 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/index.vue 427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/assets/public/require.png
culture/src/assets/public/selectType.png
culture/src/components/Table/index.vue
@@ -1,5 +1,5 @@
<template>
    <div class="table-container">
    <div class="table-container" style="width: 100%;">
        <el-table border v-bind="$attrs" v-on="$listeners" :height="height">
            <slot></slot>
        </el-table>
culture/src/components/confirm-storage-dialog/index.vue
New file
@@ -0,0 +1,151 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title=""
    width="520px"
    :close-on-click-modal="false"
    custom-class="record-detail-dialog"
    @close="handleClose"
  >
    <div class="dialog-content">
      <div class="confirm-tip">
        {{ text }}
        <!-- <span class="danger">确认后将无法再次编辑菌种传代项内容</span> -->
      </div>
      <el-form :model="form" :rules="rules" ref="form" label-position="top">
        <el-form-item required>
          <template #label>
            <span>{{ name }}</span>
            <el-button
              type="primary"
              class="sign-btn"
              @click="showSignature = true"
              >签名</el-button
            >
          </template>
          <div class="signature-area" :class="{ waiting: !form.signature }">
            <template v-if="form.signature">
              <img :src="form.signature" :alt="name" />
            </template>
            <template v-else>
              <span class="waiting-text">等待确认</span>
            </template>
          </div>
        </el-form-item>
      </el-form>
    </div>
    <div class="footer-btns">
      <el-button @click="handleClose" style="margin-right: 16px"
        >取消</el-button
      >
      <el-button type="primary" @click="handleConfirm">确认</el-button>
    </div>
    <signature-canvas
      :visible.sync="showSignature"
      @confirm="handleSignatureConfirm"
    />
  </el-dialog>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
export default {
  name: "ConfirmStorageDialog",
  components: { SignatureCanvas },
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    text: {
      type: String,
      default: "",
    },
    name: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      form: {
        signature: "",
      },
      rules: {
        signature: [{ required: true, message: "请签名", trigger: "change" }],
      },
      showSignature: false,
    };
  },
  methods: {
    handleClose() {
      this.$emit("update:visible", false);
    },
    handleConfirm() {
      this.$refs.form.validate((valid) => {
        if (!valid) return;
        this.$emit("confirm", { ...this.form });
        this.handleClose();
      });
    },
    handleSignatureConfirm(dataUrl) {
      this.form.signature = dataUrl;
      this.showSignature = false;
    },
  },
};
</script>
<style lang="less" scoped>
.confirm-tip {
  color: #f5222d;
  font-size: 16px;
  margin-bottom: 24px;
  .danger {
    margin-left: 12px;
  }
}
.signature-area {
  height: 120px;
  width: 100%;
  background: #f5f7fa;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #dcdfe6;
  overflow: hidden;
  padding: 0;
}
.signature-area.waiting {
  border-style: dashed;
  background: #fafafa;
}
.signature-area img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-btn {
  height: 32px;
  border-radius: 4px;
  font-size: 14px;
  padding: 0 20px;
  font-weight: 400;
  margin-left: 12px;
}
.footer-btns {
  display: flex;
  justify-content: center;
  padding: 24px;
  padding-top: 0;
  .el-button {
    width: 150px;
  }
}
</style>
culture/src/router/index.js
@@ -218,18 +218,80 @@
                path: 'add-pedigree',
                name: 'AddPedigree',
                meta: {
                    title: "新增菌种传代生产谱系图",
                    title: "新增母代菌种传代生产谱系图",
                    hide: true
                },
                component: () => import("../views/pedigree-chart/add"),
            },
            {
                path: 'add-pedigree',
                name: 'AddPedigree',
                path: 'add-progenitor',
                name: 'AddProgenitor',
                meta: {
                    title: "新增菌种传代生产谱系图",
                    title: "新增祖代菌种传代生产谱系图",
                    hide: true
                },
                component: () => import("../views/pedigree-chart/add"),
            }
                component: () => import("../views/pedigree-chart/addProgenitor"),
            },
            // {
            //     path: "strain-flow-chart",
            //     name: "StrainFlowChart",
            //     meta: {
            //         title: "菌种传代产生流程图",
            //         keepAlive: true,
            //     },
            //     component: () => import("../views/strain-library/strain-flow-chart"),
            // },
            {
                path: 'breeding-record',
                name: 'BreedingRecord',
                meta: {
                    title: "菌种选育保藏记录",
                },
                component: () => import("../views/strain-library/breeding-record"),
            },
            {
                path: 'add-breeding-record',
                name: 'AddBreedingRecord',
                meta: {
                    title: "新增菌种选育保藏记录",
                    hide: true,
                },
                component: () => import("../views/strain-library/breeding-record/add"),
            },
            {
                path: 'validation',
                meta: {
                    title: "菌种验证数据资料",
                },
                component: Parent,
                children: [
                    {
                        path: 'primitive-cell',
                        name: 'PrimitiveCell',
                        meta: {
                            title: '原始细胞库资料',
                        },
                        component: () => import("../views/strain-library/validation/primitive-cell/index.vue")
                    },
                    {
                        path: 'add-primitive-cell',
                        name: 'AddPrimitiveCell',
                        meta: {
                            title: '新增原始细胞库资料',
                            hide: true
                        },
                        component: () => import("../views/strain-library/validation/primitive-cell/add.vue")
                    },
                    {
                        path: 'chief-cell',
                        name: 'ChiefCell',
                        meta: {
                            title: '主细胞库资料'
                        },
                        component: () => import("../views/strain-library/validation/chief-cell")
                    }
                ]
            },
        ]
    }, {
        path: "/strainReportLibrary",
culture/src/views/pedigree-chart/add.vue
@@ -1,79 +1,93 @@
<template>
  <el-form
    :model="form"
    :rules="rules"
    ref="pedigreeForm"
    label-position="top"
    class="strain-form"
  >
    <div class="card">
      <div class="form-items-row">
        <el-form-item label="菌种源" prop="strainSource" required>
          <div class="flex-row">
            <div class="input-wrapper">
              <el-input
                v-model="form.strainSource"
                placeholder="请输入"
                class="fixed-width-input"
              ></el-input>
  <div>
    <el-form :model="form" :rules="rules" ref="pedigreeForm" label-position="top" class="strain-form">
      <div class="card">
        <div class="form-items-row">
          <el-form-item label="菌种源" prop="strainSource" required>
            <div class="flex-row">
              <div class="input-wrapper">
                <el-input v-model="form.strainSource" placeholder="请输入" class="fixed-width-input"></el-input>
              </div>
              <span class="form-text">代—</span>
              <div class="input-wrapper">
                <el-input v-model="form.generation" placeholder="请输入" class="fixed-width-input"></el-input>
              </div>
              <span class="form-text">细胞库</span>
            </div>
            <span class="form-text">代—</span>
            <div class="input-wrapper">
              <el-input
                v-model="form.generation"
                placeholder="请输入"
                class="fixed-width-input"
              ></el-input>
            </div>
            <span class="form-text">细胞库</span>
          </div>
        </el-form-item>
        <el-form-item label="传代菌种编号" prop="strainNo" required>
          <el-input
            v-model="form.strainNo"
            placeholder="请输入"
            class="fixed-width-input"
          ></el-input>
        </el-form-item>
        <el-form-item label="传代菌种名称" prop="strainName" required>
          <el-input
            v-model="form.strainName"
            placeholder="请输入"
            class="fixed-width-input"
          ></el-input>
        </el-form-item>
      </div>
    </div>
    <div class="chart">
      <div class="header">
        <div class="title">菌种传代生产谱系图</div>
        <div class="option-btn">
          <el-button type="primary" class="el-icon-plus"> 新增</el-button>
          <el-button type="primary">设置传代计划数</el-button>
          <el-button type="primary">详情</el-button>
          </el-form-item>
          <el-form-item label="传代菌种编号" prop="strainNo" required>
            <el-input v-model="form.strainNo" placeholder="请输入" class="fixed-width-input"></el-input>
          </el-form-item>
          <el-form-item label="传代菌种名称" prop="strainName" required>
            <el-input v-model="form.strainName" placeholder="请输入" class="fixed-width-input"></el-input>
          </el-form-item>
        </div>
      </div>
    </div>
    <div class="end-btn">
      <el-button type="primary" @click="handleSubmit">提交</el-button>
      <el-button @click="handleDraft">存草稿</el-button>
      <el-button @click="handleCancel">取消</el-button>
    </div>
    <!-- 签字确认组件 -->
    <SignatureCanvas
      :visible.sync="signatureVisible"
      @confirm="handleSignatureConfirm"
    />
  </el-form>
      <div class="card" style="margin-top: 30px;">
        <Table :height="null" :total="0" :tableData="tableData">
          <el-table-column label="接种操作人" prop="strainSource" />
          <el-table-column label="接种操作时间" prop="strainNo" />
          <el-table-column label="传代菌种编号" prop="strainName" />
          <el-table-column label="传代菌种名称" prop="strainName" />
          <el-table-column label="接种菌种编号" prop="strainName" />
          <el-table-column label="接种菌种名称" prop="strainName" />
          <el-table-column label="入库总数" prop="strainName" />
          <el-table-column label="保存/废弃" prop="strainName" />
          <el-table-column label="入库时间" prop="strainName" />
          <el-table-column label="操作">
            <template #default="{ row }">
              <el-button type="text">确认入库</el-button>
            </template>
          </el-table-column>
        </Table>
      </div>
      <div class="chart">
        <div class="header">
          <div class="title">菌种传代生产谱系图</div>
          <div class="option-btn">
            <el-button type="primary" class="el-icon-plus" @click="addNode"> 新增</el-button>
            <el-button type="primary" @click="setGenerationPlan">设置传代计划数</el-button>
            <el-button type="primary" @click="showDetail">详情</el-button>
          </div>
        </div>
        <div class="strain-flow-chart">
          <div id="mountNode"></div>
        </div>
      </div>
      <div class="end-btn">
        <el-button type="primary" @click="handleSubmit">提交</el-button>
        <el-button @click="handleDraft">存草稿</el-button>
        <el-button @click="handleCancel">取消</el-button>
      </div>
      <!-- 签字确认组件 -->
      <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </el-form>
    <ParentForm ref="parentForm" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
    <AddSublevelPlan ref="addSublevelPlan" @addNodeSign="addNodeSign" />
  </div>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import G6 from '@antv/g6';
import ParentForm from "./components/ParentForm.vue";
import PlanForm from "./components/PlanForm.vue";
import AddSublevelForm from "./components/AddSublevelForm.vue";
import AddSublevelPlan from "./components/AddSublevelPlan.vue";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
    ParentForm,
    PlanForm,
    AddSublevelForm,
    AddSublevelPlan
  },
  data() {
    return {
@@ -97,9 +111,51 @@
          { required: true, message: "请输入传代菌种名称", trigger: "blur" },
        ],
      },
      graph: null,
      nodeCount: 0,
      selectedNode: null,
      graphData: {
        nodes: [],
        edges: []
      },
      // 弹窗相关数据
      dialogVisible: false,
      dialogTitle: '',
      formLabel: '',
      inputType: 'text',
      showDiscarded: false,
      isAddingNode: false,
      nodeData: {},
      nodeType: '',//1母代 2计划数 3子孙代
    };
  },
  computed: {
    canAddNode() {
      // 如果没有节点,可以新增母代
      if (this.graphData.nodes.length === 0) {
        return true;
      }
      // 如果选中了传代计划数节点,可以新增下一代
      if (this.selectedNode && this.selectedNode.label === '传代计划数') {
        return true;
      }
      return false;
    }
  },
  mounted() {
    this.initGraph();
    this.initEvents();
  },
  beforeDestroy() {
    this.graph?.destroy();
    window.removeEventListener('resize', this.handleResize);
  },
  methods: {
    addNodeSign(value, type) {
      this.nodeData = value
      this.nodeType = type
      this.signatureVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
@@ -116,10 +172,555 @@
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      console.log("submit form with signature:", signatureImage);
      if (this.nodeType === 1) {
        this.handleAddParent(this.nodeData)
      } else if (this.nodeType === 2) {
        this.handleAddPlan(this.nodeData)
      } else if (this.nodeType === 3) {
        this.handleAddSublevel(this.nodeData)
      }
      // 处理提交逻辑
      console.log("submit form with signature:", this.form, signatureImage);
      this.$router.back();
    },
    initGraph() {
      const container = document.getElementById('mountNode');
      const width = container.scrollWidth;
      const height = container.scrollHeight || 600;
      // 自定义节点
      G6.registerNode('custom-node', {
        draw(cfg, group) {
          const width = 120;
          const titleHeight = 30;
          const contentHeight = 40;
          const gap = 4;
          const totalHeight = titleHeight + gap + contentHeight;
          // 根据节点状态设置颜色
          const isDiscarded = !cfg.isDiscarded;
          const titleFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'l(0) 0:#0ACBCA 1:#049C9A' : 'l(0) 0:#0ACBCA 1:#049C9A');
          const contentFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'rgba(4,156,154,0.2)' : 'rgba(4,156,154,0.1)');
          const textFill = isDiscarded ? 'rgba(144, 147, 153, 1)' : '#049C9A';
          const stroke = isDiscarded ? '#DCDFE6' : (cfg.selected ? '#049C9A' : 'transparent');
          // 创建渐变
          const gradient = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2,
              width: width,
              height: titleHeight,
              radius: 20,
              fill: titleFill,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'title-box',
          });
          // 下部分 - 内容背景
          const contentBox = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2 + titleHeight + gap,
              width: width,
              height: contentHeight,
              fill: contentFill,
              radius: 15,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'content-box',
          });
          // 标题文本
          if (cfg.label) {
            group.addShape('text', {
              attrs: {
                text: cfg.label,
                x: 0,
                y: -totalHeight / 2 + titleHeight / 2,
                fill: isDiscarded ? 'rgba(144, 147, 153, 1)' : '#fff',
                fontSize: 12,
                textAlign: 'center',
                textBaseline: 'middle',
                fontWeight: 'bold',
                cursor: 'move',
              },
              name: 'title-text',
            });
          }
          // 内容文本
          let content = '';
          if (cfg.label === '传代计划数') {
            content = `${cfg.planCount || 0}`;
          } else if (cfg.number) {
            content = cfg.label === '母代' ? `代传菌种编号:${cfg.number}` : `接种菌种编号:${cfg.number}`;
          }
          if (content) {
            group.addShape('text', {
              attrs: {
                text: content,
                x: 0,
                y: -totalHeight / 2 + titleHeight + gap + contentHeight / 2,
                fill: textFill,
                fontSize: 10,
                textAlign: 'center',
                textBaseline: 'middle',
                cursor: 'move',
              },
              name: 'content-text',
            });
          }
          return gradient;
        },
        getAnchorPoints() {
          return [
            [0.5, 0], // 上
            [1, 0.5], // 右
            [0.5, 1], // 下
            [0, 0.5], // 左
          ];
        },
        setState(name, value, item) {
          // 移除悬浮效果,保持节点样式始终一致
        },
      });
      this.graph = new G6.Graph({
        container: 'mountNode',
        width,
        height,
        fitView: true,
        fitViewPadding: 30,
        animate: false,
        enabledStack: false,
        renderer: 'canvas',
        minZoom: 0.3,
        maxZoom: 2,
        defaultZoom: 1,
        layout: {
          type: 'dagre',
          rankdir: 'LR',
          align: 'UL',
          nodesep: 30,  // 减小节点间距
          ranksep: 50,  // 减小层级间距
          controlPoints: true,
        },
        modes: {
          default: [
            {
              type: 'drag-canvas',
              enableOptimize: true,
              direction: 'both',
              scalableRange: 0.1,
              dragTimesOfScale: 0.1,
              onlyChangeComputeZoom: true,
            },
            {
              type: 'zoom-canvas',
              sensitivity: 1.5,
              enableOptimize: true,
            },
            {
              type: 'drag-node',
              enableDelegate: true,
              delegateStyle: {
                fill: '#f3f3f3',
                stroke: '#ccc',
                opacity: 0.5,
              },
              updateEdge: false,
              enableOptimize: true,
              optimizeZoom: 0.7,
              damping: 0.1,
            }
          ]
        },
        defaultNode: {
          type: 'custom-node',
          style: {
            fill: 'l(0) 0:#0ACBCA 1:#049C9A',
          },
        },
        defaultEdge: {
          type: 'cubic-horizontal',
          style: {
            stroke: 'rgba(4, 156, 154, 1)',
            lineWidth: 1,
            opacity: 0.5,
            endArrow: {
              path: G6.Arrow.triangle(6, 6),
              fill: 'rgba(4, 156, 154, 1)',
              stroke: 'rgba(4, 156, 154, 1)',
            },
          },
        },
        optimizeEdge: true,
        optimizeLayoutAnimation: true,
      });
      const canvas = this.graph.get('canvas');
      canvas.set('localRefresh', false);
      canvas.set('autoDraw', true);
      canvas.set('animating', false);
      let throttleTimer = null;
      const throttleInterval = 16;
      this.graph.on('node:dragstart', () => {
        canvas.set('localRefresh', false);
        this.graph.get('canvas').draw();
      });
      this.graph.on('node:drag', (e) => {
        if (throttleTimer) return;
        throttleTimer = setTimeout(() => {
          const model = e.item.get('model');
          const edges = this.graph.getEdges().filter(edge => {
            const source = edge.getSource();
            const target = edge.getTarget();
            return source.get('id') === model.id || target.get('id') === model.id;
          });
          edges.forEach(edge => {
            this.graph.refreshItem(edge);
          });
          throttleTimer = null;
        }, throttleInterval);
      });
      this.graph.on('node:dragend', (e) => {
        if (throttleTimer) {
          clearTimeout(throttleTimer);
          throttleTimer = null;
        }
        const model = e.item.get('model');
        const edges = this.graph.getEdges().filter(edge => {
          const source = edge.getSource();
          const target = edge.getTarget();
          return source.get('id') === model.id || target.get('id') === model.id;
        });
        edges.forEach(edge => {
          this.graph.refreshItem(edge);
        });
        canvas.set('localRefresh', true);
        this.graph.get('canvas').draw();
      });
      this.graph.data(this.graphData);
      this.graph.render();
      let debounceTimer = null;
      this.graph.on('afterchange', () => {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
          if (!canvas.get('destroyed')) {
            canvas.draw();
          }
        }, 16);
      });
    },
    initEvents() {
      // 监听窗口大小变化
      window.addEventListener('resize', this.handleResize);
      // 节点点击事件
      this.graph.on('node:click', (evt) => {
        const node = evt.item;
        const nodeModel = node.getModel();
        // 如果节点已废弃,不允许任何操作
        if (!nodeModel.isDiscarded) {
          this.$message.warning('该节点已废弃,不能进行操作');
          return;
        }
        // 更新选中节点
        this.selectedNode = nodeModel;
        // 更新节点选中状态
        this.graphData.nodes.forEach(n => {
          n.selected = n.id === nodeModel.id;
        });
        this.graph.changeData(this.graphData);
      });
      // 画布点击事件,取消选中节点
      this.graph.on('canvas:click', () => {
        this.selectedNode = null;
        this.graphData.nodes.forEach(n => {
          n.selected = false;
        });
        this.graph.changeData(this.graphData);
      });
    },
    handleResize() {
      if (this.graph) {
        const container = document.getElementById('mountNode');
        const width = container.scrollWidth;
        const height = container.scrollHeight || 600;
        this.graph.changeSize(width, height);
      }
    },
    addNode() {
      // 如果没有节点,新增母代
      if (this.graphData.nodes.length === 0) {
        this.$refs.pedigreeForm.validate((valid) => {
          if (valid) {
            this.$refs.parentForm.openInitData({
              strainName: this.form.strainName,
              strainNo: this.form.strainNo,
              strainSource: this.form.strainSource,
              generation: this.form.generation,
            });
          }
        })
        return
      }
      // 如果选中了传代计划数节点,新增下一代
      if (this.selectedNode && this.selectedNode.label === '传代计划数') {
        const nodeModel = this.selectedNode;
        // 检查是否已达到计划数
        if (nodeModel.currentCount >= nodeModel.planCount) {
          this.$message.warning('已达到计划数,不能再添加');
          return;
        }
        // 获取父节点
        const parentEdge = this.graphData.edges.find(e => e.target === nodeModel.id);
        const parentNode = this.graphData.nodes.find(n => n.id === parentEdge.source);
        // 如果父节点是孙代,不允许添加
        if (parentNode.label === '孙代') {
          this.$message.warning('孙代节点不能再生成下一代');
          return;
        }
        const isParent = parentNode.label === '母代';
        const nextLevel = isParent ? '子代' : '孙代';
        this.showDiscarded = true;
        this.isAddingNode = true;
        this.$refs.addSublevelForm.openInitData({
          title: `新增${nextLevel}`,
          form: {
            strainName: this.form.strainName,
            strainNo: this.form.strainNo,
            isDiscarded: true
          }
        })
      } else {
        this.$message.warning('请选择传代计划数节点');
      }
    },
    handleAddParent(value) {
      const parentId = `parent-${++this.nodeCount}`;
      this.graphData.nodes.push({
        id: parentId,
        label: '母代',
        number: value.strainNo.trim(),
        data: value,
        isDiscarded: true,
        x: 200,
        y: 200,
        style: {
          fill: '#00B5AA',
        },
      });
      this.graph.changeData(this.graphData);
      this.$message.success('母代节点添加成功');
      this.$refs.parentForm.closeDialog();
    },
    handleDialogClose() {
      this.$refs.form.resetFields();
    },
    handleAddSublevel(value) {
      if (this.isAddingNode) {
        // 新增节点的处理逻辑
        const nodeModel = this.selectedNode;
        const parentEdge = this.graphData.edges.find(e => e.target === nodeModel.id);
        const parentNode = this.graphData.nodes.find(n => n.id === parentEdge.source);
        const isParent = parentNode.label === '母代';
        const nextLevel = isParent ? '子代' : '孙代';
        const childId = `child-${++this.nodeCount}`;
        this.graphData.nodes.push({
          id: childId,
          label: nextLevel,
          number: value.inoculateNo.trim(),
          isDiscarded: value.isDiscarded,
          data: value,
          style: {
            fill: value.isDiscarded ? '#999' : '#00B5AA',
            opacity: value.isDiscarded ? 0.3 : (isParent ? 0.6 : 0.4),
          },
        });
        this.graphData.edges.push({
          source: nodeModel.id,
          target: childId,
          style: {
            stroke: 'rgba(4, 156, 154, 1)',
            lineWidth: 1,
          },
        });
        const nodeIndex = this.graphData.nodes.findIndex(n => n.id === nodeModel.id);
        this.graphData.nodes[nodeIndex].currentCount++;
        this.graph.changeData(this.graphData);
        this.$message.success(`${nextLevel}添加成功`);
        this.$refs.addSublevelForm.closeDialog();
        this.isAddingNode = false;
      } else {
        // 编辑节点的处理逻辑
        const nodeModel = this.selectedNode;
        const nodeIndex = this.graphData.nodes.findIndex(n => n.id === nodeModel.id);
        if (nodeIndex > -1) {
          if (nodeModel.label === '传代计划数') {
            this.graphData.nodes[nodeIndex].planCount = parseInt(value.value);
          } else {
            this.graphData.nodes[nodeIndex].number = value.value.trim();
            if (this.showDiscarded) {
              this.graphData.nodes[nodeIndex].isDiscarded = value.isDiscarded;
              // 如果设置为废弃状态,同时废弃所有子节点
              if (value.isDiscarded) {
                const discardChildren = (parentId) => {
                  const childEdges = this.graphData.edges.filter(e => e.source === parentId);
                  childEdges.forEach(edge => {
                    const childNode = this.graphData.nodes.find(n => n.id === edge.target);
                    if (childNode) {
                      const childIndex = this.graphData.nodes.findIndex(n => n.id === childNode.id);
                      if (childIndex > -1) {
                        this.graphData.nodes[childIndex].isDiscarded = true;
                        this.graphData.nodes[childIndex].style.fill = '#999';
                        this.graphData.nodes[childIndex].style.opacity = 0.3;
                        // 递归处理子节点的子节点
                        discardChildren(childNode.id);
                      }
                    }
                  });
                };
                discardChildren(nodeModel.id);
              }
            }
          }
          this.graph.changeData(this.graphData);
          this.$message.success('修改成功');
          this.dialogVisible = false;
        }
      }
    },
    setGenerationPlan() {
      if (!this.selectedNode) {
        this.$message.warning('请先选择节点');
        return;
      }
      const nodeModel = this.selectedNode;
      if (nodeModel.label === '孙代') {
        this.$message.warning('孙代节点不能再生成传代计划数');
        return;
      }
      if (nodeModel.label === '传代计划数') {
        this.$message.warning('传代计划数节点不能再设置计划数');
        return;
      }
      const hasGenerationNode = this.graphData.edges.some(e =>
        e.source === nodeModel.id &&
        this.graphData.nodes.some(n => n.id === e.target && n.label === '传代计划数')
      );
      if (hasGenerationNode) {
        this.$message.warning('该节点已经存在传代计划数节点');
        return;
      }
      if (nodeModel.label === '子代') {
        this.$refs.addSublevelPlan.openInitData({
          ...nodeModel.data,
          label: nodeModel.label,
          strainName: this.form.strainName,
          strainNo: this.form.strainNo,
          strainSource: this.form.strainSource,
          generation: this.form.generation,
        })
      } else {
        this.$refs.planForm.openInitData({
          ...nodeModel.data,
          label: nodeModel.label,
          strainName: this.form.strainName,
          strainNo: this.form.strainNo,
          strainSource: this.form.strainSource,
          generation: this.form.generation,
        })
      }
    },
    handleAddPlan(value) {
      const nodeModel = this.selectedNode;
      const generationId = `generation-${++this.nodeCount}`;
      this.graphData.nodes.push({
        id: generationId,
        label: '传代计划数',
        planCount: value.count,
        data: value,
        isDiscarded: true,
        currentCount: 0,
        style: {
          fill: '#00B5AA',
        },
      });
      this.graphData.edges.push({
        source: nodeModel.id,
        target: generationId,
        style: {
          stroke: 'rgba(4, 156, 154, 1)',
          lineWidth: 1,
        },
      });
      this.graph.changeData(this.graphData);
      this.$message.success('传代计划数设置成功');
      this.$refs.planForm.closeDialog()
    },
    showDetail() {
      if (!this.selectedNode) {
        this.$message.warning('请先选择节点');
        return;
      }
      const nodeModel = this.selectedNode;
      if (nodeModel.label === '母代') {
        this.$refs.parentForm.openInitData({ ...nodeModel.data, status: 'detail' });
      } else if (nodeModel.label === '子代' || nodeModel.label === '孙代') {
        this.dialogTitle = `${nodeModel.label}详情`;
        this.$refs.addSublevelForm.openInitData({
          title: `${nodeModel.label}详情`,
          form: { ...nodeModel.data }
        })
      } else if (nodeModel.label === '传代计划数') {
        const nodeIndex = this.graphData.nodes.findIndex(n => n.id === nodeModel.id);
        if (this.graphData.nodes[nodeIndex - 1].label === '子代') {
          this.$refs.addSublevelPlan.openInitData({ ...nodeModel.data, status: 'detail' });
        } else {
          this.$refs.planForm.openInitData({ ...nodeModel.data, status: 'detail' });
        }
      }
    }
  },
};
</script>
@@ -135,26 +736,31 @@
  display: flex;
  align-items: center;
}
.chart {
  padding: 20px 38px;
  background: rgba(255,255,255,0.8);
  box-shadow: 0px 10px 19px 0px rgba(0,0,0,0.06);
  background: rgba(255, 255, 255, 0.8);
  box-shadow: 0px 10px 19px 0px rgba(0, 0, 0, 0.06);
  border-radius: 16px;
  border: 4px solid #FFFFFF;
  margin-top: 30px;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .title {
        font-size: 18px;
      font-size: 18px;
    }
    .option-btn {
        display: flex;
        gap: 10px;
        .el-button {
          margin-left: 0;
        }
      display: flex;
      gap: 10px;
      .el-button {
        margin-left: 0;
      }
    }
  }
}
@@ -253,4 +859,28 @@
  flex-wrap: wrap;
  gap: 10px;
}
</style>
.strain-flow-chart {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .toolbar {
    margin-bottom: 16px;
    display: flex;
    gap: 12px;
    .el-button {
      margin-right: 0;
    }
  }
  #mountNode {
    flex: 1;
    width: 100%;
    min-height: 500px;
    border-radius: 4px;
  }
}
</style>
culture/src/views/pedigree-chart/addProgenitor.vue
New file
@@ -0,0 +1,801 @@
<template>
  <div>
    <el-form :model="form" :rules="rules" ref="pedigreeForm" label-position="top" class="strain-form">
      <div class="card">
        <div class="form-items-row">
          <el-form-item label="传代菌种编号" prop="strainNo" required>
            <el-input v-model="form.strainNo" placeholder="请输入" class="fixed-width-input"></el-input>
          </el-form-item>
          <el-form-item label="传代菌种名称" prop="strainName" required>
            <el-input v-model="form.strainName" placeholder="请输入" class="fixed-width-input"></el-input>
          </el-form-item>
        </div>
      </div>
      <div class="card" style="margin-top: 30px;">
        <Table :height="null" :total="0" :tableData="tableData">
          <el-table-column label="菌株类型" prop="strainSource" />
          <el-table-column label="来源获得/菌落编号" prop="strainNo" />
          <el-table-column label="菌种编号" prop="strainName" />
          <el-table-column label="菌种名称" prop="strainName" />
          <el-table-column label="入库总数" prop="strainName" />
          <el-table-column label="保存/废弃" prop="strainName" />
          <el-table-column label="菌种入库时间" prop="strainName" />
          <el-table-column label="接种操作人" prop="strainName" />
          <el-table-column label="操作">
            <template #default="{ row }">
              <el-button type="text">确认入库</el-button>
            </template>
          </el-table-column>
        </Table>
      </div>
      <div class="chart">
        <div class="header">
          <div class="title">菌种传代生产谱系图</div>
          <div class="option-btn">
            <el-button type="primary" class="el-icon-plus" @click="addNode"> 新增</el-button>
            <el-button type="primary" @click="setGenerationPlan">设置传代计划数</el-button>
            <el-button type="primary" @click="showDetail">详情</el-button>
          </div>
        </div>
        <div class="strain-flow-chart">
          <div id="mountNode"></div>
        </div>
      </div>
      <div class="end-btn">
        <el-button type="primary" @click="handleSubmit">提交</el-button>
        <el-button @click="handleDraft">存草稿</el-button>
        <el-button @click="handleCancel">取消</el-button>
      </div>
      <!-- 签字确认组件 -->
      <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </el-form>
    <AddAncestor ref="addAncestor" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
  </div>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import G6 from '@antv/g6';
import PlanForm from "./progenitorComponents/PlanForm.vue";
import AddAncestor from "./progenitorComponents/AddAncestor.vue";
import AddSublevelForm from "./progenitorComponents/AddSublevelForm.vue";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
    PlanForm,
    AddAncestor,
    AddSublevelForm
  },
  data() {
    return {
      signatureVisible: false,
      form: {
        strainSource: "",
        generation: "",
        cellBank: "",
        strainNo: "",
        strainName: "",
        remarks: "",
      },
      rules: {
        strainSource: [
          { required: true, message: "请输入菌种源", trigger: "blur" },
        ],
        strainNo: [
          { required: true, message: "请输入传代菌种编号", trigger: "blur" },
        ],
        strainName: [
          { required: true, message: "请输入传代菌种名称", trigger: "blur" },
        ],
      },
      graph: null,
      nodeCount: 0,
      selectedNode: null,
      graphData: {
        nodes: [],
        edges: []
      },
      // 弹窗相关数据
      dialogVisible: false,
      dialogTitle: '',
      formLabel: '',
      inputType: 'text',
      showDiscarded: false,
      isAddingNode: false,
      nodeData: {},
      nodeType: '',//1祖代 2计划数 3母代
    };
  },
  computed: {
    canAddNode() {
      // 如果没有节点,可以新增母代
      if (this.graphData.nodes.length === 0) {
        return true;
      }
      // 如果选中了传代计划数节点,可以新增下一代
      if (this.selectedNode && this.selectedNode.label === '传代计划数') {
        return true;
      }
      return false;
    }
  },
  mounted() {
    this.initGraph();
    this.initEvents();
  },
  beforeDestroy() {
    this.graph?.destroy();
    window.removeEventListener('resize', this.handleResize);
  },
  methods: {
    addNodeSign(value, type) {
      this.nodeData = value
      this.nodeType = type
      this.signatureVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.signatureVisible = true;
        }
      });
    },
    handleDraft() {
      // 实现存草稿逻辑
      console.log("save draft", this.form);
    },
    handleCancel() {
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      console.log("submit form with signature:", signatureImage);
      if (this.nodeType === 1) {
        this.handleAddParent(this.nodeData)
      } else if (this.nodeType === 2) {
        this.handleAddPlan(this.nodeData)
      } else if (this.nodeType === 3) {
        this.handleAddSublevel(this.nodeData)
      }
      // 处理提交逻辑
    },
    addNode() {
      // 如果没有节点,新增祖代
      if (this.graphData.nodes.length === 0) {
        this.$refs.addAncestor.openInitData({
          strainName: this.form.strainName,
          strainNo: this.form.strainNo,
          status: 'add',
          activeType: null,
          isDiscarded: true,
        });
        return
      }
      // 如果选中了传代计划数节点,新增母代
      if (this.selectedNode && this.selectedNode.label === '传代计划数') {
        const nodeModel = this.selectedNode;
        // 检查是否已达到计划数
        if (nodeModel.currentCount >= nodeModel.planCount) {
          this.$message.warning('已达到计划数,不能再添加');
          return;
        }
        // 获取父节点
        const parentEdge = this.graphData.edges.find(e => e.target === nodeModel.id);
        const parentNode = this.graphData.nodes.find(n => n.id === parentEdge.source);
        // 如果父节点是母代,不允许添加
        if (parentNode.label === '母代') {
          this.$message.warning('母代节点不能再生成下一代');
          return;
        }
        this.showDiscarded = true;
        this.isAddingNode = true;
        this.$refs.addSublevelForm.openInitData({
          title: '新增菌种传代项',
          form: {
            strainName: this.form.strainName,
            strainNo: this.form.strainNo,
            isDiscarded: true
          }
        })
      } else {
        this.$message.warning('请选择传代计划数节点');
      }
    },
    handleAddParent(value) {
      const parentId = `parent-${++this.nodeCount}`;
      this.graphData.nodes.push({
        id: parentId,
        label: '祖代',
        number: value.inoculateNo.trim(),
        data: value,
        isDiscarded: true,
        x: 200,
        y: 200,
        style: {
          fill: '#00B5AA',
        },
      });
      this.graph.changeData(this.graphData);
      this.$message.success('祖代节点添加成功');
      this.$refs.addAncestor.closeDialog();
    },
    handleDialogClose() {
      this.$refs.form.resetFields();
    },
    handleAddSublevel(value) {
      if (this.isAddingNode) {
        // 新增节点的处理逻辑
        const nodeModel = this.selectedNode;
        const childId = `child-${++this.nodeCount}`;
        this.graphData.nodes.push({
          id: childId,
          label: '母代',
          number: value.inoculateNo.trim(),
          isDiscarded: value.isDiscarded,
          data: value,
          style: {
            fill: value.isDiscarded ? '#999' : '#00B5AA',
            opacity: value.isDiscarded ? 0.3 : 0.6,
          },
        });
        this.graphData.edges.push({
          source: nodeModel.id,
          target: childId,
          style: {
            stroke: 'rgba(4, 156, 154, 1)',
            lineWidth: 1,
          },
        });
        const nodeIndex = this.graphData.nodes.findIndex(n => n.id === nodeModel.id);
        this.graphData.nodes[nodeIndex].currentCount++;
        this.graph.changeData(this.graphData);
        this.$message.success('母代添加成功');
        this.$refs.addSublevelForm.closeDialog();
        this.isAddingNode = false;
      } else {
        // 编辑节点的处理逻辑
        const nodeModel = this.selectedNode;
        const nodeIndex = this.graphData.nodes.findIndex(n => n.id === nodeModel.id);
        if (nodeIndex > -1) {
          if (nodeModel.label === '传代计划数') {
            this.graphData.nodes[nodeIndex].planCount = parseInt(value.value);
          } else {
            this.graphData.nodes[nodeIndex].number = value.value.trim();
            if (this.showDiscarded) {
              this.graphData.nodes[nodeIndex].isDiscarded = value.isDiscarded;
              // 如果设置为废弃状态,同时废弃所有子节点
              if (value.isDiscarded) {
                const discardChildren = (parentId) => {
                  const childEdges = this.graphData.edges.filter(e => e.source === parentId);
                  childEdges.forEach(edge => {
                    const childNode = this.graphData.nodes.find(n => n.id === edge.target);
                    if (childNode) {
                      const childIndex = this.graphData.nodes.findIndex(n => n.id === childNode.id);
                      if (childIndex > -1) {
                        this.graphData.nodes[childIndex].isDiscarded = true;
                        this.graphData.nodes[childIndex].style.fill = '#999';
                        this.graphData.nodes[childIndex].style.opacity = 0.3;
                        // 递归处理子节点的子节点
                        discardChildren(childNode.id);
                      }
                    }
                  });
                };
                discardChildren(nodeModel.id);
              }
            }
          }
          this.graph.changeData(this.graphData);
          this.$message.success('修改成功');
          this.dialogVisible = false;
        }
      }
    },
    setGenerationPlan() {
      if (!this.selectedNode) {
        this.$message.warning('请先选择节点');
        return;
      }
      const nodeModel = this.selectedNode;
      if (nodeModel.label === '母代') {
        this.$message.warning('母代节点不能再生成传代计划数');
        return;
      }
      if (nodeModel.label === '传代计划数') {
        this.$message.warning('传代计划数节点不能再设置计划数');
        return;
      }
      const hasGenerationNode = this.graphData.edges.some(e =>
        e.source === nodeModel.id &&
        this.graphData.nodes.some(n => n.id === e.target && n.label === '传代计划数')
      );
      if (hasGenerationNode) {
        this.$message.warning('该节点已经存在传代计划数节点');
        return;
      }
      this.$refs.planForm.openInitData({
        ...nodeModel.data,
        label: nodeModel.label,
        strainName: this.form.strainName,
        strainNo: this.form.strainNo,
      })
    },
    handleAddPlan(value) {
      const nodeModel = this.selectedNode;
      const generationId = `generation-${++this.nodeCount}`;
      this.graphData.nodes.push({
        id: generationId,
        label: '传代计划数',
        planCount: value.count,
        data: value,
        isDiscarded: true,
        currentCount: 0,
        style: {
          fill: '#00B5AA',
        },
      });
      this.graphData.edges.push({
        source: nodeModel.id,
        target: generationId,
        style: {
          stroke: 'rgba(4, 156, 154, 1)',
          lineWidth: 1,
        },
      });
      this.graph.changeData(this.graphData);
      this.$message.success('传代计划数设置成功');
      this.$refs.planForm.closeDialog()
    },
    showDetail() {
      if (!this.selectedNode) {
        this.$message.warning('请先选择节点');
        return;
      }
      const nodeModel = this.selectedNode;
      if (nodeModel.label === '祖代') {
        this.$refs.addAncestor.openInitData({ ...nodeModel.data, status: 'detail' });
      } else if (nodeModel.label === '母代') {
        this.dialogTitle = '母代详情';
        this.$refs.addSublevelForm.openInitData({
          title: '母代详情',
          form: { ...nodeModel.data }
        })
      } else if (nodeModel.label === '传代计划数') {
        this.$refs.planForm.openInitData({ ...nodeModel.data, status: 'detail' });
      }
    },
    initGraph() {
      const container = document.getElementById('mountNode');
      const width = container.scrollWidth;
      const height = container.scrollHeight || 600;
      // 自定义节点
      G6.registerNode('custom-node', {
        draw(cfg, group) {
          const width = 120;
          const titleHeight = 30;
          const contentHeight = 40;
          const gap = 4;
          const totalHeight = titleHeight + gap + contentHeight;
          // 根据节点状态设置颜色
          const isDiscarded = !cfg.isDiscarded;
          const titleFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'l(0) 0:#0ACBCA 1:#049C9A' : 'l(0) 0:#0ACBCA 1:#049C9A');
          const contentFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'rgba(4,156,154,0.2)' : 'rgba(4,156,154,0.1)');
          const textFill = isDiscarded ? 'rgba(144, 147, 153, 1)' : '#049C9A';
          const stroke = isDiscarded ? '#DCDFE6' : (cfg.selected ? '#049C9A' : 'transparent');
          // 创建渐变
          const gradient = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2,
              width: width,
              height: titleHeight,
              radius: 20,
              fill: titleFill,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'title-box',
          });
          // 下部分 - 内容背景
          const contentBox = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2 + titleHeight + gap,
              width: width,
              height: contentHeight,
              fill: contentFill,
              radius: 15,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'content-box',
          });
          // 标题文本
          if (cfg.label) {
            group.addShape('text', {
              attrs: {
                text: cfg.label,
                x: 0,
                y: -totalHeight / 2 + titleHeight / 2,
                fill: isDiscarded ? 'rgba(144, 147, 153, 1)' : '#fff',
                fontSize: 12,
                textAlign: 'center',
                textBaseline: 'middle',
                fontWeight: 'bold',
                cursor: 'move',
              },
              name: 'title-text',
            });
          }
          // 内容文本
          let content = '';
          if (cfg.label === '传代计划数') {
            content = `${cfg.planCount || 0}`;
          } else if (cfg.number) {
            content = cfg.label === '母代' ? `代传菌种编号:${cfg.number}` : `接种菌种编号:${cfg.number}`;
          }
          if (content) {
            group.addShape('text', {
              attrs: {
                text: content,
                x: 0,
                y: -totalHeight / 2 + titleHeight + gap + contentHeight / 2,
                fill: textFill,
                fontSize: 10,
                textAlign: 'center',
                textBaseline: 'middle',
                cursor: 'move',
              },
              name: 'content-text',
            });
          }
          return gradient;
        },
        getAnchorPoints() {
          return [
            [0.5, 0], // 上
            [1, 0.5], // 右
            [0.5, 1], // 下
            [0, 0.5], // 左
          ];
        },
        setState(name, value, item) {
          // 移除悬浮效果,保持节点样式始终一致
        },
      });
      this.graph = new G6.Graph({
        container: 'mountNode',
        width,
        height,
        fitView: true,
        fitViewPadding: 30,
        animate: false,
        enabledStack: false,
        renderer: 'canvas',
        minZoom: 0.3,
        maxZoom: 2,
        defaultZoom: 1,
        layout: {
          type: 'dagre',
          rankdir: 'LR',
          align: 'UL',
          nodesep: 30,  // 减小节点间距
          ranksep: 50,  // 减小层级间距
          controlPoints: true,
        },
        modes: {
          default: [
            {
              type: 'drag-canvas',
              enableOptimize: true,
              direction: 'both',
              scalableRange: 0.1,
              dragTimesOfScale: 0.1,
              onlyChangeComputeZoom: true,
            },
            {
              type: 'zoom-canvas',
              sensitivity: 1.5,
              enableOptimize: true,
            },
            {
              type: 'drag-node',
              enableDelegate: true,
              delegateStyle: {
                fill: '#f3f3f3',
                stroke: '#ccc',
                opacity: 0.5,
              },
              updateEdge: false,
              enableOptimize: true,
              optimizeZoom: 0.7,
              damping: 0.1,
            }
          ]
        },
        defaultNode: {
          type: 'custom-node',
          style: {
            fill: 'l(0) 0:#0ACBCA 1:#049C9A',
          },
        },
        defaultEdge: {
          type: 'cubic-horizontal',
          style: {
            stroke: 'rgba(4, 156, 154, 1)',
            lineWidth: 1,
            opacity: 0.5,
            endArrow: {
              path: G6.Arrow.triangle(6, 6),
              fill: 'rgba(4, 156, 154, 1)',
              stroke: 'rgba(4, 156, 154, 1)',
            },
          },
        },
        optimizeEdge: true,
        optimizeLayoutAnimation: true,
      });
      const canvas = this.graph.get('canvas');
      canvas.set('localRefresh', false);
      canvas.set('autoDraw', true);
      canvas.set('animating', false);
      let throttleTimer = null;
      const throttleInterval = 16;
      this.graph.on('node:dragstart', () => {
        canvas.set('localRefresh', false);
        this.graph.get('canvas').draw();
      });
      this.graph.on('node:drag', (e) => {
        if (throttleTimer) return;
        throttleTimer = setTimeout(() => {
          const model = e.item.get('model');
          const edges = this.graph.getEdges().filter(edge => {
            const source = edge.getSource();
            const target = edge.getTarget();
            return source.get('id') === model.id || target.get('id') === model.id;
          });
          edges.forEach(edge => {
            this.graph.refreshItem(edge);
          });
          throttleTimer = null;
        }, throttleInterval);
      });
      this.graph.on('node:dragend', (e) => {
        if (throttleTimer) {
          clearTimeout(throttleTimer);
          throttleTimer = null;
        }
        const model = e.item.get('model');
        const edges = this.graph.getEdges().filter(edge => {
          const source = edge.getSource();
          const target = edge.getTarget();
          return source.get('id') === model.id || target.get('id') === model.id;
        });
        edges.forEach(edge => {
          this.graph.refreshItem(edge);
        });
        canvas.set('localRefresh', true);
        this.graph.get('canvas').draw();
      });
      this.graph.data(this.graphData);
      this.graph.render();
      let debounceTimer = null;
      this.graph.on('afterchange', () => {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
          if (!canvas.get('destroyed')) {
            canvas.draw();
          }
        }, 16);
      });
    },
    initEvents() {
      // 监听窗口大小变化
      window.addEventListener('resize', this.handleResize);
      // 节点点击事件
      this.graph.on('node:click', (evt) => {
        const node = evt.item;
        const nodeModel = node.getModel();
        // 如果节点已废弃,不允许任何操作
        if (!nodeModel.isDiscarded) {
          this.$message.warning('该节点已废弃,不能进行操作');
          return;
        }
        // 更新选中节点
        this.selectedNode = nodeModel;
        // 更新节点选中状态
        this.graphData.nodes.forEach(n => {
          n.selected = n.id === nodeModel.id;
        });
        this.graph.changeData(this.graphData);
      });
      // 画布点击事件,取消选中节点
      this.graph.on('canvas:click', () => {
        this.selectedNode = null;
        this.graphData.nodes.forEach(n => {
          n.selected = false;
        });
        this.graph.changeData(this.graphData);
      });
    },
    handleResize() {
      if (this.graph) {
        const container = document.getElementById('mountNode');
        const width = container.scrollWidth;
        const height = container.scrollHeight || 600;
        this.graph.changeSize(width, height);
      }
    },
  },
};
</script>
<style scoped lang="less">
.card {
  min-height: 145px;
  background: rgba(255, 255, 255, 0.8);
  box-shadow: 0px 10px 19px 0px rgba(0, 0, 0, 0.06);
  border-radius: 16px;
  border: 4px solid #ffffff;
  padding: 0 20px;
  display: flex;
  align-items: center;
}
.chart {
  padding: 20px 38px;
  background: rgba(255, 255, 255, 0.8);
  box-shadow: 0px 10px 19px 0px rgba(0, 0, 0, 0.06);
  border-radius: 16px;
  border: 4px solid #FFFFFF;
  margin-top: 30px;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .title {
      font-size: 18px;
    }
    .option-btn {
      display: flex;
      gap: 10px;
      .el-button {
        margin-left: 0;
      }
    }
  }
}
.form-items-row {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  align-items: center;
  @media (min-width: 1200px) {
    flex-direction: row;
  }
  @media (max-width: 1199px) {
    flex-direction: column;
    align-items: flex-start;
  }
}
.strain-form {
  width: 100%;
  :deep(.el-form-item) {
    margin-bottom: 15px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    @media (min-width: 1200px) {
      margin-right: 10px;
    }
    @media (max-width: 1199px) {
      width: 100%;
      margin-right: 0;
    }
    .el-form-item__label {
      padding: 0 0 8px;
      line-height: 1.2;
      text-align: left;
    }
    .el-form-item__content {
      width: 100%;
    }
  }
}
.end-btn {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.strain-flow-chart {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .toolbar {
    margin-bottom: 16px;
    display: flex;
    gap: 12px;
    .el-button {
      margin-right: 0;
    }
  }
  #mountNode {
    flex: 1;
    width: 100%;
    min-height: 500px;
    border-radius: 4px;
  }
}
</style>
culture/src/views/pedigree-chart/components/AddSublevelForm.vue
New file
@@ -0,0 +1,167 @@
<template>
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%" @close="closeDialog"
        :close-on-click-modal="false">
        <el-form :model="form" :rules="rules" ref="form" label-position="top">
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人" prop="thisName">
                        <el-input disabled v-model="form.thisName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种操作时间" prop="thisTime">
                        <el-input disabled v-model="form.thisTime"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种编号" prop="strainNo">
                        <el-input disabled v-model="form.strainNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="form.strainName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种编号" prop="inoculateNo">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateNo"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种名称" prop="inoculateName">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateName"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-form-item label="保存/废弃" required>
                <div class="flex-row">
                    <div @click="handleStatus('save')" :class="form.isDiscarded && 'active'">保存</div>
                    <div @click="handleStatus('discard')" :class="!form.isDiscarded && 'active'">废弃</div>
                </div>
            </el-form-item>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item v-if="!form.isDiscarded" label="废弃原因说明" required>
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.discardReason"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="菌种入库时间" prop="inTime">
                        <el-input disabled v-model="form.inTime"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row v-if="!dialogTitle.includes('新增')" :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种保藏人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="dialogTitle.includes('新增')" class="dialog-footer">
            <el-button type="primary" @click="handleSubmit">提交</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            dialogVisible: false,
            dialogTitle: '',
            form: {
                isDiscarded: true
            },
            rules: {
                isDiscarded: [
                    { required: true, message: '请选择废弃状态', trigger: 'blur' }
                ],
                inoculateNo: [
                    { required: true, message: '请输入接种菌种编号', trigger: 'blur' }
                ],
                inoculateName: [
                    { required: true, message: '请输入接种菌种名称', trigger: 'blur' }
                ],
            },
        }
    },
    methods: {
        openInitData(value) {
            this.dialogTitle = value.title
            this.form = value.form
            this.dialogVisible = true
        },
        closeDialog() {
            this.dialogVisible = false
        },
        handleSubmit() {
            this.$emit('addNodeSign', this.form, 3)
        },
        handleStatus(status) {
            if (!this.dialogTitle.includes('新增')) return
            this.form.isDiscarded = status === 'save'
            this.$forceUpdate()
        }
    }
}
</script>
<style scoped lang="less">
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
.flex-row {
    width: 370px;
    display: flex;
    align-items: center;
    font-size: 16px;
    color: #333333;
    padding: 4px;
    border-radius: 10px;
    border: 2px solid rgba(4, 156, 154, 0.5);
    font-family: 'PingFangSCRegular';
    .flex-row-save {
        background: #049C9A;
        color: #fff;
    }
    div {
        width: 183px;
        height: 32px;
        text-align: center;
        flex-shrink: 0;
        cursor: pointer;
    }
    .active {
        font-family: 'SourceHanSansCN-Medium';
        color: #049C9A;
        background: #EBFEFD;
        box-shadow: 0px 0px 6px 0px rgba(10, 109, 108, 0.25);
        border-radius: 10px;
    }
}
</style>
culture/src/views/pedigree-chart/components/AddSublevelPlan.vue
New file
@@ -0,0 +1,190 @@
<template>
    <!-- 设置传代计划数弹窗 -->
    <el-dialog :title="planForm.status === 'detail' ? '传代计划数详情' : '设置菌种传代计划数'" :visible.sync="planDialogVisible"
        width="40%" :close-on-click-modal="false">
        <el-form :model="planForm" :rules="planRules" ref="planForm" label-position="top">
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="传代菌种编号" prop="strainNo">
                        <el-input disabled v-model="planForm.strainNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="planForm.strainName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种编号" prop="inoculateNo">
                        <el-input disabled v-model="planForm.inoculateNo" placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种名称" prop="inoculateName">
                        <el-input disabled v-model="planForm.inoculateName" placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="保存/废弃">
                        <div class="flex-row">
                            <div :class="planForm.isDiscarded && 'active'">保存</div>
                            <div :class="!planForm.isDiscarded && 'active'">废弃</div>
                        </div>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="菌种入库时间">
                        <el-input disabled v-model="planForm.inTime"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="传代计划数" prop="count">
                        <el-input-number :disabled="planForm.status === 'detail'" v-model="planForm.count"
                            :controls="false" :min="1" placeholder="请输入" />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="planForm.status !== 'detail'" class="dialog-footer">
            <el-button type="primary" @click="handleAddPlan">提交签字</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            planDialogVisible: false,
            planForm: {
                status: 'add',
            },
            planRules: {
                count: [
                    { required: true, message: '请输入传代计划数', trigger: 'blur' }
                ]
            }
        }
    },
    methods: {
        openInitData(value) {
            this.planForm = value
            this.openDialog()
        },
        openDialog() {
            this.planDialogVisible = true
        },
        closeDialog() {
            this.planDialogVisible = false
        },
        handleAddPlan() {
            this.$refs.planForm.validate((valid) => {
                if (valid) {
                    this.$emit('addNodeSign', this.planForm, 2)
                }
            })
        }
    }
}
</script>
<style scoped lang="less">
.flex-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    @media (max-width: 768px) {
        flex-direction: column;
        align-items: flex-start;
        width: 100%;
    }
}
.input-wrapper {
    @media (min-width: 769px) {
        width: 290px;
        min-width: 290px;
    }
    @media (max-width: 768px) {
        width: 100%;
    }
}
.fixed-width-input {
    width: 100%;
    @media (min-width: 769px) {
        width: 290px !important;
        min-width: 290px !important;
    }
}
.form-text {
    margin: 0 8px;
    white-space: nowrap;
    @media (max-width: 768px) {
        margin: 8px 0;
    }
}
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
::v-deep .el-input-number .el-input__inner {
    text-align: left;
}
.el-input-number--small {
    width: 100%;
}
.flex-row {
    width: 370px;
    display: flex;
    align-items: center;
    font-size: 16px;
    color: #333333;
    padding: 4px;
    border-radius: 10px;
    border: 2px solid rgba(4, 156, 154, 0.5);
    font-family: 'PingFangSCRegular';
    .flex-row-save {
        background: #049C9A;
        color: #fff;
    }
    div {
        width: 183px;
        height: 32px;
        text-align: center;
        flex-shrink: 0;
        cursor: pointer;
    }
    .active {
        font-family: 'SourceHanSansCN-Medium';
        color: #049C9A;
        background: #EBFEFD;
        box-shadow: 0px 0px 6px 0px rgba(10, 109, 108, 0.25);
        border-radius: 10px;
    }
}
</style>
culture/src/views/pedigree-chart/components/ParentForm.vue
New file
@@ -0,0 +1,127 @@
<template>
    <!-- 新增母代弹窗 -->
    <el-dialog :title="parentForm.status === 'detail' ? '母代详情' : '新增母代'" :visible.sync="addParentDialogVisible"
        width="40%" :close-on-click-modal="false">
        <el-form :model="parentForm" ref="parentForm" label-position="top">
            <el-form-item label="菌种源" prop="strainSource">
                <div class="flex-row">
                    <div class="input-wrapper">
                        <el-input disabled v-model="parentForm.strainSource" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">代—</span>
                    <div class="input-wrapper">
                        <el-input disabled v-model="parentForm.generation" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">细胞库</span>
                </div>
            </el-form-item>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="代传菌种编号" prop="strainNo">
                        <el-input disabled v-model="parentForm.strainNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="代传菌种名称" prop="strainName">
                        <el-input disabled v-model="parentForm.strainName"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row v-if="parentForm.status === 'detail'" :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种保藏人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="parentForm.status !== 'detail'" class="dialog-footer">
            <el-button type="primary" @click="handleAddParent">提交</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            addParentDialogVisible: false,
            parentForm: {},
        }
    },
    methods: {
        openInitData(value) {
            this.parentForm = value
            this.openDialog()
        },
        openDialog() {
            this.addParentDialogVisible = true
        },
        closeDialog() {
            this.addParentDialogVisible = false
        },
        handleAddParent() {
            this.$emit('addNodeSign', this.parentForm, 1)
        }
    }
}
</script>
<style scoped lang="less">
.flex-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    @media (max-width: 768px) {
        flex-direction: column;
        align-items: flex-start;
        width: 100%;
    }
}
.input-wrapper {
    @media (min-width: 769px) {
        width: 290px;
        min-width: 290px;
    }
    @media (max-width: 768px) {
        width: 100%;
    }
}
.fixed-width-input {
    width: 100%;
    @media (min-width: 769px) {
        width: 290px !important;
        min-width: 290px !important;
    }
}
.form-text {
    margin: 0 8px;
    white-space: nowrap;
    @media (max-width: 768px) {
        margin: 8px 0;
    }
}
.dialog-footer {
    margin-top: 115px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
</style>
culture/src/views/pedigree-chart/components/PlanForm.vue
New file
@@ -0,0 +1,138 @@
<template>
    <!-- 设置传代计划数弹窗 -->
    <el-dialog :title="planForm.status === 'detail' ? '传代计划数详情' : '设置传代计划数'" :visible.sync="planDialogVisible"
        width="40%" :close-on-click-modal="false">
        <el-form :model="planForm" :rules="planRules" ref="planForm" label-position="top">
            <el-form-item label="菌种源" prop="strainSource">
                <div class="flex-row">
                    <div class="input-wrapper">
                        <el-input disabled v-model="planForm.strainSource" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">代—</span>
                    <div class="input-wrapper">
                        <el-input disabled v-model="planForm.generation" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">细胞库</span>
                </div>
            </el-form-item>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="传代菌种编号" prop="strainNo">
                        <el-input disabled v-model="planForm.strainNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="planForm.strainName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代计划数" prop="count">
                        <el-input-number :disabled="planForm.status === 'detail'" v-model="planForm.count"
                            :controls="false" :min="1" placeholder="请输入" />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="planForm.status !== 'detail'" class="dialog-footer">
            <el-button type="primary" @click="handleAddPlan">提交签字</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            planDialogVisible: false,
            planForm: {},
            planRules: {
                count: [
                    { required: true, message: '请输入传代计划数', trigger: 'blur' }
                ]
            }
        }
    },
    methods: {
        openInitData(value) {
            this.planForm = value
            this.openDialog()
        },
        openDialog() {
            this.planDialogVisible = true
        },
        closeDialog() {
            this.planDialogVisible = false
        },
        handleAddPlan() {
            this.$refs.planForm.validate((valid) => {
                if (valid) {
                    this.$emit('addNodeSign', this.planForm, 2)
                }
            })
        }
    }
}
</script>
<style scoped lang="less">
.flex-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    @media (max-width: 768px) {
        flex-direction: column;
        align-items: flex-start;
        width: 100%;
    }
}
.input-wrapper {
    @media (min-width: 769px) {
        width: 290px;
        min-width: 290px;
    }
    @media (max-width: 768px) {
        width: 100%;
    }
}
.fixed-width-input {
    width: 100%;
    @media (min-width: 769px) {
        width: 290px !important;
        min-width: 290px !important;
    }
}
.form-text {
    margin: 0 8px;
    white-space: nowrap;
    @media (max-width: 768px) {
        margin: 8px 0;
    }
}
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
::v-deep .el-input-number .el-input__inner {
    text-align: left;
}
.el-input-number--small {
    width: 100%;
}
</style>
culture/src/views/pedigree-chart/index.vue
@@ -21,47 +21,23 @@
      <template #setting>
        <div class="tableTitle">
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'list' }"
              @click="handleTypeChange('list')"
            >
            <div class="title" :class="{ active: currentType === 'list' }" @click="handleTypeChange('list')">
              菌种选育保藏记录列表
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              @click="handleTypeChange('draft')"
            >
            <div class="drafts" :class="{ active: currentType === 'draft' }" @click="handleTypeChange('draft')">
              草稿箱
            </div>
          </div>
          <div class="flex a-center">
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              style="margin-right: 12px"
              >新增祖代起传</el-button
            >
            <el-button
              @click="handleBatchAdd"
              class="el-icon-plus"
              type="primary"
              >新增母代起传</el-button
            >
            <el-button @click="handleNewStrain" class="el-icon-plus" type="primary"
              style="margin-right: 12px">新增祖代起传</el-button>
            <el-button @click="handleBatchAdd" class="el-icon-plus" type="primary">新增母代起传</el-button>
          </div>
        </div>
      </template>
      <template #table>
        <el-table-column
          prop="planCode"
          label="项目课题方案编号"
        ></el-table-column>
        <el-table-column
          prop="planName"
          label="项目课题方案名称"
        ></el-table-column>
        <el-table-column prop="planCode" label="项目课题方案编号"></el-table-column>
        <el-table-column prop="planName" label="项目课题方案名称"></el-table-column>
        <el-table-column prop="stage" label="起传类型"></el-table-column>
        <el-table-column prop="creator" label="菌种源"></el-table-column>
        <el-table-column prop="createTime" label="菌种编号"></el-table-column>
@@ -70,15 +46,11 @@
        <el-table-column prop="approver" label="创建人"></el-table-column>
        <el-table-column label="操作" width="250">
          <template slot-scope="scope">
            <el-button type="text" @click="handleDetail(scope.row)"
              >详情</el-button
            >
            <el-button type="text" @click="handleEdit(scope.row)"
              >编辑</el-button
            >
            <el-button type="text" @click="handleDelete(scope.row)"
              >删除</el-button
            >
            <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
            <!-- 菌种超级管理员 -->
            <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
            <!-- 菌种超级管理员 -->
            <el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
@@ -167,6 +139,11 @@
    this.getTableData();
  },
  methods: {
    handleBatchAdd() {
      this.$router.push({
        path: "/strain/add-pedigree",
      });
    },
    resetForm() {
      this.form = {
        planName: "",
@@ -179,7 +156,7 @@
    },
    handleNewStrain() {
      this.$router.push({
        path: "/strain/add-pedigree",
        path: "/strain/add-progenitor",
      });
    },
    handleSearch() {
@@ -269,15 +246,18 @@
.list {
  height: 100%;
}
.flex {
  display: flex;
  align-items: center;
}
.tableTitle {
  display: flex;
  padding-bottom: 20px;
  justify-content: space-between;
  align-items: center;
  .title {
    background: #fafafc;
    border-radius: 8px 8px 0px 0px;
@@ -289,6 +269,7 @@
    width: unset;
    cursor: pointer;
  }
  .drafts {
    padding: 16px 65px;
    background: #fafafc;
@@ -300,6 +281,7 @@
    margin-left: 16px;
    cursor: pointer;
  }
  .active {
    color: #049c9a;
    background: #ffffff;
culture/src/views/pedigree-chart/progenitorComponents/AddAncestor.vue
New file
@@ -0,0 +1,260 @@
<template>
    <!-- 设置传代计划数弹窗 -->
    <el-dialog :title="planForm.status == 'add' ? '新增菌种传代项' : '菌种传代项详情'" :visible.sync="planDialogVisible" width="40%"
        :close-on-click-modal="false">
        <el-form :model="planForm" :rules="planRules" ref="planForm" label-position="top">
            <el-row :gutter="20">
                <el-col :span="16">
                    <el-form-item label="菌株类型" required>
                        <div class="type-box" v-if="planForm.status == 'add'">
                            <div @click="handleType(index)" v-for="(item, index) in ['原始祖代菌株SO', '分离菌落 CO', '祖代菌株 O']"
                                :key="item" class="type-box-item"
                                :class="index + 1 == planForm.activeType && 'activeType'">
                                <div class="type-box-item-text">{{ item }}</div>
                                <img v-if="index + 1 == planForm.activeType" class="type-box-item-select"
                                    src="../../../assets/public/selectType.png" />
                            </div>
                        </div>
                        <div v-else class="type-box">
                            <div class="type-box-item activeType">
                                {{ ['原始祖代菌株SO', '分离菌落 CO', '祖代菌株 O'][planForm.activeType - 1] }}
                            </div>
                        </div>
                    </el-form-item>
                </el-col>
                <el-col :span="20">
                    <el-form-item label="来源获得" prop="source">
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.source"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种名称" prop="inoculateName">
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.inoculateName"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种编号" prop="inoculateNo">
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.inoculateNo"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="保存/废弃" required>
                        <div class="flex-row" v-if="planForm.status == 'add'">
                            <div @click="handleStatus('save')" :class="planForm.isDiscarded && 'active'">保存</div>
                            <div @click="handleStatus('discard')" :class="!planForm.isDiscarded && 'active'">废弃</div>
                        </div>
                        <div v-else class="activeStatus">
                            保存
                        </div>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20" v-if="!planForm.isDiscarded">
                <el-col :span="10">
                    <el-form-item label="废弃原因说明" required>
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.discardReason"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="菌种入库时间">
                        <el-input disabled v-model="planForm.inTime"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row v-if="planForm.status != 'add'" :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种保藏人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="planForm.status == 'add'" class="dialog-footer">
            <el-button>保存草稿</el-button>
            <el-button type="primary" @click="handleAddPlan">提交</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            planDialogVisible: false,
            planForm: {
                status: 'add',
                activeType: 1,
                isDiscarded: true,
            },
            planRules: {
                inoculateNo: [
                    { required: true, message: '请输入菌种编号', trigger: 'blur' }
                ],
                inoculateName: [
                    { required: true, message: '请输入菌种名称', trigger: 'blur' }
                ],
                source: [
                    { required: true, message: '请输入来源获得', trigger: 'blur' }
                ],
            },
            dialogTitle: ''
        }
    },
    methods: {
        openInitData(value) {
            this.planForm = value
            this.openDialog()
        },
        openDialog() {
            this.planDialogVisible = true
        },
        closeDialog() {
            this.planDialogVisible = false
        },
        handleAddPlan() {
            this.$refs.planForm.validate((valid) => {
                if (valid) {
                    if (!this.planForm.activeType) {
                        this.$message.warning('请选择菌株类型');
                        return
                    }
                    this.$emit('addNodeSign', this.planForm, 1)
                }
            })
        },
        handleStatus(status) {
            if (this.planForm.status != 'add') return
            this.planForm.isDiscarded = status === 'save'
            this.$forceUpdate()
        },
        handleType(index) {
            if (this.planForm.status != 'add') return
            this.planForm.activeType = index + 1
        }
    }
}
</script>
<style scoped lang="less">
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    gap: 60px;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
    .el-button--default {
        width: 150px;
        height: 40px;
        border-radius: 4px;
    }
}
::v-deep .el-input-number .el-input__inner {
    text-align: left;
}
.el-input-number--small {
    width: 100%;
}
.flex-row {
    width: 370px;
    display: flex;
    align-items: center;
    font-size: 16px;
    color: #333333;
    padding: 4px;
    border-radius: 10px;
    border: 2px solid rgba(4, 156, 154, 0.5);
    font-family: 'PingFangSCRegular';
    .flex-row-save {
        background: #049C9A;
        color: #fff;
    }
    div {
        width: 183px;
        height: 32px;
        text-align: center;
        flex-shrink: 0;
        cursor: pointer;
    }
    .active {
        font-family: 'SourceHanSansCN-Medium';
        color: #049C9A;
        background: #EBFEFD;
        box-shadow: 0px 0px 6px 0px rgba(10, 109, 108, 0.25);
        border-radius: 10px;
    }
}
.activeStatus {
    font-family: 'SourceHanSansCN-Medium';
    color: #049C9A;
    background: #EBFEFD;
    border-radius: 10px;
    width: 183px;
    line-height: 40px;
    border-radius: 10px;
    text-align: center;
    font-weight: 500;
    font-size: 16px;
}
.type-box {
    display: flex;
    align-items: center;
    gap: 11px;
    .activeType {
        background: #EBFEFD;
        font-weight: 500;
        color: #049C9A;
    }
    &-item {
        cursor: pointer;
        position: relative;
        width: 150px;
        text-align: center;
        background: #F5F5F5;
        border-radius: 4px;
        font-weight: 400;
        font-size: 16px;
        color: #333333;
        &-text {
            line-height: 40px;
        }
        &-select {
            position: absolute;
            bottom: 0;
            right: 0;
            width: 21px;
            height: 17px;
        }
    }
}
</style>
culture/src/views/pedigree-chart/progenitorComponents/AddSublevelForm.vue
New file
@@ -0,0 +1,167 @@
<template>
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%" @close="closeDialog"
        :close-on-click-modal="false">
        <el-form :model="form" :rules="rules" ref="form" label-position="top">
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人" prop="thisName">
                        <el-input disabled v-model="form.thisName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种操作时间" prop="thisTime">
                        <el-input disabled v-model="form.thisTime"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种编号" prop="strainNo">
                        <el-input disabled v-model="form.strainNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="form.strainName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种编号" prop="inoculateNo">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateNo"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种名称" prop="inoculateName">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateName"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-form-item label="保存/废弃" required>
                <div class="flex-row">
                    <div @click="handleStatus('save')" :class="form.isDiscarded && 'active'">保存</div>
                    <div @click="handleStatus('discard')" :class="!form.isDiscarded && 'active'">废弃</div>
                </div>
            </el-form-item>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item v-if="!form.isDiscarded" label="废弃原因说明" required>
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.discardReason"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="菌种入库时间" prop="inTime">
                        <el-input disabled v-model="form.inTime"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row v-if="!dialogTitle.includes('新增')" :gutter="20">
                <el-col :span="10">
                    <el-form-item label="接种操作人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种保藏人签字">
                        <el-image />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="dialogTitle.includes('新增')" class="dialog-footer">
            <el-button type="primary" @click="handleSubmit">提交</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            dialogVisible: false,
            dialogTitle: '',
            form: {
                isDiscarded: true
            },
            rules: {
                isDiscarded: [
                    { required: true, message: '请选择废弃状态', trigger: 'blur' }
                ],
                inoculateNo: [
                    { required: true, message: '请输入接种菌种编号', trigger: 'blur' }
                ],
                inoculateName: [
                    { required: true, message: '请输入接种菌种名称', trigger: 'blur' }
                ],
            },
        }
    },
    methods: {
        openInitData(value) {
            this.dialogTitle = value.title
            this.form = value.form
            this.dialogVisible = true
        },
        closeDialog() {
            this.dialogVisible = false
        },
        handleSubmit() {
            this.$emit('addNodeSign', this.form, 3)
        },
        handleStatus(status) {
            if (!this.dialogTitle.includes('新增')) return
            this.form.isDiscarded = status === 'save'
            this.$forceUpdate()
        }
    }
}
</script>
<style scoped lang="less">
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
.flex-row {
    width: 370px;
    display: flex;
    align-items: center;
    font-size: 16px;
    color: #333333;
    padding: 4px;
    border-radius: 10px;
    border: 2px solid rgba(4, 156, 154, 0.5);
    font-family: 'PingFangSCRegular';
    .flex-row-save {
        background: #049C9A;
        color: #fff;
    }
    div {
        width: 183px;
        height: 32px;
        text-align: center;
        flex-shrink: 0;
        cursor: pointer;
    }
    .active {
        font-family: 'SourceHanSansCN-Medium';
        color: #049C9A;
        background: #EBFEFD;
        box-shadow: 0px 0px 6px 0px rgba(10, 109, 108, 0.25);
        border-radius: 10px;
    }
}
</style>
culture/src/views/pedigree-chart/progenitorComponents/PlanForm.vue
New file
@@ -0,0 +1,135 @@
<template>
    <!-- 设置传代计划数弹窗 -->
    <el-dialog :title="planForm.status === 'detail' ? '祖代传代计划数详情' : '设置祖代传代计划数'" :visible.sync="planDialogVisible"
        width="40%" :close-on-click-modal="false">
        <el-form :model="planForm" :rules="planRules" ref="planForm" label-position="top">
            <el-row :gutter="20">
                <el-col :span="16">
                    <el-form-item label="菌株类型">
                        <div class="activeType">{{ ['原始祖代菌株SO', '分离菌落 CO', '祖代菌株 O'][planForm.activeType - 1] }}</div>
                    </el-form-item>
                </el-col>
                <el-col :span="20">
                    <el-form-item label="来源获得" prop="source">
                        <el-input disabled v-model="planForm.source" placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种名称" prop="inoculateName">
                        <el-input disabled v-model="planForm.inoculateName" placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="菌种编号" prop="inoculateNo">
                        <el-input disabled v-model="planForm.inoculateNo" placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="保存/废弃">
                        <div class="active">保存</div>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="菌种入库时间">
                        <el-input disabled v-model="planForm.inTime"></el-input>
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10">
                    <el-form-item label="传代计划数" prop="count">
                        <el-input-number :disabled="planForm.status === 'detail'" v-model="planForm.count"
                            :controls="false" :min="1" placeholder="请输入" />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <div v-if="planForm.status !== 'detail'" class="dialog-footer">
            <el-button type="primary" @click="handleAddPlan">提交签字</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    data() {
        return {
            planDialogVisible: false,
            planForm: {},
            planRules: {
                count: [
                    { required: true, message: '请输入传代计划数', trigger: 'blur' }
                ]
            }
        }
    },
    methods: {
        openInitData(value) {
            this.planForm = value
            this.openDialog()
        },
        openDialog() {
            this.planDialogVisible = true
        },
        closeDialog() {
            this.planDialogVisible = false
        },
        handleAddPlan() {
            this.$refs.planForm.validate((valid) => {
                if (valid) {
                    this.$emit('addNodeSign', this.planForm, 2)
                }
            })
        }
    }
}
</script>
<style scoped lang="less">
.dialog-footer {
    margin-top: 39px;
    display: flex;
    justify-content: center;
    .el-button--primary {
        width: 150px;
        height: 40px;
        background: #049C9A;
        border-radius: 4px;
    }
}
::v-deep .el-input-number .el-input__inner {
    text-align: left;
}
.el-input-number--small {
    width: 100%;
}
.active {
    font-family: 'SourceHanSansCN-Medium';
    color: #049C9A;
    background: #EBFEFD;
    border-radius: 10px;
    width: 183px;
    line-height: 40px;
    border-radius: 10px;
    text-align: center;
    font-weight: 500;
    font-size: 16px;
}
.activeType {
    width: 150px;
    background: #EBFEFD;
    font-weight: 500;
    color: #049C9A;
    border-radius: 4px;
    font-size: 16px;
    line-height: 40px;
    text-align: center;
}
</style>
culture/src/views/strain-library/breeding-record/add.vue
New file
@@ -0,0 +1,432 @@
<template>
  <div>
    <Card>
      <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
        <div class="header-title" style="width: 100%">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>来源类型</div>
          </div>
        </div>
        <div class="flex" style="margin-bottom: 20px">
          <div class="tabs">
            <div :class="{ active: activeTab === 'strain' }" @click="activeTab = 'strain'">
              来源菌株
            </div>
            <div :class="{ active: activeTab === 'material' }" @click="activeTab = 'material'">
              来源物资
            </div>
          </div>
        </div>
        <!-- 来源菌株 -->
        <el-row v-if="activeTab === 'strain'" :gutter="10">
          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
            <el-form-item label="菌株编号" prop="strainCode" required>
              <el-input v-model="form.strainCode" class="w-380" placeholder="请输入菌株编号" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
            <el-form-item label="菌株名称" prop="strainName" required>
              <el-input v-model="form.strainName" class="w-380" placeholder="请输入菌株名称" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
            <el-form-item label="培养基配方" prop="mediumFormula" required>
              <el-input v-model="form.mediumFormula" class="w-380" placeholder="请输入培养基配方" />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- 来源物资 -->
        <el-row v-if="activeTab === 'material'" :gutter="10">
          <el-col :span="24">
            <el-form-item label="来源物资、时间及批号" prop="materialCode" required>
              <el-input v-model="form.materialCode" placeholder="请输入物资编号" />
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="培养基配方" prop="materialName" required>
              <el-input v-model="form.materialName" placeholder="请输入物资名称" />
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="分离菌落编号" prop="materialDescription" required>
              <el-input v-model="form.materialDescription" placeholder="请输入物资描述" />
            </el-form-item>
          </el-col>
        </el-row>
        <div class="header-title" style="margin-top: 20px;">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>培养条件</div>
          </div>
        </div>
        <el-row :gutter="10">
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="培养基" prop="strainCode" required>
              <el-input v-model="form.strainCode" class="w-380" placeholder="请输入培养基" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="培养温度" prop="strainName" required>
              <el-input v-model="form.strainName" class="w-380" placeholder="请输入培养温度" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="需氧类型" prop="mediumFormula" required>
              <el-input v-model="form.mediumFormula" class="w-380" placeholder="请输入需氧类型" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="培养时间" prop="mediumFormula" required>
              <el-input v-model="form.mediumFormula" class="w-380" placeholder="请输入培养时间" />
            </el-form-item>
          </el-col>
        </el-row>
        <div class="header-title" style="margin-top: 20px;">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>一、培养基分离记录</div>
          </div>
          <div class="header-title-right">
            <el-button @click="showChoose = true" class="el-icon-circle-plus-outline" type="primary">
              新增培养皿分离记录</el-button>
          </div>
        </div>
        <Table :height="null" :queryForm="queryForm" :total="0">
          <template>
            <el-table-column prop="name" label="培养皿序号" />
            <el-table-column prop="age" label="分离菌落编号" />
            <el-table-column prop="age" label="接种操作人签字" />
            <el-table-column prop="age" label="操作时间" />
            <el-table-column prop="address" label="操作">
              <template slot-scope="scope"><el-button type="text"
                @click="handleEdit(scope.row)">编辑</el-button>
                <el-button type="text"
                @click="handleEdit(scope.row)">删除</el-button></template>
            </el-table-column>
          </template>
        </Table>
        <div class="header-title" style="margin-top: 20px;">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>二、培养皿生物学形态观察记录</div>
          </div>
          <div class="header-title-right">
            <el-button @click="showChoose = true" class="el-icon-circle-plus-outline" type="primary">
              新增观察记录</el-button>
          </div>
        </div>
        <Table :height="null" :queryForm="queryForm" :total="0">
          <template>
            <el-table-column prop="age" label="分离菌落编号" />
            <el-table-column prop="age" label="形状强壮度排名" />
            <el-table-column prop="address" label="操作">
              <template slot-scope="scope"><el-button type="text"
                @click="handleEdit(scope.row)">形态记录</el-button>
                <el-button type="text"
                @click="handleEdit(scope.row)">删除</el-button></template>
            </el-table-column>
          </template>
        </Table>
        <div class="header-title" style="margin-top: 20px;">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>三、接种斜面记录</div>
          </div>
          <div class="header-title-right">
            <el-button @click="showChoose = true" class="el-icon-circle-plus-outline" type="primary">
              新增斜面记录</el-button>
          </div>
        </div>
        <Table :height="null" :queryForm="queryForm" :total="0">
          <template>
            <el-table-column prop="age" label="分离菌落编号" />
            <el-table-column prop="age" label="接种斜面编号" />
            <el-table-column prop="age" label="接种操作人" />
            <el-table-column prop="age" label="接种操作人签字" />
            <el-table-column prop="age" label="接种操作时间" />
            <el-table-column prop="age" label="入库/废弃" />
            <el-table-column prop="age" label="入库总数(只)" />
            <el-table-column prop="age" label="菌种保藏人签字" />
            <el-table-column prop="age" label="入库保藏/废弃时间" />
            <el-table-column prop="address" label="操作">
              <template slot-scope="scope">
                <el-button type="text"
                @click="handleEdit(scope.row)">删除</el-button></template>
            </el-table-column>
          </template>
        </Table>
        <div class="header-title" style="margin-top: 20px;">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>四、菌种保藏记录</div>
          </div>
          <div class="header-title-right">
            <el-button @click="showChoose = true" class="el-icon-circle-plus-outline" type="primary">
              新增菌种保藏记录</el-button>
          </div>
        </div>
        <Table :height="null" :queryForm="queryForm" :total="0">
          <template>
            <el-table-column prop="age" label="用于保藏的菌种编号" />
            <el-table-column prop="age" label="实验验证结论" />
            <el-table-column prop="age" label="保藏方法" />
            <el-table-column prop="age" label="保藏菌种编号" />
            <el-table-column prop="age" label="菌种保藏人签字" />
            <el-table-column prop="age" label="保藏时间" />
            <el-table-column prop="address" label="操作">
              <template slot-scope="scope">
                <el-button type="text"
                @click="handleEdit(scope.row)">删除</el-button></template>
            </el-table-column>
          </template>
        </Table>
        <div class="end-btn">
          <el-button type="primary">发送</el-button>
          <el-button type="default">存草稿</el-button>
        </div>
      </el-form>
    </Card>
  </div>
</template>
<script>
import AiEditor from "@/components/AiEditor";
export default {
  components: { AiEditor },
  name: "AddBreedingRecord",
  data() {
    return {
      form: {
        strainCode: "",
        strainName: "",
        mediumFormula: "",
        materialCode: "",
        materialName: "",
        materialDescription: "",
        planName: "",
        planCode: "",
        stage: "",
        creator: "",
        createTime: "",
        approvalComment: "",
        status: "pending",
        approver: "",
        approveTime: "",
      },
      rules: {
        strainCode: [{ required: true, message: "请输入菌株编号", trigger: "blur" }],
        strainName: [{ required: true, message: "请输入菌株名称", trigger: "blur" }],
        mediumFormula: [{ required: true, message: "请输入培养基配方", trigger: "blur" }],
        materialCode: [{ required: true, message: "请输入物资编号", trigger: "blur" }],
        materialName: [{ required: true, message: "请输入物资名称", trigger: "blur" }],
        materialDescription: [{ required: true, message: "请输入物资描述", trigger: "blur" }],
      },
      activeTab: "strain", // 默认显示来源菌株表单
      fileList: [], // 附件列表
      showChoose: false,
      radio1: 1,
      status: "1",
      remark: "",
      queryForm: {},
    };
  },
};
</script>
<style lang="less" scoped>
::v-deep(.el-form-item) {
  width: 100%;
  .el-input__inner {
    width: 100%;
  }
  .w-380 {
    width: 280px;
  }
}
.mt-unset {
  margin-top: unset !important;
}
.flex {
  display: flex;
}
.tabs {
  display: flex;
  align-items: center;
  border: 2px solid rgba(4, 156, 154, 0.5);
  border-radius: 10px;
  padding: 4px;
  div {
    width: 183px;
    height: 32px;
    border-radius: 10px;
    text-align: center;
    line-height: 32px;
    font-size: 16px;
    cursor: pointer;
  }
  .active {
    background: #ebfefd;
    color: #049c9a;
  }
  .inactive {
    background: #fff;
    color: #333333;
  }
}
.header-title {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 20px;
  gap: 13px;
  .header-title-left {
    display: flex;
    align-items: center;
    gap: 13px;
    img {
      width: 12px;
      height: 19px;
    }
    div {
      flex-shrink: 0;
      font-weight: bold;
      font-size: 18px;
      color: #222222;
      line-height: 27px;
      font-family: "Source Han Sans CN Bold Bold";
    }
    span {
      flex-shrink: 0;
      font-weight: bold;
      font-size: 18px;
      color: #222222;
      line-height: 27px;
      font-family: "Source Han Sans CN Bold Bold";
    }
  }
  .header-title-left :first-child {
    margin-top: 0;
  }
}
.header-title:first-child {
  .header-title-left {
    margin-top: 0;
  }
}
.end-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  button {
    width: 180px;
    height: 36px;
    // background: #409EFF;
  }
}
.member-list {
  margin-top: 18px;
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  .member-list-card {
    width: 340px;
    height: 400px;
    border-radius: 8px;
    border: 1px solid #dcdfe6;
    background: linear-gradient(to bottom,
        rgba(5, 160, 193, 0.2) 0%,
        rgba(5, 242, 194, 0) 70%);
    .member-item {
      height: 100%;
      display: flex;
      flex-direction: column;
      .member-title {
        margin-top: 20px;
        width: 100%;
        font-family: "Source Han Sans CN Bold Bold";
        font-weight: bold;
        font-size: 16px;
        color: rgba(0, 0, 0, 0.8);
        line-height: 16px;
        text-align: center;
      }
      .flex1 {
        flex: 1;
      }
      .member-name-box {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .member-name-box-2 {
        flex: 1;
        padding: 0 20px;
        padding-top: 40px;
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 20px;
        justify-items: center;
        align-items: start;
      }
      .member-name {
        width: 60px;
        height: 60px;
        background: #7d8b79;
        border-radius: 50%;
        text-align: center;
        line-height: 60px;
        font-weight: 500;
        font-size: 16px;
        color: #ffffff;
        margin: 0;
      }
      .member-change {
        display: flex;
        justify-content: center;
        padding: 10px 0;
        margin-top: auto;
        cursor: pointer;
        .member-change-btn {
          background: #fff1f0;
          border-radius: 4px;
          border: 1px solid #ffccc7;
          padding: 1px 8px;
          font-weight: 400;
          font-size: 12px;
          color: #ff4d4f;
        }
      }
    }
  }
}
</style>
culture/src/views/strain-library/breeding-record/confirm-storage-dialog.vue
New file
@@ -0,0 +1,135 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title=""
    width="520px"
    :close-on-click-modal="false"
    custom-class="record-detail-dialog"
    @close="handleClose"
  >
    <div class="dialog-content">
      <div class="confirm-tip">
        是否确认该项菌种信息?
        <span class="danger">确认后将无法再次编辑菌种传代项内容</span>
      </div>
      <el-form :model="form" :rules="rules" ref="form" label-position="top">
        <el-form-item required>
          <template #label>
            <span>菌种保藏人签字</span>
            <el-button type="primary" class="sign-btn" @click="showSignature = true">签名</el-button>
          </template>
          <div class="signature-area" :class="{ 'waiting': !form.signature }">
            <template v-if="form.signature">
              <img :src="form.signature" alt="菌种保藏人签字" />
            </template>
            <template v-else>
              <span class="waiting-text">等待确认</span>
            </template>
          </div>
        </el-form-item>
      </el-form>
    </div>
    <div class="footer-btns">
      <el-button @click="handleClose" style="margin-right: 16px;">取消</el-button>
      <el-button type="primary" @click="handleConfirm">确认</el-button>
    </div>
    <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm" />
  </el-dialog>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue';
export default {
  name: 'ConfirmStorageDialog',
  components: { SignatureCanvas },
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      form: {
        signature: ''
      },
      rules: {
        signature: [
          { required: true, message: '请签名', trigger: 'change' }
        ]
      },
      showSignature: false
    }
  },
  methods: {
    handleClose() {
      this.$emit('update:visible', false)
    },
    handleConfirm() {
      this.$refs.form.validate(valid => {
        if (!valid) return
        this.$emit('confirm', { ...this.form })
        this.handleClose()
      })
    },
    handleSignatureConfirm(dataUrl) {
      this.form.signature = dataUrl
      this.showSignature = false
    }
  }
}
</script>
<style lang="less" scoped>
.confirm-tip {
  color: #f5222d;
  font-size: 16px;
  margin-bottom: 24px;
  .danger {
    margin-left: 12px;
  }
}
.signature-area {
  height: 120px;
  width: 100%;
  background: #F5F7FA;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #DCDFE6;
  overflow: hidden;
  padding: 0;
}
.signature-area.waiting {
  border-style: dashed;
  background: #FAFAFA;
}
.signature-area img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-btn {
  height: 32px;
  border-radius: 4px;
  font-size: 14px;
  padding: 0 20px;
  font-weight: 400;
  margin-left: 12px;
}
.footer-btns {
  display: flex;
  justify-content: center;
  padding: 24px;
  padding-top: 0;
  .el-button {
    width: 150px;
  }
}
</style>
culture/src/views/strain-library/breeding-record/index.vue
New file
@@ -0,0 +1,300 @@
<template>
  <div class="list">
    <TableCustom :queryForm="form" :tableData="tableData" :total="total">
      <template #search>
        <el-form :model="form" labelWidth="auto" inline>
          <el-form-item label="来源类型:">
            <el-input v-model="form.planName" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="培养基:">
            <el-input v-model="form.planCode" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="培养基配方:">
            <el-input v-model="form.planCode" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="需氧类型">
            <el-input v-model="form.creator" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="">
            <el-button type="default" @click="resetForm">重置</el-button>
            <el-button type="primary" style="margin-left: 10px;" @click="handleSearch">查询</el-button>
          </el-form-item>
        </el-form>
      </template>
      <template #setting>
        <div class="tableTitle">
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'list' }"
              @click="handleTypeChange('list')"
            >
              菌种传代生产谱系图
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              @click="handleTypeChange('draft')"
            >
              草稿箱
            </div>
          </div>
          <div class="flex a-center">
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              style="margin-right: 12px"
              >新增保藏记录</el-button
            >
          </div>
        </div>
      </template>
      <template #table>
        <el-table-column prop="stage" label="来源类型"></el-table-column>
        <el-table-column prop="creator" label="培养基"></el-table-column>
        <el-table-column prop="creator" label="培养基配方"></el-table-column>
        <el-table-column prop="createTime" label="培养温度"></el-table-column>
        <el-table-column prop="approver" label="需氧类型"></el-table-column>
        <el-table-column prop="approver" label="创建人"></el-table-column>
        <el-table-column prop="approveTime" label="创建时间"></el-table-column>
        <el-table-column label="操作" width="250">
          <template slot-scope="scope">
            <el-button type="text" @click="handleDetail(scope.row)"
              >详情</el-button
            >
            <el-button type="text" @click="handleEdit(scope.row)"
              >编辑</el-button
            >
            <el-button type="text" @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </template>
    </TableCustom>
  </div>
</template>
<script>
export default {
  name: "BreedingRecord",
  components: {},
  data() {
    return {
      currentType: "list", // 当前显示类型:list-列表,draft-草稿箱
      form: {
        planName: "",
        planCode: "",
        creator: "",
        createTime: [],
        approver: "",
        status: "",
      },
      tableData: [],
      total: 0,
      // 模拟数据
      mockListData: [
        {
          planCode: "PLAN-2024-001",
          planName: "2024年度实验室设备升级方案",
          stage: "规划阶段",
          creator: "张三",
          createTime: "2024-03-15",
          status: "pending",
          approver: "李四",
          approveTime: "2024-03-16",
        },
        {
          planCode: "PLAN-2024-002",
          planName: "实验室安全管理制度更新方案",
          stage: "实施阶段",
          creator: "王五",
          createTime: "2024-03-14",
          status: "approved",
          approver: "赵六",
          approveTime: "2024-03-15",
        },
        {
          planCode: "PLAN-2024-003",
          planName: "实验室人员培训计划",
          stage: "准备阶段",
          creator: "孙七",
          createTime: "2024-03-13",
          status: "rejected",
          approver: "周八",
          approveTime: "2024-03-14",
        },
      ],
      mockDraftData: [
        {
          planCode: "DRAFT-2024-001",
          planName: "实验室设备采购计划(草稿)",
          stage: "规划阶段",
          creator: "张三",
          createTime: "2024-03-16",
          status: "draft",
          approver: "",
          approveTime: "",
        },
        {
          planCode: "DRAFT-2024-002",
          planName: "实验室改造方案(草稿)",
          stage: "准备阶段",
          creator: "李四",
          createTime: "2024-03-15",
          status: "draft",
          approver: "",
          approveTime: "",
        },
      ],
      approvalDialogVisible: false,
      approvalDialogType: "approve",
      currentApprovalData: null,
    };
  },
  created() {
    this.getTableData();
  },
  methods: {
    resetForm() {
      this.form = {
        planName: "",
        planCode: "",
        creator: "",
        createTime: [],
        approver: "",
        status: "",
      };
    },
    handleNewStrain() {
      this.$router.push({
        path: "/strain/add-breeding-record",
      });
    },
    handleSearch() {
      // 实现查询逻辑
      console.log("查询条件:", this.form);
    },
    getStatusType(status) {
      const statusMap = {
        pending: "warning",
        rejected: "danger",
        approved: "success",
        archived: "info",
        draft: "info",
      };
      return statusMap[status] || "info";
    },
    getStatusText(status) {
      const statusMap = {
        pending: "待审批",
        rejected: "已驳回",
        approved: "已通过",
        archived: "已封存",
        draft: "草稿",
      };
      return statusMap[status] || "未知";
    },
    handleAddPlan() {
      this.$router.push({
        path: "/dataManagement/addPlan",
      });
    },
    handleApprove(row) {
      this.currentApprovalData = row;
      this.approvalDialogType = "approve";
      this.approvalDialogVisible = true;
    },
    handleApproveSubmit(data) {
      // 处理审批通过
      console.log("审批通过:", data);
      this.approvalDialogVisible = false;
      this.$message.success("审批通过成功");
      this.getTableData();
    },
    handleRejectSubmit(data) {
      // 处理审批驳回
      console.log("审批驳回:", data);
      this.approvalDialogVisible = false;
      this.$message.success("审批驳回成功");
      this.getTableData();
    },
    handleRevokeApprove(row) {
      // 实现撤销审批逻辑
      console.log("撤销审批数据:", row);
    },
    handleEdit(row) {
      // 实现编辑逻辑
      console.log("编辑数据:", row);
    },
    handleDelete(row) {
      // 实现删除逻辑
      console.log("删除数据:", row);
    },
    handleDetail(row) {
      this.currentApprovalData = row;
      this.approvalDialogType = "view";
      this.approvalDialogVisible = true;
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.getTableData();
    },
    getTableData() {
      // 根据currentType请求不同的数据
      if (this.currentType === "list") {
        this.tableData = this.mockListData;
        this.total = this.mockListData.length;
      } else {
        this.tableData = this.mockDraftData;
        this.total = this.mockDraftData.length;
      }
    },
  },
};
</script>
<style scoped lang="less">
.list {
  height: 100%;
}
.flex {
  display: flex;
  align-items: center;
}
.tableTitle {
  display: flex;
  padding-bottom: 20px;
  justify-content: space-between;
  align-items: center;
  .title {
    background: #fafafc;
    border-radius: 8px 8px 0px 0px;
    border: 1px solid #dcdfe6;
    padding: 16px 29px;
    font-weight: bold;
    font-size: 18px;
    color: #606266;
    width: unset;
    cursor: pointer;
  }
  .drafts {
    padding: 16px 65px;
    background: #fafafc;
    border-radius: 8px 8px 0px 0px;
    border: 1px solid #dcdfe6;
    font-weight: 400;
    font-size: 18px;
    color: #606266;
    margin-left: 16px;
    cursor: pointer;
  }
  .active {
    color: #049c9a;
    background: #ffffff;
    border-radius: 8px 8px 0px 0px;
    border: 1px solid #049c9a;
  }
}
</style>
culture/src/views/strain-library/breeding-record/inoculation-slope-record-dialog.vue
New file
@@ -0,0 +1,186 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title="新增接种斜面记录"
    width="700px"
    @close="handleClose"
  >
    <el-form
      :model="form"
      :rules="rules"
      ref="form"
      label-width="120px"
      label-position="top"
    >
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="分离菌落编号" prop="colonyCode" required>
            <el-input v-model="form.colonyCode" placeholder="请输入" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="接种斜面编号" prop="slopeCode" required>
            <el-input v-model="form.slopeCode" placeholder="请输入" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="保存/废弃" prop="status" required>
        <el-button
          :type="form.status === '保存' ? 'primary' : 'default'"
          @click="form.status = '保存'"
          >保存</el-button
        >
        <el-button
          :type="form.status === '废弃' ? 'primary' : 'default'"
          @click="form.status = '废弃'"
          >废弃</el-button
        >
      </el-form-item>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="菌种入库时间" prop="storageTime" required>
            <el-input
              v-model="form.storageTime"
              disabled
              placeholder="自动回填"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="接种操作时间" prop="operationTime" required>
            <el-input
              v-model="form.operationTime"
              disabled
              placeholder="自动填入"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item required>
            <template #label>
              <span>接种操作人签字</span>
              <el-button type="primary" class="sign-btn" @click="showSignature = true">签名</el-button>
            </template>
            <div class="signature-area" :class="{ 'waiting': !form.signature }">
              <template v-if="form.signature">
                <img :src="form.signature" alt="接种操作人签字" />
              </template>
              <template v-else>
                <span class="waiting-text">等待确认</span>
              </template>
            </div>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div style="text-align: center; margin-top: 20px">
      <el-button @click="handleClose" style="margin-right: 16px;">取消</el-button>
      <el-button type="primary" @click="handleSave">确认</el-button>
    </div>
    <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm" />
  </el-dialog>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue';
export default {
  components: { SignatureCanvas },
  props: { visible: Boolean },
  data() {
    return {
      form: {
        colonyCode: "",
        slopeCode: "",
        status: "保存",
        storageTime: this.getNowTime(),
        operationTime: this.getNowTime(),
        signature: "",
      },
      rules: {
        colonyCode: [
          { required: true, message: "请输入分离菌落编号", trigger: "blur" },
        ],
        slopeCode: [
          { required: true, message: "请输入接种斜面编号", trigger: "blur" },
        ],
        status: [
          { required: true, message: "请选择保存/废弃", trigger: "change" },
        ],
        signature: [{ required: true, message: "请签名", trigger: "change" }],
      },
      showSignature: false,
    };
  },
  methods: {
    getNowTime() {
      const d = new Date();
      return (
        d.getFullYear() +
        "-" +
        (d.getMonth() + 1).toString().padStart(2, "0") +
        "-" +
        d.getDate().toString().padStart(2, "0") +
        " " +
        d.getHours().toString().padStart(2, "0") +
        ":" +
        d.getMinutes().toString().padStart(2, "0")
      );
    },
    handleSave() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.$emit("save", { ...this.form });
          this.handleClose();
        }
      });
    },
    handleClose() {
      this.$emit("update:visible", false);
    },
    handleSignatureConfirm(dataUrl) {
      this.form.signature = dataUrl;
      this.showSignature = false;
    },
  },
};
</script>
<style scoped>
.signature-area {
  height: 120px;
  width: 100%;
  background: #F5F7FA;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #DCDFE6;
  overflow: hidden;
  padding: 0;
}
.signature-area.waiting {
  border-style: dashed;
  background: #FAFAFA;
}
.signature-area img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-btn {
  height: 32px;
  border-radius: 4px;
  font-size: 14px;
  padding: 0 20px;
  font-weight: 400;
  margin-left: 12px;
}
</style>
culture/src/views/strain-library/breeding-record/preserve-strain-record-dialog.vue
New file
@@ -0,0 +1,153 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title="新增菌种保藏记录"
    width="900px"
    @close="handleClose"
  >
    <el-form
      :model="form"
      :rules="rules"
      ref="form"
      label-width="120px"
      label-position="top"
    >
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="用于保藏的菌种编号" prop="strainCode" required>
            <el-input v-model="form.strainCode" placeholder="请输入" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="实验验证结论" prop="experimentConclusion" required>
        <el-input type="textarea" :rows="4" v-model="form.experimentConclusion" placeholder="请输入" />
      </el-form-item>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="保藏方法" prop="preserveMethod" required>
            <el-input v-model="form.preserveMethod" placeholder="请输入" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="保藏菌种编号" prop="preserveStrainCode" required>
            <el-input v-model="form.preserveStrainCode" placeholder="请输入" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item required>
            <template #label>
              <span>操作人签字</span>
              <el-button type="primary" class="sign-btn" @click="showSignature = true">签名</el-button>
            </template>
            <div class="signature-area" :class="{ 'waiting': !form.signature }">
              <template v-if="form.signature">
                <img :src="form.signature" alt="操作人签字" />
              </template>
              <template v-else>
                <span class="waiting-text">等待确认</span>
              </template>
            </div>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div style="text-align: center; margin-top: 20px">
      <el-button @click="handleClose" style="margin-right: 16px;">取消</el-button>
      <el-button type="primary" @click="handleSave">保存</el-button>
    </div>
    <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm" />
  </el-dialog>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue';
export default {
  components: { SignatureCanvas },
  props: { visible: Boolean },
  data() {
    return {
      form: {
        strainCode: '',
        experimentConclusion: '',
        preserveMethod: '',
        preserveStrainCode: '',
        signature: '',
      },
      rules: {
        strainCode: [
          { required: true, message: '请输入用于保藏的菌种编号', trigger: 'blur' },
        ],
        experimentConclusion: [
          { required: true, message: '请输入实验验证结论', trigger: 'blur' },
        ],
        preserveMethod: [
          { required: true, message: '请输入保藏方法', trigger: 'blur' },
        ],
        preserveStrainCode: [
          { required: true, message: '请输入保藏菌种编号', trigger: 'blur' },
        ],
        signature: [
          { required: true, message: '请签名', trigger: 'change' },
        ],
      },
      showSignature: false,
    };
  },
  methods: {
    handleSave() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.$emit('save', { ...this.form });
          this.handleClose();
        }
      });
    },
    handleClose() {
      this.$emit('update:visible', false);
    },
    handleSignatureConfirm(dataUrl) {
      this.form.signature = dataUrl;
      this.showSignature = false;
    },
  },
};
</script>
<style scoped>
.signature-area {
  height: 120px;
  width: 100%;
  background: #F5F7FA;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #DCDFE6;
  overflow: hidden;
  padding: 0;
}
.signature-area.waiting {
  border-style: dashed;
  background: #FAFAFA;
}
.signature-area img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-btn {
  height: 32px;
  border-radius: 4px;
  font-size: 14px;
  padding: 0 20px;
  font-weight: 400;
  margin-left: 12px;
}
</style>
culture/src/views/strain-library/breeding-record/separation-record-dialog.vue
New file
@@ -0,0 +1,164 @@
<template>
    <el-dialog
      title="添加出入库记录"
      :visible.sync="visible"
      width="520px"
      :close-on-click-modal="false"
      custom-class="record-detail-dialog"
      @close="handleClose"
    >
      <div class="dialog-content">
        <el-form :model="formData" label-position="top">
          <el-form-item label="分离菌落编号" required>
            <el-input v-model="formData.colonyCode" placeholder="请输入" />
          </el-form-item>
          <el-form-item required>
            <template #label>
              <span>操作人签字</span>
              <el-button type="primary" class="sign-btn" @click="showSignature = true">签名</el-button>
            </template>
            <div class="signature-area" :class="{ 'waiting': !formData.operatorSignature }">
              <template v-if="formData.operatorSignature">
                <img :src="formData.operatorSignature" alt="操作人签字" />
              </template>
              <template v-else>
                <span class="waiting-text">等待确认</span>
              </template>
            </div>
          </el-form-item>
        </el-form>
      </div>
      <div class="footer-btns">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="handleConfirm">确认</el-button>
      </div>
      <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm" />
    </el-dialog>
  </template>
  <script>
  import SignatureCanvas from '@/components/SignatureCanvas.vue'
  export default {
    name: 'AddRecordDialog',
    components: { SignatureCanvas },
    props: {
      visible: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        formData: {
          type: '出库',
          operatorSignature: ''
        },
        showSignature: false
      }
    },
    methods: {
      handleClose() {
        this.$emit('update:visible', false)
        this.$emit('close')
      },
      handleConfirm() {
        if (!this.formData.operatorSignature) {
          this.$message.warning('请先签名')
          return
        }
        this.$emit('confirm', this.formData)
        this.handleClose()
      },
      handleSignatureConfirm(dataUrl) {
        this.formData.operatorSignature = dataUrl
        this.showSignature = false
      }
    }
  }
  </script>
  <style lang="less" scoped>
  .record-detail-dialog {
    :deep(.el-dialog__header) {
      padding: 20px 24px;
      margin: 0;
      border-bottom: 1px solid #DCDFE6;
      .el-dialog__title {
        font-size: 16px;
        font-weight: 600;
        color: #303133;
      }
    }
    :deep(.el-dialog__body) {
      padding: 24px;
    }
  }
  .dialog-content {
    :deep(.el-form-item__label) {
      padding-bottom: 8px;
      line-height: 20px;
      font-size: 14px;
      color: #606266;
      &::before {
        content: '*';
        color: #F56C6C;
        margin-right: 4px;
      }
    }
    .type-buttons {
      display: flex;
      gap: 12px;
      .el-button {
        width: 80px;
        border-radius: 4px;
        font-size: 14px;
        font-weight: 400;
        box-sizing: border-box;
      }
    }
    .signature-area {
      height: 120px;
      width: 100%;
      background: #F5F7FA;
      border-radius: 4px;
      display: flex;
      align-items: center;
      justify-content: center;
      border: 1px solid #DCDFE6;
      overflow: hidden;
      padding: 0;
      &.waiting {
        border-style: dashed;
        background: #FAFAFA;
      }
      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
      }
      .waiting-text {
        color: #909399;
        font-size: 14px;
      }
    }
    .sign-btn {
      height: 32px;
      border-radius: 4px;
      font-size: 14px;
      padding: 0 20px;
      font-weight: 400;
      margin-left: 12px;
    }
  }
  .footer-btns {
    display: flex;
    justify-content: space-between;
    padding: 24px;
    padding-top: 0;
    .el-button {
      width: 150px;
    }
  }
  </style>
culture/src/views/strain-library/validation/chief-cell/index.vue
New file
@@ -0,0 +1,465 @@
<template>
    <div class="list">
        <el-card class="header-box">
            <div class="box-title">
                <img src="@/assets/public/notice.png" class="header-icon">
                <span>菌种源保藏出/入细胞库登记表说明</span>
                <el-button type="text" class="view-more" @click="handleViewMore">查看全部 >></el-button>
            </div>
            <div class="header-content" :class="{ 'collapsed': true }">
                <p>1、菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,请将来源有3 类菌经。</p>
                <p>1.1 原净土管理日油性的源头菌种:入细胞细胞库(现代-O)。</p>
                <p>1.2 是到菌的源头菌种:接种入主细胞库(现代-O),经过百种、验证后,菌种被保存日油管理沙土菌种,入细胞细胞库(现代-O)。</p>
                <p>1.3 是否菌种能自己分离后获得的源头菌种,接种入主细胞库:经过产验证后,保藏为少土管理日油管,入细胞细胞库(现代-O)。</p>
            </div>
            <!-- 查看全部弹窗 -->
            <el-dialog
                title="菌种源保藏出/入细胞库登记表说明"
                :visible.sync="dialogVisible"
                width="50%"
                class="view-all-dialog"
            >
                <div class="dialog-content">
                    <p>1、菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,请将来源有3 类菌经。</p>
                    <p>1.1 原净土管理日油性的源头菌种:入细胞细胞库(现代-O)。</p>
                    <p>1.2 是到菌的源头菌种:接种入主细胞库(现代-O),经过百种、验证后,菌种被保存日油管理沙土菌种,入细胞细胞库(现代-O)。</p>
                    <p>1.3 是否菌种能自己分离后获得的源头菌种,接种入主细胞库:经过产验证后,保藏为少土管理日油管,入细胞细胞库(现代-O)。</p>
                </div>
            </el-dialog>
        </el-card>
        <!-- Table -->
        <TableCustom :queryForm="queryForm" :tableData="tableData" :total="total" @currentChange="handleCurrentChange"
            @sizeChange="handleSizeChange">
            <template #search>
                <el-form :model="form" label-width="auto" inline>
                    <el-form-item label="菌种编号:">
                        <el-input v-model="form.strainNo" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="菌种名称:">
                        <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="状态:">
                        <el-select v-model="form.status" placeholder="请选择">
                            <el-option label="全部" value=""></el-option>
                            <el-option label="已入库" value="1"></el-option>
                            <el-option label="已出库" value="2"></el-option>
                            <el-option label="入库待确认" value="3"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item class="search-btn-box">
                            <el-button type="default" @click="resetForm">重置</el-button>
                            <el-button type="primary" @click="searchData">查询</el-button>
                    </el-form-item>
                </el-form>
            </template>
            <template #setting>
                <div class="tableTitle">
                    <div class="flex a-center">
                        <div class="title" :class="{ active: currentType === 'list' }"
                            @click="handleTypeChange('list')">
                            原始细胞列表</div>
                        <div class="drafts" :class="{ active: currentType === 'draft' }"
                            @click="handleTypeChange('draft')">
                            草稿箱</div>
                    </div>
                    <div class="flex a-center">
                        <el-button @click="handleNewStrain" class="el-icon-plus" type="primary" style="margin-right: 12px;">新增原始细胞</el-button>
                        <el-button @click="handleBatchAdd" class="el-icon-plus" type="primary">批量新增</el-button>
                    </div>
                </div>
            </template>
            <template #table>
                <el-table-column prop="strainNo" label="菌种编号" />
                <el-table-column prop="strainName" label="菌种名称" />
                <el-table-column prop="source" label="菌种来源" />
                <el-table-column prop="method" label="鉴定方法" />
                <el-table-column prop="certificate" label="特征描述" />
                <el-table-column prop="storage" label="菌种保存方法" />
                <el-table-column prop="amount" label="保存位置" />
                <el-table-column prop="inventory" label="库存余量" />
                <el-table-column prop="notes" label="备注" />
                <el-table-column prop="status" label="当前状态">
                    <template #default="{ row }">
                        <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="200">
                    <template #default="{ row }">
                        <el-button type="text" @click="handleDetail(row)">详情</el-button>
                        <el-button type="text" @click="handleEdit(row)">编辑</el-button>
                        <el-button type="text" @click="handleRecord(row)">出入库记录</el-button>
                    </template>
                </el-table-column>
            </template>
        </TableCustom>
    </div>
</template>
<script>
export default {
    name: 'ChiefCell',
    components: {
    },
    data() {
        return {
            dialogVisible: false,
            currentType: 'list',
            detailVisible: false,
            currentDetail: {},
            form: {
                strainNo: '',
                strainName: '',
                status: ''
            },
            queryForm: {
                pageSize: 10,
                pageNum: 1
            },
            total: 800,
            tableData: [
                {
                    strainNo: 'YX-2024001',
                    strainName: '大肠杆菌',
                    source: '实验室分离',
                    method: '形态学鉴定、生理生化试验',
                    certificate: '革兰氏阴性杆菌,可发酵葡萄糖产酸产气,IMViC试验++--',
                    storage: '斜面培养',
                    amount: 'A区-01-001',
                    inventory: '50',
                    notes: '用于质粒转化',
                    status: '1'
                },
                {
                    strainNo: 'YX-2024002',
                    strainName: '枯草芽孢杆菌',
                    source: '菌种保藏中心',
                    method: '16S rDNA测序',
                    certificate: '革兰氏阳性芽孢杆菌,可水解淀粉,产生溶菌素',
                    storage: '冷冻保存',
                    amount: 'B区-02-005',
                    inventory: '30',
                    notes: '工业发酵菌种',
                    status: '1'
                },
                {
                    strainNo: 'YX-2024003',
                    strainName: '酿酒酵母',
                    source: '发酵工厂',
                    method: '显微镜观察、生理特性',
                    certificate: '椭圆形单细胞真菌,可发酵葡萄糖产生乙醇',
                    storage: '甘油管保存',
                    amount: 'A区-03-002',
                    inventory: '40',
                    notes: '发酵工艺优化',
                    status: '2'
                },
                {
                    strainNo: 'YX-2024004',
                    strainName: '乳酸菌',
                    source: '乳制品分离',
                    method: '生化鉴定、API条',
                    certificate: '革兰氏阳性球菌,产生乳酸,耐酸性强',
                    storage: '冷冻干燥',
                    amount: 'C区-01-003',
                    inventory: '25',
                    notes: '益生菌研究',
                    status: '3'
                },
                {
                    strainNo: 'YX-2024005',
                    strainName: '青霉菌',
                    source: '环境样本',
                    method: '形态学特征、ITS测序',
                    certificate: '丝状真菌,产生蓝绿色分生孢子,可产青霉素',
                    storage: '斜面培养',
                    amount: 'B区-04-001',
                    inventory: '35',
                    notes: '次级代谢产物研究',
                    status: '1'
                }
            ]
        }
    },
    methods: {
        handleViewMore() {
            this.dialogVisible = true;
        },
        resetForm() {
            this.form = {
                strainNo: '',
                strainName: '',
                status: ''
            }
            this.searchData()
        },
        searchData() {
            // 模拟搜索逻辑
            const { strainNo, strainName, status } = this.form
            let filteredData = [...this.tableData]
            if (strainNo) {
                filteredData = filteredData.filter(item =>
                    item.strainNo.toLowerCase().includes(strainNo.toLowerCase())
                )
            }
            if (strainName) {
                filteredData = filteredData.filter(item =>
                    item.strainName.toLowerCase().includes(strainName.toLowerCase())
                )
            }
            if (status) {
                filteredData = filteredData.filter(item =>
                    item.status === status
                )
            }
            this.total = filteredData.length
            // 实际项目中这里应该调用API
            console.log('搜索条件:', this.form)
            console.log('分页信息:', this.queryForm)
        },
        handleNewStrain() {
            this.$router.push('/strain-library/strain-library-manage/add')
            // Implement new strain logic
        },
        handleBatchAdd() {
            // Implement batch add logic
        },
        handleDetail(row) {
            this.currentDetail = row;
            this.detailVisible = true;
        },
        handleEdit(row) {
            // Implement edit logic
        },
        handleRecord(row) {
            this.$router.push({
                path: '/strain-library/strain-library-manage/record',
                query: {
                    id: row.strainNo
                }
            })
        },
        handleCurrentChange(page) {
            this.queryForm.pageNum = page
            // Implement page change logic
        },
        handleSizeChange(size) {
            this.queryForm.pageSize = size
            // Implement size change logic
        },
        handleTypeChange(type) {
            this.currentType = type;
            // Implement type change logic
        },
        getStatusType(status) {
            const types = {
                1: 'success',
                2: 'info',
                3: 'warning'
            }
            return types[status] || 'info'
        },
        getStatusText(status) {
            const texts = {
                1: '已入库',
                2: '已出库',
                3: '入库待确认'
            }
            return texts[status] || '未知状态'
        }
    }
}
</script>
<style scoped lang="less">
.list {
    padding: 20px;
}
.header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
    .box-title {
        display: flex;
        align-items: center;
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 15px;
        position: relative;
        .header-icon {
            width: 20px;
            height: 20px;
            margin-right: 10px;
        }
        .view-more {
            position: absolute;
            right: 0;
            color: #049C9A;
        }
    }
    .header-content {
        color: rgba(0, 0, 0, 0.88);
        font-size: 14px;
        line-height: 1.8;
        margin-left: 30px;
        transition: max-height 0.3s ease-in-out;
        overflow: hidden;
        &.collapsed {
            max-height: 48px;
            overflow: hidden;
        }
        p {
            margin: 5px 0;
        }
    }
}
.search-form {
    margin-bottom: 20px;
    background: #F5F7FA;
    padding: 24px;
    border-radius: 8px;
    .el-form-item {
        margin-right: 20px;
        margin-bottom: 0;
    }
    .el-button {
        margin-left: 10px;
    }
}
.action-buttons {
    margin-bottom: 20px;
    .el-button {
        margin-right: 10px;
    }
}
.tab-container {
    display: flex;
    margin-bottom: 20px;
    .tab {
        padding: 10px 30px;
        border: 1px solid #DCDFE6;
        border-bottom: none;
        border-radius: 8px 8px 0 0;
        cursor: pointer;
        margin-right: 10px;
        background: #F5F7FA;
        &.active {
            background: #fff;
            border-color: #049C9A;
            color: #049C9A;
            font-weight: bold;
        }
    }
}
.flex {
    display: flex;
    align-items: center;
}
.tableTitle {
    display: flex;
    padding-bottom: 20px;
    justify-content: space-between;
    align-items: center;
    .title {
        background: #fafafc;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #dcdfe6;
        font-weight: bold;
        font-size: 18px;
        color: #606266;
        width: unset;
        cursor: pointer;
        height: 50px;
        line-height: 50px;
        width: 166px;
        text-align: center;
    }
    .drafts {
        background: #fafafc;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #dcdfe6;
        font-weight: 400;
        font-size: 18px;
        color: #606266;
        margin-left: 16px;
        cursor: pointer;
        height: 50px;
        line-height: 50px;
        width: 166px;
        text-align: center;
    }
    .active {
        color: #049c9a;
        background: #ffffff;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #049c9a;
    }
}
.view-all-dialog {
    :deep(.el-dialog__header) {
        padding: 20px;
        border-bottom: 1px solid #EBEEF5;
        margin-right: 0;
        .el-dialog__title {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
        }
    }
    :deep(.el-dialog__body) {
        padding: 20px;
        .dialog-content {
            font-size: 14px;
            line-height: 1.8;
            color: #606266;
            p {
                margin: 12px 0;
                &:first-child {
                    margin-top: 0;
                }
                &:last-child {
                    margin-bottom: 0;
                }
            }
        }
    }
}
</style>
culture/src/views/strain-library/validation/primitive-cell/add.vue
New file
@@ -0,0 +1,370 @@
<template>
    <Card>
        <!-- <div class="header-title">
            <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>新增原始细胞</div>
            </div>
        </div> -->
        <el-form
            :model="form"
            :rules="rules"
            ref="strainForm"
            label-position="top"
            class="strain-form"
        >
            <div class="form-row">
                <el-form-item label="菌种来源" prop="identificationMethod" required>
                    <el-input v-model="form.identificationMethod" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row three-columns">
                <el-form-item label="鉴别菌株编号" prop="storageLocation" required>
                    <el-input v-model="form.storageLocation" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="鉴别菌株名称" prop="preservationMethod" required>
                    <el-input v-model="form.preservationMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <div class="form-item-placeholder"></div>
            </div>
            <div class="form-row three-columns">
                <el-form-item label="验证实验编号" prop="storageLocation" required>
                    <el-input v-model="form.storageLocation" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="实验时间" prop="preservationMethod" required>
                    <el-input v-model="form.preservationMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <div class="form-item-placeholder"></div>
            </div>
            <div class="end-btn" style="margin-top: 400px">
                <el-button type="primary" @click="handleSubmit">提交</el-button>
                <el-button @click="handleDraft">存草稿</el-button>
            </div>
        </el-form>
        <!-- 批量新增弹窗 -->
        <el-dialog
            title="批量新增"
            :visible.sync="batchAddDialogVisible"
            width="520px"
            :close-on-click-modal="false"
            :close-on-press-escape="false"
            custom-class="batch-add-dialog"
        >
            <div class="dialog-content">
                <el-form :model="batchForm" ref="batchFormRef" label-position="top" class="batch-form">
                    <el-form-item
                        label="批量新增数量"
                        prop="count"
                        required
                        :rules="[{ required: true, message: '请输入批量新增数量', trigger: 'blur' }]"
                    >
                        <el-input v-model.number="batchForm.count" placeholder="请输入" />
                    </el-form-item>
                </el-form>
                <div class="dialog-notice">
                    <p>注意:操作批量新增后,系统会自动生成对应数量的菌种数据,</p>
                    <p>这些菌种的基础信息内容都是一致的,唯独菌种编号内容系统</p>
                    <p>不会自动生成,需要操作员自行编辑</p>
                </div>
            </div>
            <template #footer>
                <div class="end-btn">
                    <el-button type="primary" @click="handleConfirmBatchAdd">确认新增</el-button>
                </div>
            </template>
        </el-dialog>
        <!-- 签字确认组件 -->
        <SignatureCanvas
            :visible.sync="signatureVisible"
            @confirm="handleSignatureConfirm"
        />
    </Card>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue'
export default {
    name: 'AddprimitiveCell',
    components: {
        SignatureCanvas
    },
    data() {
        return {
            batchAddDialogVisible: false,
            signatureVisible: false,
            currentAction: '', // 'submit' or 'batchAdd'
            batchForm: {
                count: ''
            },
            form: {
                strainNo: '',
                strainName: '',
                source: '',
                identificationMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remarks: ''
            },
            rules: {
                strainNo: [{ required: true, message: '请输入菌种编号', trigger: 'blur' }],
                strainName: [{ required: true, message: '请输入菌种名称', trigger: 'blur' }],
                source: [{ required: true, message: '请输入菌种来源', trigger: 'blur' }],
                identificationMethod: [{ required: true, message: '请输入鉴定方法', trigger: 'blur' }],
                characteristics: [{ required: true, message: '请输入特征描述', trigger: 'blur' }],
                storageLocation: [{ required: true, message: '请输入保存位置', trigger: 'blur' }],
                preservationMethod: [{ required: true, message: '请输入菌种保存方法', trigger: 'blur' }]
            }
        }
    },
    methods: {
        handleSubmit() {
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.currentAction = 'submit'
                    this.signatureVisible = true
                }
            })
        },
        handleBatchAdd() {
            this.batchAddDialogVisible = true
        },
        handleConfirmBatchAdd() {
            this.$refs.batchFormRef.validate((valid) => {
                if (valid) {
                    this.currentAction = 'batchAdd'
                    this.batchAddDialogVisible = false
                    this.signatureVisible = true
                }
            })
        },
        handleDraft() {
            // 实现存草稿逻辑
            console.log('save draft', this.form)
        },
        handleSignatureConfirm(signatureImage) {
            this.signatureVisible = false
            this.$router.back()
            if (this.currentAction === 'submit') {
                // 处理提交逻辑
                console.log('submit form with signature:', this.form, signatureImage)
            } else if (this.currentAction === 'batchAdd') {
                // 处理批量新增逻辑
                console.log('batch add with signature:', this.batchForm.count, signatureImage)
            }
        }
    }
}
</script>
<style scoped lang="less">
.add-strain {
    height: 100%;
    background: #F5F7FA;
    .form-card {
        background: #fff;
        border-radius: 8px;
    }
}
.header-title {
    margin-bottom: 24px;
    &-left {
        display: flex;
        align-items: center;
        img {
            width: 20px;
            height: 20px;
            margin-right: 8px;
        }
        div {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
        }
    }
}
.end-btn{
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    button{
        width: 180px;
        height: 36px;
        // background: #409EFF;
    }
}
.strain-form {
    padding: 0 40px;
    .form-row {
        display: flex;
        flex-wrap: wrap;
        gap: 24px;
        margin-bottom: 24px;
        &.three-columns {
            .el-form-item, .form-item-placeholder {
                flex: 1;
                min-width: 280px;
                @media screen and (max-width: 1200px) {
                    min-width: calc(50% - 12px);
                }
                @media screen and (max-width: 768px) {
                    min-width: 100%;
                }
            }
            .form-item-placeholder {
                @media screen and (max-width: 1200px) {
                    display: none;
                }
            }
        }
        .el-form-item {
            margin-bottom: 0;
            &.full-width {
                width: 100%;
            }
        }
    }
    :deep(.el-form-item__label) {
        font-weight: normal;
        color: #606266;
        padding-bottom: 8px;
        line-height: 20px;
    }
    :deep(.el-form-item__content) {
        line-height: unset;
    }
    :deep(.el-input__inner) {
        border-radius: 4px;
        height: 36px;
        line-height: 36px;
    }
    :deep(.el-textarea__inner) {
        border-radius: 4px;
        padding: 8px 12px;
        min-height: 120px;
    }
}
.batch-add-dialog {
    :deep(.el-dialog__header) {
        margin: 0;
        padding: 20px;
        text-align: center;
        border-bottom: 1px solid #EBEEF5;
        .el-dialog__title {
            font-size: 16px;
            font-weight: 600;
            color: #303133;
        }
    }
    :deep(.el-dialog__body) {
        padding: 20px;
    }
    .dialog-content {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .batch-form {
        width: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        :deep(.el-form-item) {
            width: 320px;
            margin-bottom: 0;
        }
        :deep(.el-form-item__label) {
            width: 100%;
            color: #606266;
            font-weight: normal;
            padding-bottom: 8px;
            &::before {
                color: #F56C6C;
            }
        }
        :deep(.el-input) {
            width: 100%;
            input {
            width: 100%;
            }
        }
    }
    .dialog-notice {
        margin-top: 16px;
        text-align: center;
        p {
            margin: 0;
            line-height: 22px;
            color: #606266;
            font-size: 14px;
        }
    }
    :deep(.el-dialog__footer) {
        padding: 0 20px 20px;
        text-align: center;
        .el-button {
            width: 180px;
            height: 36px;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 14px;
            border-radius: 4px;
            margin: 0;
        }
    }
}
.end-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    :deep(.el-button) {
        width: 180px;
        height: 36px;
        padding: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        border-radius: 4px;
        margin: 0;
    }
}
</style>
culture/src/views/strain-library/validation/primitive-cell/index.vue
New file
@@ -0,0 +1,427 @@
<template>
    <div class="list">
        <!-- Table -->
        <TableCustom :queryForm="queryForm" :tableData="tableData" :total="total" @currentChange="handleCurrentChange"
            @sizeChange="handleSizeChange">
            <template #search>
                <el-form :model="form" label-width="auto" inline>
                    <el-form-item label="鉴别菌株编号:">
                        <el-input v-model="form.strainNo" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="鉴别菌株名称:">
                        <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="验证实验编号:">
                        <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item class="search-btn-box">
                            <el-button type="default" @click="resetForm">重置</el-button>
                            <el-button type="primary" @click="searchData">查询</el-button>
                    </el-form-item>
                </el-form>
            </template>
            <template #setting>
                <div class="tableTitle">
                    <div class="flex a-center">
                        <div class="title" :class="{ active: currentType === 'list' }"
                            @click="handleTypeChange('list')">
                            原始细胞库验证资料</div>
                        <div class="drafts" :class="{ active: currentType === 'draft' }"
                            @click="handleTypeChange('draft')">
                            草稿箱</div>
                    </div>
                    <div class="flex a-center">
                        <el-button @click="handleNewStrain" class="el-icon-plus" type="primary" style="margin-right: 12px;">新增原始细胞库资料</el-button>
                    </div>
                </div>
            </template>
            <template #table>
                <el-table-column prop="source" label="菌种来源" />
                <el-table-column prop="method" label="鉴别菌株编号" />
                <el-table-column prop="certificate" label="鉴别菌株名称" />
                <el-table-column prop="storage" label="验证实验编号" />
                <el-table-column prop="amount" label="创建人" />
                <el-table-column prop="inventory" label="创建时间" />
                <el-table-column prop="status" label="状态">
                    <template #default="{ row }">
                        <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="200">
                    <template #default="{ row }">
                        <el-button type="text" @click="handleDetail(row)">确认</el-button>
                        <el-button type="text" @click="handleDetail(row)">详情</el-button>
                        <el-button type="text" @click="handleEdit(row)">编辑</el-button>
                        <el-button type="text" @click="handleRecord(row)">删除</el-button>
                    </template>
                </el-table-column>
            </template>
        </TableCustom>
    </div>
</template>
<script>
export default {
    name: 'PrimitiveCell',
    components: {
    },
    data() {
        return {
            dialogVisible: false,
            currentType: 'list',
            detailVisible: false,
            currentDetail: {},
            form: {
                strainNo: '',
                strainName: '',
                status: ''
            },
            queryForm: {
                pageSize: 10,
                pageNum: 1
            },
            total: 800,
            tableData: [
                {
                    strainNo: 'YX-2024001',
                    strainName: '大肠杆菌',
                    source: '实验室分离',
                    method: '形态学鉴定、生理生化试验',
                    certificate: '革兰氏阴性杆菌,可发酵葡萄糖产酸产气,IMViC试验++--',
                    storage: '斜面培养',
                    amount: 'A区-01-001',
                    inventory: '50',
                    notes: '用于质粒转化',
                    status: '1'
                },
                {
                    strainNo: 'YX-2024002',
                    strainName: '枯草芽孢杆菌',
                    source: '菌种保藏中心',
                    method: '16S rDNA测序',
                    certificate: '革兰氏阳性芽孢杆菌,可水解淀粉,产生溶菌素',
                    storage: '冷冻保存',
                    amount: 'B区-02-005',
                    inventory: '30',
                    notes: '工业发酵菌种',
                    status: '1'
                },
                {
                    strainNo: 'YX-2024003',
                    strainName: '酿酒酵母',
                    source: '发酵工厂',
                    method: '显微镜观察、生理特性',
                    certificate: '椭圆形单细胞真菌,可发酵葡萄糖产生乙醇',
                    storage: '甘油管保存',
                    amount: 'A区-03-002',
                    inventory: '40',
                    notes: '发酵工艺优化',
                    status: '2'
                },
                {
                    strainNo: 'YX-2024004',
                    strainName: '乳酸菌',
                    source: '乳制品分离',
                    method: '生化鉴定、API条',
                    certificate: '革兰氏阳性球菌,产生乳酸,耐酸性强',
                    storage: '冷冻干燥',
                    amount: 'C区-01-003',
                    inventory: '25',
                    notes: '益生菌研究',
                    status: '3'
                },
                {
                    strainNo: 'YX-2024005',
                    strainName: '青霉菌',
                    source: '环境样本',
                    method: '形态学特征、ITS测序',
                    certificate: '丝状真菌,产生蓝绿色分生孢子,可产青霉素',
                    storage: '斜面培养',
                    amount: 'B区-04-001',
                    inventory: '35',
                    notes: '次级代谢产物研究',
                    status: '1'
                }
            ]
        }
    },
    methods: {
        handleViewMore() {
            this.dialogVisible = true;
        },
        resetForm() {
            this.form = {
                strainNo: '',
                strainName: '',
                status: ''
            }
            this.searchData()
        },
        searchData() {
            // 模拟搜索逻辑
            const { strainNo, strainName, status } = this.form
            let filteredData = [...this.tableData]
            if (strainNo) {
                filteredData = filteredData.filter(item =>
                    item.strainNo.toLowerCase().includes(strainNo.toLowerCase())
                )
            }
            if (strainName) {
                filteredData = filteredData.filter(item =>
                    item.strainName.toLowerCase().includes(strainName.toLowerCase())
                )
            }
            if (status) {
                filteredData = filteredData.filter(item =>
                    item.status === status
                )
            }
            this.total = filteredData.length
            // 实际项目中这里应该调用API
            console.log('搜索条件:', this.form)
            console.log('分页信息:', this.queryForm)
        },
        handleNewStrain() {
            this.$router.push('/strain/validation/add-primitive-cell')
            // Implement new strain logic
        },
        handleBatchAdd() {
            // Implement batch add logic
        },
        handleDetail(row) {
            this.currentDetail = row;
            this.detailVisible = true;
        },
        handleEdit(row) {
            // Implement edit logic
        },
        handleRecord(row) {
            this.$router.push({
                path: '/strain-library/strain-library-manage/record',
                query: {
                    id: row.strainNo
                }
            })
        },
        handleCurrentChange(page) {
            this.queryForm.pageNum = page
            // Implement page change logic
        },
        handleSizeChange(size) {
            this.queryForm.pageSize = size
            // Implement size change logic
        },
        handleTypeChange(type) {
            this.currentType = type;
            // Implement type change logic
        },
        getStatusType(status) {
            const types = {
                1: 'success',
                2: 'info',
                3: 'warning'
            }
            return types[status] || 'info'
        },
        getStatusText(status) {
            const texts = {
                1: '已入库',
                2: '已出库',
                3: '入库待确认'
            }
            return texts[status] || '未知状态'
        }
    }
}
</script>
<style scoped lang="less">
.list {
    padding: 20px;
}
.header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
    .box-title {
        display: flex;
        align-items: center;
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 15px;
        position: relative;
        .header-icon {
            width: 20px;
            height: 20px;
            margin-right: 10px;
        }
        .view-more {
            position: absolute;
            right: 0;
            color: #049C9A;
        }
    }
    .header-content {
        color: rgba(0, 0, 0, 0.88);
        font-size: 14px;
        line-height: 1.8;
        margin-left: 30px;
        transition: max-height 0.3s ease-in-out;
        overflow: hidden;
        &.collapsed {
            max-height: 48px;
            overflow: hidden;
        }
        p {
            margin: 5px 0;
        }
    }
}
.search-form {
    margin-bottom: 20px;
    background: #F5F7FA;
    padding: 24px;
    border-radius: 8px;
    .el-form-item {
        margin-right: 20px;
        margin-bottom: 0;
    }
    .el-button {
        margin-left: 10px;
    }
}
.action-buttons {
    margin-bottom: 20px;
    .el-button {
        margin-right: 10px;
    }
}
.tab-container {
    display: flex;
    margin-bottom: 20px;
    .tab {
        padding: 10px 30px;
        border: 1px solid #DCDFE6;
        border-bottom: none;
        border-radius: 8px 8px 0 0;
        cursor: pointer;
        margin-right: 10px;
        background: #F5F7FA;
        &.active {
            background: #fff;
            border-color: #049C9A;
            color: #049C9A;
            font-weight: bold;
        }
    }
}
.flex {
    display: flex;
    align-items: center;
}
.tableTitle {
    display: flex;
    padding-bottom: 20px;
    justify-content: space-between;
    align-items: center;
    .title {
        background: #fafafc;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #dcdfe6;
        font-weight: bold;
        font-size: 18px;
        color: #606266;
        width: unset;
        cursor: pointer;
        height: 50px;
        line-height: 50px;
        width: 166px;
        text-align: center;
    }
    .drafts {
        background: #fafafc;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #dcdfe6;
        font-weight: 400;
        font-size: 18px;
        color: #606266;
        margin-left: 16px;
        cursor: pointer;
        height: 50px;
        line-height: 50px;
        width: 166px;
        text-align: center;
    }
    .active {
        color: #049c9a;
        background: #ffffff;
        border-radius: 8px 8px 0px 0px;
        border: 1px solid #049c9a;
    }
}
.view-all-dialog {
    :deep(.el-dialog__header) {
        padding: 20px;
        border-bottom: 1px solid #EBEEF5;
        margin-right: 0;
        .el-dialog__title {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
        }
    }
    :deep(.el-dialog__body) {
        padding: 20px;
        .dialog-content {
            font-size: 14px;
            line-height: 1.8;
            color: #606266;
            p {
                margin: 12px 0;
                &:first-child {
                    margin-top: 0;
                }
                &:last-child {
                    margin-bottom: 0;
                }
            }
        }
    }
}
</style>