13404089107
2 天以前 993e5fd593398926af72af660cb5ed6aba8e4e2b
对接接口
60个文件已修改
42个文件已添加
22176 ■■■■ 已修改文件
culture/src/assets/public/require.png 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/assets/public/selectType.png 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/SelectMember/service.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/confirm-storage-dialog/index.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/layouts/components/HeaderNav.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/router/index.js 815 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/detail.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/index.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/service.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/add.vue 808 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/addProgenitor.vue 807 ●●●●● 补丁 | 查看 | 原始文档 | 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 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddAncestor.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddSublevelForm.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/PlanForm.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/service.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/addProject.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/detailProject.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/editProject.vue 285 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/service.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/SlantRecordDialog.vue 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/breeding-record/add.vue 478 ●●●●● 补丁 | 查看 | 原始文档 | 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/main-cell-library/add.vue 573 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/main-cell-library/index.vue 851 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/main-cell-library/record.vue 684 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/main-cell-library/service.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/production-cell-library/add.vue 384 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/production-cell-library/index.vue 495 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/production-cell-library/record.vue 864 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/production-cell-library/service.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/add.vue 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/components/AddRecordDialog.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/components/RecordDetailDialog.vue 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/components/RecordTimeline.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/components/StrainDetail.vue 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/index.vue 843 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/record.vue 795 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-library-manage/service.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/chief-cell/index.vue 435 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/DetailConditionDialog.vue 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/EditConditionDialog.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/add.vue 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/confirm-detail.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/index.vue 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/validation/primitive-cell/primitive-cell-detail-dialog.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/components/add-edit.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/components/reset-password.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/App.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/addTableData.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/addTableHeader.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/index.vue 491 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/SelectMember/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/SelectMember/service.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/SelectMemberSimple/index.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/TableSlot/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/approvalProcess/index.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/router/index.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/approvalPlan/list.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/components/add-test-item.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/components/add.vue 313 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/components/confirm-dialog.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/components/experimental-scheduling.vue 165 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/components/review-dialog.vue 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/index.vue 203 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/confirmation-sheet/service.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/dispatching/addDispatch.vue 226 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/dispatching/editDispatch.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/dispatching/list.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/dispatching/service.js 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue 689 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue 786 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/components/experimental-scheduling.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/list.vue 449 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/service.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue 178 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/suspendExperiment/components/approvalDialog.vue 199 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/suspendExperiment/list.vue 296 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/suspendExperiment/service.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityReport/add.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityReport/components/approval/index.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityReport/index.vue 273 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityReport/service.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityStudy/add.vue 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityStudy/components/approval/index.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityStudy/index.vue 166 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/feasibilityStudy/service.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/processDevelopment/add.vue 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/processDevelopment/components/approval/index.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/processDevelopment/index.vue 272 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/reportLibrary/processDevelopment/service.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/assets/public/require.png
culture/src/assets/public/selectType.png
culture/src/components/SelectMember/service.js
New file
@@ -0,0 +1,16 @@
import axios from '@/utils/request';
// 列表
export const getProjectList = (data) => {
    return axios.post('/api/t-project-team/pageList', { ...data })
}
// 用户列表
export const getUserList = (data) => {
    return axios.post('/system/user/list', { ...data })
}
// 角色列表不分页
export const getRoleList = (data) => {
    return axios.post('/system/role/listNotPage', { ...data })
}
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/layouts/components/HeaderNav.vue
@@ -17,7 +17,7 @@
      </div>
      <div class="user-info">
        <img src="@/assets/public/photo.png" />
        <div class="user-info-text">欢迎您,admin</div>
        <div class="user-info-text">欢迎您,{{ userInfo.nickName }}</div>
        <div class="user-info-line"></div>
        <div @click="outLogin" class="user-info-out">
          <img src="@/assets/public/logOut.png" />
culture/src/router/index.js
@@ -1,14 +1,14 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Layouts from "../layouts";
import Parent from "../layouts/components/AppContent"
import Parent from "../layouts/components/AppContent";
import store from "../store";
Vue.use(VueRouter);
const originalPush = VueRouter.prototype.push
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
    return originalPush.call(this, location).catch(err => err)
}
  return originalPush.call(this, location).catch((err) => err);
};
/**
 *  path: "/login",   ------页面地址
@@ -24,382 +24,509 @@
 */
const routes = [
    {
        path: "/",
        redirect: "/login",
  {
    path: "/",
    redirect: "/login",
  },
  {
    path: "/login",
    meta: {
      title: "登录",
      oneself: true,
      hide: true,
    },
    {
        path: "/login",
    component: () => import("../views/login"),
  },
  {
    path: "/projectList",
    meta: {
      title: "项目组管理",
    },
    component: Layouts,
    children: [
      {
        path: "list",
        name: "ProjectList",
        meta: {
            title: "登录",
            oneself: true,
            hide: true,
          title: "菌种库项目组管理",
        },
        component: () => import("../views/login"),
    },
    {
        path: "/projectList",
        component: () => import("../views/projectList"),
      },
      {
        path: "addProject",
        name: "AddProject",
        meta: {
            title: "项目组管理",
          title: "新增菌种库项目组",
          hide: true,
          keepAlive: true,
        },
        component: Layouts,
        children: [
            {
                path: "list",
                name: "ProjectList",
                meta: {
                    title: "菌种库项目组管理",
                },
                component: () => import("../views/projectList"),
            },
            {
                path: "addProject",
                name: "AddProject",
                meta: {
                    title: "新增菌种库项目组",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/projectList/addProject"),
            }
        ]
    },
    {
        path: "/system",
        component: () => import("../views/projectList/addProject"),
      },
      {
        path: "editProject",
        name: "EditProject",
        meta: {
            title: "系统管理",
          title: "编辑菌种库项目组",
          hide: true,
        },
        component: Layouts,
        children: [
            {
                path: "user",
                name: "User",
                meta: {
                    title: "人员管理",
                },
                component: () => import("../views/system/user"),
            },
            {
                path: "role",
                name: "Role",
                meta: {
                    title: "角色管理",
                },
                component: () => import("../views/system/role"),
            },
            {
                path: "add-role",
                name: "AddRole",
                meta: {
                    title: "新增角色",
                    hide: true,
                },
                component: () => import("../views/system/role/add"),
            },
            {
                path: "edit-role",
                name: "EditRole",
                meta: {
                    title: "编辑角色",
                    hide: true,
                },
                component: () => import("../views/system/role/edit"),
            },
            {
                path: "detail-role",
                name: "DetailRole",
                meta: {
                    title: "角色详情",
                    hide: true,
                },
                component: () => import("../views/system/role/detail"),
            },
            {
                path: "operation-log",
                meta: {
                    title: "操作日志",
                },
                component: () => import("../views/system/operation-log"),
            },
        ]
    },
    {
        path: "/strain",
        component: Layouts,
        component: () => import("../views/projectList/editProject"),
      },
      {
        path: "detailProject",
        name: "DetailProject",
        meta: {
            title: "菌种库",
          title: "菌种库项目组详情",
          hide: true,
        },
        component: () => import("../views/projectList/detailProject"),
      },
    ],
  },
  {
    path: "/system",
    meta: {
      title: "系统管理",
    },
    component: Layouts,
    children: [
      {
        path: "user",
        name: "User",
        meta: {
          title: "人员管理",
        },
        component: () => import("../views/system/user"),
      },
      {
        path: "role",
        name: "Role",
        meta: {
          title: "角色管理",
        },
        component: () => import("../views/system/role"),
      },
      {
        path: "add-role",
        name: "AddRole",
        meta: {
          title: "新增角色",
          hide: true,
        },
        component: () => import("../views/system/role/add"),
      },
      {
        path: "edit-role",
        name: "EditRole",
        meta: {
          title: "编辑角色",
          hide: true,
        },
        component: () => import("../views/system/role/edit"),
      },
      {
        path: "detail-role",
        name: "DetailRole",
        meta: {
          title: "角色详情",
          hide: true,
        },
        component: () => import("../views/system/role/detail"),
      },
      {
        path: "operation-log",
        meta: {
          title: "操作日志",
        },
        component: () => import("../views/system/operation-log"),
      },
    ],
  },
  {
    path: "/strain",
    component: Layouts,
    meta: {
      title: "菌种库",
    },
    children: [
      {
        path: "/strain-library",
        component: Parent,
        meta: {
          title: "菌种库管理",
        },
        children: [
            {
                path: "/strain-library",
                component: Parent,
                meta: {
                    title: "菌种库管理",
                },
                children: [
                    {
                        path: "strain-library-manage",
                        name: "StrainLibraryManage",
                        meta: {
                            title: "原始细胞库",
                            keepAlive: true,
                        },
                        component: () => import("../views/strain-library/strain-library-manage"),
                    },
                    {
                        path: "strain-library-manage/add",
                        name: "StrainLibraryManageAdd",
                        meta: {
                            title: "新增原始细胞库",
                            keepAlive: true,
                            hide: true
                        },
                        component: () => import("../views/strain-library/strain-library-manage/add.vue"),
                    },
                    {
                        path: "strain-library-manage/record",
                        name: "StrainRecord",
                        meta: {
                            title: "出入库记录",
                            keepAlive: true,
                            hide: true
                        },
                        component: () => import("../views/strain-library/strain-library-manage/record.vue"),
                    },
                    {
                        path: "main-cell-library",
                        name: "MainCellLibrary",
                        meta: {
                            title: "主细胞库",
                            keepAlive: true,
                        },
                        component: () => import("../views/strain-library/main-cell-library"),
                    },
                    {
                        path: "main-cell-library/add",
                        name: "MainCellLibraryAdd",
                        meta: {
                            title: "新增主细胞",
                            keepAlive: true,
                            hide: true
                        },
                        component: () => import("../views/strain-library/main-cell-library/add.vue"),
                    },
                    {
                        path: "production-cell-library",
                        name: "ProductionCellLibrary",
                        meta: {
                            title: "生产细胞库",
                            keepAlive: true,
                        },
                        component: () => import("../views/strain-library/production-cell-library"),
                    },
                    {
                        path: "production-cell-library/add",
                        name: "ProductionCellLibraryAdd",
                        meta: {
                            title: "新增生产细胞",
                            keepAlive: true,
                            hide: true
                        },
                        component: () => import("../views/strain-library/production-cell-library/add.vue"),
                    }
                ]
            },
            {
                path: 'pedigree-vhart',
                name: 'PedigreeChart',
                meta: {
                    title: "菌种传代生产谱系图",
                },
                component: () => import("../views/pedigree-chart"),
            },
            {
                path: 'add-pedigree',
                name: 'AddPedigree',
                meta: {
                    title: "新增菌种传代生产谱系图",
                },
                component: () => import("../views/pedigree-chart/add"),
            },
            {
                path: 'add-pedigree',
                name: 'AddPedigree',
                meta: {
                    title: "新增菌种传代生产谱系图",
                },
                component: () => import("../views/pedigree-chart/add"),
            }
        ]
    }, {
        path: "/strainReportLibrary",
        component: Layouts,
        meta: {
            title: "菌种报告库",
        },
        children: [{
            path: "reportLibraryOne",
          {
            path: "strain-library-manage",
            name: "StrainLibraryManage",
            meta: {
                title: "报告库一",
                keepAlive: true,
              title: "原始细胞库",
              keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOne/index.vue"),
        },
        {
            path: "add",
            component: () =>
              import("../views/strain-library/strain-library-manage"),
          },
          {
            path: "strain-library-manage/add",
            name: "StrainLibraryManageAdd",
            meta: {
                title: "新增报告",
                hide: true,
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOne/add.vue"),
        },
        {
            path: "reportLibraryTwo",
            meta: {
                title: "报告库二",
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneTWO/index.vue"),
        },
        {
            path: "addTwo",
            meta: {
                title: "新增报告",
                hide: true,
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneTWO/add.vue"),
        },
        {
            path: "reportLibraryThree",
            meta: {
                title: "报告库三",
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneThree/index.vue"),
        },
        {
            path: "addThree",
            meta: {
                title: "新增报告",
                hide: true,
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneThree/add.vue"),
        },
        {
            path: "reportLibraryFour",
            meta: {
                title: "报告库四",
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneFour/index.vue"),
        },
        {
            path: "addFour",
            meta: {
                title: "新增报告",
                hide: true,
                keepAlive: true,
            },
            component: () => import("../views/strainReportLibrary/reportLibraryOneFour/add.vue"),
        },
              title: "新增原始细胞库",
              keepAlive: true,
              hide: true,
            },
            component: () =>
              import("../views/strain-library/strain-library-manage/add.vue"),
          },
          {
            path: "strain-library-manage/record",
            name: "StrainRecord",
            meta: {
              title: "出入库记录",
              hide: true,
            },
            component: () =>
              import(
                "../views/strain-library/strain-library-manage/record.vue"
              ),
          },
          {
            path: "main-cell-library",
            name: "MainCellLibrary",
            meta: {
              title: "主细胞库",
              keepAlive: true,
            },
            component: () =>
              import("../views/strain-library/main-cell-library"),
          },
          {
            path: "main-cell-library/add",
            name: "MainCellLibraryAdd",
            meta: {
              title: "新增主细胞",
              keepAlive: true,
              hide: true,
            },
            component: () =>
              import("../views/strain-library/main-cell-library/add.vue"),
          },
          {
            path: "production-cell-library",
            name: "ProductionCellLibrary",
            meta: {
              title: "生产细胞库",
              keepAlive: true,
            },
            component: () =>
              import("../views/strain-library/production-cell-library"),
          },
          {
            path: "production-cell-library/add",
            name: "ProductionCellLibraryAdd",
            meta: {
              title: "新增生产细胞",
              keepAlive: true,
              hide: true,
            },
            component: () =>
              import("../views/strain-library/production-cell-library/add.vue"),
          },
        ],
    },
    {
        path: "/deliveryAssessment",
        component: Layouts,
      },
      {
        path: "pedigree-vhart",
        name: "PedigreeChart",
        meta: {
            title: "菌种报告评定",
          title: "菌种传代生产谱系图",
        },
        children: [{
            path: "projectTeamIntegral",
        component: () => import("../views/pedigree-chart"),
      },
      {
        path: "add-pedigree",
        name: "AddPedigree",
        meta: {
          title: "新增母代菌种传代生产谱系图",
          hide: true,
        },
        component: () => import("../views/pedigree-chart/add"),
      },
      {
        path: "add-progenitor",
        name: "AddProgenitor",
        meta: {
          title: "新增祖代菌种传代生产谱系图",
          hide: true,
        },
        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: "菌种项目组评定表",
              title: "原始细胞库资料",
            },
            component: () => import("../views/deliveryAssessment/projectTeamIntegral"),
        },
        {
            path: "projectTeamIntegral-detail",
            component: () =>
              import(
                "../views/strain-library/validation/primitive-cell/index.vue"
              ),
          },
          {
            path: "add-primitive-cell",
            name: "AddPrimitiveCell",
            meta: {
                title: "评定详情",
                hide: true
              title: "新增原始细胞库资料",
              hide: true,
            },
            component: () => import("../views/deliveryAssessment/projectTeamIntegral/detail.vue"),
            component: () =>
              import(
                "../views/strain-library/validation/primitive-cell/add.vue"
              ),
          },
          {
            path: "confirm-detail",
            name: "ConfirmDetail",
            meta: {
              title: "确认原始细胞库资料",
              hide: true,
            },
            component: () =>
              import(
                "../views/strain-library/validation/primitive-cell/confirm-detail.vue"
              ),
          },
          {
            path: "chief-cell",
            name: "ChiefCell",
            meta: {
              title: "主细胞库资料",
            },
            component: () =>
              import("../views/strain-library/validation/chief-cell"),
          },
        ],
      },
    ],
  },
  {
    path: "/strainReportLibrary",
    component: Layouts,
    meta: {
      title: "菌种报告库",
    },
    children: [
      {
        path: "reportLibraryOne",
        meta: {
          title: "报告库一",
          keepAlive: true,
        },
        ]
    }
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOne/index.vue"),
      },
      {
        path: "add",
        meta: {
          title: "新增报告",
          hide: true,
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOne/add.vue"),
      },
      {
        path: "reportLibraryTwo",
        meta: {
          title: "报告库二",
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOneTWO/index.vue"),
      },
      {
        path: "addTwo",
        meta: {
          title: "新增报告",
          hide: true,
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOneTWO/add.vue"),
      },
      {
        path: "reportLibraryThree",
        meta: {
          title: "报告库三",
          keepAlive: true,
        },
        component: () =>
          import(
            "../views/strainReportLibrary/reportLibraryOneThree/index.vue"
          ),
      },
      {
        path: "addThree",
        meta: {
          title: "新增报告",
          hide: true,
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOneThree/add.vue"),
      },
      {
        path: "reportLibraryFour",
        meta: {
          title: "报告库四",
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOneFour/index.vue"),
      },
      {
        path: "addFour",
        meta: {
          title: "新增报告",
          hide: true,
          keepAlive: true,
        },
        component: () =>
          import("../views/strainReportLibrary/reportLibraryOneFour/add.vue"),
      },
    ],
  },
  {
    path: "/deliveryAssessment",
    component: Layouts,
    meta: {
      title: "菌种报告评定",
    },
    children: [
      {
        path: "projectTeamIntegral",
        meta: {
          title: "菌种项目组评定表",
        },
        component: () =>
          import("../views/deliveryAssessment/projectTeamIntegral"),
      },
      {
        path: "projectTeamIntegral-detail",
        meta: {
          title: "评定详情",
          hide: true,
        },
        component: () =>
          import("../views/deliveryAssessment/projectTeamIntegral/detail.vue"),
      },
    ],
  },
];
const router = new VueRouter({
    mode: "hash",
    base: process.env.BASE_URL,
    routes,
  mode: "hash",
  base: process.env.BASE_URL,
  routes,
});
// 前置路由拦截器
router.beforeEach((to, from, next) => {
    // 设置当前页签名称
    document.title = to.meta.title || '实验室流程';
  // 设置当前页签名称
  document.title = to.meta.title || "实验室流程";
    // 登录验证
    // 排除登录页的校验
    // if (to.path === "/login") {
    //     if (sessionStorage.getItem('token')) {
    //         next('/projectList');  // 已登录状态访问登录页时重定向到系统首页
    //         return;
    //     }
    //     next();
    //     return;
    // }
    // // 登录状态校验
    // const isAuthenticated = sessionStorage.getItem('token');
    // if (!isAuthenticated) {
    //     next('/login');  // 未登录用户重定向到登录页
    //     return;
    // }
    // 判断是否拥有要跳转菜单权限
    let menus = store.state.menus
    if (to.meta.hasOwnProperty('privilege') && !menus.includes(to.meta.privilege)) {
        return
  // 登录验证
  // 排除登录页的校验
  if (to.path === "/login") {
    if (sessionStorage.getItem("token")) {
      next("/projectList"); // 已登录状态访问登录页时重定向到系统首页
      return;
    }
    next();
    return;
  }
    // 设置标签列表
    if (!to.meta.hide || !to.meta.oneself) {
        let tagList = JSON.parse(sessionStorage.getItem('tagList') || '[]')
        // 判断是否存在
        let isExist = tagList.some(item => item.path === to.path)
        if (!isExist) {
            // 只保存必要的信息
            const tagInfo = {
                path: to.path,
                name: to.name,
                meta: to.meta,
                query: to.query,
            }
            tagList.push(tagInfo)
            sessionStorage.setItem('tagList', JSON.stringify(tagList))
            store.commit('SET_TAGLIST', tagList)
        }
  // 登录状态校验
  const isAuthenticated = sessionStorage.getItem("token");
  if (!isAuthenticated) {
    next("/login"); // 未登录用户重定向到登录页
    return;
  }
  // 判断是否拥有要跳转菜单权限
  let menus = store.state.menus;
  if (
    to.meta.hasOwnProperty("privilege") &&
    !menus.includes(to.meta.privilege)
  ) {
    return;
  }
  // 设置标签列表
  if (!to.meta.hide || !to.meta.oneself) {
    let tagList = JSON.parse(sessionStorage.getItem("tagList") || "[]");
    // 判断是否存在
    let isExist = tagList.some((item) => item.path === to.path);
    if (!isExist) {
      // 只保存必要的信息
      const tagInfo = {
        path: to.path,
        name: to.name,
        meta: to.meta,
        query: to.query,
      };
      tagList.push(tagInfo);
      sessionStorage.setItem("tagList", JSON.stringify(tagList));
      store.commit("SET_TAGLIST", tagList);
    }
  }
    // 判断是否需要缓存
    if (to.meta.keepAlive) {
        let keepAliveList = JSON.parse(sessionStorage.getItem('keepAliveList') || '[]')
        // 判断是否已经缓存
        let isExist = keepAliveList.includes(to.name)
        if (!isExist) {
            keepAliveList.push(to.name)
            sessionStorage.setItem('keepAliveList', JSON.stringify(keepAliveList))
            store.commit('SET_KEEPALIVELIST', keepAliveList)
        }
  // 判断是否需要缓存
  if (to.meta.keepAlive) {
    let keepAliveList = JSON.parse(
      sessionStorage.getItem("keepAliveList") || "[]"
    );
    // 判断是否已经缓存
    let isExist = keepAliveList.includes(to.name);
    if (!isExist) {
      keepAliveList.push(to.name);
      sessionStorage.setItem("keepAliveList", JSON.stringify(keepAliveList));
      store.commit("SET_KEEPALIVELIST", keepAliveList);
    }
  }
    next()
  next();
});
export default router;
culture/src/views/deliveryAssessment/projectTeamIntegral/detail.vue
@@ -1,20 +1,23 @@
<template>
    <div>
    <div v-if="Object.keys(detailData).length">
        <div class="top-box-header">
            <div class="top-box-header-title">
                <div>项目组总积分表</div>
                <div class="top-box-header-time">
                    <div>评定开始时间:2024-02-09</div>
                    <div>评定结束始时间:2024-02-09</div>
                    <div>评定开始时间:{{ detailData.startTime }}</div>
                    <div>评定结束始时间:{{ detailData.endTime }}</div>
                </div>
            </div>
            <div class="top-box-integral">
                <div :style="{ backgroundColor: ['rgba(232, 250, 246, 1)', 'rgba(254, 237, 220, 1)', 'rgba(239, 248, 255, 1)', 'rgba(255, 237, 238, 1)'][item - 1] }"
                    v-for="item in 4" :key="item" class="top-box-integral-card">
                    <div class="top-box-integral-card-title">{{ ['项目组总积分', '化验师积分', '实验员积分', '实验终止次数'][item -
                    <div class="top-box-integral-card-title">{{ ['项目组总积分', '菌种工程师积分', '菌种实验员积分', '菌种实验失败次数'][item -
                        1] }}</div>
                    <div :style="{ color: ['rgba(4, 156, 154, 1)', 'rgba(255, 147, 0, 1)', 'rgba(23, 119, 213, 1)', 'rgba(255, 73, 85, 1)'][item - 1] }"
                        class="top-box-integral-card-num">99.9</div>
                        class="top-box-integral-card-num">{{
                            detailData[['teamIntegral', 'engineerIntegral', 'experimenterIntegral', 'failCount'][item - 1]]
                        }}
                    </div>
                </div>
            </div>
        </div>
@@ -27,8 +30,13 @@
                    </div>
                </div>
                <div class="integral-content-box-right">
                    <div v-show="actionsLeftTab != 1" @wheel.prevent="handleWheel" class="integral-content-box-right-nameTab">
                        <div @click="changeActiveName(item)" :class="activeNameTab == item && 'activeName'" class="integral-content-box-right-nameTab-name" v-for="item in 8" :key="item">张三</div>
                    <div v-show="actionsLeftTab != 1" @wheel.prevent="handleWheel"
                        class="integral-content-box-right-nameTab">
                        <div @click="changeActiveName(item.userName)"
                            :class="activeNameTab == item.userName && 'activeName'"
                            class="integral-content-box-right-nameTab-name"
                            v-for="{ item, index } in detailData.detailExperimentVOS" :key="index">{{ item.userName }}
                        </div>
                    </div>
                    <div class="integral-content-box-right-thead">
                        <div>评定项</div>
@@ -37,14 +45,31 @@
                        <div>结束时间</div>
                    </div>
                    <div class="integral-content-box-right-body">
                        <div v-for="item in itemList" :key="item" class="integral-content-box-right-body-item">
                        <div v-for="(item, index) in itemList" :key="index"
                            class="integral-content-box-right-body-item">
                            <div>{{ item.gainer }}</div>
                            <div>
                                <div v-if="item.situationOne">{{ item.situationOne }}</div>
                                <div v-if="item.situationTwo">{{ item.situationTwo }}</div>
                                <div>{{ item.situationOne }}{{
                                    actionsLeftTab === 2 ?
                                        (selectedExperimenter || {})[item.keys[0]] :
                                        detailData[item.keys[0]]
                                }}</div>
                                <div>{{ item.situationTwo }}{{
                                    actionsLeftTab === 2 ?
                                        (selectedExperimenter || {})[item.keys[1]] :
                                        detailData[item.keys[1]]
                                }}</div>
                            </div>
                            <div></div>
                            <div></div>
                            <div>{{
                                actionsLeftTab === 2 ?
                                    (selectedExperimenter || {})[item.keys[2]] :
                                    detailData[item.keys[2]]
                            }}</div>
                            <div>{{
                                actionsLeftTab === 2 ?
                                    (selectedExperimenter || {})[item.keys[3]] :
                                    detailData[item.keys[3]]
                            }}</div>
                        </div>
                    </div>
                </div>
@@ -54,27 +79,30 @@
</template>
<script>
import { getDetailData } from './service'
export default {
    data() {
        return {
            actionsLeftTab: 1,
            activeNameTab: 1,
            actionspPersonnel: null,
            activeNameTab: '',
            craftList: [
                {
                    gainer: '1、创新型课题',
                    situationOne: '课题数:',
                    situationTwo: '积分数:',
                    keys: ['innovateCount', 'innovateIntegral', 'innovateStartTime', 'innovateEndTime']
                },
                {
                    gainer: '2、规程型课题',
                    situationOne: '课题数:',
                    situationTwo: '积分数:',
                    keys: ['regulationCount', 'regulationIntegral', 'regulationStartTime', 'regulationEndTime']
                },
                {
                    gainer: '3、实验操作评定',
                    situationOne: '考核数:',
                    situationTwo: '积分数:',
                    keys: ['handleCount', 'handleIntegral', 'handleStartTime', 'handleEndTime']
                },
            ],//菌种工程师
            assayList: [
@@ -82,8 +110,11 @@
                    gainer: '1、实验操作评定',
                    situationOne: '考核数:',
                    situationTwo: '积分数:',
                    keys: ['handleCount', 'handleIntegral', 'handleStartTime', 'handleEndTime']
                },
            ],//菌种实验员
            detailData: {},
            selectedExperimenter: null
        }
    },
    computed: {
@@ -99,14 +130,29 @@
        }
    },
    created() {
        getDetailData(this.$route.query.id).then(res => {
            this.detailData = res;
            // 确保detailExperimentVOS存在且是数组
            this.detailData.detailExperimentVOS = this.detailData.detailExperimentVOS || [];
            if (this.detailData.detailExperimentVOS.length) {
                this.selectedExperimenter = this.detailData.detailExperimentVOS[0];
                this.activeNameTab = this.selectedExperimenter.userName;
            } else {
                // 设置空对象保护
                this.selectedExperimenter = {};
                this.activeNameTab = '';
            }
        })
    },
    methods: {
        changeActiveItem(item) {
            this.actionsLeftTab = item
        },
        changeActiveName(item) {
            this.activeNameTab = item
        changeActiveName(userName) {
            this.activeNameTab = userName;
            this.selectedExperimenter = this.detailData.detailExperimentVOS.find(
                item => item.userName === userName
            );
        },
        handleWheel(e) {
            if (this.scrollTimer) {
culture/src/views/deliveryAssessment/projectTeamIntegral/index.vue
@@ -5,34 +5,38 @@
            <template #search>
                <el-form :model="form" label-width="140px" inline>
                    <el-form-item label="项目组名称:">
                        <el-input v-model="form.name" placeholder="请输入"></el-input>
                        <el-input v-model="form.teamName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="创建日期:">
                        <el-date-picker v-model="value1" type="daterange" range-separator="至" start-placeholder="开始日期"
                            end-placeholder="结束日期">
                        <el-date-picker v-model="form.date" type="daterange" range-separator="至"
                            start-placeholder="开始日期" end-placeholder="结束日期">
                        </el-date-picker>
                    </el-form-item>
                    <el-form-item label="状态:">
                        <el-select placeholder="请选择"></el-select>
                        <el-select placeholder="请选择" v-model="form.status">
                            <el-option label="待审核" :value="1"></el-option>
                            <el-option label="待评定" :value="2"></el-option>
                            <el-option label="已评定" :value="3"></el-option>
                            <el-option label="已驳回" :value="4"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item class="search-btn-box">
                        <el-button>重置</el-button>
                        <el-button type="primary">查询</el-button>
                        <el-button @click="reset">重置</el-button>
                        <el-button type="primary" @click="search">查询</el-button>
                    </el-form-item>
                </el-form>
            </template>
            <template #table>
                <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="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="teamName" label="所属项目组" />
                <el-table-column prop="teamIntegral" label="菌种项目组总积分" />
                <el-table-column prop="engineerIntegral" label="菌种工程师积分" />
                <el-table-column prop="experimenterIntegral" label="菌种实验员积分" />
                <el-table-column prop="failCount" label="菌种实验员失败次数" />
                <el-table-column prop="startTime" label="评定开始时间" />
                <el-table-column prop="endTime" label="评定结束时间" />
                <el-table-column label="操作">
                    <template #default="{ row }">
                        <el-button @click="goDetail" type="text">详情</el-button>
                        <el-button @click="goDetail(row.projectId)" type="text">详情</el-button>
                    </template>
                </el-table-column>
            </template>
@@ -41,12 +45,12 @@
</template>
<script>
import { getListData } from './service'
export default {
    name: 'ProjectTeamIntegral',
    data() {
        return {
            form: {
            },
            form: {},
            tableData: [],
            queryForm: {
                pageSize: 10,
@@ -55,10 +59,13 @@
            total: 0
        }
    },
    created() {
        this.getList()
    },
    methods: {
        goDetail() {
        goDetail(id) {
            this.$router.push({
                path: '/projectList/addProject'
                path: `/deliveryAssessment/projectTeamIntegral-detail?id=${id}`,
            })
        },
        handleCurrentChange(page) {
@@ -70,7 +77,28 @@
            this.getList()
        },
        getList() {
            let obj = {
                ...this.queryForm
            }
            if (obj.date) {
                obj.startTime = moment(obj.date[0]).format('YYYY-MM-DD')
                obj.endTime = moment(obj.date[1]).format('YYYY-MM-DD')
                delete obj.date
            }
            getListData(obj).then(res => {
                this.tableData = res.data.records
                this.total = res.data.total
            })
        },
        reset() {
            this.queryForm = {
                pageSize: 10,
                pageNum: 1
            }
            this.getList()
        },
        search() {
            this.getList()
        }
    }
}
culture/src/views/deliveryAssessment/projectTeamIntegral/service.js
New file
@@ -0,0 +1,11 @@
import axios from '@/utils/request';
// 列表
export const getListData = (data) => {
    return axios.post('/api/t-strain-report/pageListProject', { ...data })
}
// 详情
export const getDetailData = (id) => {
    return axios.get(`/open/t-strain-report/getDetailByIdProject?id=${id}`)
}
culture/src/views/pedigree-chart/add.vue
@@ -1,93 +1,117 @@
<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="菌种源" required>
            <div class="flex-row">
              <div class="input-wrapper">
                <el-form-item prop="strainSourceStart" style="margin-bottom: 0;">
                  <el-input v-model="form.strainSourceStart" placeholder="请输入" class="fixed-width-input"></el-input>
                </el-form-item>
              </div>
              <span class="form-text">代—</span>
              <div class="input-wrapper">
                <el-form-item prop="strainSourceEnd" style="margin-bottom: 0;">
                  <el-input v-model="form.strainSourceEnd" placeholder="请输入" class="fixed-width-input"></el-input>
                </el-form-item>
              </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>
        <el-button type="primary" @click="handleSubmit" style="width: 150px;">保存</el-button>
      </div>
      <div class="end-btn">
        <!-- <el-button @click="handleDraft">存草稿</el-button>
        <el-button @click="handleCancel">取消</el-button> -->
      </div>
    </el-form>
    <ParentForm ref="parentForm" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
    <AddSublevelPlan ref="addSublevelPlan" @addNodeSign="addNodeSign" />
    <ConfirmStorageDialog name="接种操作人签字" :visible.sync="confirmStorageDialogVisible"
      @confirm="handleSignatureConfirm" />
    <!-- 菌种工程师 -->
    <ConfirmStorageDialog name="菌种保藏人签字" text="是否确认该项菌种信息入库" :visible.sync="storageVisible"
      @confirm="handleSignatureConfirm" />
  </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";
import ConfirmStorageDialog from "@/components/confirm-storage-dialog";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
    ParentForm,
    PlanForm,
    AddSublevelForm,
    AddSublevelPlan,
    ConfirmStorageDialog
  },
  data() {
    return {
      signatureVisible: false,
      form: {
        strainSource: "",
        generation: "",
        strainSourceStart: "",
        strainSourceEnd: "",
        cellBank: "",
        strainNo: "",
        strainName: "",
        remarks: "",
      },
      rules: {
        strainSource: [
        strainSourceStart: [
          { required: true, message: "请输入菌种源", trigger: "blur" },
        ],
        strainSourceEnd: [
          { required: true, message: "请输入菌种源", trigger: "blur" },
        ],
        strainNo: [
@@ -97,13 +121,58 @@
          { 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子孙代
      tableData: [],
      confirmStorageDialogVisible: false,
      storageVisible: false
    };
  },
  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.confirmStorageDialogVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.signatureVisible = true;
          this.confirmStorageDialogVisible = true;
        }
      });
    },
@@ -115,11 +184,559 @@
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      this.confirmStorageDialogVisible = false;
      if (this.nodeType === 1) {
        this.handleAddParent({ ...this.nodeData, signature: signatureImage.signature })
      } 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);// 添加触摸事件处理
      const handleNodeClick = (evt) => {
        evt.preventDefault(); // 阻止默认触摸行为
        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('node:click', handleNodeClick);
      this.graph.on('node:touchstart', handleNodeClick);
      // 画布点击事件,取消选中节点(添加触摸支持)
      const handleCanvasClick = (evt) => {
        evt.preventDefault();
        this.selectedNode = null;
        this.graphData.nodes.forEach(n => {
          n.selected = false;
        });
        this.graph.changeData(this.graphData);
      };
      this.graph.on('canvas:click', handleCanvasClick);
      this.graph.on('canvas:touchstart', handleCanvasClick);
    },
    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,
              strainSourceStart: this.form.strainSourceStart,
              strainSourceEnd: this.form.strainSourceEnd,
            });
          }
        })
        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) {
      console.log(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,
          strainSourceStart: this.form.strainSourceStart,
          strainSourceEnd: this.form.strainSourceEnd,
        })
      } else {
        this.$refs.planForm.openInitData({
          ...nodeModel.data,
          label: nodeModel.label,
          strainName: this.form.strainName,
          strainNo: this.form.strainNo,
          strainSourceStart: this.form.strainSourceStart,
          strainSourceEnd: this.form.strainSourceEnd,
        })
      }
    },
    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 +752,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 +875,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,807 @@
<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>
    </el-form>
    <AddAncestor ref="addAncestor" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
    <ConfirmStorageDialog name="接种操作人签字" :visible.sync="confirmStorageDialogVisible"
      @confirm="handleSignatureConfirm" />
    <!-- 菌种工程师 -->
    <ConfirmStorageDialog name="菌种保藏人签字" text="是否确认该项菌种信息入库" :visible.sync="storageVisible"
      @confirm="handleSignatureConfirm" />
  </div>
</template>
<script>
import G6 from '@antv/g6';
import PlanForm from "./progenitorComponents/PlanForm.vue";
import AddAncestor from "./progenitorComponents/AddAncestor.vue";
import AddSublevelForm from "./progenitorComponents/AddSublevelForm.vue";
import ConfirmStorageDialog from "@/components/confirm-storage-dialog";
export default {
  name: "AddPedigree",
  components: {
    PlanForm,
    AddAncestor,
    AddSublevelForm,
    ConfirmStorageDialog
  },
  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母代
      tableData: [],
      confirmStorageDialogVisible: false,
      storageVisible: false
    };
  },
  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.confirmStorageDialogVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.confirmStorageDialogVisible = true;
        }
      });
    },
    handleDraft() {
      // 实现存草稿逻辑
      console.log("save draft", this.form);
    },
    handleCancel() {
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.confirmStorageDialogVisible = 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: 1,
          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: {
            isDiscarded: true,
            ...nodeModel.data
          }
        })
      } 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);
      const handleNodeClick = (evt) => {
        evt.preventDefault(); // 阻止默认触摸行为
        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('node:click', handleNodeClick);
      this.graph.on('node:touchstart', handleNodeClick);
      // 画布点击事件,取消选中节点(添加触摸支持)
      const handleCanvasClick = (evt) => {
        evt.preventDefault();
        this.selectedNode = null;
        this.graphData.nodes.forEach(n => {
          n.selected = false;
        });
        this.graph.changeData(this.graphData);
      };
      this.graph.on('canvas:click', handleCanvasClick);
      this.graph.on('canvas:touchstart', handleCanvasClick);
    },
    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="strainSourceStart">
                <div class="flex-row">
                    <div class="input-wrapper">
                        <el-input disabled v-model="parentForm.strainSourceStart" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">代—</span>
                    <div class="input-wrapper">
                        <el-input disabled v-model="parentForm.strainSourceEnd" 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="strainSourceStart">
                <div class="flex-row">
                    <div class="input-wrapper">
                        <el-input disabled v-model="planForm.strainSourceStart" class="fixed-width-input"></el-input>
                    </div>
                    <span class="form-text">代—</span>
                    <div class="input-wrapper">
                        <el-input disabled v-model="planForm.strainSourceEnd" 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
@@ -4,64 +4,43 @@
      <template #search>
        <el-form :model="form" labelWidth="auto" inline>
          <el-form-item label="菌种编号:">
            <el-input v-model="form.planName" placeholder="请输入"></el-input>
            <el-input v-model="form.strainCode" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="菌种名称:">
            <el-input v-model="form.planCode" placeholder="请输入"></el-input>
            <el-input v-model="form.strainName" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="起传类型">
            <el-input v-model="form.creator" placeholder="请输入"></el-input>
            <el-select v-model="form.generationType" placeholder="请选择">
              <el-option label="母代" :value="1"></el-option>
              <el-option label="祖代" :value="2"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="">
            <el-button type="default" @click="resetForm">重置</el-button>
            <el-button type="primary" @click="handleSearch">查询</el-button>
            <el-button style="margin-left: 10px;" type="primary" @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 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 +49,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>
@@ -87,6 +62,7 @@
</template>
<script>
import { getList } from "./service";
export default {
  name: "PedigreeChart",
  components: {},
@@ -94,12 +70,11 @@
    return {
      currentType: "list", // 当前显示类型:list-列表,draft-草稿箱
      form: {
        planName: "",
        planCode: "",
        creator: "",
        createTime: [],
        approver: "",
        status: "",
        strainCode: "",
        strainName: "",
        generationType: "",
        pageNum: 1,
        pageSize: 10
      },
      tableData: [],
      total: 0,
@@ -167,24 +142,28 @@
    this.getTableData();
  },
  methods: {
    resetForm() {
      this.form = {
        planName: "",
        planCode: "",
        creator: "",
        createTime: [],
        approver: "",
        status: "",
      };
    },
    handleNewStrain() {
    handleBatchAdd() {
      this.$router.push({
        path: "/strain/add-pedigree",
      });
    },
    resetForm() {
      this.form = {
        strainCode: "",
        strainName: "",
        generationType: "",
        pageNum: 1,
        pageSize: 10
      };
    },
    handleNewStrain() {
      this.$router.push({
        path: "/strain/add-progenitor",
      });
    },
    handleSearch() {
      // 实现查询逻辑
      console.log("查询条件:", this.form);
      this.form.pageNum = 1;
      this.getTableData();
    },
    getStatusType(status) {
      const statusMap = {
@@ -252,14 +231,12 @@
      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;
      }
      getList(this.form).then(res => {
        if (res.code === 200) {
          this.tableData = res.data.list;
          this.total = res.data.total;
        }
      });
    },
  },
};
@@ -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,262 @@
<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" v-if="planForm.activeType == 1">
                    <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" v-else>
                    <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: {},
            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.inoculateNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="form.inoculateName"></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.num"
                            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.name"
                            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/pedigree-chart/service.js
New file
@@ -0,0 +1,11 @@
import axios from '@/utils/request';
// 列表
export const getList = (data) => {
  return axios.post('/api/t-pedigree-chart/pageList', { ...data })
}
// 删除菌种库
export const deleteStrainLibrary = (params) => {
  return axios.delete('/open/t-train-library/deleteById', { params })
}
culture/src/views/projectList/addProject.vue
@@ -2,11 +2,11 @@
    <Card>
        <template>
            <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="name" label="项目组名称">
                    <el-input v-model="form.name" placeholder="请输入" />
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="description" label="项目负责人">
                    <el-input v-model="form.description" placeholder="请输入" />
                <el-form-item prop="personCharge" label="项目负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
@@ -17,41 +17,73 @@
                <el-button class="el-icon-plus" type="primary" @click="addMember"> 添加项目组成员</el-button>
            </div>
            <div class="member-list">
                <div v-for="(item,index) in ['审批人', '菌种工程师', '菌种实验员']" :key="item" class="member-list-card">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ item }}</div>
                        <div :class="index == 0 || index == 1 ? 'member-name-box' : 'member-name-box-2'">
                            <div v-for="i in memberList(index+1)" :key="i" class="member-name">张三</div>
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                        <div class="member-edit">修改</div>
                        <div class="member-edit" v-if="memberList(item).length != 0" @click="editUserList">修改</div>
                    </div>
                </div>
            </div>
            <div class="add-project-footer">
                <el-button type="primary">保存</el-button>
                <el-button @click="submitForm" type="primary">保存</el-button>
            </div>
        </template>
        <SelectMember ref="selectMember" />
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { addProject } from './service'
export default {
    name: 'AddProject',
    data() {
        return {
            form: {},
            rules: {
                name: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                description: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            }
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            // 角色配置常量
            ROLE_CONFIG: {
                1: { key: 'approver', limit: 1, label: '菌种审批人' },
                2: { key: 'engineer', limit: 1, label: '菌种工程师' },
            },
        }
    },
    methods: {
        submitForm() {
            this.$refs.form.validate((valid) => {
                if (valid) {
                    console.log('submit!')
                    if (this.selectMemberData.length == 0) {
                        this.$message.error('请选择项目组成员')
                        return
                    }
                    const ROLE_NAME_TO_TYPE = {
                        '菌种审批人': 1,
                        '菌种工程师': 2,
                        '菌种实验员': 3,
                    };
                    const data = {
                        teamName: this.form.teamName,
                        personCharge: this.form.personCharge,
                        staffs: this.selectMemberData.map(member => ({
                            userId: member.userId,
                            roleType: ROLE_NAME_TO_TYPE[member.roleName]
                        }))
                    }
                    addProject(data).then(res => {
                        if (res.code == 200) {
                            this.$message.success('添加成功')
                            this.$router.push({ name: 'ProjectList' })
                        }
                    })
                }
            })
        },
@@ -61,16 +93,31 @@
        memberList(i) {
            switch (i) {
                case 1:
                    return [1]
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return [1]
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return [1, 2, 3, 4, 5, 6, 7, 8]
                case 4:
                    return [1, 2, 3, 4, 5, 6, 7, 8]
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
        selectUser(data) {
            for (const [roleId, config] of Object.entries(this.ROLE_CONFIG)) {
                const members = data.filter(item => item.roleName === config.label);
                if (members.length > config.limit) {
                    this.$message.error(`${config.label}最多只能选择${config.limit}个`);
                    return
                }
            }
            this.selectMemberData = data;
            this.$refs.selectMember.close();
        },
        editUserList() {
            this.$refs.selectMember.open();
            this.$nextTick(() => {
                this.$refs.selectMember.setSelection(this.selectMemberData);
            });
        }
    }
}
@@ -188,6 +235,10 @@
                font-weight: 500;
                font-size: 16px;
                color: #FFFFFF;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
culture/src/views/projectList/detailProject.vue
New file
@@ -0,0 +1,233 @@
<template>
    <Card>
        <template>
            <el-form disabled ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="personCharge" label="项目组负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
                <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>项目组成员</div>
                </div>
            </div>
            <div class="member-list">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                    </div>
                </div>
            </div>
        </template>
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { getProjectDetail } from './service'
export default {
    name: 'EddProject',
    data() {
        return {
            form: {},
            rules: {
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            ROLE_NAME_TO_TYPE: {
                '菌种审批人': 1,
                '菌种工程师': 2,
                '菌种实验员': 3,
            }
        }
    },
    created() {
        getProjectDetail({ id: this.$route.query.id }).then(res => {
            this.form = {
                teamName: res.teamName,
                personCharge: res.personCharge
            }
            this.selectMemberData = res.staffs.map(item => ({
                userId: Number(item.userId),
                roleName: ['菌种审批人', '菌种工程师', '菌种实验员'][item.roleType - 1],
                nickName: item.nickName
            }))
        })
    },
    methods: {
        addMember() {
            this.$refs.selectMember.open()
        },
        memberList(i) {
            switch (i) {
                case 1:
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
    }
}
</script>
<style scoped lang="less">
.el-form--inline .el-form-item {
    margin-right: 83px;
}
.header-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    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';
            &:before {
                content: '*';
                color: #F56C6C;
                margin-right: 4px;
            }
        }
    }
}
.member-list {
    margin-top: 18px;
    display: flex;
    flex-wrap: wrap;
    gap: 28px;
    .member-list-card {
        position: relative;
        width: 340px;
        height: 400px;
        border-radius: 8px;
        border: 1px solid #DCDFE6;
        &:nth-child(1) {
            background: linear-gradient(to bottom, rgba(4, 156, 154, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(2) {
            background: linear-gradient(to bottom, rgba(5, 160, 193, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(3) {
            background: linear-gradient(to bottom, rgba(255, 77, 79, 0.20) 0%, rgba(255, 242, 194, 0) 70%);
        }
        &:nth-child(4) {
            background: linear-gradient(to bottom, rgba(250, 199, 20, 0.21) 0%, rgba(255, 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;
            }
            .member-name-box {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .member-name-box-2 {
                padding: 0 20px;
                padding-top: 40px;
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                align-items: flex-start;
                flex-wrap: wrap;
                gap: 20px;
                justify-content: center;
            }
            .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;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
                cursor: pointer;
                position: absolute;
                bottom: 10px;
                left: 50%;
                transform: translateX(-50%);
                font-weight: 400;
                font-size: 12px;
                color: #FF4D4F;
                line-height: 22px;
                width: 40px;
                background: #FFF1F0;
                border-radius: 4px;
                border: 1px solid #FFCCC7;
                text-align: center;
            }
        }
    }
}
.add-project-footer {
    margin-top: 43px;
    button {
        width: 220px;
    }
}
</style>
culture/src/views/projectList/editProject.vue
New file
@@ -0,0 +1,285 @@
<template>
    <Card>
        <template>
            <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="personCharge" label="项目组负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
                <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>项目组成员</div>
                </div>
                <el-button class="el-icon-plus" type="primary" @click="addMember"> 添加项目组成员</el-button>
            </div>
            <div class="member-list">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                        <div class="member-edit" v-if="memberList(item).length != 0" @click="editUserList">修改</div>
                    </div>
                </div>
            </div>
            <div class="add-project-footer">
                <el-button @click="submitForm" type="primary">保存</el-button>
            </div>
        </template>
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { getProjectDetail, editProject } from './service'
export default {
    name: 'EddProject',
    data() {
        return {
            form: {},
            rules: {
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            // 角色配置常量
            ROLE_CONFIG: {
                1: { key: 'approver', limit: 1, label: '菌种审批人' },
                2: { key: 'engineer', limit: 1, label: '菌种工程师' },
            },
            ROLE_NAME_TO_TYPE: {
                '菌种审批人': 1,
                '菌种工程师': 2,
                '菌种实验员': 3,
            }
        }
    },
    created() {
        getProjectDetail({ id: this.$route.query.id }).then(res => {
            this.form = {
                teamName: res.teamName,
                personCharge: res.personCharge
            }
            this.selectMemberData = res.staffs.map(item => ({
                userId: Number(item.userId),
                roleName: ['菌种审批人', '菌种工程师', '菌种实验员'][item.roleType - 1],
                nickName: item.nickName
            }))
        })
    },
    methods: {
        submitForm() {
            this.$refs.form.validate((valid) => {
                if (valid) {
                    if (this.selectMemberData.length == 0) {
                        this.$message.error('请选择项目组成员')
                        return
                    }
                    const data = {
                        id: this.$route.query.id,
                        teamName: this.form.teamName,
                        personCharge: this.form.personCharge,
                        staffs: this.selectMemberData.map(member => ({
                            userId: member.userId,
                            roleType: this.ROLE_NAME_TO_TYPE[member.roleName]
                        }))
                    }
                    editProject(data).then(res => {
                        if (res.code == 200) {
                            this.$message.success('添加成功')
                            this.$router.push({ name: 'ProjectList' })
                        }
                    })
                }
            })
        },
        addMember() {
            this.$refs.selectMember.open()
        },
        memberList(i) {
            switch (i) {
                case 1:
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
        selectUser(data) {
            for (const [roleId, config] of Object.entries(this.ROLE_CONFIG)) {
                const members = data.filter(item => item.roleName === config.label);
                if (members.length > config.limit) {
                    this.$message.error(`${config.label}最多只能选择${config.limit}个`);
                    return
                }
            }
            this.selectMemberData = data;
            this.$refs.selectMember.close();
        },
        editUserList() {
            this.$refs.selectMember.open();
            this.$nextTick(() => {
                this.$refs.selectMember.setSelection(this.selectMemberData);
            });
        }
    }
}
</script>
<style scoped lang="less">
.el-form--inline .el-form-item {
    margin-right: 83px;
}
.header-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    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';
            &:before {
                content: '*';
                color: #F56C6C;
                margin-right: 4px;
            }
        }
    }
}
.member-list {
    margin-top: 18px;
    display: flex;
    flex-wrap: wrap;
    gap: 28px;
    .member-list-card {
        position: relative;
        width: 340px;
        height: 400px;
        border-radius: 8px;
        border: 1px solid #DCDFE6;
        &:nth-child(1) {
            background: linear-gradient(to bottom, rgba(4, 156, 154, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(2) {
            background: linear-gradient(to bottom, rgba(5, 160, 193, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(3) {
            background: linear-gradient(to bottom, rgba(255, 77, 79, 0.20) 0%, rgba(255, 242, 194, 0) 70%);
        }
        &:nth-child(4) {
            background: linear-gradient(to bottom, rgba(250, 199, 20, 0.21) 0%, rgba(255, 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;
            }
            .member-name-box {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .member-name-box-2 {
                padding: 0 20px;
                padding-top: 40px;
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                align-items: flex-start;
                flex-wrap: wrap;
                gap: 20px;
                justify-content: center;
            }
            .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;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
                cursor: pointer;
                position: absolute;
                bottom: 10px;
                left: 50%;
                transform: translateX(-50%);
                font-weight: 400;
                font-size: 12px;
                color: #FF4D4F;
                line-height: 22px;
                width: 40px;
                background: #FFF1F0;
                border-radius: 4px;
                border: 1px solid #FFCCC7;
                text-align: center;
            }
        }
    }
}
.add-project-footer {
    margin-top: 43px;
    button {
        width: 220px;
    }
}
</style>
culture/src/views/projectList/service.js
@@ -2,30 +2,30 @@
// 列表
export const getProjectList = (data) => {
    return axios.post('/t_project_team/api/pageList', { ...data })
    return axios.post('/api/t_project_team/pageList', { ...data })
}
// 新增
export const addProject = (data) => {
    return axios.post('/t_project_team/api/add', { ...data })
    return axios.post('/api/t_project_team/add', { ...data })
}
// 编辑
export const editProject = (data) => {
    return axios.post('/t_project_team/api/update', { ...data })
    return axios.post('/api/t_project_team/update', { ...data })
}
// 详情
export const getProjectDetail = (data) => {
    return axios.get(`/t_project_team/open/getDetailById?id=${data.id}`)
    return axios.get(`/open/t_project_team/getDetailById?id=${data.id}`)
}
// 修改项目组状态
export const changeStatus = (data) => {
    return axios.post('/t_project_team/api/upAndDown', { ...data })
    return axios.post('/api/t_project_team/upAndDown', { ...data })
}
// 删除项目组
export const deleteProject = (data) => {
    return axios.delete(`/t_project_team/open/deleteById?id=${data.id}`)
    return axios.delete(`/open/t_project_team/deleteById?id=${data.id}`)
}
culture/src/views/strain-library/breeding-record/SlantRecordDialog.vue
New file
@@ -0,0 +1,196 @@
<template>
 <el-dialog :visible.sync="visible" title="新增培养皿观察记录" width="80%" @close="handleClose">
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" label-position="top">
      <el-row :gutter="24">
        <el-col :span="12">
          <el-form-item label="分离菌落编号" prop="colonyNo" required>
            <el-input v-model="form.colonyNo" placeholder="请输入分离菌落编号" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="形状强壮度排名" prop="rank" required>
            <el-input v-model="form.rank" placeholder="请输入形状强壮度排名" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <el-table :data="tableData" border style="width: 100%; margin-bottom: 16px;">
      <el-table-column prop="index" label="记录次数" width="90">
        <template #default="{ row }">
          第{{ row.index }}次
        </template>
      </el-table-column>
      <el-table-column prop="desc" label="形态记录">
        <template #default="{ row }">
          <el-input v-model="row.desc" placeholder="请输入形态记录" style="width: 100%;" />
        </template>
      </el-table-column>
      <el-table-column prop="images" label="拍照上传" width="120">
        <template #default="{ row }">
          <el-upload
            :file-list="row.images"
            list-type="picture-card"
            :on-preview="file => handlePreview(row, file)"
            :on-remove="(file, fileList) => handleRemove(row, file, fileList)"
            :on-success="(res, file, fileList) => handleUpload(row, file, fileList)"
            :before-upload="beforeUpload"
            action="#"
            :limit="5"
            class="mini-upload"
          >
            <i class="el-icon-plus"></i>
          </el-upload>
        </template>
      </el-table-column>
      <el-table-column prop="time" label="记录时间" width="160">
        <template #default="{ row }">
          {{ row.time }}
        </template>
      </el-table-column>
    </el-table>
    <div style="text-align: center;">
      <el-button type="primary" @click="handleOk">保存</el-button>
    </div>
    <el-dialog :visible.sync="previewVisible" width="400px">
      <img :src="previewImg" alt="图片预览" style="width: 100%;" />
    </el-dialog>
  </el-dialog>
</template>
<script>
export default {
  name: 'SlantRecordDialog',
  props: {
    visible: Boolean,
    value: {
      type: Object,
      default: () => ({ colonyNo: '', rank: '', records: [] })
    }
  },
  data() {
    return {
      form: {
        colonyNo: '',
        rank: ''
      },
      rules: {
        colonyNo: [{ required: true, message: '请输入分离菌落编号', trigger: 'blur' }],
        rank: [{ required: true, message: '请输入形状强壮度排名', trigger: 'blur' }]
      },
      tableData: [],
      previewVisible: false,
      previewImg: ''
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(val) {
        this.form.colonyNo = val.colonyNo || ''
        this.form.rank = val.rank || ''
        this.tableData = (val.records && val.records.length === 10)
          ? val.records.map((item, i) => ({ ...item, index: i + 1 }))
          : Array.from({ length: 10 }, (_, i) => ({ index: i + 1, desc: '', images: [], time: this.getNowTime() }))
      }
    },
    visible(val) {
      if (!val) {
        this.reset()
      }
    }
  },
  methods: {
    getNowTime() {
      const d = new Date()
      return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`
    },
    reset() {
      this.form.colonyNo = ''
      this.form.rank = ''
      this.tableData = Array.from({ length: 10 }, (_, i) => ({ index: i + 1, desc: '', images: [], time: this.getNowTime() }))
    },
    handleOk() {
      this.$refs.formRef.validate(valid => {
        if (!valid) return
        // 校验每行形态记录必填
        for (let i = 0; i < this.tableData.length; i++) {
          if (!this.tableData[i].desc) {
            this.$message.error(`第${i + 1}次形态记录不能为空`)
            return
          }
        }
        this.$emit('ok', {
          colonyNo: this.form.colonyNo,
          rank: this.form.rank,
          records: this.tableData
        })
        this.handleClose()
      })
    },
    handleClose() {
      this.$emit('update:visible', false)
    },
    beforeUpload(file) {
      // 这里只做本地预览
      return new Promise(resolve => {
        const reader = new FileReader()
        reader.onload = e => {
          resolve()
        }
        reader.readAsDataURL(file)
      })
    },
    handleUpload(row, file, fileList) {
      // 这里只做本地预览
      row.images = fileList.map(f => ({ ...f, url: f.url || URL.createObjectURL(f.raw) }))
    },
    handleRemove(row, file, fileList) {
      row.images = fileList
    },
    handlePreview(row, file) {
      this.previewImg = file.url
      this.previewVisible = true
    }
  }
}
</script>
<style scoped lang="less">
::v-deep(.el-upload--picture-card) {
  width: 40px !important;
  height: 40px !important;
  line-height: 40px !important;
}
::v-deep(.mini-upload .el-upload-list--picture-card .el-upload-list__item) {
  width: 40px !important;
  height: 40px !important;
}
::v-deep(.mini-upload .el-upload-list--picture-card .el-upload-list__item-thumbnail) {
  width: 40px !important;
  height: 40px !important;
  object-fit: cover;
}
::v-deep(.mini-upload .el-upload-list--picture-card .el-upload-list__item-preview),
::v-deep(.mini-upload .el-upload-list--picture-card .el-upload-list__item-delete) {
  width: 18px;
  height: 18px;
  font-size: 14px;
}
::v-deep(.el-upload--picture-card) {
  width: 40px !important;
  height: 40px !important;
  line-height: 40px !important;
  display: flex;
  align-items: center;
  justify-content: center;
}
::v-deep(.mini-upload .el-upload--picture-card i.el-icon-plus) {
  font-size: 18px;      /* 缩小icon */
  color: #999;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
</style>
culture/src/views/strain-library/breeding-record/add.vue
New file
@@ -0,0 +1,478 @@
<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="showSeparationDialog = 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="showObservationDialog = 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="showInoculationDialog = 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="showPreserveDialog = 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>
        <!-- 弹窗组件 -->
        <SeparationRecordDialog
          :visible.sync="showSeparationDialog"
          @confirm="handleSeparationConfirm"
        />
        <SlantRecordDialog
          :visible.sync="showObservationDialog"
          @ok="handleObservationConfirm"
        />
        <InoculationSlopeRecordDialog
          :visible.sync="showInoculationDialog"
          @save="handleInoculationConfirm"
        />
        <PreserveStrainRecordDialog
          :visible.sync="showPreserveDialog"
          @save="handlePreserveConfirm"
        />
        <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";
import SeparationRecordDialog from "./separation-record-dialog.vue";
import SlantRecordDialog from "./SlantRecordDialog.vue";
import InoculationSlopeRecordDialog from "./inoculation-slope-record-dialog.vue";
import PreserveStrainRecordDialog from "./preserve-strain-record-dialog.vue";
export default {
  components: {
    AiEditor,
    SeparationRecordDialog,
    SlantRecordDialog,
    InoculationSlopeRecordDialog,
    PreserveStrainRecordDialog,
  },
  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: {},
      showSeparationDialog: false,
      showObservationDialog: false,
      showInoculationDialog: false,
      showPreserveDialog: false,
    };
  },
  methods: {
    handleSeparationConfirm(data) {
      console.log("培养皿分离记录确认", data);
    },
    handleObservationConfirm(data) {
      console.log("培养皿观察记录确认", data);
    },
    handleInoculationConfirm(data) {
      console.log("接种斜面记录确认", data);
    },
    handlePreserveConfirm(data) {
      console.log("菌种保藏记录确认", data);
    },
  },
};
</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/main-cell-library/add.vue
@@ -1,190 +1,429 @@
<template>
  <Card>
      <el-form
          :model="form"
          :rules="rules"
          ref="strainForm"
          label-position="top"
          class="strain-form"
      >
          <div class="form-grid">
              <el-form-item label="菌种编号" prop="strainNo" required>
                  <el-input v-model="form.strainNo" placeholder="请输入"></el-input>
              </el-form-item>
              <el-form-item label="菌种名称" prop="strainName" required>
                  <el-input v-model="form.strainName" placeholder="请输入"></el-input>
              </el-form-item>
              <el-form-item label="菌种来源" prop="source" required>
                  <el-input v-model="form.source" placeholder="请输入"></el-input>
              </el-form-item>
              <el-form-item label="鉴定方法" prop="identificationMethod" required>
                  <el-input v-model="form.identificationMethod" placeholder="请输入"></el-input>
              </el-form-item>
              <el-form-item label="特征描述" prop="characteristics" required>
                  <el-input v-model="form.characteristics" 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>
              <el-form-item label="保存位置" prop="storageLocation" required>
                  <el-input v-model="form.storageLocation" placeholder="请输入"></el-input>
              </el-form-item>
          </div>
    <Card>
        <el-form :model="form" :rules="rules" ref="strainForm" label-position="top" class="strain-form">
            <div class="form-row three-columns">
                <el-form-item label="菌种编号" prop="strainCode">
                    <el-input v-model="form.strainCode" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种名称" prop="strainName">
                    <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种来源" prop="strainSource">
                    <el-input v-model="form.strainSource" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
          <div class="form-row">
              <el-form-item label="备注" prop="remarks" class="full-width">
                  <el-input
                      type="textarea"
                      v-model="form.remarks"
                      :rows="4"
                      placeholder="请输入"
                  ></el-input>
              </el-form-item>
          </div>
          <div class="end-btn" style="margin-top: 38px">
              <el-button type="primary" @click="handleSubmit">提交</el-button>
              <el-button @click="handleDraft">存草稿</el-button>
          </div>
      </el-form>
            <div class="form-row">
                <el-form-item label="鉴定方法" prop="appraisalMethod">
                    <el-input v-model="form.appraisalMethod" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
      <!-- 签字确认组件 -->
      <SignatureCanvas
          :visible.sync="signatureVisible"
          @confirm="handleSignatureConfirm"
      />
  </Card>
            <div class="form-row">
                <el-form-item label="特征描述" prop="features" class="full-width">
                    <el-input type="textarea" v-model="form.features" :rows="4" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row three-columns">
                <el-form-item label="保藏位置" prop="saveLocation">
                    <el-input v-model="form.saveLocation" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种保存方法" prop="saveMethod">
                    <el-input v-model="form.saveMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <div class="form-item-placeholder"></div>
            </div>
            <div class="form-row">
                <el-form-item label="备注" prop="remarks" class="full-width">
                    <el-input type="textarea" v-model="form.remark" :rows="4" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="end-btn" style="margin-top: 38px">
                <el-button type="primary" @click="handleSubmit(0)">提交</el-button>
                <el-button v-if="!$route.query.id" type="primary" @click="handleBatchAdd">批量新增</el-button>
                <el-button @click="handleSubmit(1)">存草稿</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"
                        :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'
import { add, edit, getDetail, addBatch } from './service'
export default {
  name: 'AddMainCell',
  components: {
      SignatureCanvas
  },
  data() {
      return {
          signatureVisible: false,
          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.signatureVisible = true
              }
          })
      },
      handleDraft() {
          // 实现存草稿逻辑
          console.log('save draft', this.form)
          this.$message.success('草稿保存成功')
      },
      handleSignatureConfirm(signatureImage) {
          this.signatureVisible = false
          // 处理提交逻辑
          console.log('submit form with signature:', this.form, signatureImage)
          this.$message.success('提交成功')
          this.$router.back()
      }
  }
    name: 'StrainLibraryManageAdd',
    components: {
        SignatureCanvas
    },
    data() {
        return {
            batchAddDialogVisible: false,
            signatureVisible: false,
            currentAction: '', // 'submit' or 'batchAdd'
            batchForm: {
                count: ''
            },
            form: {
                strainCode: '',
                strainName: '',
                source: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remark: ''
            },
            rules: {
                strainCode: [{
                    validator: (rule, value, callback) => {
                        if (this.currentAction === 'submit' && !value) {
                            callback(new Error('请输入菌种编号'));
                        } else {
                            callback();
                        }
                    },
                    trigger: 'change'
                }],
                strainName: [{ required: true, message: '请输入菌种名称', trigger: 'blur' }],
                strainSource: [{ required: true, message: '请输入菌种来源', trigger: 'blur' }],
                appraisalMethod: [{ required: true, message: '请输入鉴定方法', trigger: 'blur' }],
                features: [{ required: true, message: '请输入特征描述', trigger: 'blur' }],
                saveLocation: [{ required: true, message: '请输入保存位置', trigger: 'blur' }],
                saveMethod: [{ required: true, message: '请输入菌种保存方法', trigger: 'blur' }]
            }
        }
    },
    activated() {
        if (this.$route.query.id) {
            getDetail({ id: this.$route.query.id }).then(res => {
                this.form = res
            })
        }
    },
    watch: {
        '$route.query.id'() {
            this.form = {
                strainCode: '',
                strainName: '',
                source: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remark: ''
            }
        }
    },
    methods: {
        handleSubmit(isDraft) {
            this.currentAction = 'submit'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.form.isDraft = isDraft
                    if (isDraft == 1) {
                        //存草稿
                        this.handleSignatureConfirm('')
                    } else {
                        this.signatureVisible = true
                    }
                }
            })
        },
        handleBatchAdd() {
            this.currentAction = 'batchAdd'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.batchAddDialogVisible = true
                }
            })
        },
        handleConfirmBatchAdd() {
            this.$refs.batchFormRef.validate((valid) => {
                if (valid) {
                    this.batchAddDialogVisible = false
                    this.signatureVisible = true
                }
            })
        },
        async handleSignatureConfirm(signatureImage) {
            let requestData = {
                strainCode: this.form.strainCode,
                strainName: this.form.strainName,
                strainSource: this.form.strainSource,
                appraisalMethod: this.form.appraisalMethod,
                features: this.form.features,
                saveLocation: this.form.saveLocation,
                saveMethod: this.form.saveMethod,
                remark: this.form.remark,
                signature: signatureImage,
                type: 2,
            };
            if (this.currentAction === 'batchAdd') {
                requestData.batchCount = this.batchForm.count;
            } else {
                requestData.isDraft = this.form.isDraft
            }
            try {
                if (this.$route.query.id) {
                    requestData.id = this.$route.query.id;
                    await edit(requestData);
                } else if (this.currentAction === 'batchAdd') {
                    await addBatch(requestData);
                } else {
                    await add(requestData);
                }
                this.signatureVisible = false;
                this.$router.back();
                this.$message.success('操作成功');
            } catch (error) {
                this.$message.error('操作失败');
            }
        }
    }
}
</script>
<style scoped lang="less">
.strain-form {
  padding: 0 40px;
.add-strain {
    height: 100%;
    background: #F5F7FA;
  .form-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 24px;
      margin-bottom: 24px;
      @media screen and (max-width: 1200px) {
          grid-template-columns: repeat(2, 1fr);
      }
      @media screen and (max-width: 768px) {
          grid-template-columns: 1fr;
      }
  }
    .form-card {
        background: #fff;
        border-radius: 8px;
    }
}
  .form-row {
      display: flex;
      flex-wrap: wrap;
      gap: 24px;
      margin-bottom: 24px;
.header-title {
    margin-bottom: 24px;
      .el-form-item {
          margin-bottom: 0;
    &-left {
        display: flex;
        align-items: center;
          &.full-width {
              width: 100%;
          }
      }
  }
        img {
            width: 20px;
            height: 20px;
            margin-right: 8px;
        }
  :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;
  }
        div {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
        }
    }
}
.end-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
    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;
  }
    button {
        width: 180px;
        height: 36px;
        // background: #409EFF;
    }
}
</style>
.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/main-cell-library/index.vue
@@ -1,474 +1,491 @@
<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、菌种全部集中登记在【菌种源保藏出/入主细胞库登记表】,请分类管理。</p>
                <p>1.1 接种入主细胞库(现代-M)的菌株经过培养和保藏。</p>
                <p>1.2 从原始细胞库转入的菌株需要按照标准程序进行记录和管理。</p>
                <p>1.3 主细胞库的菌株可用于科研项目和工业生产中的应用研究。</p>
            </div>
  <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
          条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
          是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
          是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。
          2.
          菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
          原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
          生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
          细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
          24 年 9 月 19
          接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
          传代编码方式演例:祖代:DD-O-240919-01
          传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
          编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
          细胞库说明:3.2.1
          直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
          从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
          主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
          菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用 a-01、a-02
          等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
          接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
        </p>
      </div>
            <!-- 查看全部弹窗 -->
            <el-dialog
                title="菌种源保藏出/入主细胞库登记表说明"
                :visible.sync="dialogVisible"
                width="50%"
                class="view-all-dialog"
      <!-- 查看全部弹窗 -->
      <el-dialog
        title="菌种源保藏出/入细胞库登记表说明"
        :visible.sync="dialogVisible"
        width="50%"
        class="view-all-dialog"
      >
        <div class="dialog-content">
          <p>
            1. 菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,菌种来源有 3
            条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
            是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
            是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。2.
            菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
            原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
            生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
            细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
            24 年 9 月 19
            接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
            传代编码方式演例:祖代:DD-O-240919-01
            传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
            编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
            细胞库说明:3.2.1
            直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
            从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
            主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
            菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用
            a-01、a-02 等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
            接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
          </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 v-if="roleType == 4" 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-option label="入库待确认" value="4"></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 class="dialog-content">
                    <p>1、菌种全部集中登记在【菌种源保藏出/入主细胞库登记表】,请分类管理。</p>
                    <p>1.1 接种入主细胞库(现代-M)的菌株经过培养和保藏。</p>
                    <p>1.2 从原始细胞库转入的菌株需要按照标准程序进行记录和管理。</p>
                    <p>1.3 主细胞库的菌株可用于科研项目和工业生产中的应用研究。</p>
                    <p>1.4 菌株转出时需要严格记录去向和用途,确保可追溯性。</p>
                    <p>1.5 主细胞库的菌株保存应当遵循标准操作规程,确保活性和稳定性。</p>
                </div>
            </el-dialog>
        </el-card>
              主细胞列表
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              @click="handleTypeChange('draft')"
            >
              草稿箱
            </div>
          </div>
          <div v-if="roleType == 4" class="flex a-center">
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              style="margin-right: 12px"
              >新增主细胞</el-button
            >
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              >批量新增</el-button
            >
          </div>
        </div>
      </template>
        <!-- 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>
        <StrainDetail
            :visible.sync="detailVisible"
            :detail="currentDetail"
        />
    </div>
      <template #table>
        <el-table-column prop="strainCode" label="菌种编号" />
        <el-table-column prop="strainName" label="菌种名称" />
        <el-table-column prop="strainSource" label="菌种来源" />
        <el-table-column prop="appraisalMethod" label="鉴定方法" />
        <el-table-column prop="features" label="特征描述" />
        <el-table-column prop="saveMethod" label="菌种保存方法" />
        <el-table-column prop="saveLocation" label="保藏位置" />
        <el-table-column prop="stock" label="库存余量" />
        <el-table-column prop="remark" label="备注" />
        <el-table-column
          v-if="currentType === 'list'"
          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 v-if="row.status == 2 || row.status == 4" type="text" @click="handleEdit(row)">编辑</el-button>
            <el-button
              v-if="currentType === 'list'"
              type="text"
              @click="handleRecord(row)"
              >出入库记录</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <StrainDetail :visible.sync="detailVisible" :detail="currentDetail" />
  </div>
</template>
<script>
import StrainDetail from '../strain-library-manage/components/StrainDetail.vue'
import StrainDetail from "../strain-library-manage/components/StrainDetail.vue";
import { getList, deleteStrainLibrary } from "../strain-library-manage/service";
export default {
    name: 'MainCellLibrary',
    components: {
        StrainDetail
  name: "StrainLibraryManage",
  components: {
    StrainDetail,
  },
  data() {
    return {
      dialogVisible: false,
      currentType: "list",
      detailVisible: false,
      currentDetail: {},
      form: {
        strainNo: "",
        strainName: "",
        status: "",
      },
      queryForm: {
        pageSize: 10,
        pageNum: 1,
      },
      total: 800,
      tableData: [],
      roleType: "",
    };
  },
  activated() {
    this.searchData();
    // 角色类型 1=超级管理员 2=审批人 3=工程师 4=实验员
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
  },
  methods: {
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteStrainLibrary({ id: row.id }).then((res) => {
            this.$message.success("删除成功");
            this.searchData();
        });
      });
    },
    data() {
        return {
            dialogVisible: false,
            currentType: 'list',
            detailVisible: false,
            currentDetail: {},
            form: {
                strainNo: '',
                strainName: '',
                status: ''
            },
            queryForm: {
                pageSize: 10,
                pageNum: 1
            },
            total: 800,
            tableData: [
                {
                    strainNo: 'M-2024001',
                    strainName: '大肠杆菌BL21',
                    source: '原始细胞库',
                    method: '分子生物学鉴定',
                    certificate: '常用表达宿主菌,含有DE3溶源体,适合蛋白表达',
                    storage: '甘油冷冻',
                    amount: 'M区-01-001',
                    inventory: '100',
                    notes: '高效表达宿主',
                    status: '1'
                },
                {
                    strainNo: 'M-2024002',
                    strainName: '乳酸菌L.plantarum',
                    source: '菌种保藏中心',
                    method: '16S rDNA测序',
                    certificate: '革兰氏阳性杆菌,产乳酸,益生特性',
                    storage: '冷冻保存',
                    amount: 'M区-02-005',
                    inventory: '80',
                    notes: '发酵剂开发',
                    status: '1'
                },
                {
                    strainNo: 'M-2024003',
                    strainName: '酵母S.cerevisiae',
                    source: '原始细胞库',
                    method: '生理生化鉴定',
                    certificate: '椭圆形单细胞真菌,高效发酵能力',
                    storage: '斜面培养',
                    amount: 'M区-03-002',
                    inventory: '60',
                    notes: '酒精发酵',
                    status: '2'
                },
                {
                    strainNo: 'M-2024004',
                    strainName: '枯草芽孢杆菌',
                    source: '环境样本分离',
                    method: '形态学观察和生化鉴定',
                    certificate: '革兰氏阳性芽孢杆菌,可产多种酶类',
                    storage: '冻干保存',
                    amount: 'M区-01-003',
                    inventory: '90',
                    notes: '工业酶生产',
                    status: '3'
                },
                {
                    strainNo: 'M-2024005',
                    strainName: '链霉菌S.griseus',
                    source: '原始细胞库',
                    method: 'PCR鉴定',
                    certificate: '丝状菌,产生灰色气生菌丝和分生孢子',
                    storage: '液氮保存',
                    amount: 'M区-04-001',
                    inventory: '70',
                    notes: '抗生素研究',
                    status: '1'
                }
            ]
        }
    handleRecord(row) {
      this.$router.push({
        path: `/strain-library/strain-library-manage/record?id=${row.id}`,
      });
    },
    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/main-cell-library/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] || '未知状态'
        }
    }
}
    handleNewStrain() {
      this.$router.push({ path: "/strain-library/main-cell-library/add" });
    },
    handleEdit(row) {
      this.$router.push({
        path: `/strain-library/main-cell-library/add?id=${row.id}`,
      });
    },
    handleDetail(row) {
      this.currentDetail = row;
      this.detailVisible = true;
    },
    handleViewMore() {
      this.dialogVisible = true;
    },
    resetForm() {
      this.form = {
        strainNo: "",
        strainName: "",
        status: "",
      };
      this.searchData();
    },
    searchData() {
      const params = {
        pageNum: this.queryForm.pageNum,
        pageSize: this.queryForm.pageSize,
        strainCode: this.form.strainNo,
        strainName: this.form.strainName,
        isDraft: this.currentType === "draft" ? 1 : 0,
        status: this.form.status,
        type: 2,
      };
      console.log(params);
      getList(params)
        .then((res) => {
          if (res.code === 200) {
            this.tableData = res.data.records;
            this.total = res.data.total;
          }
        })
        .catch((err) => {
          this.$message.error("数据加载失败");
        });
    },
    handleCurrentChange(page) {
      this.queryForm.pageNum = page;
      this.searchData();
    },
    handleSizeChange(size) {
      this.queryForm.pageSize = size;
      this.searchData();
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.searchData();
    },
    getStatusType(status) {
      const types = {
        1: "warning",
        2: "warning",
        3: "success",
        4: "success",
      };
      return types[status] || "info";
    },
    getStatusText(status) {
      const texts = {
        1: "已出库",
        2: "出库待确认",
        3: "已入库",
        4: "入库待确认",
      };
      return texts[status] || "未知状态";
    },
  },
};
</script>
<style scoped lang="less">
.list {
    padding: 20px;
  padding: 20px;
}
.header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
  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;
  .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-icon {
      width: 20px;
      height: 20px;
      margin-right: 10px;
    }
    .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;
        }
    .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;
  margin-bottom: 20px;
  background: #f5f7fa;
  padding: 24px;
  border-radius: 8px;
    .el-form-item {
        margin-right: 20px;
        margin-bottom: 0;
    }
  .el-form-item {
    margin-right: 20px;
    margin-bottom: 0;
  }
    .el-button {
        margin-left: 10px;
    }
  .el-button {
    margin-left: 10px;
  }
}
.action-buttons {
    margin-bottom: 20px;
  margin-bottom: 20px;
    .el-button {
        margin-right: 10px;
    }
  .el-button {
    margin-right: 10px;
  }
}
.tab-container {
    display: flex;
    margin-bottom: 20px;
  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;
  .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;
        }
    &.active {
      background: #fff;
      border-color: #049c9a;
      color: #049c9a;
      font-weight: bold;
    }
  }
}
.flex {
    display: flex;
    align-items: center;
}
.a-center {
    align-items: center;
  display: flex;
  align-items: center;
}
.tableTitle {
    display: flex;
    padding-bottom: 20px;
    justify-content: space-between;
    align-items: center;
  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;
  .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;
  }
    .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;
    }
  .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__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;
  :deep(.el-dialog__body) {
    padding: 20px;
        .dialog-content {
            font-size: 14px;
            line-height: 1.8;
            color: #606266;
    .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;
                }
            }
      p {
        margin: 12px 0;
        &:first-child {
          margin-top: 0;
        }
        &:last-child {
          margin-bottom: 0;
        }
      }
    }
  }
}
</style>
</style>
culture/src/views/strain-library/main-cell-library/record.vue
@@ -1,352 +1,438 @@
<template>
  <div class="record-page">
    <div class="page-header">
      <div class="header-left">
        <el-page-header @back="goBack" content="主细胞出入库记录"></el-page-header>
      </div>
      <div class="header-right">
        <el-button type="primary" icon="el-icon-plus" @click="handleAddRecord">新增记录</el-button>
      </div>
    </div>
    <el-card class="record-card">
      <div class="strain-info">
    <!-- 基本信息展示区域 -->
    <el-card class="header-box">
      <div class="header-content">
        <!-- 第一行 -->
        <div class="info-row">
          <div class="info-item">
          <div class="info-item left-column">
            <span class="label">菌种编号:</span>
            <span class="value">{{ strainInfo.strainNo }}</span>
            <span class="value">{{ detail.strainCode }}</span>
          </div>
          <div class="info-item">
            <span class="label">菌种名称:</span>
            <span class="value">{{ strainInfo.strainName }}</span>
          </div>
          <div class="info-item">
            <span class="label">菌种来源:</span>
            <span class="value">{{ strainInfo.source }}</span>
          </div>
        </div>
        <div class="info-row">
          <div class="info-item">
          <div class="info-item flex-column">
            <span class="label">鉴定方法:</span>
            <span class="value">{{ strainInfo.method }}</span>
            <span class="value">{{ detail.appraisalMethod }}</span>
          </div>
          <div class="info-item full">
            <span class="label">特征描述:</span>
            <span class="value">{{ strainInfo.certificate }}</span>
          <div class="info-item flex-column">
            <span class="label">保藏位置:</span>
            <span class="value">{{ detail.saveLocation }}</span>
          </div>
        </div>
        <!-- 第二行 -->
        <div class="info-row">
          <div class="info-item">
            <span class="label">菌种保存方法:</span>
            <span class="value">{{ strainInfo.storage }}</span>
          <div class="info-item left-column">
            <span class="label">菌种名称:</span>
            <span class="value">{{ detail.strainName }}</span>
          </div>
          <div class="info-item">
            <span class="label">保存位置:</span>
            <span class="value">{{ strainInfo.amount }}</span>
          </div>
          <div class="info-item">
            <span class="label">出入库状态:</span>
            <span class="value status">{{ strainInfo.statusText }}</span>
          <div class="info-item flex-column full-width">
            <span class="label">特性描述:</span>
            <span class="value">{{ detail.features }}</span>
          </div>
        </div>
      </div>
      <div class="record-timeline-container">
        <h3 class="section-title">出入库记录</h3>
        <RecordTimeline :list="recordList" />
        <!-- 第三行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种来源:</span>
            <span class="value">{{ detail.strainSource }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">菌种保存方法:</span>
            <span class="value">{{ detail.saveMethod }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- 新增记录弹窗 -->
    <el-dialog
      title="新增出入库记录"
      :visible.sync="dialogVisible"
      width="500px"
    <!-- 出入库记录表格 -->
    <TableCustom
      :queryForm="queryForm"
      :tableData="recordList"
      :total="total"
      @currentChange="handlePageChange"
    >
      <el-form ref="recordForm" :model="recordForm" :rules="recordRules" label-width="100px">
        <el-form-item label="操作类型" prop="type">
          <el-radio-group v-model="recordForm.type">
            <el-radio label="入库">入库</el-radio>
            <el-radio label="出库">出库</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="操作人" prop="operator">
          <el-input v-model="recordForm.operator" placeholder="请输入操作人"></el-input>
        </el-form-item>
        <el-form-item label="操作时间" prop="operateTime">
          <el-date-picker
            v-model="recordForm.operateTime"
            type="datetime"
            placeholder="选择日期时间"
            value-format="yyyy-MM-dd HH:mm:ss"
          ></el-date-picker>
        </el-form-item>
        <el-form-item label="保藏人" prop="reviewer">
          <el-input v-model="recordForm.reviewer" placeholder="请输入保藏人"></el-input>
        </el-form-item>
        <el-form-item label="操作数量" prop="amount">
          <el-input-number v-model="recordForm.amount" :min="1" :max="100"></el-input-number>
        </el-form-item>
        <el-form-item label="备注" prop="remarks">
          <el-input
            type="textarea"
            v-model="recordForm.remarks"
            :rows="3"
            placeholder="请输入备注"
          ></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitRecord">确 定</el-button>
      </span>
    </el-dialog>
      <template #setting>
        <div class="tableTitle">
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'table' }"
              @click="handleTypeChange('table')"
            >
              原始细胞保藏出/入库登记表
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'timeline' }"
              @click="handleTypeChange('timeline')"
            >
              原始细胞保藏出/入库时间轴
            </div>
          </div>
          <div class="flex a-center">
            <el-button
              v-if="roleType == 4"
              @click="handleAddRecord"
              class="el-icon-plus"
              type="primary"
              >新增出入库记录</el-button
            >
          </div>
        </div>
      </template>
      <template #table v-if="currentType === 'table'">
        <el-table-column prop="type" label="出库/入库">
          <template #default="{ row }">
            <span>
              {{ row.type === 1 ? "出库" : "入库" }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="boundTime" label="操作时间" />
        <el-table-column prop="handleSignature" label="操作人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.handleSignature"
              style="width: 100px; height: 100px"
              :src="row.handleSignature"
              :preview-src-list="[row.handleSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="preserveSignature" label="菌种保藏人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.preserveSignature"
              style="width: 100px; height: 100px"
              :src="row.preserveSignature"
              :preview-src-list="[row.preserveSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="row.preserveSignature ? 'success' : 'warning'">
              {{ row.preserveSignature ? "已确认" : "待确认" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180">
          <template #default="{ row }">
            <el-button
              v-if="!row.preserveSignature && roleType == 3"
              type="text"
              class="operation-btn"
              @click="handleConfirm(row)"
              >确认</el-button
            >
            <el-button
              type="text"
              class="operation-btn"
              @click="handleView(row)"
              >详情</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
      <template #tableCustom v-if="currentType === 'timeline'">
        <record-timeline :list="timelineList" />
      </template>
    </TableCustom>
    <!-- 详情弹窗 -->
    <record-detail-dialog
      :visible.sync="dialogVisible"
      :record-data="currentRecord"
      @close="handleDialogClose"
      @confirm="handleOutbound"
      :type="dialogType"
    />
    <!-- 新增出入库记录弹窗 -->
    <add-record-dialog
      :visible.sync="addDialogVisible"
      @confirm="handleAddRecordConfirm"
    />
  </div>
</template>
<script>
import RecordTimeline from '../strain-library-manage/components/RecordTimeline.vue'
import RecordDetailDialog from "../strain-library-manage/components/RecordDetailDialog.vue";
import AddRecordDialog from "../strain-library-manage/components/AddRecordDialog.vue";
import RecordTimeline from "../strain-library-manage/components/RecordTimeline.vue";
import {
  timeList,
  getDetail,
  addWarehousing,
  getDetailById,
  confirmWarehousing,
} from "./service";
export default {
  name: 'MainCellRecord',
  name: "StrainRecord",
  components: {
    RecordTimeline
    RecordDetailDialog,
    AddRecordDialog,
    RecordTimeline,
  },
  data() {
    return {
      strainId: '',
      strainInfo: {
        strainNo: 'M-2024001',
        strainName: '大肠杆菌BL21',
        source: '原始细胞库',
        method: '分子生物学鉴定',
        certificate: '常用表达宿主菌,含有DE3溶源体,适合蛋白表达',
        storage: '甘油冷冻',
        amount: 'M区-01-001',
        inventory: '100',
        status: '1',
        statusText: '已入库'
      currentType: "table",
      detail: {},
      currentPage: 1,
      pageSize: 10,
      total: 0,
      queryForm: {
        pageSize: 10,
        pageNum: 1,
      },
      recordList: [
        {
          type: '入库',
          operator: '张三',
          operateTime: '2024-05-01 10:30:00',
          reviewer: '李四',
          confirmTime: '2024-05-01 14:20:00'
        },
        {
          type: '出库',
          operator: '王五',
          operateTime: '2024-05-15 09:45:00',
          reviewer: '赵六',
          confirmTime: '2024-05-15 11:30:00'
        },
        {
          type: '入库',
          operator: '钱七',
          operateTime: '2024-05-20 14:00:00',
          reviewer: '孙八',
          confirmTime: '2024-05-20 16:15:00'
        }
      ],
      recordList: [],
      timelineList: [],
      dialogVisible: false,
      recordForm: {
        type: '入库',
        operator: '',
        operateTime: '',
        reviewer: '',
        amount: 1,
        remarks: ''
      },
      recordRules: {
        type: [
          { required: true, message: '请选择操作类型', trigger: 'change' }
        ],
        operator: [
          { required: true, message: '请输入操作人', trigger: 'blur' }
        ],
        operateTime: [
          { required: true, message: '请选择操作时间', trigger: 'change' }
        ],
        reviewer: [
          { required: true, message: '请输入保藏人', trigger: 'blur' }
        ],
        amount: [
          { required: true, message: '请输入操作数量', trigger: 'blur' }
        ]
      }
    }
      currentRecord: {},
      addDialogVisible: false,
      dialogType: "detail",
      roleType: "",
    };
  },
  created() {
    // 获取路由参数中的菌种ID
    this.strainId = this.$route.query.id
    // 实际项目中这里应该根据ID加载菌种信息和记录列表
    console.log('加载菌种ID:', this.strainId)
  activated() {
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
    // 获取路由参数中的菌种信息
    const strainId = this.$route.query.id;
    this.queryForm.id = strainId;
    if (strainId) {
      this.getStrainDetail(strainId);
      this.getRecordList();
    }
  },
  methods: {
    goBack() {
      this.$router.push('/strain-library/main-cell-library')
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteWarehousing({ id: row.id }).then((res) => {
          this.$message.success("删除成功");
          this.getRecordList();
        });
      });
    },
    getStrainDetail(id) {
      // 这里应该调用接口获取菌种详情
      getDetail({ id }).then((res) => {
        this.detail = res;
      });
    },
    getRecordList() {
      // 这里应该调用接口获取出入库记录
      timeList(this.queryForm).then((res) => {
        this.timelineList = res.data;
      });
      getDetailById({ id: this.$route.query.id }).then((res) => {
        this.recordList = res.warehousingList.records;
        this.total = res.warehousingList.total;
      });
    },
    handleView(row) {
      this.dialogType = "detail";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    handleConfirm(row) {
      this.dialogType = "confirm";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    handlePageChange(page) {
      this.queryForm.pageNum = page;
      // 这里应该调用接口获取对应页码的数据
    },
    handleTypeChange(type) {
      this.currentType = type;
    },
    handleAddRecord() {
      this.dialogVisible = true
      this.resetRecordForm()
      this.addDialogVisible = true;
    },
    submitRecord() {
      this.$refs.recordForm.validate(valid => {
        if (valid) {
          // 表单验证通过,提交数据
          console.log('提交的记录数据:', this.recordForm)
          // 模拟添加记录到列表
          const newRecord = {
            type: this.recordForm.type,
            operator: this.recordForm.operator,
            operateTime: this.recordForm.operateTime,
            reviewer: this.recordForm.reviewer,
            confirmTime: new Date().toLocaleString()
          }
          // 添加到记录列表的开头
          this.recordList.unshift(newRecord)
          // 关闭弹窗
          this.dialogVisible = false
          // 显示成功消息
          this.$message.success('记录添加成功')
    handleDialogClose() {
      this.currentRecord = {};
      this.dialogVisible = false;
    },
    handleOutbound(data) {
      // 这里调用出库API
      confirmWarehousing({
        id: this.currentRecord.id,
        preserveSignature: data.preserveSignature,
      }).then((res) => {
        console.log(res);
        if (res.code == 200) {
          this.$message.success("操作成功");
          this.dialogVisible = false;
          // 刷新列表
          this.getRecordList();
        } else {
          this.$message.error('请正确填写表单')
          return false
          this.$message.error(res.msg);
        }
      })
      });
    },
    resetRecordForm() {
      this.recordForm = {
        type: '入库',
        operator: '',
        operateTime: '',
        reviewer: '',
        amount: 1,
        remarks: ''
      }
    }
  }
}
    handleAddRecordConfirm(record) {
      addWarehousing({ ...record, trainLibraryId: this.$route.query.id }).then(
        (res) => {
          this.$message.success("操作成功");
          this.getRecordList();
        }
      );
    },
    goBack() {
      this.$router.go(-1);
    },
  },
};
</script>
<style scoped lang="less">
<style lang="less" scoped>
.record-page {
  padding: 20px;
}
  min-height: 100vh;
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  .header-left {
    :deep(.el-page-header__content) {
      font-size: 18px;
      font-weight: bold;
      color: #333;
    }
  }
}
.record-card {
  background: #fff;
  border-radius: 16px;
  margin-bottom: 20px;
}
.strain-info {
  padding: 10px 0;
  .info-row {
    display: flex;
    flex-wrap: wrap;
    margin-bottom: 16px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .info-item {
    flex: 1;
    min-width: 200px;
    margin-right: 20px;
    &:last-child {
      margin-right: 0;
    }
    &.full {
      flex: 2;
    }
    .label {
      color: #606266;
      margin-right: 8px;
    }
    .value {
      color: #333;
      font-weight: 500;
      &.status {
        color: #67C23A;
      }
    }
  }
}
.record-timeline-container {
  margin-top: 30px;
  .section-title {
    font-size: 16px;
    color: #333;
  .header-box {
    margin-bottom: 20px;
    font-weight: 500;
  }
}
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
    height: 130px;
    overflow: hidden;
:deep(.el-dialog__body) {
  padding: 20px 30px;
}
    .header-content {
      color: rgba(0, 0, 0, 0.88);
      font-size: 14px;
      line-height: 1.5;
@media screen and (max-width: 768px) {
  .strain-info {
    .info-row {
      flex-direction: column;
      .info-item {
        margin-right: 0;
        margin-bottom: 10px;
      .info-row {
        display: flex;
        flex-wrap: wrap;
        margin-bottom: 8px;
        &:last-child {
          margin-bottom: 0;
        }
        .info-item {
          display: flex;
          align-items: flex-start;
          margin-right: 24px;
          margin-bottom: 6px;
          &.left-column {
            width: 33%;
            min-width: 200px;
          }
          &.flex-column {
            flex: 1;
            min-width: 150px;
          }
          &.full-width {
            flex: 1;
            min-width: 300px;
          }
          .label {
            color: #606266;
            margin-right: 8px;
            white-space: nowrap;
          }
          .value {
            flex: 1;
            color: #303133;
            word-break: break-all;
            display: -webkit-box;
            -webkit-line-clamp: 1;
            -webkit-box-orient: vertical;
            overflow: hidden;
            text-overflow: ellipsis;
          }
        }
      }
    }
  }
  .page-header {
    flex-direction: column;
    align-items: flex-start;
    .header-right {
      margin-top: 16px;
  .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;
      cursor: pointer;
      height: 50px;
      line-height: 50px;
      width: 280px;
      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: 280px;
      text-align: center;
    }
    .active {
      color: #049c9a;
      background: #ffffff;
      border-radius: 8px 8px 0px 0px;
      border: 1px solid #049c9a;
    }
  }
  .timeline-container {
    padding: 20px;
    background: rgba(255, 255, 255, 0.8);
    .timeline-card {
      margin-bottom: 10px;
      background: rgba(255, 255, 255, 0.8);
      h4 {
        margin: 0 0 10px;
        font-size: 16px;
        font-weight: bold;
      }
      p {
        margin: 5px 0;
        font-size: 14px;
      }
    }
  }
  .operation-btn {
    margin-right: 12px;
  }
}
</style>
</style>
culture/src/views/strain-library/main-cell-library/service.js
New file
@@ -0,0 +1,56 @@
import axios from '@/utils/request';
// 列表
export const getList = (data) => {
  return axios.post('/api/t-train-library/pageList', { ...data })
}
// 新增
export const add = (data) => {
  return axios.post('/api/t-train-library/add', { ...data })
}
// 编辑
export const edit = (data) => {
  return axios.post('/api/t-train-library/update', { ...data })
}
// 查看详情
export const getDetail = (params) => {
  return axios.get('/open/t-train-library/getDetailEditById', { params })
}
// 批量新增
export const addBatch = (data) => {
  return axios.post('/api/t-train-library/addBatch', data)
}
// 查看菌种库详情
export const getDetailById = (data) => {
  return axios.post('/open/t-train-library/getDetailById', { ...data })
}
// 获取菌种库出入库时间轴列表
export const timeList = (data) => {
  return axios.post('/api/t-train-library/timeList?id='+data.id, { ...data })
}
// 新增菌种库出入记录
export const addWarehousing = (data) => {
  return axios.post('/open/t-train-library/addWarehousing', { ...data })
}
// 确认出入库
export const confirmWarehousing = (data) => {
  return axios.post('/api/t-train-library/confirm', { ...data })
}
// 删除菌种库
export const deleteStrainLibrary = (params) => {
  return axios.delete('/open/t-train-library/deleteById', { params })
}
// 删除菌种库出入库记录
export const deleteWarehousing = (params) => {
  return axios.delete('/open/t-train-library/deleteWarehousingById', { params })
}
culture/src/views/strain-library/production-cell-library/add.vue
@@ -1,141 +1,294 @@
<template>
    <Card>
        <el-form
            :model="form"
            :rules="rules"
            ref="strainForm"
            label-position="top"
            class="strain-form"
        >
            <div class="form-grid">
                <el-form-item label="菌种编号" prop="strainNo" required>
                    <el-input v-model="form.strainNo" placeholder="请输入"></el-input>
        <el-form :model="form" :rules="rules" ref="strainForm" label-position="top" class="strain-form">
            <div class="form-row three-columns">
                <el-form-item label="菌种编号" prop="strainCode">
                    <el-input v-model="form.strainCode" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种名称" prop="strainName" required>
                <el-form-item label="菌种名称" prop="strainName">
                    <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种来源" prop="source" required>
                    <el-input v-model="form.source" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="鉴定方法" prop="identificationMethod" required>
                    <el-input v-model="form.identificationMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="特征描述" prop="characteristics" required>
                    <el-input v-model="form.characteristics" 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>
                <el-form-item label="保存位置" prop="storageLocation" required>
                    <el-input v-model="form.storageLocation" placeholder="请输入"></el-input>
                <el-form-item label="菌种来源" prop="strainSource">
                    <el-input v-model="form.strainSource" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row">
                <el-form-item label="鉴定方法" prop="appraisalMethod">
                    <el-input v-model="form.appraisalMethod" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row">
                <el-form-item label="特征描述" prop="features" class="full-width">
                    <el-input type="textarea" v-model="form.features" :rows="4" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row three-columns">
                <el-form-item label="保藏位置" prop="saveLocation">
                    <el-input v-model="form.saveLocation" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种保存方法" prop="saveMethod">
                    <el-input v-model="form.saveMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <div class="form-item-placeholder"></div>
            </div>
            <div class="form-row">
                <el-form-item label="备注" prop="remarks" class="full-width">
                    <el-input
                        type="textarea"
                        v-model="form.remarks"
                        :rows="4"
                        placeholder="请输入"
                    ></el-input>
                    <el-input type="textarea" v-model="form.remark" :rows="4" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="end-btn" style="margin-top: 38px">
                <el-button type="primary" @click="handleSubmit">提交</el-button>
                <el-button @click="handleDraft">存草稿</el-button>
                <el-button type="primary" @click="handleSubmit(0)">提交</el-button>
                <el-button v-if="!$route.query.id" type="primary" @click="handleBatchAdd">批量新增</el-button>
                <el-button @click="handleSubmit(1)">存草稿</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"
                        :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"
        />
        <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </Card>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue'
import { add, edit, getDetail, addBatch } from './service'
export default {
    name: 'AddProductionCell',
    name: 'StrainLibraryManageAdd',
    components: {
        SignatureCanvas
    },
    data() {
        return {
            batchAddDialogVisible: false,
            signatureVisible: false,
            currentAction: '', // 'submit' or 'batchAdd'
            batchForm: {
                count: ''
            },
            form: {
                strainNo: '',
                strainCode: '',
                strainName: '',
                source: '',
                identificationMethod: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remarks: ''
                remark: ''
            },
            rules: {
                strainNo: [{ required: true, message: '请输入菌种编号', trigger: 'blur' }],
                strainCode: [{
                    validator: (rule, value, callback) => {
                        if (this.currentAction === 'submit' && !value) {
                            callback(new Error('请输入菌种编号'));
                        } else {
                            callback();
                        }
                    },
                    trigger: 'change'
                }],
                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' }]
                strainSource: [{ required: true, message: '请输入菌种来源', trigger: 'blur' }],
                appraisalMethod: [{ required: true, message: '请输入鉴定方法', trigger: 'blur' }],
                features: [{ required: true, message: '请输入特征描述', trigger: 'blur' }],
                saveLocation: [{ required: true, message: '请输入保存位置', trigger: 'blur' }],
                saveMethod: [{ required: true, message: '请输入菌种保存方法', trigger: 'blur' }]
            }
        }
    },
    activated() {
        if (this.$route.query.id) {
            getDetail({ id: this.$route.query.id }).then(res => {
                this.form = res
            })
        }
    },
    watch: {
        '$route.query.id'() {
            this.form = {
                strainCode: '',
                strainName: '',
                source: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remark: ''
            }
        }
    },
    methods: {
        handleSubmit() {
        handleSubmit(isDraft) {
            this.currentAction = 'submit'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.form.isDraft = isDraft
                    if (isDraft == 1) {
                        //存草稿
                        this.handleSignatureConfirm('')
                    } else {
                        this.signatureVisible = true
                    }
                }
            })
        },
        handleBatchAdd() {
            this.currentAction = 'batchAdd'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.batchAddDialogVisible = true
                }
            })
        },
        handleConfirmBatchAdd() {
            this.$refs.batchFormRef.validate((valid) => {
                if (valid) {
                    this.batchAddDialogVisible = false
                    this.signatureVisible = true
                }
            })
        },
        handleDraft() {
            // 实现存草稿逻辑
            console.log('save draft', this.form)
            this.$message.success('草稿保存成功')
        },
        handleSignatureConfirm(signatureImage) {
            this.signatureVisible = false
            // 处理提交逻辑
            console.log('submit form with signature:', this.form, signatureImage)
            this.$message.success('提交成功')
            this.$router.back()
        async handleSignatureConfirm(signatureImage) {
            let requestData = {
                strainCode: this.form.strainCode,
                strainName: this.form.strainName,
                strainSource: this.form.strainSource,
                appraisalMethod: this.form.appraisalMethod,
                features: this.form.features,
                saveLocation: this.form.saveLocation,
                saveMethod: this.form.saveMethod,
                remark: this.form.remark,
                signature: signatureImage,
                type: 3,
            };
            if (this.currentAction === 'batchAdd') {
                requestData.batchCount = this.batchForm.count;
            } else {
                requestData.isDraft = this.form.isDraft
            }
            try {
                if (this.$route.query.id) {
                    requestData.id = this.$route.query.id;
                    await edit(requestData);
                } else if (this.currentAction === 'batchAdd') {
                    await addBatch(requestData);
                } else {
                    await add(requestData);
                }
                this.signatureVisible = false;
                this.$router.back();
                this.$message.success('操作成功');
            } catch (error) {
                this.$message.error('操作失败');
            }
        }
    }
}
</script>
<style scoped lang="less">
.strain-form {
    padding: 0 40px;
.add-strain {
    height: 100%;
    background: #F5F7FA;
    .form-grid {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 24px;
        margin-bottom: 24px;
        @media screen and (max-width: 1200px) {
            grid-template-columns: repeat(2, 1fr);
    .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;
        }
        @media screen and (max-width: 768px) {
            grid-template-columns: 1fr;
        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;
@@ -170,6 +323,91 @@
    }
}
.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;
@@ -188,4 +426,4 @@
        margin: 0;
    }
}
</style>
</style>
culture/src/views/strain-library/production-cell-library/index.vue
@@ -2,36 +2,80 @@
  <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>
        <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 class="header-content" :class="{ collapsed: true }">
        <p>
          1. 菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,菌种来源有 3
          条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
          是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
          是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。
          2.
          菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
          原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
          生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
          细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
          24 年 9 月 19
          接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
          传代编码方式演例:祖代:DD-O-240919-01
          传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
          编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
          细胞库说明:3.2.1
          直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
          从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
          主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
          菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用 a-01、a-02
          等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
          接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
        </p>
      </div>
      <!-- 查看全部弹窗 -->
      <el-dialog
        title="菌种源保藏出/入生产细胞库登记表说明"
        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>
          <p>
            1. 菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,菌种来源有 3
            条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
            是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
            是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。2.
            菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
            原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
            生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
            细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
            24 年 9 月 19
            接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
            传代编码方式演例:祖代:DD-O-240919-01
            传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
            编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
            细胞库说明:3.2.1
            直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
            从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
            主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
            菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用
            a-01、a-02 等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
            接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
          </p>
        </div>
      </el-dialog>
    </el-card>
    <!-- Table -->
    <TableCustom :queryForm="queryForm" :tableData="tableData" :total="total" @currentChange="handleCurrentChange"
      @sizeChange="handleSizeChange">
    <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="菌种编号:">
@@ -40,15 +84,13 @@
          <el-form-item label="菌种名称:">
            <el-input v-model="form.strainName" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="状态:">
          <el-form-item v-if="roleType == 4" label="状态:">
            <el-select v-model="form.status" placeholder="请选择">
              <el-option label="全部" value=""></el-option>
              <el-option
                v-for="item in statusOptions"
                :key="item.value"
                :label="item.label"
                :value="item.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-option label="入库待确认" value="4"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item class="search-btn-box">
@@ -61,236 +103,207 @@
      <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
              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="handleAdd" class="el-icon-plus" type="primary" style="margin-right: 12px;">新增生产细胞</el-button>
            <el-button @click="handleAdd" class="el-icon-plus" type="primary" style="margin-right: 12px;">批量新增</el-button>
          <div v-if="roleType == 4" class="flex a-center">
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              style="margin-right: 12px"
              >新增生产细胞</el-button
            >
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              >批量新增</el-button
            >
          </div>
        </div>
      </template>
      <template #table>
        <el-table-column type="selection" width="55" />
        <el-table-column prop="strainNo" label="菌种编号" width="150" />
        <el-table-column prop="strainName" label="菌种名称" width="180" />
        <el-table-column prop="source" label="菌种来源" width="150" />
        <el-table-column prop="preservationMethod" label="鉴定方法" width="120" />
        <el-table-column prop="storageLocation" label="特征描述" width="150" />
        <el-table-column prop="inventory" label="菌种保存方法" width="100" />
        <el-table-column prop="inventory" label="保存位置" width="100" />
        <el-table-column prop="inventory" label="库存余量" width="100" />
        <el-table-column prop="inventory" label="备注"  />
        <el-table-column prop="status" label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
        <el-table-column prop="strainCode" label="菌种编号" />
        <el-table-column prop="strainName" label="菌种名称" />
        <el-table-column prop="strainSource" label="菌种来源" />
        <el-table-column prop="appraisalMethod" label="鉴定方法" />
        <el-table-column prop="features" label="特征描述" />
        <el-table-column prop="saveMethod" label="菌种保存方法" />
        <el-table-column prop="saveLocation" label="保藏位置" />
        <el-table-column prop="stock" label="库存余量" />
        <el-table-column prop="remark" label="备注" />
        <el-table-column
          v-if="currentType === 'list'"
          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="150" fixed="right">
          <template slot-scope="scope">
            <el-button type="text" @click="handleView(scope.row)">详情</el-button>
            <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button type="text" @click="handleDelete(scope.row)" class="delete-btn">删除</el-button>
        <el-table-column label="操作" width="200">
          <template #default="{ row }">
            <el-button type="text" @click="handleDetail(row)">详情</el-button>
            <el-button v-if="row.status == 2 || row.status == 4" type="text" @click="handleEdit(row)">编辑</el-button>
            <el-button
              v-if="currentType === 'list'"
              type="text"
              @click="handleRecord(row)"
              >出入库记录</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <!-- 删除确认对话框 -->
    <el-dialog
      title="确认删除"
      :visible.sync="deleteDialogVisible"
      width="30%">
      <div class="delete-dialog-content">
        <i class="el-icon-warning-outline warning-icon"></i>
        <span>确定要删除该菌种记录吗?删除后将无法恢复。</span>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="deleteDialogVisible = false">取消</el-button>
        <el-button type="danger" @click="confirmDelete">确定</el-button>
      </span>
    </el-dialog>
    <StrainDetail :visible.sync="detailVisible" :detail="currentDetail" />
  </div>
</template>
<script>
import StrainDetail from "../strain-library-manage/components/StrainDetail.vue";
import { getList, deleteStrainLibrary } from "../strain-library-manage/service";
export default {
  name: 'ProductionCellLibrary',
  name: "StrainLibraryManage",
  components: {
    StrainDetail,
  },
  data() {
    return {
      dialogVisible: false,
      currentType: 'list',
      currentType: "list",
      detailVisible: false,
      currentDetail: {},
      form: {
        strainNo: '',
        strainName: '',
        status: ''
        strainNo: "",
        strainName: "",
        status: "",
      },
      queryForm: {
        pageSize: 10,
        pageNum: 1
        pageNum: 1,
      },
      total: 100,
      loading: false,
      sourceOptions: [
        { value: '主细胞库', label: '主细胞库' },
        { value: '工作细胞库', label: '工作细胞库' },
        { value: '外部来源', label: '外部来源' }
      ],
      statusOptions: [
        { value: '正常', label: '正常' },
        { value: '缺货', label: '缺货' },
        { value: '异常', label: '异常' },
        { value: '已停用', label: '已停用' }
      ],
      total: 800,
      tableData: [],
      selectedRows: [],
      deleteDialogVisible: false,
      deleteRow: null
    }
      roleType: "",
    };
  },
  created() {
    this.fetchData();
  activated() {
    this.searchData();
    // 角色类型 1=超级管理员 2=审批人 3=工程师 4=实验员
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
  },
  methods: {
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteStrainLibrary({ id: row.id }).then((res) => {
            this.$message.success("删除成功");
            this.searchData();
        });
      });
    },
    handleRecord(row) {
      this.$router.push({
        path: `/strain-library/strain-library-manage/record?id=${row.id}`,
      });
    },
    handleNewStrain() {
      this.$router.push({ path: "/strain-library/production-cell-library/add" });
    },
    handleEdit(row) {
      this.$router.push({
        path: `/strain-library/production-cell-library/add?id=${row.id}`,
      });
    },
    handleDetail(row) {
      this.currentDetail = row;
      this.detailVisible = true;
    },
    handleViewMore() {
      this.dialogVisible = true;
    },
    resetForm() {
      this.form = {
        strainNo: '',
        strainName: '',
        status: ''
      }
      this.searchData()
        strainNo: "",
        strainName: "",
        status: "",
      };
      this.searchData();
    },
    searchData() {
      this.queryForm.pageNum = 1;
      this.fetchData();
    },
    // 获取数据
    fetchData() {
      this.loading = true;
      // 构建请求参数
      const params = {
        page: this.queryForm.pageNum,
        pageNum: this.queryForm.pageNum,
        pageSize: this.queryForm.pageSize,
        ...this.form
        strainCode: this.form.strainNo,
        strainName: this.form.strainName,
        isDraft: this.currentType === "draft" ? 1 : 0,
        status: this.form.status,
        type: 3,
      };
      // 模拟API请求
      setTimeout(() => {
        // 模拟数据,实际项目中应替换为真实API调用
        const mockData = [];
        for (let i = 1; i <= 10; i++) {
          mockData.push({
            id: `${i}`,
            strainNo: `PCLS-2023-${String(i).padStart(3, '0')}`,
            strainName: `枯草芽孢杆菌生产株${i}`,
            source: i % 3 === 0 ? '外部来源' : (i % 2 === 0 ? '工作细胞库' : '主细胞库'),
            preservationMethod: i % 2 === 0 ? '冻干保存' : '超低温冷冻保存',
            storageLocation: `A区-A-${100 + i}-冷藏柜`,
            inventory: 10 + i,
            status: i % 4 === 0 ? '异常' : (i % 3 === 0 ? '缺货' : (i % 2 === 0 ? '已停用' : '正常')),
            preparationDate: `2023-05-${String(i).padStart(2, '0')}`,
            expiryDate: `2024-05-${String(i).padStart(2, '0')}`
          });
        }
        this.tableData = mockData;
        this.total = 100; // 模拟总数
        this.loading = false;
      }, 500);
      getList(params)
        .then((res) => {
          if (res.code === 200) {
            this.tableData = res.data.records;
            this.total = res.data.total;
          }
        })
        .catch((err) => {
          this.$message.error("数据加载失败");
        });
    },
    // 状态标签类型
    getStatusType(status) {
      switch(status) {
        case '正常':
          return 'success';
        case '缺货':
          return 'warning';
        case '异常':
          return 'danger';
        case '已停用':
          return 'info';
        default:
          return 'info';
      }
    },
    handleCurrentChange(page) {
      this.queryForm.pageNum = page
      this.fetchData();
      this.queryForm.pageNum = page;
      this.searchData();
    },
    handleSizeChange(size) {
      this.queryForm.pageSize = size
      this.queryForm.pageNum = 1
      this.fetchData();
      this.queryForm.pageSize = size;
      this.searchData();
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.fetchData();
      this.searchData();
    },
    // 表格多选
    handleSelectionChange(selection) {
      this.selectedRows = selection;
    getStatusType(status) {
      const types = {
        1: "warning",
        2: "warning",
        3: "success",
        4: "success",
      };
      return types[status] || "info";
    },
    // 新增菌种
    handleAdd() {
      this.$router.push('/strain-library/production-cell-library/add');
    getStatusText(status) {
      const texts = {
        1: "已出库",
        2: "出库待确认",
        3: "已入库",
        4: "入库待确认",
      };
      return texts[status] || "未知状态";
    },
    // 查看菌种详情
    handleView(row) {
      this.$router.push(`/strain-library/production-cell-library/record/${row.id}`);
    },
    // 编辑菌种
    handleEdit(row) {
      this.$router.push(`/strain-library/production-cell-library/edit/${row.id}`);
    },
    // 删除菌种
    handleDelete(row) {
      this.deleteRow = row;
      this.deleteDialogVisible = true;
    },
    // 确认删除
    confirmDelete() {
      if (!this.deleteRow) return;
      // 模拟API请求
      this.$message({
        type: 'success',
        message: `删除成功: ${this.deleteRow.strainNo} - ${this.deleteRow.strainName}`
      });
      // 移除本地数据
      const index = this.tableData.findIndex(item => item.id === this.deleteRow.id);
      if (index !== -1) {
        this.tableData.splice(index, 1);
      }
      this.deleteDialogVisible = false;
      this.deleteRow = null;
    },
    // 导出
    handleExport() {
      this.$message.info('生产细胞库菌种导出功能开发中');
      // 实际项目中应实现导出逻辑
    }
  }
}
  },
};
</script>
<style scoped lang="less">
@@ -320,7 +333,7 @@
    .view-more {
      position: absolute;
      right: 0;
      color: #049C9A;
      color: #049c9a;
    }
  }
@@ -343,8 +356,50 @@
  }
}
.search-btn-box {
  margin-left: auto;
.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 {
@@ -371,7 +426,6 @@
    line-height: 50px;
    width: 166px;
    text-align: center;
  }
  .drafts {
@@ -394,28 +448,15 @@
    background: #ffffff;
    border-radius: 8px 8px 0px 0px;
    border: 1px solid #049c9a;
  }
}
.delete-dialog-content {
  display: flex;
  align-items: center;
  padding: 20px 0;
  .warning-icon {
    font-size: 24px;
    color: #E6A23C;
    margin-right: 10px;
  }
}
.view-all-dialog {
  :deep(.el-dialog__header) {
    padding: 20px;
    border-bottom: 1px solid #EBEEF5;
    border-bottom: 1px solid #ebeef5;
    margin-right: 0;
    .el-dialog__title {
      font-size: 18px;
      font-weight: bold;
@@ -433,11 +474,11 @@
      p {
        margin: 12px 0;
        &:first-child {
          margin-top: 0;
        }
        &:last-child {
          margin-bottom: 0;
        }
@@ -445,8 +486,4 @@
    }
  }
}
.delete-btn {
  color: #F56C6C;
}
</style>
</style>
culture/src/views/strain-library/production-cell-library/record.vue
@@ -1,538 +1,438 @@
<template>
  <div class="production-cell-record">
    <!-- 页面头部 -->
    <div class="page-header">
      <div class="header-left">
        <el-button icon="el-icon-arrow-left" @click="$router.go(-1)">返回</el-button>
        <h2>生产细胞库菌种详情</h2>
      </div>
      <div class="header-actions">
        <el-button type="primary" plain icon="el-icon-edit" @click="handleEdit">编辑</el-button>
        <el-button type="primary" plain icon="el-icon-printer" @click="handlePrint">打印</el-button>
      </div>
    </div>
  <div class="record-page">
    <!-- 基本信息展示区域 -->
    <el-card class="header-box">
      <div class="header-content">
        <!-- 第一行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种编号:</span>
            <span class="value">{{ detail.strainCode }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">鉴定方法:</span>
            <span class="value">{{ detail.appraisalMethod }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">保藏位置:</span>
            <span class="value">{{ detail.saveLocation }}</span>
          </div>
        </div>
    <!-- 菌种信息卡片 -->
    <el-card class="strain-card" v-loading="loading">
      <div slot="header" class="card-header">
        <span class="card-title">菌种信息</span>
        <div class="status-tag">
          <el-tag :type="getStatusType(strainData.status)">{{ strainData.status }}</el-tag>
        <!-- 第二行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种名称:</span>
            <span class="value">{{ detail.strainName }}</span>
          </div>
          <div class="info-item flex-column full-width">
            <span class="label">特性描述:</span>
            <span class="value">{{ detail.features }}</span>
          </div>
        </div>
      </div>
      <div class="strain-info">
        <div class="info-item">
          <span class="label">菌种编号:</span>
          <span class="value">{{ strainData.strainNo }}</span>
        </div>
        <div class="info-item">
          <span class="label">菌种名称:</span>
          <span class="value">{{ strainData.strainName }}</span>
        </div>
        <div class="info-item">
          <span class="label">菌种来源:</span>
          <span class="value">{{ strainData.source }}</span>
        </div>
        <div class="info-item">
          <span class="label">生产批次:</span>
          <span class="value">{{ strainData.batchNo }}</span>
        </div>
        <div class="info-item">
          <span class="label">保存方法:</span>
          <span class="value">{{ strainData.preservationMethod }}</span>
        </div>
        <div class="info-item">
          <span class="label">保存位置:</span>
          <span class="value">{{ strainData.storageLocation }}</span>
        </div>
        <div class="info-item">
          <span class="label">当前库存:</span>
          <span class="value">{{ strainData.inventory }} 份</span>
        </div>
        <div class="info-item">
          <span class="label">状态:</span>
          <span class="value">{{ strainData.status }}</span>
        </div>
        <div class="info-item">
          <span class="label">更新时间:</span>
          <span class="value">{{ strainData.updateTime }}</span>
        </div>
        <div class="info-item">
          <span class="label">制备日期:</span>
          <span class="value">{{ strainData.preparationDate }}</span>
        </div>
        <div class="info-item">
          <span class="label">有效期至:</span>
          <span class="value">{{ strainData.expiryDate }}</span>
        </div>
        <div class="info-item full-width">
          <span class="label">特征描述:</span>
          <div class="value description">{{ strainData.description }}</div>
        </div>
        <div class="info-item full-width" v-if="strainData.certificateUrl">
          <span class="label">质量证书:</span>
          <div class="value">
            <el-button type="text" @click="handleViewCertificate">查看证书</el-button>
            <el-button type="text" @click="handleDownloadCertificate">下载证书</el-button>
        <!-- 第三行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种来源:</span>
            <span class="value">{{ detail.strainSource }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">菌种保存方法:</span>
            <span class="value">{{ detail.saveMethod }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- 使用记录 -->
    <el-card class="record-card" v-loading="loadingRecords">
      <div slot="header" class="card-header">
        <span class="card-title">使用记录</span>
        <el-button type="text" @click="handleAddUsage">新增使用记录</el-button>
      </div>
      <el-table
        :data="usageRecords"
        style="width: 100%"
        :empty-text="usageRecords.length ? '' : '暂无使用记录'"
      >
        <el-table-column prop="date" label="使用日期" width="120"></el-table-column>
        <el-table-column prop="amount" label="使用数量" width="100"></el-table-column>
        <el-table-column prop="operator" label="操作人" width="120"></el-table-column>
        <el-table-column prop="project" label="项目名称"></el-table-column>
        <el-table-column prop="batchNo" label="生产批次" width="150"></el-table-column>
        <el-table-column prop="purpose" label="使用目的"></el-table-column>
        <el-table-column label="操作" width="120">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="handleViewUsageDetail(scope.row)">查看</el-button>
            <el-button type="text" size="small" @click="handleEditUsage(scope.row)">编辑</el-button>
    <!-- 出入库记录表格 -->
    <TableCustom
      :queryForm="queryForm"
      :tableData="recordList"
      :total="total"
      @currentChange="handlePageChange"
    >
      <template #setting>
        <div class="tableTitle">
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'table' }"
              @click="handleTypeChange('table')"
            >
              原始细胞保藏出/入库登记表
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'timeline' }"
              @click="handleTypeChange('timeline')"
            >
              原始细胞保藏出/入库时间轴
            </div>
          </div>
          <div class="flex a-center">
            <el-button
              v-if="roleType == 4"
              @click="handleAddRecord"
              class="el-icon-plus"
              type="primary"
              >新增出入库记录</el-button
            >
          </div>
        </div>
      </template>
      <template #table v-if="currentType === 'table'">
        <el-table-column prop="type" label="出库/入库">
          <template #default="{ row }">
            <span>
              {{ row.type === 1 ? "出库" : "入库" }}
            </span>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container" v-if="usageRecords.length">
        <el-pagination
          background
          layout="prev, pager, next"
          :total="usageTotalCount"
          :current-page.sync="usageCurrentPage"
          :page-size="usagePageSize"
          @current-change="handleUsagePageChange">
        </el-pagination>
      </div>
    </el-card>
    <!-- 测试记录 -->
    <el-card class="record-card" v-loading="loadingTests">
      <div slot="header" class="card-header">
        <span class="card-title">测试记录</span>
        <el-button type="text" @click="handleAddTest">新增测试记录</el-button>
      </div>
      <el-table
        :data="testRecords"
        style="width: 100%"
        :empty-text="testRecords.length ? '' : '暂无测试记录'"
      >
        <el-table-column prop="date" label="测试日期" width="120"></el-table-column>
        <el-table-column prop="type" label="测试类型" width="150"></el-table-column>
        <el-table-column prop="operator" label="操作人" width="120"></el-table-column>
        <el-table-column prop="result" label="测试结果">
          <template slot-scope="scope">
            <el-tag :type="scope.row.result === '合格' ? 'success' : scope.row.result === '不合格' ? 'danger' : 'info'">
              {{ scope.row.result }}
        <el-table-column prop="boundTime" label="操作时间" />
        <el-table-column prop="handleSignature" label="操作人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.handleSignature"
              style="width: 100px; height: 100px"
              :src="row.handleSignature"
              :preview-src-list="[row.handleSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="preserveSignature" label="菌种保藏人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.preserveSignature"
              style="width: 100px; height: 100px"
              :src="row.preserveSignature"
              :preview-src-list="[row.preserveSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="row.preserveSignature ? 'success' : 'warning'">
              {{ row.preserveSignature ? "已确认" : "待确认" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="remark" label="备注"></el-table-column>
        <el-table-column label="操作" width="120">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="handleViewTestDetail(scope.row)">查看</el-button>
            <el-button type="text" size="small" @click="handleEditTest(scope.row)">编辑</el-button>
        <el-table-column label="操作" width="180">
          <template #default="{ row }">
            <el-button
              v-if="!row.preserveSignature && roleType == 3"
              type="text"
              class="operation-btn"
              @click="handleConfirm(row)"
              >确认</el-button
            >
            <el-button
              type="text"
              class="operation-btn"
              @click="handleView(row)"
              >详情</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container" v-if="testRecords.length">
        <el-pagination
          background
          layout="prev, pager, next"
          :total="testTotalCount"
          :current-page.sync="testCurrentPage"
          :page-size="testPageSize"
          @current-change="handleTestPageChange">
        </el-pagination>
      </div>
    </el-card>
      </template>
      <template #tableCustom v-if="currentType === 'timeline'">
        <record-timeline :list="timelineList" />
      </template>
    </TableCustom>
    <!-- 证书预览对话框 -->
    <el-dialog
      title="证书预览"
      :visible.sync="certificateDialogVisible"
      width="70%">
      <div v-if="strainData.certificateUrl" class="certificate-preview">
        <iframe v-if="strainData.certificateUrl.endsWith('.pdf')" :src="strainData.certificateUrl" width="100%" height="500"></iframe>
        <img v-else :src="strainData.certificateUrl" style="max-width: 100%; max-height: 500px;" />
      </div>
    </el-dialog>
    <!-- 详情弹窗 -->
    <record-detail-dialog
      :visible.sync="dialogVisible"
      :record-data="currentRecord"
      @close="handleDialogClose"
      @confirm="handleOutbound"
      :type="dialogType"
    />
    <!-- 新增出入库记录弹窗 -->
    <add-record-dialog
      :visible.sync="addDialogVisible"
      @confirm="handleAddRecordConfirm"
    />
  </div>
</template>
<script>
import RecordDetailDialog from "../strain-library-manage/components/RecordDetailDialog.vue";
import AddRecordDialog from "../strain-library-manage/components/AddRecordDialog.vue";
import RecordTimeline from "../strain-library-manage/components/RecordTimeline.vue";
import {
  timeList,
  getDetail,
  addWarehousing,
  getDetailById,
  confirmWarehousing,
} from "./service";
export default {
  name: 'ProductionCellLibraryRecord',
  name: "StrainRecord",
  components: {
    RecordDetailDialog,
    AddRecordDialog,
    RecordTimeline,
  },
  data() {
    return {
      loading: false,
      loadingRecords: false,
      loadingTests: false,
      strainId: '',
      strainData: {
        id: '',
        strainNo: '',
        strainName: '',
        source: '',
        batchNo: '',
        preservationMethod: '',
        storageLocation: '',
        inventory: 0,
        status: '',
        description: '',
        preparationDate: '',
        expiryDate: '',
        updateTime: '',
        certificateUrl: ''
      currentType: "table",
      detail: {},
      currentPage: 1,
      pageSize: 10,
      total: 0,
      queryForm: {
        pageSize: 10,
        pageNum: 1,
      },
      certificateDialogVisible: false,
      // 使用记录分页数据
      usageRecords: [],
      usageCurrentPage: 1,
      usagePageSize: 10,
      usageTotalCount: 0,
      // 测试记录分页数据
      testRecords: [],
      testCurrentPage: 1,
      testPageSize: 10,
      testTotalCount: 0
    }
      recordList: [],
      timelineList: [],
      dialogVisible: false,
      currentRecord: {},
      addDialogVisible: false,
      dialogType: "detail",
      roleType: "",
    };
  },
  created() {
    this.strainId = this.$route.params.id;
    this.fetchStrainData();
    this.fetchUsageRecords();
    this.fetchTestRecords();
  activated() {
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
    // 获取路由参数中的菌种信息
    const strainId = this.$route.query.id;
    this.queryForm.id = strainId;
    if (strainId) {
      this.getStrainDetail(strainId);
      this.getRecordList();
    }
  },
  methods: {
    // 获取菌种详情
    fetchStrainData() {
      this.loading = true;
      // 模拟API请求
      setTimeout(() => {
        // 模拟数据,实际项目中应替换为真实API调用
        this.strainData = {
          id: this.strainId,
          strainNo: 'PCLS-2023-001',
          strainName: '枯草芽孢杆菌生产株',
          source: '主细胞库',
          batchNo: 'P20230515-001',
          preservationMethod: '冻干保存',
          storageLocation: 'A区-A-102-冷藏柜',
          inventory: 12,
          status: '正常',
          description: '本菌种为工业生产级别枯草芽孢杆菌生产株,由主细胞库转入,经过严格筛选和稳定性测试。该菌株具有高产蛋白酶能力,发酵条件适应性强,适合大规模工业化生产。产品稳定性好,批次间差异小,可用于洗涤用酶制剂、食品加工酶制剂等多种产品的生产。',
          preparationDate: '2023-05-10',
          expiryDate: '2024-05-10',
          updateTime: '2023-05-15 14:30:22',
          certificateUrl: '/api/strain-library/certificates/sample.pdf'
        };
        this.loading = false;
      }, 500);
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteWarehousing({ id: row.id }).then((res) => {
          this.$message.success("删除成功");
          this.getRecordList();
        });
      });
    },
    // 获取使用记录
    fetchUsageRecords() {
      this.loadingRecords = true;
      // 模拟API请求
      setTimeout(() => {
        // 模拟数据,实际项目中应替换为真实API调用
        this.usageRecords = [
          {
            id: '1',
            date: '2023-06-05',
            amount: 2,
            operator: '张工',
            project: '酶制剂研发项目',
            batchNo: 'E20230605-001',
            purpose: '小试生产'
          },
          {
            id: '2',
            date: '2023-07-12',
            amount: 3,
            operator: '李工',
            project: '蛋白酶产品项目',
            batchNo: 'E20230712-002',
            purpose: '中试生产'
          },
          {
            id: '3',
            date: '2023-08-18',
            amount: 5,
            operator: '王工',
            project: '工业酶制剂生产',
            batchNo: 'E20230818-003',
            purpose: '规模化生产'
          }
        ];
        this.usageTotalCount = 3;
        this.loadingRecords = false;
      }, 600);
    getStrainDetail(id) {
      // 这里应该调用接口获取菌种详情
      getDetail({ id }).then((res) => {
        this.detail = res;
      });
    },
    // 获取测试记录
    fetchTestRecords() {
      this.loadingTests = true;
      // 模拟API请求
      setTimeout(() => {
        // 模拟数据,实际项目中应替换为真实API调用
        this.testRecords = [
          {
            id: '1',
            date: '2023-05-15',
            type: '活力测定',
            operator: '刘工',
            result: '合格',
            remark: '酶活性达标,符合生产要求'
          },
          {
            id: '2',
            date: '2023-05-15',
            type: '纯度检测',
            operator: '张工',
            result: '合格',
            remark: '纯度>98%,无杂菌污染'
          },
          {
            id: '3',
            date: '2023-05-16',
            type: '稳定性测试',
            operator: '李工',
            result: '合格',
            remark: '常温保存7天活力下降小于5%'
          }
        ];
        this.testTotalCount = 3;
        this.loadingTests = false;
      }, 700);
    getRecordList() {
      // 这里应该调用接口获取出入库记录
      timeList(this.queryForm).then((res) => {
        this.timelineList = res.data;
      });
      getDetailById({ id: this.$route.query.id }).then((res) => {
        this.recordList = res.warehousingList.records;
        this.total = res.warehousingList.total;
      });
    },
    // 状态标签类型
    getStatusType(status) {
      switch(status) {
        case '正常':
          return 'success';
        case '缺货':
          return 'warning';
        case '异常':
          return 'danger';
        case '已停用':
          return 'info';
        default:
          return 'info';
      }
    handleView(row) {
      this.dialogType = "detail";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    // 编辑菌种
    handleEdit() {
      this.$router.push(`/strain-library/production-cell-library/edit/${this.strainId}`);
    handleConfirm(row) {
      this.dialogType = "confirm";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    // 打印菌种信息
    handlePrint() {
      window.print();
    handlePageChange(page) {
      this.queryForm.pageNum = page;
      // 这里应该调用接口获取对应页码的数据
    },
    // 查看证书
    handleViewCertificate() {
      this.certificateDialogVisible = true;
    handleTypeChange(type) {
      this.currentType = type;
    },
    // 下载证书
    handleDownloadCertificate() {
      // 实际项目中应处理文件下载逻辑
      window.open(this.strainData.certificateUrl, '_blank');
    handleAddRecord() {
      this.addDialogVisible = true;
    },
    // 新增使用记录
    handleAddUsage() {
      this.$message.info('功能开发中:新增使用记录');
      // 实际项目中应跳转到新增使用记录页面或打开对话框
    handleDialogClose() {
      this.currentRecord = {};
      this.dialogVisible = false;
    },
    // 查看使用记录详情
    handleViewUsageDetail(row) {
      this.$message.info(`查看使用记录: ${row.id}`);
      // 实际项目中应跳转到使用记录详情页面或打开对话框
    handleOutbound(data) {
      // 这里调用出库API
      confirmWarehousing({
        id: this.currentRecord.id,
        preserveSignature: data.preserveSignature,
      }).then((res) => {
        console.log(res);
        if (res.code == 200) {
          this.$message.success("操作成功");
          this.dialogVisible = false;
          // 刷新列表
          this.getRecordList();
        } else {
          this.$message.error(res.msg);
        }
      });
    },
    // 编辑使用记录
    handleEditUsage(row) {
      this.$message.info(`编辑使用记录: ${row.id}`);
      // 实际项目中应跳转到编辑使用记录页面或打开对话框
    handleAddRecordConfirm(record) {
      addWarehousing({ ...record, trainLibraryId: this.$route.query.id }).then(
        (res) => {
          this.$message.success("操作成功");
          this.getRecordList();
        }
      );
    },
    // 使用记录分页切换
    handleUsagePageChange(page) {
      this.usageCurrentPage = page;
      this.fetchUsageRecords();
    goBack() {
      this.$router.go(-1);
    },
    // 新增测试记录
    handleAddTest() {
      this.$message.info('功能开发中:新增测试记录');
      // 实际项目中应跳转到新增测试记录页面或打开对话框
    },
    // 查看测试记录详情
    handleViewTestDetail(row) {
      this.$message.info(`查看测试记录: ${row.id}`);
      // 实际项目中应跳转到测试记录详情页面或打开对话框
    },
    // 编辑测试记录
    handleEditTest(row) {
      this.$message.info(`编辑测试记录: ${row.id}`);
      // 实际项目中应跳转到编辑测试记录页面或打开对话框
    },
    // 测试记录分页切换
    handleTestPageChange(page) {
      this.testCurrentPage = page;
      this.fetchTestRecords();
    }
  }
}
  },
};
</script>
<style scoped lang="less">
.production-cell-record {
  padding: 20px;
<style lang="less" scoped>
.record-page {
  min-height: 100vh;
  .page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    .header-left {
      display: flex;
      align-items: center;
      h2 {
        margin: 0 0 0 12px;
        font-size: 22px;
        font-weight: 500;
      }
    }
    .header-actions {
      display: flex;
      gap: 12px;
    }
  }
  .strain-card {
    margin-bottom: 24px;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      .card-title {
        font-size: 16px;
        font-weight: 500;
      }
    }
    .strain-info {
      display: flex;
      flex-wrap: wrap;
      .info-item {
        width: 33.33%;
        margin-bottom: 16px;
        &.full-width {
          width: 100%;
  .header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
    height: 130px;
    overflow: hidden;
    .header-content {
      color: rgba(0, 0, 0, 0.88);
      font-size: 14px;
      line-height: 1.5;
      .info-row {
        display: flex;
        flex-wrap: wrap;
        margin-bottom: 8px;
        &:last-child {
          margin-bottom: 0;
        }
        .label {
          font-weight: 500;
          color: #606266;
        }
        .value {
          color: #303133;
          &.description {
            white-space: pre-line;
            line-height: 1.6;
            padding: 8px 0;
        .info-item {
          display: flex;
          align-items: flex-start;
          margin-right: 24px;
          margin-bottom: 6px;
          &.left-column {
            width: 33%;
            min-width: 200px;
          }
          &.flex-column {
            flex: 1;
            min-width: 150px;
          }
          &.full-width {
            flex: 1;
            min-width: 300px;
          }
          .label {
            color: #606266;
            margin-right: 8px;
            white-space: nowrap;
          }
          .value {
            flex: 1;
            color: #303133;
            word-break: break-all;
            display: -webkit-box;
            -webkit-line-clamp: 1;
            -webkit-box-orient: vertical;
            overflow: hidden;
            text-overflow: ellipsis;
          }
        }
      }
    }
  }
  .record-card {
    margin-bottom: 24px;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      .card-title {
        font-size: 16px;
        font-weight: 500;
      }
    }
    .pagination-container {
      display: flex;
      justify-content: flex-end;
      margin-top: 20px;
    }
  }
  .certificate-preview {
  .flex {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 500px;
    background-color: #f5f7fa;
  }
  @media print {
    .page-header, .header-actions, .record-card {
      display: none;
  .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;
      cursor: pointer;
      height: 50px;
      line-height: 50px;
      width: 280px;
      text-align: center;
    }
    .strain-card {
      border: none;
      box-shadow: none;
      .card-header {
        background-color: #fff !important;
        border-bottom: 1px solid #ddd;
    .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: 280px;
      text-align: center;
    }
    .active {
      color: #049c9a;
      background: #ffffff;
      border-radius: 8px 8px 0px 0px;
      border: 1px solid #049c9a;
    }
  }
  .timeline-container {
    padding: 20px;
    background: rgba(255, 255, 255, 0.8);
    .timeline-card {
      margin-bottom: 10px;
      background: rgba(255, 255, 255, 0.8);
      h4 {
        margin: 0 0 10px;
        font-size: 16px;
        font-weight: bold;
      }
      p {
        margin: 5px 0;
        font-size: 14px;
      }
    }
  }
  .operation-btn {
    margin-right: 12px;
  }
}
</style>
</style>
culture/src/views/strain-library/production-cell-library/service.js
New file
@@ -0,0 +1,56 @@
import axios from '@/utils/request';
// 列表
export const getList = (data) => {
  return axios.post('/api/t-train-library/pageList', { ...data })
}
// 新增
export const add = (data) => {
  return axios.post('/api/t-train-library/add', { ...data })
}
// 编辑
export const edit = (data) => {
  return axios.post('/api/t-train-library/update', { ...data })
}
// 查看详情
export const getDetail = (params) => {
  return axios.get('/open/t-train-library/getDetailEditById', { params })
}
// 批量新增
export const addBatch = (data) => {
  return axios.post('/api/t-train-library/addBatch', data)
}
// 查看菌种库详情
export const getDetailById = (data) => {
  return axios.post('/open/t-train-library/getDetailById', { ...data })
}
// 获取菌种库出入库时间轴列表
export const timeList = (data) => {
  return axios.post('/api/t-train-library/timeList?id='+data.id, { ...data })
}
// 新增菌种库出入记录
export const addWarehousing = (data) => {
  return axios.post('/open/t-train-library/addWarehousing', { ...data })
}
// 确认出入库
export const confirmWarehousing = (data) => {
  return axios.post('/api/t-train-library/confirm', { ...data })
}
// 删除菌种库
export const deleteStrainLibrary = (params) => {
  return axios.delete('/open/t-train-library/deleteById', { params })
}
// 删除菌种库出入库记录
export const deleteWarehousing = (params) => {
  return axios.delete('/open/t-train-library/deleteWarehousingById', { params })
}
culture/src/views/strain-library/strain-library-manage/add.vue
@@ -1,92 +1,60 @@
<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"
        >
        <el-form :model="form" :rules="rules" ref="strainForm" label-position="top" class="strain-form">
            <div class="form-row three-columns">
                <el-form-item label="菌种编号" prop="strainNo" required>
                    <el-input v-model="form.strainNo" placeholder="请输入"></el-input>
                <el-form-item label="菌种编号" prop="strainCode">
                    <el-input v-model="form.strainCode" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种名称" prop="strainName" required>
                <el-form-item label="菌种名称" prop="strainName">
                    <el-input v-model="form.strainName" placeholder="请输入"></el-input>
                </el-form-item>
                <el-form-item label="菌种来源" prop="source" required>
                    <el-input v-model="form.source" placeholder="请输入"></el-input>
                <el-form-item label="菌种来源" prop="strainSource">
                    <el-input v-model="form.strainSource" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row">
                <el-form-item label="鉴定方法" prop="identificationMethod" required>
                    <el-input v-model="form.identificationMethod" placeholder="请输入"></el-input>
                <el-form-item label="鉴定方法" prop="appraisalMethod">
                    <el-input v-model="form.appraisalMethod" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="form-row">
                <el-form-item label="特征描述" prop="characteristics" required class="full-width">
                    <el-input
                        type="textarea"
                        v-model="form.characteristics"
                        :rows="4"
                        placeholder="请输入"
                    ></el-input>
                <el-form-item label="特征描述" prop="features" class="full-width">
                    <el-input type="textarea" v-model="form.features" :rows="4" 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 label="保藏位置" prop="saveLocation">
                    <el-input v-model="form.saveLocation" 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 label="菌种保存方法" prop="saveMethod">
                    <el-input v-model="form.saveMethod" placeholder="请输入"></el-input>
                </el-form-item>
                <div class="form-item-placeholder"></div>
            </div>
            <div class="form-row">
                <el-form-item label="备注" prop="remarks" class="full-width">
                    <el-input
                        type="textarea"
                        v-model="form.remarks"
                        :rows="4"
                        placeholder="请输入"
                    ></el-input>
                    <el-input type="textarea" v-model="form.remark" :rows="4" placeholder="请输入"></el-input>
                </el-form-item>
            </div>
            <div class="end-btn" style="margin-top: 38px">
                <el-button type="primary" @click="handleSubmit">提交</el-button>
                <el-button type="primary" @click="handleBatchAdd">批量新增</el-button>
                <el-button @click="handleDraft">存草稿</el-button>
                <el-button type="primary" @click="handleSubmit(0)">提交</el-button>
                <el-button v-if="!$route.query.id" type="primary" @click="handleBatchAdd">批量新增</el-button>
                <el-button @click="handleSubmit(1)">存草稿</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"
        >
        <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-form-item label="批量新增数量" prop="count"
                        :rules="[{ required: true, message: '请输入批量新增数量', trigger: 'blur' }]">
                        <el-input v-model.number="batchForm.count" placeholder="请输入" />
                    </el-form-item>
                </el-form>
@@ -104,18 +72,16 @@
        </el-dialog>
        <!-- 签字确认组件 -->
        <SignatureCanvas
            :visible.sync="signatureVisible"
            @confirm="handleSignatureConfirm"
        />
        <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </Card>
</template>
<script>
import SignatureCanvas from '@/components/SignatureCanvas.vue'
import { add, edit, getDetail, addBatch } from './service'
export default {
    name: 'AddStrain',
    name: 'StrainLibraryManageAdd',
    components: {
        SignatureCanvas
    },
@@ -128,60 +94,119 @@
                count: ''
            },
            form: {
                strainNo: '',
                strainCode: '',
                strainName: '',
                source: '',
                identificationMethod: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remarks: ''
                remark: ''
            },
            rules: {
                strainNo: [{ required: true, message: '请输入菌种编号', trigger: 'blur' }],
                strainCode: [{
                    validator: (rule, value, callback) => {
                        if (this.currentAction === 'submit' && !value) {
                            callback(new Error('请输入菌种编号'));
                        } else {
                            callback();
                        }
                    },
                    trigger: 'change'
                }],
                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' }]
                strainSource: [{ required: true, message: '请输入菌种来源', trigger: 'blur' }],
                appraisalMethod: [{ required: true, message: '请输入鉴定方法', trigger: 'blur' }],
                features: [{ required: true, message: '请输入特征描述', trigger: 'blur' }],
                saveLocation: [{ required: true, message: '请输入保存位置', trigger: 'blur' }],
                saveMethod: [{ required: true, message: '请输入菌种保存方法', trigger: 'blur' }]
            }
        }
    },
    activated() {
        if (this.$route.query.id) {
            getDetail({ id: this.$route.query.id }).then(res => {
                this.form = res
            })
        }
    },
    watch: {
        '$route.query.id'() {
            this.form = {
                strainCode: '',
                strainName: '',
                source: '',
                appraisalMethod: '',
                characteristics: '',
                storageLocation: '',
                preservationMethod: '',
                remark: ''
            }
        }
    },
    methods: {
        handleSubmit() {
        handleSubmit(isDraft) {
            this.currentAction = 'submit'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    this.currentAction = 'submit'
                    this.signatureVisible = true
                    this.form.isDraft = isDraft
                    if (isDraft == 1) {
                        //存草稿
                        this.handleSignatureConfirm('')
                    } else {
                        this.signatureVisible = true
                    }
                }
            })
        },
        handleBatchAdd() {
            this.batchAddDialogVisible = true
            this.currentAction = 'batchAdd'
            this.$refs.strainForm.validate((valid) => {
                if (valid) {
                    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)
        async handleSignatureConfirm(signatureImage) {
            let requestData = {
                strainCode: this.form.strainCode,
                strainName: this.form.strainName,
                strainSource: this.form.strainSource,
                appraisalMethod: this.form.appraisalMethod,
                features: this.form.features,
                saveLocation: this.form.saveLocation,
                saveMethod: this.form.saveMethod,
                remark: this.form.remark,
                signature: signatureImage,
                type: 1,
            };
            if (this.currentAction === 'batchAdd') {
                requestData.batchCount = this.batchForm.count;
            } else {
                requestData.isDraft = this.form.isDraft
            }
            try {
                if (this.$route.query.id) {
                    requestData.id = this.$route.query.id;
                    await edit(requestData);
                } else if (this.currentAction === 'batchAdd') {
                    await addBatch(requestData);
                } else {
                    await add(requestData);
                }
                this.signatureVisible = false;
                this.$router.back();
                this.$message.success('操作成功');
            } catch (error) {
                this.$message.error('操作失败');
            }
        }
    }
@@ -205,7 +230,7 @@
    &-left {
        display: flex;
        align-items: center;
        img {
            width: 20px;
            height: 20px;
@@ -219,18 +244,20 @@
        }
    }
}
.end-btn{
.end-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    button{
    button {
        width: 180px;
        height: 36px;
        // background: #409EFF; 
    }
}
.strain-form {
    padding: 0 40px;
@@ -241,14 +268,16 @@
        margin-bottom: 24px;
        &.three-columns {
            .el-form-item, .form-item-placeholder {
            .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%;
                }
@@ -300,7 +329,7 @@
        padding: 20px;
        text-align: center;
        border-bottom: 1px solid #EBEEF5;
        .el-dialog__title {
            font-size: 16px;
            font-weight: 600;
@@ -334,6 +363,7 @@
            color: #606266;
            font-weight: normal;
            padding-bottom: 8px;
            &::before {
                color: #F56C6C;
            }
@@ -341,8 +371,9 @@
        :deep(.el-input) {
            width: 100%;
            input {
            width: 100%;
                width: 100%;
            }
        }
    }
@@ -350,6 +381,7 @@
    .dialog-notice {
        margin-top: 16px;
        text-align: center;
        p {
            margin: 0;
            line-height: 22px;
@@ -361,7 +393,7 @@
    :deep(.el-dialog__footer) {
        padding: 0 20px 20px;
        text-align: center;
        .el-button {
            width: 180px;
            height: 36px;
@@ -394,4 +426,4 @@
        margin: 0;
    }
}
</style>
</style>
culture/src/views/strain-library/strain-library-manage/components/AddRecordDialog.vue
@@ -12,12 +12,12 @@
        <el-form-item label="出库/入库" required>
          <div class="type-buttons">
            <el-button
              :type="formData.type === '出库' ? 'primary' : 'default'"
              @click="formData.type = '出库'"
              :type="formData.type === '1' ? 'primary' : 'default'"
              @click="formData.type = '1'"
            >出库</el-button>
            <el-button
              :type="formData.type === '入库' ? 'primary' : 'default'"
              @click="formData.type = '入库'"
              :type="formData.type === '2' ? 'primary' : 'default'"
              @click="formData.type = '2'"
            >入库</el-button>
          </div>
        </el-form-item>
@@ -27,9 +27,9 @@
            <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="操作人签字" />
          <div class="signature-area" :class="{ 'waiting': !formData.handleSignature }">
            <template v-if="formData.handleSignature">
              <img :src="formData.handleSignature" alt="操作人签字" />
            </template>
            <template v-else>
              <span class="waiting-text">等待确认</span>
@@ -60,8 +60,8 @@
  data() {
    return {
      formData: {
        type: '出库',
        operatorSignature: ''
        type: '1',
        handleSignature: ''
      },
      showSignature: false
    }
@@ -72,7 +72,7 @@
      this.$emit('close')
    },
    handleConfirm() {
      if (!this.formData.operatorSignature) {
      if (!this.formData.handleSignature) {
        this.$message.warning('请先签名')
        return
      }
@@ -80,7 +80,7 @@
      this.handleClose()
    },
    handleSignatureConfirm(dataUrl) {
      this.formData.operatorSignature = dataUrl
      this.formData.handleSignature = dataUrl
      this.showSignature = false
    }
  }
culture/src/views/strain-library/strain-library-manage/components/RecordDetailDialog.vue
@@ -1,28 +1,20 @@
<template>
    <el-dialog
        title="出/入库详情"
        :visible.sync="visible"
        width="520px"
        :close-on-click-modal="false"
        custom-class="record-detail-dialog"
        @close="handleClose"
    >
    <el-dialog :title="type == 'detail' ? '出/入库详情' : '确认出入库'" :visible.sync="visible" width="550px"
        :close-on-click-modal="false" custom-class="record-detail-dialog" @close="handleClose" @opened="opened">
        <div class="dialog-content">
            <el-form :model="formData" label-position="top">
                <el-form-item label="出库/入库" required>
                    <div class="type-buttons">
                        <el-button
                            type="primary"
                            @click="handleOutbound"
                        >出库</el-button>
                        <el-button v-if="formData.type == '1'" type="primary">出库</el-button>
                        <el-button v-if="formData.type == '2'" type="primary">入库</el-button>
                    </div>
                </el-form-item>
                <div class="signature-row">
                    <el-form-item label="操作人签字" required class="signature-item">
                        <div class="signature-area" :class="{ 'waiting': !formData.operatorSignature }">
                            <template v-if="formData.operatorSignature">
                                <img :src="formData.operatorSignature" alt="操作人签字" />
                        <div class="signature-area" :class="{ 'waiting': !formData.handleSignature }">
                            <template v-if="formData.handleSignature">
                                <img :src="formData.handleSignature" alt="操作人签字" />
                            </template>
                            <template v-else>
                                <span class="waiting-text">等待确认</span>
@@ -30,13 +22,8 @@
                        </div>
                    </el-form-item>
                    <el-form-item
                        v-if="formData.operatorSignature"
                        label="出库时间"
                        required
                        class="time-item"
                    >
                        <div class="time-value">{{ formData.operateTime }}</div>
                    <el-form-item v-if="formData.handleSignature" label="出库时间" required class="time-item">
                        <div class="time-value">{{ formData.boundTime }}</div>
                    </el-form-item>
                </div>
@@ -44,11 +31,12 @@
                    <el-form-item required class="signature-item">
                        <template #label>
                            <span>保藏人签字</span>
                            <el-button type="primary" class="edit-sign-btn" @click="showSignature = true">修改签名</el-button>
                            <el-button v-if="type != 'detail'" type="primary" class="edit-sign-btn"
                                @click="showSignature = true">修改签名</el-button>
                        </template>
                        <div class="signature-area" :class="{ 'waiting': !formData.reviewerSignature }">
                            <template v-if="formData.reviewerSignature">
                                <img :src="formData.reviewerSignature" alt="保藏人签字" />
                        <div class="signature-area" :class="{ 'waiting': !formData.preserveSignature }">
                            <template v-if="formData.preserveSignature">
                                <img :src="formData.preserveSignature" alt="保藏人签字" />
                            </template>
                            <template v-else>
                                <span class="waiting-text">等待确认</span>
@@ -56,18 +44,18 @@
                        </div>
                    </el-form-item>
                    <el-form-item
                        v-if="formData.reviewerSignature"
                        label="确认时间"
                        required
                        class="time-item"
                    >
                    <el-form-item v-if="formData.preserveSignature && type == 'detail'" label="确认时间" required
                        class="time-item">
                        <div class="time-value">{{ formData.confirmTime }}</div>
                    </el-form-item>
                </div>
            </el-form>
            <div class="confirm-btn" v-if="type != 'detail'" style="text-align: center;margin-top: 20px;">
                <el-button type="primary" style="width: 80px;" @click="handleOutbound">确认</el-button>
            </div>
        </div>
        <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm" @cancel="showSignature = false" />
        <signature-canvas :visible.sync="showSignature" @confirm="handleSignatureConfirm"
            @cancel="showSignature = false" />
    </el-dialog>
</template>
@@ -84,11 +72,17 @@
        recordData: {
            type: Object,
            default: () => ({})
        },
        type: {
            type: String,
            default: 'detail'
        }
    },
    data() {
        return {
            formData: {},
            formData: {
                type: '1',
            },
            showSignature: false
        }
    },
@@ -101,12 +95,14 @@
        }
    },
    methods: {
        opened() {
            this.formData.type = this.recordData.type
        },
        handleClose() {
            this.$emit('update:visible', false)
            this.$emit('close')
        },
        handleOutbound() {
            if (!this.formData.operatorSignature || !this.formData.reviewerSignature) {
            if (!this.formData.preserveSignature) {
                this.$message.warning('请等待所有签字确认后再进行出库操作')
                return
            }
@@ -114,7 +110,7 @@
            this.handleClose()
        },
        handleSignatureConfirm(dataUrl) {
            this.formData.reviewerSignature = dataUrl
            this.formData.preserveSignature = dataUrl
            this.showSignature = false
            // 可选:this.formData.confirmTime = new Date().toLocaleString()
        }
@@ -128,7 +124,7 @@
        padding: 20px 24px;
        margin: 0;
        border-bottom: 1px solid #DCDFE6;
        .el-dialog__title {
            font-size: 16px;
            font-weight: 600;
@@ -156,11 +152,13 @@
    }
    .type-buttons {
        display: flex;
        gap: 12px;
        .el-button {
            width: 80px;
            background: #409EFF;
            border-color: #409EFF;
            color: #FFFFFF;
            &:hover {
                opacity: 0.8;
@@ -232,4 +230,4 @@
.edit-sign-btn {
    margin-left: 12px;
}
</style>
</style>
culture/src/views/strain-library/strain-library-manage/components/RecordTimeline.vue
@@ -1,29 +1,23 @@
<template>
  <div class="record-timeline">
    <el-timeline>
      <el-timeline-item
        v-for="(item, idx) in list"
        :key="idx"
        :type="item.type === '入库' ? 'primary' : 'warning'"
        :color="item.type === '入库' ? '#04A9A7' : '#FF9900'"
        :icon="''"
        :timestamp="''"
      >
      <el-timeline-item v-for="(item, idx) in list" :key="idx" :type="item.type == '2' ? 'primary' : 'warning'"
        :color="item.type == '2' ? '#04A9A7' : '#FF9900'" :icon="''" :timestamp="''">
        <div class="timeline-row">
          <div :class="['left-block', item.type === '入库' ? 'in' : 'out']">
            <div class="type-tag">{{ item.type }}</div>
          <div :class="['left-block', item.type == '2' ? 'in' : 'out']">
            <div class="type-tag">{{ item.type == 2 ? '入库' : '出库' }}</div>
            <div class="info-main">
              <div class="info-title">操作人:{{ item.operator || '--' }}</div>
              <div class="info-time">操作时间:{{ item.operateTime || '--' }}</div>
              <div class="info-title">操作人:{{ item.handleName || '--' }}</div>
              <div class="info-time">操作时间:{{ item.boundTime || '--' }}</div>
            </div>
          </div>
          <div :class="[
            'right-block',
            item.confirmTime && item.confirmTime !== '--' ?
              (item.type === '入库' ? 'confirmed-in' : 'confirmed-out') :
            'right-block',
            item.confirmTime && item.confirmTime !== '--' ?
              (item.type === '2' ? 'confirmed-in' : 'confirmed-out') :
              'unconfirmed'
          ]">
            <div class="info-title">保藏人:{{ item.reviewer || '--' }}</div>
            <div class="info-title">保藏人:{{ item.preserveName || '--' }}</div>
            <div class="info-time">确认时间:{{ item.confirmTime || '--' }}</div>
          </div>
        </div>
@@ -71,7 +65,8 @@
  width: 12px !important;
  height: 12px !important;
  left: -6px;
  top: 34px !important; /* 微调位置使其看起来完全居中 */
  top: 34px !important;
  /* 微调位置使其看起来完全居中 */
  margin: 0 !important;
  background: #ffffff;
  box-shadow: none !important;
@@ -83,18 +78,23 @@
  border-left: 3px solid #e6e6e6;
  left: -1px;
  top: 0;
  height: 114px; /* 84px + 30px */
  height: 114px;
  /* 84px + 30px */
  z-index: 1;
}
:deep(.el-timeline-item:first-child .el-timeline-item__tail) {
  top: 34px; /* 与节点位置对应 */
  height: 80px; /* 调整为与新节点位置匹配 */
  top: 34px;
  /* 与节点位置对应 */
  height: 80px;
  /* 调整为与新节点位置匹配 */
}
:deep(.el-timeline-item:last-child .el-timeline-item__tail) {
  height: 34px; /* 调整为与新节点位置匹配 */
  display: block !important; /* 确保显示 */
  height: 34px;
  /* 调整为与新节点位置匹配 */
  display: block !important;
  /* 确保显示 */
}
:deep(.el-timeline-item__content) {
@@ -113,18 +113,23 @@
  gap: 14px;
  height: 84px;
  width: 100%;
  flex-wrap: wrap; /* 允许在小屏幕上换行 */
  flex-wrap: wrap;
  /* 允许在小屏幕上换行 */
}
.left-block, .right-block {
.left-block,
.right-block {
  width: 330px;
  border-radius: 10px;
  padding: 0;
  background: #f5f7fa;
  display: flex;
  min-width: 270px; /* 减小最小宽度 */
  max-width: 330px; /* 设置最大宽度 */
  width: 100%; /* 使用百分比宽度 */
  min-width: 270px;
  /* 减小最小宽度 */
  max-width: 330px;
  /* 设置最大宽度 */
  width: 100%;
  /* 使用百分比宽度 */
  height: 84px;
  box-sizing: border-box;
}
@@ -170,12 +175,13 @@
  font-size: 16px;
  font-weight: bold;
  color: #fff;
  background: linear-gradient( 180deg, #0ACBCA 0%, #049C9A 100%);
  letter-spacing: 8px; /* 增加字间距 */
  background: linear-gradient(180deg, #0ACBCA 0%, #049C9A 100%);
  letter-spacing: 8px;
  /* 增加字间距 */
}
.left-block.out .type-tag {
  background: linear-gradient( 180deg, #FDBF2D 0%, #FA8B14 100%);
  background: linear-gradient(180deg, #FDBF2D 0%, #FA8B14 100%);
}
.info-main {
@@ -245,10 +251,12 @@
/* 添加媒体查询,适配小屏幕设备 */
@media screen and (max-width: 1200px) {
  .left-block, .right-block {
  .left-block,
  .right-block {
    min-width: 240px;
  }
  .timeline-row {
    gap: 10px;
  }
@@ -256,56 +264,58 @@
@media screen and (max-width: 992px) {
  .timeline-row {
    flex-direction: column; /* 垂直排列 */
    flex-direction: column;
    /* 垂直排列 */
    height: auto;
    gap: 8px;
  }
  .left-block, .right-block {
  .left-block,
  .right-block {
    width: 100%;
    max-width: 100%;
  }
  :deep(.el-timeline-item) {
    position: relative;
    height: auto;
    min-height: 176px;
    margin-bottom: 40px;
  }
  :deep(.el-timeline-item__wrapper) {
    height: auto !important;
    min-height: 176px;
  }
  :deep(.el-timeline-item__tail) {
    height: calc(100% + 40px);
  }
  :deep(.el-timeline-item:last-of-type) {
    margin-bottom: 0;
  }
  :deep(.el-timeline-item:last-of-type .el-timeline-item__tail) {
    height: 34px;
  }
  /* 第一个元素的轴线需要特殊处理 */
  :deep(.el-timeline-item:first-of-type .el-timeline-item__tail) {
    top: 34px;
    height: calc(100% + 40px - 34px);
  }
  /* 确保所有轴线正确连接 */
  :deep(.el-timeline-item__tail) {
    top: 0;
    height: calc(100% + 40px);
  }
  /* 修正内容区域的高度 */
  :deep(.el-timeline-item__content) {
    height: auto;
    min-height: 176px;
  }
}
</style>
</style>
culture/src/views/strain-library/strain-library-manage/components/StrainDetail.vue
@@ -1,207 +1,286 @@
<template>
  <div>
    <el-dialog
        title="原始细胞库详情"
        :visible.sync="visible"
        width="70%"
        :close-on-click-modal="false"
        custom-class="strain-detail-dialog"
        append-to-body
        @close="$emit('update:visible', false)"
      title="原始细胞库详情"
      :visible.sync="visible"
      width="70%"
      :close-on-click-modal="false"
      custom-class="strain-detail-dialog"
      @close="$emit('update:visible', false)"
      @opened="fetchDetail"
    >
        <div class="strain-info">
            <!-- 第一行信息 -->
            <div class="info-row">
                <div class="info-item left-column">
                    <span class="label">菌种编号:</span>
                    <span class="value">{{ detail.strainNo }}</span>
                </div>
                <div class="info-item flex-column">
                    <span class="label">鉴定方法:</span>
                    <span class="value">{{ detail.method }}</span>
                </div>
                <div class="info-item flex-column">
                    <span class="label">保存位置:</span>
                    <span class="value">{{ detail.amount }}</span>
                </div>
            </div>
            <!-- 第二行信息 -->
            <div class="info-row">
                <div class="info-item left-column">
                    <span class="label">菌种名称:</span>
                    <span class="value">{{ detail.strainName }}</span>
                </div>
                <div class="info-item flex-column full-width">
                    <span class="label">特性描述:</span>
                    <span class="value">{{ detail.certificate }}</span>
                </div>
            </div>
            <!-- 第三行信息 -->
            <div class="info-row">
                <div class="info-item left-column">
                    <span class="label">菌种来源:</span>
                    <span class="value">{{ detail.source }}</span>
                </div>
                <div class="info-item flex-column">
                    <span class="label">菌种保存方法:</span>
                    <span class="value">{{ detail.storage }}</span>
                </div>
                <div class="info-item flex-column">
                    <span class="label">出入库状态:</span>
                    <span class="value">{{ detail.operator }}</span>
                </div>
            </div>
      <div class="strain-info">
        <!-- 第一行信息 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种编号:</span>
            <span class="value">{{ detail.strainCode }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">鉴定方法:</span>
            <span class="value">{{ detail.appraisalMethod }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">保藏位置:</span>
            <span class="value">{{ detail.saveLocation }}</span>
          </div>
        </div>
        <div class="record-table">
            <div class="table-title">原始细胞库出/入库记录</div>
            <el-table :data="detail.records" style="width: 100%">
                <el-table-column prop="type" label="出库/入库" />
                <el-table-column prop="operateTime" label="操作时间" />
                <el-table-column prop="operator" label="操作人姓名" />
                <el-table-column prop="reviewer" label="签核确认人姓名" />
                <el-table-column prop="status" label="状态">
                    <template #default="{ row }">
                        <el-tag :type="row.status === '已确认' ? 'success' : 'warning'">
                            {{ row.status }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="100">
                    <template #default="{ row }">
                        <el-button type="text" @click="handleView(row)">详情</el-button>
                    </template>
                </el-table-column>
            </el-table>
            <div class="pagination">
                <el-pagination
                    :current-page.sync="currentPage"
                    :page-size="10"
                    layout="total, prev, pager, next"
                    :total="total"
                    @current-change="handlePageChange"
                />
            </div>
        <!-- 第二行信息 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种名称:</span>
            <span class="value">{{ detail.strainName }}</span>
          </div>
          <div class="info-item flex-column full-width">
            <span class="label">特性描述:</span>
            <span class="value">{{ detail.features }}</span>
          </div>
        </div>
        <!-- 第三行信息 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种来源:</span>
            <span class="value">{{ detail.strainSource }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">菌种保存方法:</span>
            <span class="value">{{ detail.saveMethod }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">出入库状态:</span>
            <span class="value">{{
              {
                1: "已出库",
                2: "出库待确认",
                3: "已入库",
                4: "入库待确认",
              }[detail.status] || ""
            }}</span>
          </div>
        </div>
      </div>
      <div class="record-table">
        <div class="table-title">原始细胞库出/入库记录</div>
        <el-table :data="detail.records" style="width: 100%">
          <el-table-column label="出库/入库">
            <template #default="{ row }">
              {{ { 1: "出库", 2: "入库" }[row.type] || "" }}
            </template>
          </el-table-column>
          <el-table-column prop="boundTime" label="操作时间" />
          <el-table-column prop="handleName" label="操作人姓名" />
          <el-table-column prop="preserveName" label="签核确认人姓名" />
          <el-table-column label="状态">
            <template #default="{ row }">
              <el-tag :type="row.confirmTime ? 'success' : 'warning'">
                {{ row.confirmTime ? "已确认" : "待确认" }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="100">
            <template #default="{ row }">
              <el-button v-if="!row.confirmTime && roleType == 3" style="margin-right: 10px" type="text" @click="handleConfirm(row)">确认</el-button>
              <el-button
                type="text"
                @click="handleView(row)"
                >详情</el-button
              >
            </template>
          </el-table-column>
        </el-table>
        <div class="pagination">
          <el-pagination
            :current-page.sync="currentPage"
            :page-size="10"
            layout="total, prev, pager, next"
            :total="total"
            @current-change="handlePageChange"
          />
        </div>
      </div>
    </el-dialog>
    <RecordDetailDialog
      :visible="visibleRecordDetailDialog"
      :recordData="recordData"
      @close="handleDialogClose"
      @confirm="handleOutbound"
      :type="dialogType"
    />
  </div>
</template>
<script>
import { getDetailById,confirmWarehousing } from "../service";
import RecordDetailDialog from "./RecordDetailDialog.vue";
export default {
    name: 'StrainDetail',
    props: {
        visible: {
            type: Boolean,
            default: false
        },
        detail: {
            type: Object,
            default: () => ({})
        }
  components: { RecordDetailDialog },
  name: "StrainDetail",
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    data() {
        return {
            currentPage: 1,
            total: 0
        }
    detail: {
      type: Object,
      default: () => ({}),
    },
    methods: {
        handleView(row) {
            console.log('View record:', row)
        },
        handlePageChange(page) {
            this.currentPage = page
            this.$emit('page-change', page)
  },
  data() {
    return {
      visibleRecordDetailDialog: false,
      recordData: {},
      currentPage: 1,
      total: 0,
      dialogType: "",
      query: {
        endTime: "",
        id: "",
        pageNum: 1,
        pageSize: 10,
        startTime: "",
        roleType: "",
      },
    };
  },
  methods: {
    handleDialogClose() {
      this.recordData = {};
      this.visibleRecordDetailDialog = false;
    },
    fetchDetail() {
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
      this.query.id = this.detail.id;
      getDetailById(this.query).then((res) => {
        this.detail.records = res.warehousingList?.records || [];
        this.total = res.warehousingList?.total || 0;
        this.currentPage = res.warehousingList?.current || 1;
        this.$forceUpdate();
      });
    },
    handleView(row) {
        this.dialogType = "detail";
      this.recordData = row;
      this.visibleRecordDetailDialog = true;
    },
    handleOutbound(data) {
      // 这里调用出库API
      confirmWarehousing({
        id: this.recordData.id,
        preserveSignature: data.preserveSignature,
      }).then((res) => {
        if (res.code == 200) {
          this.$message.success("操作成功");
          this.visibleRecordDetailDialog = false;
          // 刷新列表
          this.fetchDetail();
        } else {
          this.$message.error(res.msg);
        }
    }
}
      });
    },
    handleConfirm(row) {
      this.dialogType = "confirm";
      this.recordData = row;
      this.visibleRecordDetailDialog = true;
    },
    handlePageChange(page) {
      this.currentPage = page;
      this.$emit("page-change", page);
    },
  },
};
</script>
<style lang="less" scoped>
.strain-detail-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__header) {
    padding: 20px;
    border-bottom: 1px solid #ebeef5;
    margin-right: 0;
    :deep(.el-dialog__body) {
        padding: 20px;
    .el-dialog__title {
      font-size: 18px;
      font-weight: bold;
      color: #303133;
    }
  }
  :deep(.el-dialog__body) {
    padding: 20px;
  }
}
.strain-info {
    background: #F5F7FA;
    border-radius: 4px;
    padding: 20px;
    margin-bottom: 20px;
  background: #f5f7fa;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 20px;
    .info-row {
        display: flex;
        flex-wrap: wrap;
        margin-bottom: 16px;
        &:last-child {
            margin-bottom: 0;
        }
        .info-item {
            display: flex;
            align-items: flex-start;
            margin-right: 24px;
            margin-bottom: 8px;
            &.left-column {
                width: 33%;
                min-width: 200px;
            }
            &.flex-column {
                flex: 1;
                min-width: 150px;
            }
            &.full-width {
                flex: 1;
                min-width: 300px;
            }
            .label {
                color: #606266;
                margin-right: 8px;
                white-space: nowrap;
            }
            .value {
                flex: 1;
                color: #303133;
                word-break: break-all;
            }
        }
  .info-row {
    display: flex;
    flex-wrap: wrap;
    margin-bottom: 16px;
    &:last-child {
      margin-bottom: 0;
    }
    .info-item {
      display: flex;
      align-items: flex-start;
      margin-right: 24px;
      margin-bottom: 8px;
      &.left-column {
        width: 33%;
        min-width: 200px;
      }
      &.flex-column {
        flex: 1;
        min-width: 150px;
      }
      &.full-width {
        flex: 1;
        min-width: 300px;
      }
      .label {
        color: #606266;
        margin-right: 8px;
        white-space: nowrap;
      }
      .value {
        flex: 1;
        color: #303133;
        word-break: break-all;
      }
    }
  }
}
.record-table {
    .table-title {
        font-size: 16px;
        font-weight: bold;
        color: #303133;
        margin-bottom: 16px;
        padding-left: 8px;
        border-left: 4px solid #049C9A;
    }
  .table-title {
    font-size: 16px;
    font-weight: bold;
    color: #303133;
    margin-bottom: 16px;
    padding-left: 8px;
    border-left: 4px solid #049c9a;
  }
    .pagination {
        margin-top: 20px;
        display: flex;
        justify-content: center;
    }
  .pagination {
    margin-top: 20px;
    display: flex;
    justify-content: center;
  }
}
</style>
</style>
culture/src/views/strain-library/strain-library-manage/index.vue
@@ -1,470 +1,489 @@
<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>
  <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
          条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
          是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
          是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。
          2.
          菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
          原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
          生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
          细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
          24 年 9 月 19
          接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
          传代编码方式演例:祖代:DD-O-240919-01
          传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
          编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
          细胞库说明:3.2.1
          直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
          从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
          主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
          菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用 a-01、a-02
          等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
          接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
        </p>
      </div>
            <!-- 查看全部弹窗 -->
            <el-dialog
                title="菌种源保藏出/入细胞库登记表说明"
                :visible.sync="dialogVisible"
                width="50%"
                class="view-all-dialog"
      <!-- 查看全部弹窗 -->
      <el-dialog
        title="菌种源保藏出/入细胞库登记表说明"
        :visible.sync="dialogVisible"
        width="50%"
        class="view-all-dialog"
      >
        <div class="dialog-content">
          <p>
            1. 菌种全部集中登记在【菌种源保藏出/入细胞库登记表】,菌种来源有 3
            条路径。1.1 是沙土管或甘油管的源头菌种;入原始细胞库(祖代-O)。1.2
            是斜面的源头菌种;接种入主细胞库(祖代-O)。经过育种、验证后,菌种保藏为甘油管或沙土管的,入原始细胞库(祖代-0)1.3
            是含菌物质自己分离后获得的斜面源头菌种,接种入主细胞库;经生产验证后,保藏为沙土管或甘油管,入原始细胞库(祖代-O)。2.
            菌种细胞库,分类入三库,进行传代运行管理。三类库存空间进行区分,保藏菌种。2.1
            原始细胞库(祖代-O)、2.2 主细胞库(母代-M)、2.3
            生产细胞库(子代-S)、(孙代-G)3. 细胞库编码规则3.1
            细胞库编码规则:DD-M-240919-01-(O-0109-01)DD:代表项目组。M:“O”代表祖代原始细胞库,”M“代表母代主细胞库,”S“代表子代生产细胞库,“G”代表孙代生产细胞库。240919:代表在
            24 年 9 月 19
            接种批次的菌种;或收到外来菌种时间的入库批次。01:代表两位序列号。(O-0109-01):代表传代菌种的编号3.1.1
            传代编码方式演例:祖代:DD-O-240919-01
            传母代:DD-M-241017-01-(O-091901)DD-M-241017-02-(O-091901)DD-M-241017-03-(O-091901)子代:DD-S-241019-01-(M-1017-02)版权归奥利元生物所有,禁止外传。DD-S-241019-02-(M-1017-03)孙代:DD-G-241109-01-(S-1019-02)3.1.2
            编码规则实现了编码唯一,编码可溯源,编码直观、方便。3.2
            细胞库说明:3.2.1
            直接购买、自行从(土壤、相关物料、商品)等分离出来菌株进入原始细胞库。3.2.2
            从原始细胞库中选取出来再次纯化、改造、提高性能的菌株进入主细胞库。3.2.3
            主细胞库中选取出稳定,生产性能良好的菌株扩培后保种进入生产细胞库。4.
            菌种选育-保藏过程编号说明4.1 菌种选育时,培养皿的编号可使用
            a-01、a-02 等用于清晰形态观察记录;菌落编号使用序号 1/2/3等。4.2
            接种斜面菌种编码(-O)使用原始细胞库编码;斜面转菌种保藏使用与斜面一致的编码(-O);斜面传代入主细胞库的传代菌种,按编码器规则编码(-M)。
          </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 v-if="roleType == 4" 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-option label="入库待确认" value="4"></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 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>
              原始细胞列表
            </div>
            <div
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              @click="handleTypeChange('draft')"
            >
              草稿箱
            </div>
          </div>
          <div v-if="roleType == 4" class="flex a-center">
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              style="margin-right: 12px"
              >新增原始细胞</el-button
            >
            <el-button
              @click="handleNewStrain"
              class="el-icon-plus"
              type="primary"
              >批量新增</el-button
            >
          </div>
        </div>
      </template>
        <!-- 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>
        <StrainDetail
            :visible.sync="detailVisible"
            :detail="currentDetail"
        />
    </div>
      <template #table>
        <el-table-column prop="strainCode" label="菌种编号" />
        <el-table-column prop="strainName" label="菌种名称" />
        <el-table-column prop="strainSource" label="菌种来源" />
        <el-table-column prop="appraisalMethod" label="鉴定方法" />
        <el-table-column prop="features" label="特征描述" />
        <el-table-column prop="saveMethod" label="菌种保存方法" />
        <el-table-column prop="saveLocation" label="保藏位置" />
        <el-table-column prop="stock" label="库存余量" />
        <el-table-column prop="remark" label="备注" />
        <el-table-column
          v-if="currentType === 'list'"
          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 v-if="row.status == 2 || row.status == 4" type="text" @click="handleEdit(row)">编辑</el-button>
            <el-button
              v-if="currentType === 'list'"
              type="text"
              @click="handleRecord(row)"
              >出入库记录</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <StrainDetail :visible.sync="detailVisible" :detail="currentDetail" />
  </div>
</template>
<script>
import StrainDetail from './components/StrainDetail.vue'
import StrainDetail from "./components/StrainDetail.vue";
import { getList, deleteStrainLibrary } from "./service";
export default {
    name: 'StrainLibraryManage',
    components: {
        StrainDetail
  name: "StrainLibraryManage",
  components: {
    StrainDetail,
  },
  data() {
    return {
      dialogVisible: false,
      currentType: "list",
      detailVisible: false,
      currentDetail: {},
      form: {
        strainNo: "",
        strainName: "",
        status: "",
      },
      queryForm: {
        pageSize: 10,
        pageNum: 1,
      },
      total: 800,
      tableData: [],
      roleType: "",
    };
  },
  activated() {
    this.searchData();
    // 角色类型 1=超级管理员 2=审批人 3=工程师 4=实验员
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
  },
  methods: {
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteStrainLibrary({ id: row.id }).then((res) => {
            this.$message.success("删除成功");
            this.searchData();
        });
      });
    },
    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'
                }
            ]
        }
    handleRecord(row) {
      this.$router.push({
        path: `/strain-library/strain-library-manage/record?id=${row.id}`,
      });
    },
    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] || '未知状态'
        }
    }
}
    handleNewStrain() {
      this.$router.push({ path: "/strain-library/strain-library-manage/add" });
    },
    handleEdit(row) {
      this.$router.push({
        path: `/strain-library/strain-library-manage/add?id=${row.id}`,
      });
    },
    handleDetail(row) {
      this.currentDetail = row;
      this.detailVisible = true;
    },
    handleViewMore() {
      this.dialogVisible = true;
    },
    resetForm() {
      this.form = {
        strainNo: "",
        strainName: "",
        status: "",
      };
      this.searchData();
    },
    searchData() {
      const params = {
        pageNum: this.queryForm.pageNum,
        pageSize: this.queryForm.pageSize,
        strainCode: this.form.strainNo,
        strainName: this.form.strainName,
        isDraft: this.currentType === "draft" ? 1 : 0,
        status: this.form.status,
        type: 1,
      };
      getList(params)
        .then((res) => {
          if (res.code === 200) {
            this.tableData = res.data.records;
            this.total = res.data.total;
          }
        })
        .catch((err) => {
          this.$message.error("数据加载失败");
        });
    },
    handleCurrentChange(page) {
      this.queryForm.pageNum = page;
      this.searchData();
    },
    handleSizeChange(size) {
      this.queryForm.pageSize = size;
      this.searchData();
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.searchData();
    },
    getStatusType(status) {
      const types = {
        1: "warning",
        2: "warning",
        3: "success",
        4: "success",
      };
      return types[status] || "info";
    },
    getStatusText(status) {
      const texts = {
        1: "已出库",
        2: "出库待确认",
        3: "已入库",
        4: "入库待确认",
      };
      return texts[status] || "未知状态";
    },
  },
};
</script>
<style scoped lang="less">
.list {
    padding: 20px;
  padding: 20px;
}
.header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
  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;
  .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-icon {
      width: 20px;
      height: 20px;
      margin-right: 10px;
    }
    .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;
        }
    .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;
  margin-bottom: 20px;
  background: #f5f7fa;
  padding: 24px;
  border-radius: 8px;
    .el-form-item {
        margin-right: 20px;
        margin-bottom: 0;
    }
  .el-form-item {
    margin-right: 20px;
    margin-bottom: 0;
  }
    .el-button {
        margin-left: 10px;
    }
  .el-button {
    margin-left: 10px;
  }
}
.action-buttons {
    margin-bottom: 20px;
  margin-bottom: 20px;
    .el-button {
        margin-right: 10px;
    }
  .el-button {
    margin-right: 10px;
  }
}
.tab-container {
    display: flex;
    margin-bottom: 20px;
  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;
  .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;
        }
    &.active {
      background: #fff;
      border-color: #049c9a;
      color: #049c9a;
      font-weight: bold;
    }
  }
}
.flex {
    display: flex;
    align-items: center;
  display: flex;
  align-items: center;
}
.tableTitle {
    display: flex;
    padding-bottom: 20px;
    justify-content: space-between;
    align-items: center;
  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;
  .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;
  }
    .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;
    }
  .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__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;
  :deep(.el-dialog__body) {
    padding: 20px;
        .dialog-content {
            font-size: 14px;
            line-height: 1.8;
            color: #606266;
    .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;
                }
            }
      p {
        margin: 12px 0;
        &:first-child {
          margin-top: 0;
        }
        &:last-child {
          margin-bottom: 0;
        }
      }
    }
  }
}
</style>
culture/src/views/strain-library/strain-library-manage/record.vue
@@ -1,417 +1,438 @@
<template>
    <div class="record-page">
        <!-- 基本信息展示区域 -->
        <el-card class="header-box">
            <div class="header-content">
                <!-- 第一行 -->
                <div class="info-row">
                    <div class="info-item left-column">
                        <span class="label">菌种编号:</span>
                        <span class="value">{{ detail.strainNo }}</span>
                    </div>
                    <div class="info-item flex-column">
                        <span class="label">鉴定方法:</span>
                        <span class="value">{{ detail.method }}</span>
                    </div>
                    <div class="info-item flex-column">
                        <span class="label">保藏位置:</span>
                        <span class="value">{{ detail.amount }}</span>
                    </div>
                </div>
                <!-- 第二行 -->
                <div class="info-row">
                    <div class="info-item left-column">
                        <span class="label">菌种名称:</span>
                        <span class="value">{{ detail.strainName }}</span>
                    </div>
                    <div class="info-item flex-column full-width">
                        <span class="label">特性描述:</span>
                        <span class="value">{{ detail.certificate }}</span>
                    </div>
                </div>
                <!-- 第三行 -->
                <div class="info-row">
                    <div class="info-item left-column">
                        <span class="label">菌种来源:</span>
                        <span class="value">{{ detail.source }}</span>
                    </div>
                    <div class="info-item flex-column">
                        <span class="label">菌种保存方法:</span>
                        <span class="value">{{ detail.storage }}</span>
                    </div>
                </div>
  <div class="record-page">
    <!-- 基本信息展示区域 -->
    <el-card class="header-box">
      <div class="header-content">
        <!-- 第一行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种编号:</span>
            <span class="value">{{ detail.strainCode }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">鉴定方法:</span>
            <span class="value">{{ detail.appraisalMethod }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">保藏位置:</span>
            <span class="value">{{ detail.saveLocation }}</span>
          </div>
        </div>
        <!-- 第二行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种名称:</span>
            <span class="value">{{ detail.strainName }}</span>
          </div>
          <div class="info-item flex-column full-width">
            <span class="label">特性描述:</span>
            <span class="value">{{ detail.features }}</span>
          </div>
        </div>
        <!-- 第三行 -->
        <div class="info-row">
          <div class="info-item left-column">
            <span class="label">菌种来源:</span>
            <span class="value">{{ detail.strainSource }}</span>
          </div>
          <div class="info-item flex-column">
            <span class="label">菌种保存方法:</span>
            <span class="value">{{ detail.saveMethod }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- 出入库记录表格 -->
    <TableCustom
      :queryForm="queryForm"
      :tableData="recordList"
      :total="total"
      @currentChange="handlePageChange"
    >
      <template #setting>
        <div class="tableTitle">
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'table' }"
              @click="handleTypeChange('table')"
            >
              原始细胞保藏出/入库登记表
            </div>
        </el-card>
            <div
              class="drafts"
              :class="{ active: currentType === 'timeline' }"
              @click="handleTypeChange('timeline')"
            >
              原始细胞保藏出/入库时间轴
            </div>
          </div>
          <div class="flex a-center">
            <el-button
              v-if="roleType == 4"
              @click="handleAddRecord"
              class="el-icon-plus"
              type="primary"
              >新增出入库记录</el-button
            >
          </div>
        </div>
      </template>
        <!-- 出入库记录表格 -->
        <TableCustom :queryForm="queryForm" :tableData="recordList" :total="total" @currentChange="handlePageChange">
            <template #setting>
                <div class="tableTitle">
                    <div class="flex a-center">
                        <div class="title" :class="{ active: currentType === 'table' }"
                            @click="handleTypeChange('table')">
                            原始细胞保藏出/入库登记表</div>
                        <div class="drafts" :class="{ active: currentType === 'timeline' }"
                            @click="handleTypeChange('timeline')">
                            原始细胞保藏出/入库时间轴</div>
                    </div>
                    <div class="flex a-center">
                        <el-button @click="handleAddRecord" class="el-icon-plus" type="primary">新增出入库记录</el-button>
                    </div>
                </div>
            </template>
      <template #table v-if="currentType === 'table'">
        <el-table-column prop="type" label="出库/入库">
          <template #default="{ row }">
            <span>
              {{ row.type === 1 ? "出库" : "入库" }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="boundTime" label="操作时间" />
        <el-table-column prop="handleSignature" label="操作人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.handleSignature"
              style="width: 100px; height: 100px"
              :src="row.handleSignature"
              :preview-src-list="[row.handleSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="preserveSignature" label="菌种保藏人签字">
          <template #default="{ row }">
            <el-image
              v-if="row.preserveSignature"
              style="width: 100px; height: 100px"
              :src="row.preserveSignature"
              :preview-src-list="[row.preserveSignature]"
            >
            </el-image>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="row.preserveSignature ? 'success' : 'warning'">
              {{ row.preserveSignature ? "已确认" : "待确认" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180">
          <template #default="{ row }">
            <el-button
              v-if="!row.preserveSignature && roleType == 3"
              type="text"
              class="operation-btn"
              @click="handleConfirm(row)"
              >确认</el-button
            >
            <el-button
              type="text"
              class="operation-btn"
              @click="handleView(row)"
              >详情</el-button
            >
            <el-button v-if="roleType == 1" type="text" @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </template>
      <template #tableCustom v-if="currentType === 'timeline'">
        <record-timeline :list="timelineList" />
      </template>
    </TableCustom>
            <template #table v-if="currentType === 'table'">
                <el-table-column prop="type" label="出库/入库" />
                <el-table-column prop="operateTime" label="操作时间" />
                <el-table-column prop="operator" label="操作人签字" />
                <el-table-column prop="reviewer" label="菌种保藏人签字" />
                    <el-table-column prop="status" label="状态">
                        <template #default="{ row }">
                            <el-tag :type="row.status === '已确认' ? 'success' : 'warning'">
                                {{ row.status }}
                            </el-tag>
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" width="180">
                        <template #default="{ row }">
                            <el-button type="text" class="operation-btn" @click="handleView(row)">详情</el-button>
                            <el-button type="text" class="operation-btn" @click="handleEdit(row)">编辑</el-button>
                            <el-button type="text" @click="handleDelete(row)">删除</el-button>
                    </template>
                </el-table-column>
            </template>
            <template #tableCustom v-if="currentType === 'timeline'">
                <record-timeline :list="timelineList" />
            </template>
        </TableCustom>
        <!-- 详情弹窗 -->
        <record-detail-dialog
            :visible.sync="dialogVisible"
            :record-data="currentRecord"
            @close="handleDialogClose"
            @confirm="handleOutbound"
        />
        <add-record-dialog
            :visible.sync="addDialogVisible"
            @confirm="handleAddRecordConfirm"
        />
    </div>
    <!-- 详情弹窗 -->
    <record-detail-dialog
      :visible.sync="dialogVisible"
      :record-data="currentRecord"
      @close="handleDialogClose"
      @confirm="handleOutbound"
      :type="dialogType"
    />
    <!-- 新增出入库记录弹窗 -->
    <add-record-dialog
      :visible.sync="addDialogVisible"
      @confirm="handleAddRecordConfirm"
    />
  </div>
</template>
<script>
import RecordDetailDialog from './components/RecordDetailDialog.vue'
import AddRecordDialog from './components/AddRecordDialog.vue'
import RecordTimeline from './components/RecordTimeline.vue'
import RecordDetailDialog from "./components/RecordDetailDialog.vue";
import AddRecordDialog from "./components/AddRecordDialog.vue";
import RecordTimeline from "./components/RecordTimeline.vue";
import {
  timeList,
  getDetail,
  addWarehousing,
  getDetailById,
  confirmWarehousing,
} from "./service";
export default {
    name: 'StrainRecord',
    components: {
        RecordDetailDialog,
        AddRecordDialog,
        RecordTimeline
    },
    data() {
        return {
            currentType: 'table',
            detail: {},
            currentPage: 1,
            pageSize: 10,
            total: 0,
            queryForm: {
                pageSize: 10,
                pageNum: 1
            },
            recordList: [
                {
                    type: '入库',
                    operateTime: '2025-1-21 15:46:50',
                    operator: '张三',
                    reviewer: '李四',
                    status: '已确认'
                },
                {
                    type: '出库',
                    operateTime: '2025-1-21 15:46:50',
                    operator: '张三',
                    reviewer: '李四',
                    status: '已确认'
                },
                {
                    type: '入库',
                    operateTime: '2025-1-21 15:46:50',
                    operator: '张三',
                    reviewer: '李四',
                    status: '已确认'
                },
                {
                    type: '出库',
                    operateTime: '2025-1-21 15:46:50',
                    operator: '张三',
                    reviewer: '李四',
                    status: '已确认'
                },
                {
                    type: '入库',
                    operateTime: '2025-1-21 15:46:50',
                    operator: '李四',
                    reviewer: '李四',
                    status: '已确认'
                }
            ],
            dialogVisible: false,
            currentRecord: {},
            addDialogVisible: false
        }
    },
    computed: {
        timelineList() {
            // 可根据需要处理数据格式,这里直接用 recordList
            return this.recordList.map(item => ({
                ...item,
                confirmTime: item.confirmTime || item.operateTime // 若无确认时间则用操作时间
            }))
        }
    },
    created() {
        // 获取路由参数中的菌种信息
        const strainId = this.$route.query.id
        if (strainId) {
            this.getStrainDetail(strainId)
            this.getRecordList()
        }
    },
    methods: {
        getStrainDetail(id) {
            // 这里应该调用接口获取菌种详情
            // 暂时使用模拟数据
            this.detail = {
                strainNo: '3418732431',
                strainName: '名称名称名称',
                source: '来源11111111111',
                method: '1231231',
                certificate: '特性描述',
                storage: '方法方法',
                amount: '位置位置位置位置位置位置位置位置',
                operator: '入库'
            }
        },
        getRecordList() {
            // 这里应该调用接口获取出入库记录
            // 暂时使用已有模拟数据
            this.total = this.recordList.length
        },
        handleView(row) {
            this.currentRecord = {
                ...row,
                operatorSignature: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', // 模拟操作人签字图片
                reviewerSignature: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', // 模拟保藏人签字图片
                operateTime: '2025-1-22 13:49:51',
                confirmTime: '2025-1-22 14:30:00'
            }
            this.dialogVisible = true
        },
        handlePageChange(page) {
            this.queryForm.pageNum = page
            // 这里应该调用接口获取对应页码的数据
        },
        handleTypeChange(type) {
            this.currentType = type
        },
        handleAddRecord() {
            this.addDialogVisible = true
        },
        handleEdit(row) {
            console.log('编辑记录:', row)
            // 实现编辑记录逻辑
        },
        handleDelete(row) {
            this.$confirm('确认删除该记录吗?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }).then(() => {
                console.log('删除记录:', row)
                // 实际项目中这里应该调用删除API
                this.$message({
                    type: 'success',
                    message: '删除成功!'
                })
            }).catch(() => {
                this.$message({
                    type: 'info',
                    message: '已取消删除'
                })
            })
        },
        handleDialogClose() {
            this.currentRecord = {}
            this.dialogVisible = false
        },
        handleOutbound(data) {
            // 这里调用出库API
            console.log('出库操作:', data)
            this.$message.success('出库成功')
            this.dialogVisible = false
            // 刷新列表
            this.getRecordList()
        },
        handleAddRecordConfirm(record) {
            // 这里可以将新记录添加到 recordList 或调用后端API
            this.$message.success('新增出入库记录成功')
            // 例如:this.recordList.push(record)
            this.getRecordList() // 或刷新列表
        },
        goBack() {
            this.$router.go(-1)
        }
  name: "StrainRecord",
  components: {
    RecordDetailDialog,
    AddRecordDialog,
    RecordTimeline,
  },
  data() {
    return {
      currentType: "table",
      detail: {},
      currentPage: 1,
      pageSize: 10,
      total: 0,
      queryForm: {
        pageSize: 10,
        pageNum: 1,
      },
      recordList: [],
      timelineList: [],
      dialogVisible: false,
      currentRecord: {},
      addDialogVisible: false,
      dialogType: "detail",
      roleType: "",
    };
  },
  created() {
    this.roleType = JSON.parse(sessionStorage.getItem("userInfo")).roleType;
    // 获取路由参数中的菌种信息
    const strainId = this.$route.query.id;
    this.queryForm.id = strainId;
    if (strainId) {
      this.getStrainDetail(strainId);
      this.getRecordList();
    }
}
  },
  methods: {
    handleDelete(row) {
      this.$confirm("确定删除该数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        deleteWarehousing({ id: row.id }).then((res) => {
          this.$message.success("删除成功");
          this.getRecordList();
        });
      });
    },
    getStrainDetail(id) {
      // 这里应该调用接口获取菌种详情
      getDetail({ id }).then((res) => {
        this.detail = res;
      });
    },
    getRecordList() {
      // 这里应该调用接口获取出入库记录
      timeList(this.queryForm).then((res) => {
        this.timelineList = res.data;
      });
      getDetailById({ id: this.$route.query.id }).then((res) => {
        this.recordList = res.warehousingList.records;
        this.total = res.warehousingList.total;
      });
    },
    handleView(row) {
      this.dialogType = "detail";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    handleConfirm(row) {
      this.dialogType = "confirm";
      this.currentRecord = row;
      this.dialogVisible = true;
    },
    handlePageChange(page) {
      this.queryForm.pageNum = page;
      // 这里应该调用接口获取对应页码的数据
    },
    handleTypeChange(type) {
      this.currentType = type;
    },
    handleAddRecord() {
      this.addDialogVisible = true;
    },
    handleDialogClose() {
      this.currentRecord = {};
      this.dialogVisible = false;
    },
    handleOutbound(data) {
      // 这里调用出库API
      confirmWarehousing({
        id: this.currentRecord.id,
        preserveSignature: data.preserveSignature,
      }).then((res) => {
        console.log(res);
        if (res.code == 200) {
          this.$message.success("操作成功");
          this.dialogVisible = false;
          // 刷新列表
          this.getRecordList();
        } else {
          this.$message.error(res.msg);
        }
      });
    },
    handleAddRecordConfirm(record) {
      addWarehousing({ ...record, trainLibraryId: this.$route.query.id }).then(
        (res) => {
          this.$message.success("操作成功");
          this.getRecordList();
        }
      );
    },
    goBack() {
      this.$router.go(-1);
    },
  },
};
</script>
<style lang="less" scoped>
.record-page {
    min-height: 100vh;
  min-height: 100vh;
    .header-box {
        margin-bottom: 20px;
        border-radius: 16px;
        background: rgba(255, 255, 255, 0.8);
        height: 130px;
        overflow: hidden;
  .header-box {
    margin-bottom: 20px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.8);
    height: 130px;
    overflow: hidden;
        .header-content {
            color: rgba(0, 0, 0, 0.88);
            font-size: 14px;
            line-height: 1.5;
    .header-content {
      color: rgba(0, 0, 0, 0.88);
      font-size: 14px;
      line-height: 1.5;
            .info-row {
                display: flex;
                flex-wrap: wrap;
                margin-bottom: 8px;
                &:last-child {
                    margin-bottom: 0;
                }
                .info-item {
                    display: flex;
                    align-items: flex-start;
                    margin-right: 24px;
                    margin-bottom: 6px;
                    &.left-column {
                        width: 33%;
                        min-width: 200px;
                    }
                    &.flex-column {
                        flex: 1;
                        min-width: 150px;
                    }
                    &.full-width {
                        flex: 1;
                        min-width: 300px;
                    }
                    .label {
                        color: #606266;
                        margin-right: 8px;
                        white-space: nowrap;
                    }
                    .value {
                        flex: 1;
                        color: #303133;
                        word-break: break-all;
                        display: -webkit-box;
                        -webkit-line-clamp: 1;
                        -webkit-box-orient: vertical;
                        overflow: hidden;
                        text-overflow: ellipsis;
                    }
                }
            }
        }
    }
    .flex {
      .info-row {
        display: flex;
        align-items: center;
    }
        flex-wrap: wrap;
        margin-bottom: 8px;
    .tableTitle {
        display: flex;
        padding-bottom: 20px;
        justify-content: space-between;
        align-items: center;
        &:last-child {
          margin-bottom: 0;
        }
        .title {
            background: #fafafc;
            border-radius: 8px 8px 0px 0px;
            border: 1px solid #dcdfe6;
            font-weight: bold;
            font-size: 18px;
        .info-item {
          display: flex;
          align-items: flex-start;
          margin-right: 24px;
          margin-bottom: 6px;
          &.left-column {
            width: 33%;
            min-width: 200px;
          }
          &.flex-column {
            flex: 1;
            min-width: 150px;
          }
          &.full-width {
            flex: 1;
            min-width: 300px;
          }
          .label {
            color: #606266;
            cursor: pointer;
            height: 50px;
            line-height: 50px;
            width: 280px;
            text-align: center;
        }
            margin-right: 8px;
            white-space: nowrap;
          }
        .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: 280px;
            text-align: center;
          .value {
            flex: 1;
            color: #303133;
            word-break: break-all;
            display: -webkit-box;
            -webkit-line-clamp: 1;
            -webkit-box-orient: vertical;
            overflow: hidden;
            text-overflow: ellipsis;
          }
        }
      }
    }
  }
        .active {
            color: #049c9a;
            background: #ffffff;
            border-radius: 8px 8px 0px 0px;
            border: 1px solid #049c9a;
        }
  .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;
      cursor: pointer;
      height: 50px;
      line-height: 50px;
      width: 280px;
      text-align: center;
    }
    .timeline-container {
        padding: 20px;
        background: rgba(255, 255, 255, 0.8);
        .timeline-card {
            margin-bottom: 10px;
            background: rgba(255, 255, 255, 0.8);
            h4 {
                margin: 0 0 10px;
                font-size: 16px;
                font-weight: bold;
            }
            p {
                margin: 5px 0;
                font-size: 14px;
            }
        }
    .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: 280px;
      text-align: center;
    }
    .operation-btn {
        margin-right: 12px;
    .active {
      color: #049c9a;
      background: #ffffff;
      border-radius: 8px 8px 0px 0px;
      border: 1px solid #049c9a;
    }
  }
  .timeline-container {
    padding: 20px;
    background: rgba(255, 255, 255, 0.8);
    .timeline-card {
      margin-bottom: 10px;
      background: rgba(255, 255, 255, 0.8);
      h4 {
        margin: 0 0 10px;
        font-size: 16px;
        font-weight: bold;
      }
      p {
        margin: 5px 0;
        font-size: 14px;
      }
    }
  }
  .operation-btn {
    margin-right: 12px;
  }
}
</style>
</style>
culture/src/views/strain-library/strain-library-manage/service.js
New file
@@ -0,0 +1,56 @@
import axios from '@/utils/request';
// 列表
export const getList = (data) => {
  return axios.post('/api/t-train-library/pageList', { ...data })
}
// 新增
export const add = (data) => {
  return axios.post('/api/t-train-library/add', { ...data })
}
// 编辑
export const edit = (data) => {
  return axios.post('/api/t-train-library/update', { ...data })
}
// 查看详情
export const getDetail = (params) => {
  return axios.get('/open/t-train-library/getDetailEditById', { params })
}
// 批量新增
export const addBatch = (data) => {
  return axios.post('/api/t-train-library/addBatch', data)
}
// 查看菌种库详情
export const getDetailById = (data) => {
  return axios.post('/open/t-train-library/getDetailById', { ...data })
}
// 获取菌种库出入库时间轴列表
export const timeList = (data) => {
  return axios.post('/api/t-train-library/timeList?id='+data.id, { ...data })
}
// 新增菌种库出入记录
export const addWarehousing = (data) => {
  return axios.post('/open/t-train-library/addWarehousing', { ...data })
}
// 确认出入库
export const confirmWarehousing = (data) => {
  return axios.post('/api/t-train-library/confirm', { ...data })
}
// 删除菌种库
export const deleteStrainLibrary = (params) => {
  return axios.delete('/open/t-train-library/deleteById', { params })
}
// 删除菌种库出入库记录
export const deleteWarehousing = (params) => {
  return axios.delete('/open/t-train-library/deleteWarehousingById', { params })
}
culture/src/views/strain-library/validation/chief-cell/index.vue
New file
@@ -0,0 +1,435 @@
<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 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="strainNo" label="菌种来源" />
                <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="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>
                        <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/DetailConditionDialog.vue
New file
@@ -0,0 +1,282 @@
<template>
  <el-dialog :visible.sync="visible" width="80%" @close="handleClose">
    <el-card class="top-card">
      <el-row :gutter="24" class="top-info-row">
        <el-col :span="8" class="info-col">
          <div class="info-item">
            <span class="label">菌种来源:</span>{{ detail.source }}
          </div>
          <div class="info-item">
            <span class="label">鉴别菌株编号:</span>{{ detail.strainNo }}
          </div>
          <div class="info-item">
            <span class="label">鉴别菌株名称:</span>{{ detail.strainName }}
          </div>
        </el-col>
        <el-col :span="8" class="info-col">
          <div class="info-item">
            <span class="label">验证实验编号:</span>{{ detail.verifyNo }}
          </div>
          <div class="info-item">
            <span class="label">实验时间:</span>{{ detail.experimentTime }}
          </div>
          <div class="info-item"></div>
        </el-col>
        <el-col :span="8" class="info-col">
          <div class="info-item sign-label">
            <span class="label">菌种实验员签字</span>
          </div>
          <div class="info-item signature-item">
            <div class="signature-area">
              <img v-if="detail.signature" :src="detail.signature" alt="签字" />
              <span v-else class="waiting-text">暂无签名</span>
            </div>
          </div>
        </el-col>
      </el-row>
    </el-card>
    <div class="section-card" style="margin-top: 24px">
      <el-form label-width="100px" label-position="top">
        <el-form-item label="实验结论">
          <el-input
            type="textarea"
            v-model="detail.conclusion"
            :rows="3"
            placeholder="请输入"
          />
        </el-form-item>
        <el-form-item label="批准菌株用途">
          <el-checkbox-group v-model="detail.usage">
            <el-checkbox label="传代" />
            <el-checkbox label="菌种保藏" />
            <el-checkbox label="废弃" />
          </el-checkbox-group>
        </el-form-item>
      </el-form>
    </div>
    <EditConditionDialog
      :visible.sync="dialogVisible"
      :isEdit="dialogIsEdit"
      :isFixed="dialogIsFixed"
      :value="dialogValue"
      @ok="handleDialogOk"
    />
    <div class="section-card" style="margin-top: 12px">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        :row-class-name="getRowClassName"
      >
        <el-table-column prop="condition" label="菌种培养工艺条件" />
        <el-table-column prop="record" label="菌种培养工艺实况记录" />
        <el-table-column prop="process" label="菌种培养标准工艺" />
        <el-table-column label="操作" width="120">
          <template #default="{ row }">
            <el-button type="text" @click="handleEdit(row)">详情</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </el-dialog>
</template>
<script>
import EditConditionDialog from "./EditConditionDialog.vue";
import DetailConditionDialog from "./DetailConditionDialog.vue";
export default {
  name: "DetailConditionDialog",
  components: { EditConditionDialog, DetailConditionDialog },
  props: {
    visible: Boolean,
    value: {
      type: Object,
      default: () => ({ condition: "", record: "", process: "" }),
    },
  },
  data() {
    return {
      detail: {
        source: "内容的内容内容内容",
        strainNo: "3411234",
        strainName: "名称名称名称",
        verifyNo: "34133214",
        experimentTime: "2025-1-23 11:10:28",
        signature: "", // 签名图片url
        conclusion: "",
        usage: [],
      },
      activeTab: "condition",
      initialTableData: [
        {
          condition: "平板培养基",
          record: "文字内容文字内容文字内容文字内容文字内容文字内容",
          process: "文字内容文字内容文字内容文字内容文字内容文字内容",
        },
        { condition: "培养温度", record: "", process: "" },
        { condition: "培养时间", record: "", process: "" },
        { condition: "摇瓶培养基", record: "", process: "" },
        { condition: "接种量", record: "", process: "" },
        { condition: "培养时间", record: "", process: "" },
        { condition: "发酵时间", record: "", process: "" },
        { condition: "检测数据及结果", record: "", process: "" },
      ],
      tableData: [
        {
          condition: "平板培养基",
          record: "文字内容文字内容文字内容文字内容文字内容文字内容",
          process: "文字内容文字内容文字内容文字内容文字内容文字内容",
        },
        { condition: "培养温度", record: "", process: "" },
        { condition: "培养时间", record: "", process: "" },
        { condition: "摇瓶培养基", record: "", process: "" },
        { condition: "接种量", record: "", process: "" },
        { condition: "培养时间", record: "", process: "" },
        { condition: "发酵时间", record: "", process: "" },
        { condition: "检测数据及结果", record: "", process: "" },
      ],
      dialogVisible: false,
      dialogIsEdit: false,
      dialogIsFixed: false,
      dialogValue: {},
      dialogIndex: null,
    };
  },
  methods: {
    handleClose() {
      this.$emit("update:visible", false);
    },
    handleEdit(row) {
      const idx = this.tableData.indexOf(row);
      this.dialogVisible = true;
      this.dialogIsEdit = true;
      this.dialogIsFixed = idx < this.initialTableData.length;
      this.dialogValue = { ...row };
      this.dialogIndex = idx;
    },
    handleSave() {
      this.dialogVisible = true;
      this.dialogIsEdit = false;
      this.dialogIsFixed = false;
      this.dialogValue = { condition: "", record: "", process: "" };
      this.dialogIndex = null;
    },
    handleDraft() {
      // 存稿逻辑
    },
    handleDialogOk(val) {
      if (this.dialogIsEdit && this.dialogIndex !== null) {
        this.$set(this.tableData, this.dialogIndex, val);
      } else {
        this.tableData.push(val);
      }
    },
    getRowClassName({ rowIndex }) {
      return rowIndex < this.initialTableData.length ? "fixed-row" : "";
    },
  },
};
</script>
<style scoped lang="less">
.el-dialog__body {
  padding-bottom: 0;
}
.top-card {
  margin-bottom: 0;
  background: rgba(239, 239, 239, 1);
  border-radius: 16px;
}
.top-info-row {
  align-items: stretch;
}
.info-col {
  display: flex;
  flex-direction: column;
}
.info-item {
  display: flex;
  font-size: 15px;
  height: 45px;
  line-height: 45px;
}
.label {
  color: #666;
  font-weight: 500;
}
.sign-col {
  align-items: center;
  text-align: center;
}
.sign-label {
  justify-content: center;
}
.signature-item {
  justify-content: center;
}
.signature-area {
  min-height: 80px;
  min-width: 120px;
  background: #f5f7fa;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 8px;
}
.signature-area img {
  max-width: 100%;
  max-height: 100%;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-time {
  justify-content: center;
  text-align: center;
  color: #666;
  font-size: 14px;
}
.section-card {
  margin-bottom: 0;
}
.footer-btns {
  display: flex;
  justify-content: center;
  padding: 24px;
  padding-bottom: 0;
  gap: 24px;
  .el-button {
    width: 150px;
  }
}
::v-deep(.fixed-row) {
  background-color: rgb(228, 248, 250) !important;
}
@media (max-width: 900px) {
  .info-col {
    height: auto;
  }
}
</style>
culture/src/views/strain-library/validation/primitive-cell/EditConditionDialog.vue
New file
@@ -0,0 +1,66 @@
<template>
  <el-dialog :visible.sync="visible"  width="800px" @close="handleClose">
    <el-form label-width="120px" label-position="top">
      <el-form-item label="菌种培养工艺条件">
        <el-input v-model="form.condition" :disabled="isFixed" placeholder="请输入" />
      </el-form-item>
      <el-form-item label="菌种培养工艺实况记录">
        <el-input type="textarea" v-model="form.record" :rows="7" placeholder="请输入文本内容" />
      </el-form-item>
      <el-form-item label="菌种培养标准工艺">
        <el-input type="textarea" v-model="form.process" :rows="7" placeholder="请输入文本内容" />
      </el-form-item>
    </el-form>
    <div style="text-align: center; margin-top: 24px;">
      <el-button type="primary" @click="handleOk">保存</el-button>
    </div>
  </el-dialog>
</template>
<script>
export default {
  name: 'EditConditionDialog',
  props: {
    visible: Boolean,
    isEdit: Boolean,
    isFixed: Boolean, // true: 固定的8个条件,false: 新增条件
    value: {
      type: Object,
      default: () => ({ condition: '', record: '', process: '' })
    }
  },
  data() {
    return {
      form: { condition: '', record: '', process: '' }
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(val) {
        this.form = { ...val }
      }
    },
    visible(val) {
      if (!val) {
        this.form = { condition: '', record: '', process: '' }
      }
    }
  },
  methods: {
    handleOk() {
      this.$emit('ok', { ...this.form })
      this.handleClose()
    },
    handleClose() {
      this.$emit('update:visible', false)
    }
  }
}
</script>
<style scoped>
.el-dialog__body {
  padding-bottom: 0;
}
</style>
culture/src/views/strain-library/validation/primitive-cell/add.vue
New file
@@ -0,0 +1,372 @@
<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>
        <!-- 签字确认组件 -->
        <ConfirmStorageDialog
            :visible.sync="signatureVisible"
            @confirm="handleSignatureConfirm"
            name="操作人签字"
            text="是否确认提交该项原始细胞库资料信息?"
        />
    </Card>
</template>
<script>
import ConfirmStorageDialog from '@/components/confirm-storage-dialog/index.vue'
export default {
    name: 'AddprimitiveCell',
    components: {
        ConfirmStorageDialog
    },
    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/confirm-detail.vue
New file
@@ -0,0 +1,254 @@
<template>
  <Card class="confirm-detail-page">
    <el-card class="top-card">
      <el-row :gutter="24" class="top-info-row">
        <el-col :span="8" class="info-col">
          <div class="info-item"><span class="label">菌种来源:</span>{{ detail.source }}</div>
          <div class="info-item"><span class="label">鉴别菌株编号:</span>{{ detail.strainNo }}</div>
          <div class="info-item"><span class="label">鉴别菌株名称:</span>{{ detail.strainName }}</div>
        </el-col>
        <el-col :span="8" class="info-col">
          <div class="info-item"><span class="label">验证实验编号:</span>{{ detail.verifyNo }}</div>
          <div class="info-item"><span class="label">实验时间:</span>{{ detail.experimentTime }}</div>
          <div class="info-item"></div>
        </el-col>
        <el-col :span="8" class="info-col ">
          <div class="info-item sign-label"><span class="label">菌种实验员签字</span></div>
          <div class="info-item signature-item">
            <div class="signature-area">
              <img v-if="detail.signature" :src="detail.signature" alt="签字" />
              <span v-else class="waiting-text">暂无签名</span>
            </div>
          </div>
        </el-col>
      </el-row>
    </el-card>
    <div class="section-card" style="margin-top: 24px;">
      <el-form label-width="100px" label-position="top">
        <el-form-item label="实验结论">
          <el-input type="textarea" v-model="detail.conclusion" :rows="3" placeholder="请输入" />
        </el-form-item>
        <el-form-item label="批准菌株用途">
          <el-checkbox-group v-model="detail.usage">
            <el-checkbox label="传代" />
            <el-checkbox label="菌种保藏" />
            <el-checkbox label="废弃" />
          </el-checkbox-group>
        </el-form-item>
      </el-form>
    </div>
    <el-button type="primary" @click="handleSave" class="el-icon-plus"> 新增</el-button>
    <EditConditionDialog
      :visible.sync="dialogVisible"
      :isEdit="dialogIsEdit"
      :isFixed="dialogIsFixed"
      :value="dialogValue"
      @ok="handleDialogOk"
    />
    <div class="section-card" style="margin-top: 12px;">
      <el-table :data="tableData" border style="width: 100%;" :row-class-name="getRowClassName">
        <el-table-column prop="condition" label="菌种培养工艺条件" />
        <el-table-column prop="record" label="菌种培养工艺实况记录" />
        <el-table-column prop="process" label="菌种培养标准工艺" />
        <el-table-column label="操作" width="120">
          <template #default="{ row }">
            <el-button type="text" @click="handleEdit(row)">编辑</el-button>
            <el-button type="text" @click="handleDetail(row)">详情2</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="footer-btns">
      <el-button type="primary" @click="handleSave">保存</el-button>
      <el-button @click="handleDraft">存稿</el-button>
    </div>
    <DetailConditionDialog
      :visible.sync="detailDialogVisible"
      :value="detailDialogValue"
    />
  </Card>
</template>
<script>
import EditConditionDialog from './EditConditionDialog.vue'
import DetailConditionDialog from './DetailConditionDialog.vue'
export default {
  name: 'ConfirmDetail',
  components: { EditConditionDialog, DetailConditionDialog },
  data() {
    return {
      detail: {
        source: '内容的内容内容内容',
        strainNo: '3411234',
        strainName: '名称名称名称',
        verifyNo: '34133214',
        experimentTime: '2025-1-23 11:10:28',
        signature: '', // 签名图片url
        conclusion: '',
        usage: []
      },
      activeTab: 'condition',
      initialTableData: [
        { condition: '平板培养基', record: '文字内容文字内容文字内容文字内容文字内容文字内容', process: '文字内容文字内容文字内容文字内容文字内容文字内容' },
        { condition: '培养温度', record: '', process: '' },
        { condition: '培养时间', record: '', process: '' },
        { condition: '摇瓶培养基', record: '', process: '' },
        { condition: '接种量', record: '', process: '' },
        { condition: '培养时间', record: '', process: '' },
        { condition: '发酵时间', record: '', process: '' },
        { condition: '检测数据及结果', record: '', process: '' }
      ],
      tableData: [
        { condition: '平板培养基', record: '文字内容文字内容文字内容文字内容文字内容文字内容', process: '文字内容文字内容文字内容文字内容文字内容文字内容' },
        { condition: '培养温度', record: '', process: '' },
        { condition: '培养时间', record: '', process: '' },
        { condition: '摇瓶培养基', record: '', process: '' },
        { condition: '接种量', record: '', process: '' },
        { condition: '培养时间', record: '', process: '' },
        { condition: '发酵时间', record: '', process: '' },
        { condition: '检测数据及结果', record: '', process: '' }
      ],
      dialogVisible: false,
      dialogIsEdit: false,
      dialogIsFixed: false,
      dialogValue: {},
      dialogIndex: null,
      detailDialogVisible: false,
      detailDialogValue: {}
    }
  },
  methods: {
    handleEdit(row) {
      const idx = this.tableData.indexOf(row)
      this.dialogVisible = true
      this.dialogIsEdit = true
      this.dialogIsFixed = idx < this.initialTableData.length
      this.dialogValue = { ...row }
      this.dialogIndex = idx
    },
    handleDetail(row) {
      this.detailDialogVisible = true
      this.detailDialogValue = { ...row }
    },
    handleSave() {
      this.dialogVisible = true
      this.dialogIsEdit = false
      this.dialogIsFixed = false
      this.dialogValue = { condition: '', record: '', process: '' }
      this.dialogIndex = null
    },
    handleDraft() {
      // 存稿逻辑
    },
    handleDialogOk(val) {
      if (this.dialogIsEdit && this.dialogIndex !== null) {
        this.$set(this.tableData, this.dialogIndex, val)
      } else {
        this.tableData.push(val)
      }
    },
    getRowClassName({ rowIndex }) {
      return rowIndex < this.initialTableData.length ? 'fixed-row' : '';
    }
  }
}
</script>
<style scoped lang="less">
.top-card {
  margin-bottom: 0;
  background: rgba(239, 239, 239, 1);
  border-radius: 16px;
}
.top-info-row {
  align-items: stretch;
}
.info-col {
  display: flex;
  flex-direction: column;
}
.info-item {
  display: flex;
  font-size: 15px;
  height: 45px;
  line-height: 45px;
}
.label {
  color: #666;
  font-weight: 500;
}
.sign-col {
  align-items: center;
  text-align: center;
}
.sign-label {
  justify-content: center;
}
.signature-item {
  justify-content: center;
}
.signature-area {
  min-height: 80px;
  min-width: 120px;
  background: #f5f7fa;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 8px;
}
.signature-area img {
  max-width: 100%;
  max-height: 100%;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
.sign-time {
  justify-content: center;
  text-align: center;
  color: #666;
  font-size: 14px;
}
.section-card {
  margin-bottom: 0;
}
.footer-btns {
  display: flex;
  justify-content: center;
  padding: 24px;
  padding-bottom: 0;
  gap: 24px;
  .el-button {
    width: 150px;
  }
}
::v-deep(.fixed-row) {
  background-color: rgb(228, 248, 250) !important;
}
@media (max-width: 900px) {
  .info-col {
    height: auto;
  }
}
</style>
culture/src/views/strain-library/validation/primitive-cell/index.vue
New file
@@ -0,0 +1,464 @@
<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="$router.push('/strain/validation/confirm-detail')"
              >确认</el-button
            >
            <el-button type="text" @click="handleDetail(row)">详情1</el-button>
            <el-button type="text" @click="handleDetail2(row)">详情2</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>
    <PrimitiveCellDetailDialog
      :visible.sync="detailVisible"
      :detail="currentDetail"
    />
    <DetailConditionDialog
    :visible.sync="detailDialogVisible"
    :value="detailDialogValue"
/>
  </div>
</template>
<script>
import PrimitiveCellDetailDialog from "./primitive-cell-detail-dialog.vue";
import DetailConditionDialog from "./DetailConditionDialog.vue";
export default {
  name: "PrimitiveCell",
  components: {
    PrimitiveCellDetailDialog,
    DetailConditionDialog,
  },
  data() {
    return {
        detailDialogVisible: false,
        detailDialogValue: {},
      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: {
    handleDetail(row) {
      this.currentDetail = row;
      this.detailVisible = true;
    },
    handleDetail2(row) {
        this.detailDialogValue = row;
        this.detailDialogVisible = true;
    },
    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
    },
    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/primitive-cell-detail-dialog.vue
New file
@@ -0,0 +1,107 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title="原始细胞库资料详情"
    width="650px"
    @close="handleClose"
  >
    <el-form label-width="120px" label-position="top" class="detail-form">
        <el-row :gutter="24">
            <el-col :span="12">
            <el-form-item label="菌种源">
                <el-input v-model="detail.source" disabled placeholder="请输入" />
            </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="24">
        <el-col :span="12">
          <el-form-item label="菌种编号">
            <el-input v-model="detail.strainNo" disabled placeholder="请输入" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="菌种名称">
            <el-input v-model="detail.strainName" disabled placeholder="请输入" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验证实验编号">
            <el-input v-model="detail.verifyNo" disabled placeholder="请输入" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="实验时间">
            <el-input v-model="detail.experimentTime" disabled placeholder="请输入" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="24">
        <el-col :span="12">
          <el-form-item label="菌种实验员签字">
            <div class="signature-area">
              <img v-if="detail.signature" :src="detail.signature" alt="签字" />
              <span v-else class="waiting-text">暂无签名</span>
            </div>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="签字时间">
            <el-input v-model="detail.signTime" disabled />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </el-dialog>
</template>
<script>
export default {
  name: 'PrimitiveCellDetailDialog',
  props: {
    visible: Boolean,
    detail: {
      type: Object,
      default: () => ({
        source: '',
        strainNo: '',
        strainName: '',
        verifyNo: '',
        experimentTime: '',
        signature: '',
        signTime: ''
      })
    }
  },
  methods: {
    handleClose() {
      this.$emit('update:visible', false)
    }
  }
}
</script>
<style scoped>
.detail-form {
  margin-top: 10px;
}
.signature-area {
  min-height: 120px;
  min-width: 240px;
  background: #f5f7fa;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.signature-area img {
  max-width: 100%;
  max-height: 100%;
  display: block;
}
.waiting-text {
  color: #909399;
  font-size: 14px;
}
</style>
culture/src/views/system/user/components/add-edit.vue
@@ -20,7 +20,7 @@
              :value="item.roleId"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="登录状态" prop="status">
        <el-form-item label="启动状态" prop="status">
          <el-switch v-model="form.status"></el-switch>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
@@ -56,33 +56,51 @@
    },
  },
  data() {
    var validatePhone = (rule, value, callback) => {
      if (!value) {
        // The 'required' rule will handle empty value, so we can make this optional here
        // or keep it for a more specific message if needed.
        // For now, let's assume 'required' handles the empty case.
        callback();
        return;
      }
      const phoneRegex = new RegExp(/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,'g'); // Regex for 11-digit Chinese mobile numbers
      if (!phoneRegex.test(value)) {
        callback(new Error('请输入有效的11位手机号码'));
      } else {
        callback();
      }
    };
    return {
      form: { status: true },
      userDeptId: '',
      rules: {
        nickName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
        phonenumber: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
        roleId: [{ required: true, message: '请选择角色', trigger: 'blur' }],
        phonenumber: [
          { required: true, message: '请输入联系电话', trigger: 'blur' },
          { validator: validatePhone, trigger: 'blur' }
        ],
        roleId: [{ required: true, message: '请选择角色', trigger: 'change' }],
        userName: [{ required: true, message: '请输入登陆账号', trigger: 'blur' }],
        status: [{ required: true, message: '请选择启动状态', trigger: 'blur' }],
      },
    }
  },
  created() {
    this.form = { status: true }
    if (Object.keys(this.row).length) {
    if (this.row && this.row.userId) {
      this.form = {
        userId: this.row.userId,
        nickName: this.row.nickName,
        phonenumber: this.row.phonenumber,
        roleId: this.row.roleId,
        userName: this.row.userName,
        remark: this.row.remark,
        ...this.row,
        status: this.row.status == 0 ? true : false,
      }
    } else {
      this.form = {}
      this.form = {
        nickName: '',
        phonenumber: '',
        userName: '',
        roleId: '',
        status: true,
        remark: '',
      }
    }
  },
  mounted() { },
culture/src/views/system/user/components/reset-password.vue
@@ -3,16 +3,16 @@
    <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="重置密码" width="30%">
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="姓名" prop="nickName">
          <el-input :disabled="true" v-model="form.nickName" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input :disabled="true" v-model="form.nickName" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="登陆账号" prop="account">
          <el-input :disabled="true" v-model="form.account" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input :disabled="true" v-model="form.account" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="新密码" prop="password">
          <el-input v-model="form.password" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input v-model="form.password" type="password" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input v-model="form.confirmPassword" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input v-model="form.confirmPassword" type="password" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
      </el-form>
      <div class="select-member-footer">
culture/src/views/system/user/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="list">
    <TableCustom :queryForm="pagination" :tableData="data" :total="pagination.total"
      @currentChange="handleCurrentChange" @sizeChange="handleSizeChange">
      @handleCurrentChange="handleCurrentChange" @handleSizeChange="handleSizeChange">
      <template #search>
        <el-form label-width="100px" inline>
          <el-form-item label="人员搜索">
@@ -24,7 +24,7 @@
          </el-form-item>
          <el-form-item style="margin-left: 63px;">
            <el-button @click="reset">重置</el-button>
            <el-button type="primary" @click="onSubmit">查询</el-button>
            <el-button type="primary" @click="onSubmit" style="margin-left: 10px;">查询</el-button>
          </el-form-item>
        </el-form>
      </template>
@@ -45,10 +45,6 @@
            <div class="status_class">
              <div :class="row.status == 0 ? 'green' : 'red'"></div>
              <div>{{ row.status == 0 ? '正常' : '禁用' }}</div>
              <div v-if="row.status == 1" style="cursor: pointer"
                @click="; (dialogVisibleView = true), (rowView = row), $forceUpdate()">
                <i class="el-icon-warning"></i>
              </div>
            </div>
          </template>
        </el-table-column>
@@ -56,28 +52,26 @@
        <el-table-column label="操作" width="300">
          <template slot-scope="{ row }">
            <div>
              <el-button type="text" @click="edit(row)">编辑</el-button>
              <el-button type="text" @click="edit(row)">账号继承</el-button>
              <el-button v-if="row.status != 0" type="text" @click="updateStatus(row, true)">
              <el-button type="text" @click="edit(row)" class="action-button">编辑</el-button>
              <el-button type="text" @click="inherit(row)" class="action-button">账号继承</el-button>
              <el-button v-if="row.status != 0" type="text" @click="updateStatus(row, true)" class="action-button">
                启用
              </el-button>
              <el-button v-if="row.status == 0" type="text" @click="updateStatus(row, false)">
              <el-button v-if="row.status == 0" type="text" @click="updateStatus(row, false)" class="action-button">
                禁用
              </el-button>
              <el-button type="text" @click="detail(row)">重置密码</el-button>
              <el-button type="text" @click="del(row)">删除</el-button>
              <el-button type="text" @click="detail(row)" class="action-button">重置密码</el-button>
              <el-button type="text" @click="del(row)" class="action-button">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <AddEdit v-if="dialogVisible" :row="row" :deptList="deptList" :deptType="deptTypeList" :roleList="roleList"
    <AddEdit v-if="dialogVisible" :row="row" :deptType="deptTypeList" :roleList="roleList"
      :dialogVisible="dialogVisible" @close="dialogVisible = false, row = {}" @confirm="confirm" />
    <ResetPassword v-if="passwordVisible" :row="row" :dialogVisible="passwordVisible"
      @close="passwordVisible = false, row = {}" @confirm="passwordConfirm" />
    <ViewData v-if="dialogVisibleView" :row="rowView" :dialogVisible="dialogVisibleView"
      @close="dialogVisibleView = false, rowView = {}" />
    <Disb v-if="disbDialogVisible" :row="disbRow" :dialogVisible="disbDialogVisible"
      @close="disbDialogVisible = false, disbRow = {}" @confirm="disbConfirm" />
    <ShowDelConfirm :show="delShow" @close="delShow = false" @confirm="delConfirm" title="确认要删除该人员吗?"
@@ -90,7 +84,6 @@
<script>
import { getList, add, edit, delDept, roleList, updatePwd, changeStatus, typeList } from './service'
import AddEdit from './components/add-edit.vue'
import ViewData from './components/view-data.vue'
import Disb from './components/disb.vue'
import ResetPassword from './components/reset-password.vue'
import Inherit from './components/inherit.vue'
@@ -98,7 +91,6 @@
  name: 'User',
  components: {
    AddEdit,
    ViewData,
    Disb,
    ResetPassword,
    Inherit,
@@ -113,7 +105,6 @@
      inheritDialogVisible: false,//账号继承
      data: [],//列表数据
      nickNameOrPhone: '',//人员搜索
      deptId: [],//部门
      roleId: [],//角色
      status: '',//状态
      roleList: [],//角色列表
@@ -137,9 +128,8 @@
  },
  watch: {},
  created() {
    // this.getRoleList()
    // this.getListData()
    // this.getTypeList()
    this.getRoleList()
    this.getListData()
  },
  mounted() { },
  methods: {
@@ -150,7 +140,7 @@
    },
    getRoleList() {
      roleList().then((res) => {
        this.roleList = res.data.data
        this.roleList = res
      })
    },
    delConfirm() {
@@ -201,6 +191,10 @@
      this.row = row
      this.dialogVisible = true
    },
    inherit(row) {
      this.inheritRow = row
      this.inheritDialogVisible = true
    },
    updateStatus(row, type) {
      if (type) {
        changeStatus({ ...row, status: 0 }).then(() => {
@@ -220,18 +214,14 @@
    },
    async getListData() {
      let obj = {
        ...this.pagination,
        pageNum: this.pagination.pageNum,
        pageSize: this.pagination.pageSize,
        nickNameOrPhone: this.nickNameOrPhone,
        deptIds: this.deptId,
        roleIds: this.roleId,
        status: this.status,
      }
      this.listLoading = true
      const {
        data: {
          data: { records, total },
        },
      } = await getList(obj)
      const { records,total } = await getList(obj)
      this.data = records
      this.pagination.total = total
      this.timeOutID = setTimeout(() => {
@@ -241,7 +231,6 @@
    reset() {
      this.nickNameOrPhone = ''
      this.roleId = []
      this.deptId = []
      this.status = ''
      this.pagination.pageNum = 1
      this.getListData()
@@ -267,6 +256,10 @@
  height: 100%;
}
.action-button {
  margin-right: 8px;
}
.green {
  background-color: green;
}
culture/src/views/system/user/service.js
@@ -36,6 +36,6 @@
export const updatePwd = (data) => {
  return axios.post(`/system/user/resetPwd`, { ...data })
}
export const typeList = (data) => {
export const typeList = () => {
  return axios.get(`/t-business-dept/list/type?type=1`,)
}
laboratory/src/App.vue
@@ -145,6 +145,16 @@
    .el-input__inner {
      width: 200px;
    }
    .el-pagination__sizes{
      .el-input__inner {
      width: 100px;
    }
    }
    .el-pagination__jump{
      .el-input__inner {
      width: 50px;
    }
    }
  }
}
laboratory/src/components/DynamicComponent/addTableData.vue
@@ -38,6 +38,7 @@
                  <el-input
                    v-model="form[header.name]"
                    :placeholder="'请输入' + header.name"
                    :disabled="!checkEditPermission(header)"
                  />
                </el-form-item>
                <el-form-item
@@ -58,8 +59,10 @@
                    list-type="picture-card"
                    :on-change="handleSpectrumChange"
                    :on-remove="handleSpectrumRemove"
                    :disabled="!checkEditPermission(header)"
                  >
                    <i class="el-icon-plus"></i>
                    <!-- <div slot="tip" class="el-upload__tip">暂未连接服务器,使用默认图片</div> -->
                  </el-upload>
                </el-form-item>
                <el-form-item
@@ -77,6 +80,7 @@
                    type="datetime"
                    placeholder="选择日期时间"
                    value-format="yyyy-MM-dd HH:mm:ss"
                    :disabled="!checkEditPermission(header)"
                    :picker-options="{
                      shortcuts: [{
                        text: '今天',
@@ -116,6 +120,7 @@
                    multiple
                    filterable
                    placeholder="请选择用户"
                    :disabled="!checkEditPermission(header)"
                  >
                    <el-option
                      v-for="item in userOptions"
@@ -139,6 +144,8 @@
</template>
  
<script>
import { listByRole } from './service';
export default {
  name: "AddDialog",
  props: {
@@ -167,13 +174,8 @@
      rules: {},
      photoList: [],
      spectrumList: [],
      userOptions: [
        { value: '1', label: '用户1' },
        { value: '2', label: '用户2' },
        { value: '3', label: '用户3' },
        { value: '4', label: '用户4' },
        { value: '5', label: '用户5' }
      ]
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
      userOptions: []
    };
  },
  computed: {
@@ -185,12 +187,15 @@
        this.$emit("update:visible", val);
      },
    },
    currentUserId() {
      const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
      return userInfo.userId;
    }
  },
  watch: {
    visible: {
      handler(newVal) {
        if (newVal) {
          console.log('弹窗打开,初始化数据');
          this.showHeaderList = JSON.parse(JSON.stringify(this.headerList));
          this.$forceUpdate();
          if (this.isEdit && this.editData) {
@@ -201,8 +206,6 @@
            this.initFormData();
          }
          this.initRules();
          console.log('初始化后的表单数据:', this.form);
          console.log('初始化后的校验规则:', this.rules);
        }
      },
    },
@@ -210,7 +213,6 @@
      immediate: true,
      handler(newVal) {
        if (newVal && newVal.length) {
          console.log('headerList变化,重新初始化');
          if (this.isEdit && this.editData) {
            this.setFormData(this.editData);
          } else {
@@ -220,18 +222,28 @@
        }
      },
    },
    showHeaderList: {
      immediate: true,
      handler(newVal) {
        if (newVal ) {
          console.log("222222222222222222", JSON.stringify(newVal));
        }
      },
    },
  },
  methods: {
    getUserOptions() {
      listByRole().then(res => {
        if (res) {
          this.userOptions = res.map(user => ({
            value: user.userId,
            label: user.nickName || user.userName
          }));
        } else {
          this.$message.error('获取用户列表失败');
        }
      }).catch(err => {
        console.error('获取用户列表失败', err);
      });
    },
    checkEditPermission(header) {
      if (!header.role || !Array.isArray(header.role)) {
        return true;
      }
      return header.role.includes(this.currentUserId);
    },
    initRules() {
      // 初始化校验规则
      const rules = {};
@@ -248,7 +260,6 @@
          }
        });
      }
      console.log('生成的校验规则:', rules);
      this.rules = rules;
    },
    initFormData() {
@@ -272,8 +283,6 @@
      Object.keys(formData).forEach(key => {
        this.$set(this.form, key, formData[key]);
      });
      console.log('初始化后的表单数据:', this.form);
    },
    setFormData(data) {
      // 设置基础表单数据
@@ -341,23 +350,32 @@
      this.initFormData();
    },
    handleSubmit() {
      console.log('开始提交表单');
      console.log('表单数据:', this.form);
      console.log('校验规则:', this.rules);
      
      this.$refs.form.validate((valid) => {
        console.log('表单验证结果:', valid);
        if (valid) {
          const submitData = {
            ...this.form,
            photos: this.photoList,
            spectrums: this.spectrumList,
          };
          console.log('提交数据:', submitData);
          // 为用户类型字段添加用户完整信息
          if (this.headerList && this.headerList.length) {
            this.headerList.forEach(header => {
              if (header.type === 'user' && Array.isArray(submitData[header.name])) {
                // 为每个用户类型字段添加userInfo属性,包含用户完整信息
                submitData[`${header.name}_userInfo`] = submitData[header.name].map(userId => {
                  const userInfo = this.userOptions.find(user => user.value === userId);
                  return userInfo ? userInfo : { value: userId, label: userId };
                });
              }
            });
          }
          console.log(submitData,'修改的数据')
          this.$emit("success", submitData);
          this.handleClose();
        } else {
          console.log('表单验证失败');
          this.$message.error('请填写必填项');
        }
      });
@@ -367,20 +385,31 @@
      this.$refs.form.validateField("photos");
    },
    handleSpectrumChange(file, fileList) {
      this.spectrumList = fileList;
      // 使用默认图片替代实际上传
      this.spectrumList = [{
        name: '默认图片.jpg',
        url: this.defaultImageUrl,
        status: 'success'
      }];
      // 同时更新form中对应的字段值以通过表单验证
      const imageHeader = this.headerList.find(h => h.type === 'image');
      if (imageHeader && imageHeader.name) {
        // 保存图片URL,这样在表格中可以直接使用
        this.$set(this.form, imageHeader.name, this.defaultImageUrl);
        console.log('设置图片字段:', imageHeader.name, this.defaultImageUrl);
      }
      this.$refs.form.validateField("spectrums");
    },
    handleSpectrumRemove(file) {
      // 处理文件移除逻辑
      this.spectrumList = [];
    },
    handleIPadSpectrum() {
      // TODO: 调用 iPad 选择图谱功能
      console.log("调用 iPad 选择图谱功能");
    },
  },
  mounted() {
    console.log("初始headerList:", this.headerList);
    // 获取用户列表数据
    this.getUserOptions();
  },
};
</script>
@@ -417,7 +446,7 @@
    .form-content {
      flex: 1;
      overflow-y: auto;
      padding: 0 10px;
      padding: 10px 10px;
      max-height: calc(90vh - 250px); // 设置内容区域最大高度
      &::-webkit-scrollbar {
@@ -433,6 +462,9 @@
        background: #f5f7fa;
      }
    }
    .el-form-item::after{
      height: 10px !important;
    }
  }
}
laboratory/src/components/DynamicComponent/addTableHeader.vue
@@ -1,59 +1,40 @@
<template>
  <el-dialog
    title="新增表头"
    :visible.sync="dialogVisible"
    width="30%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
  <el-dialog title="新增表头" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false" @close="handleClose">
    <div class="sample-dialog">
      <div class="sample-content">
        <div class="form-content">
          <el-form ref="form" :model="form" :rules="rules" label-position="top">
            <el-row :gutter="24">
              <el-col :span="24">
                <el-form-item label="表头名称" prop="sampleCode">
                  <el-input
                    v-model="form.name"
                    style="width: 100%"
                    placeholder="请输入表头名称"
                  />
                <el-form-item label="表头名称" prop="name">
                  <el-input v-model="form.name" style="width: 100%" placeholder="请输入表头名称" />
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="表头类型" prop="sampleCode">
                <el-form-item label="表头类型" prop="type">
                  <el-radio-group v-model="form.type" style="width: 100%">
                    <el-radio-button  label="text">文本框</el-radio-button>
                    <el-radio-button label="text">文本框</el-radio-button>
                    <el-radio-button label="image">图片上传</el-radio-button>
                    <el-radio-button  label="date">日期选择</el-radio-button>
                    <el-radio-button  label="user">人员选择</el-radio-button>
                    <el-radio-button label="date">日期选择</el-radio-button>
                    <el-radio-button label="user">人员选择</el-radio-button>
                  </el-radio-group>
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="操作权限" prop="sampleCode">
                <el-form-item label="操作权限" prop="role">
                  <el-select v-model="form.role" placeholder="请选择" style="width: 100%" multiple>
                    <el-option
                      v-for="item in options"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                    >
                    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
                    </el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="提示文案" prop="sampleCode">
                  <el-input
                    v-model="form.message"
                    style="width: 100%"
                    placeholder="请输入提示文案"
                  />
              <el-col :span="24" v-if="['text', 'date', 'user'].includes(form.type)">
                <el-form-item label="提示文案" prop="message">
                  <el-input v-model="form.message" style="width: 100%" placeholder="请输入提示文案" />
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="是否必填" prop="testTypes">
                <el-form-item label="是否必填" prop="required">
                  <el-radio-group v-model="form.required">
                    <el-radio label="true">是</el-radio>
                    <el-radio label="false">否</el-radio>
@@ -71,7 +52,7 @@
    </div>
  </el-dialog>
</template>
<script>
export default {
  name: "AddDialog",
@@ -80,6 +61,10 @@
      type: Boolean,
      default: false,
    },
    participants: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
@@ -102,13 +87,13 @@
          {
            type: "array",
            required: true,
            message: "请至少选择一种检测类型",
            message: "请至少选择一个操作人",
            trigger: "change",
          },
        ],
        message: [
          {
            required: true,
            required: false,
            message: "请输入提示文案",
            trigger: "blur",
          },
@@ -121,23 +106,7 @@
          },
        ],
      },
      options: [{
          value: '1',
          label: '黄金糕'
        }, {
          value: '2',
          label: '双皮奶'
        }, {
          value: '3',
          label: '蚵仔煎'
        }, {
          value: '4',
          label: '龙须面'
        }, {
          value: '5',
          label: '北京烤鸭'
        }],
        value: ''
      value: ''
    };
  },
  computed: {
@@ -149,10 +118,20 @@
        this.$emit("update:visible", val);
      },
    },
    options() {
      // 将participants转换为select组件需要的格式
      let userId = JSON.parse(sessionStorage.getItem('userInfo'))?.userId
      let nickName = JSON.parse(sessionStorage.getItem('userInfo'))?.nickName
      let newList = JSON.parse(JSON.stringify(this.participants))
      newList.push({ userId, nickName })
      return newList.map(participant => ({
        value: participant.userId,
        label: participant.nickName
      }));
    }
  },
  mounted() {
    // 组件挂载时的初始化逻辑
    console.log('组件已挂载');
  },
  methods: {
    setFormData(data) {
@@ -179,6 +158,12 @@
      };
    },
    handleSubmit() {
      // 对于需要提示文案的类型,添加额外验证
      if (['text', 'date', 'user'].includes(this.form.type) && !this.form.message) {
        this.$message.error('请输入提示文案');
        return;
      }
      this.$refs.form.validate((valid) => {
        if (valid) {
          const submitData = {
@@ -191,7 +176,7 @@
  },
};
</script>
<style scoped lang="less">
::v-deep .el-dialog__body {
  padding: 0;
@@ -334,12 +319,15 @@
    .el-upload-list {
      margin-top: 10px;
    }
    .el-upload-list__item {
      transition: all 0.3s;
      &:hover {
        background-color: #f5f7fa;
      }
    }
    .el-upload__tip {
      color: #909399;
      font-size: 12px;
@@ -347,4 +335,4 @@
    }
  }
}
</style>
</style>
laboratory/src/components/DynamicComponent/index.vue
@@ -4,151 +4,96 @@
      <div class="add-group">
        <div v-if="title">*</div>
        <span v-if="title">{{ title }}</span>
        <el-button
          @click="showAddDialog = true"
          class="el-icon-plus"
          type="primary"
        >
          添加组件</el-button
        >
        <el-button v-if="editable" @click="showAddDialog = true" class="el-icon-plus" type="primary">
          添加组件</el-button>
      </div>
      <!-- 选择组件弹窗 -->
      <AddComponentDialog
        :visible="showAddDialog"
        @confirm="addComponent"
        @close="showAddDialog = false"
      />
      <AddComponentDialog v-if="editable" :visible="showAddDialog" @confirm="addComponent"
        @close="showAddDialog = false" />
      <!-- 动态渲染组件 -->
      <div
        v-for="(item, idx) in components"
        :key="item.id"
        class="dynamic-component"
      >
      <div v-for="(item, idx) in components" :key="item.id" class="dynamic-component">
        <!-- 富文本 -->
        <div v-if="item.type == 'richText'">
          <AiEditor
            :ref="`editor_${item.id}`"
            v-model="item.data.content"
            height="200px"
            placeholder="请输入内容..."
          />
          <AiEditor :ref="`editor_${item.id}`" :value="item.data.content" height="200px" :readOnly="!editable" placeholder="请输入内容..."
            :disabled="!editable" />
        </div>
        <!-- 自定义表格 -->
        <div v-else-if="item.type == 'customTable'" style="flex: 1">
          <div class="table-actions">
            <el-button size="mini" @click="showTableHeaderDialog(idx)"
              >添加表头</el-button
            >
            <el-button
              size="mini"
              type="primary"
              @click="showAddRowDialog(idx, item.data.headers)"
              >添加数据</el-button
            >
          <div v-if="editable" class="table-actions">
            <el-button size="mini"  @click="showTableHeaderDialog(idx)">添加表头</el-button>
            <el-button size="mini" type="primary" @click="showAddRowDialog(idx, item.data.headers)">添加数据</el-button>
          </div>
          <Table
            :data="item.data.rows"
            :total="null"
            :height="null"
            class="groupTable"
          >
            <!-- <el-table-column
              type="index"
              label="序号"
              width="80"
            ></el-table-column> -->
            <el-table-column
              v-for="(header, hidx) in item.data.headers"
              :key="hidx"
              :label="header.name"
              :prop="header.name"
            />
            <el-table-column
              label="更新时间"
              prop="updateTime"
              min-width="180"
            ></el-table-column>
            <el-table-column label="操作" min-width="200">
          <Table :data="item.data.rows" :total="null" :height="null" class="groupTable" :key="item.id + '_' + JSON.stringify(item.data.rows).length">
            <el-table-column v-for="(header, hidx) in item.data.headers" :key="hidx" :label="header.name"
              :prop="header.name">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)"
                  >编辑</el-button
                >
                <el-button
                  type="text"
                  @click="handleDeleteRow(idx, scope.$index)"
                  >删除</el-button
                >
                <!-- 用户类型显示 -->
                <template v-if="header.type === 'user'">
                  {{ getUserDisplayText(header.name, scope.row) }}
                </template>
                <!-- 图片类型显示 -->
                <template v-else-if="header.type === 'image'">
                  <img v-if="scope.row[header.name]"
                       :src="scope.row[header.name]"
                       alt="头像"
                       class="table-image" />
                </template>
                <!-- 其他类型 -->
                <template v-else>
                  {{ scope.row[header.name] }}
                </template>
              </template>
            </el-table-column>
            <el-table-column label="更新时间" prop="updateTime" min-width="180"></el-table-column>
            <el-table-column  label="操作" min-width="200" v-if="dialogCanEdit">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)" >编辑</el-button>
                <el-button type="text" v-if="editable" @click="handleDeleteRow(idx, scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </Table>
        </div>
        <!-- 文件上传 -->
        <div v-else-if="item.type == 'fileUpload'">
          <el-upload
            action="#"
            :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)"
            list-type="text"
          >
          <el-upload v-if="editable" action="#" :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)" list-type="text">
            <el-button size="small" icon="el-icon-upload2">点击上传</el-button>
          </el-upload>
          <div v-else>
            <div v-for="file in item.data.fileList" :key="file.uid" class="file-list-item">
              {{ file.name }}
            </div>
          </div>
        </div>
        <!-- 图片上传 -->
        <div v-else-if="item.type == 'imageUpload'">
          <el-upload
            action="#"
            :file-list="item.data.imageList"
            :on-change="(file, fileList) => handleImageChange(idx, fileList)"
            :on-success="
              (res, file, fileList) =>
                handleImageSuccess(res, file, fileList, idx)
            "
            list-type="picture-card"
          >
            <i class="el-icon-plus"></i>
            <div class="upload-text">上传图片</div>
          </el-upload>
          <div class="uploaf-notice">支持.jpg .png格式</div>
          <div class="image-upload-container">
            <el-upload v-if="editable" action="#" :file-list="item.data.imageList"
              :on-change="(file, fileList) => handleImageChange(idx, fileList)"
              :on-success="(res, file, fileList) => handleImageSuccess(res, file, fileList, idx)" :auto-upload="true"
              :http-request="() => { }" :before-upload="beforeImageUpload" list-type="picture-card"
              class="image-uploader">
              <i class="el-icon-plus"></i>
              <div class="upload-text">上传图片</div>
            </el-upload>
            <div v-else class="image-preview">
              <img v-for="image in item.data.imageList" :key="image.uid" :src="image.url" class="preview-image" />
            </div>
            <div class="uploaf-notice">支持.jpg .png格式</div>
          </div>
        </div>
        <img
          src="@/assets/public/delete.png"
          @click="removeComponent(idx)"
          alt="删除"
          class="delete-icon"
        />
        <img v-if="editable" src="@/assets/public/delete.png" @click="removeComponent(idx)" alt="删除"
          class="delete-icon" />
      </div>
    </div>
    <!-- 添加表头弹窗 -->
    <el-dialog
      :visible.sync="tableHeaderDialog.visible"
      title="添加表头"
      width="300px"
    >
      <el-input
        v-model="tableHeaderDialog.header"
        placeholder="请输入表头"
      ></el-input>
      <span slot="footer" class="dialog-footer">
        <el-button @click="tableHeaderDialog.visible = false">取消</el-button>
        <el-button type="primary" @click="confirmAddHeader">确定</el-button>
      </span>
    </el-dialog>
    <addTableHeader
      :visible.sync="tableHeaderDialog.visible"
      @confirm="confirmAddHeader"
    ></addTableHeader>
    <addTableData
      :visible.sync="rowDialog.visible"
      :headerList="rowDialog.headers"
      :editData="rowDialog.form"
      :isEdit="rowDialog.isEdit"
      @success="confirmAddRow"
    >
    <addTableHeader  :visible.sync="tableHeaderDialog.visible" :participants="participants"
      @confirm="confirmAddHeader"></addTableHeader>
    <addTableData  :visible.sync="rowDialog.visible" :headerList="rowDialog.headers"
      :editData="rowDialog.form" :isEdit="rowDialog.isEdit" @success="confirmAddRow">
    </addTableData>
  </div>
</template>
@@ -174,6 +119,22 @@
      type: String,
      default: "",
    },
    participants: {
      type: Array,
      default: () => []
    },
    dataSource: {
      type: Array,
      default: () => []
    },
    editable: {
      type: Boolean,
      default: true
    },
    dialogCanEdit: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
@@ -192,11 +153,102 @@
        headers: [],
        form: {},
      },
      headerList: [], //编辑的表头列表
      headerList: [],
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
    };
  },
  watch: {
    dataSource: {
      handler(newVal) {
        if (newVal) {
          newVal = newVal.map(component => {
            let componentData = null;
            switch (component.type) {
              case 'richText':
                componentData = { content: component.data }
                break;
              case 'customTable':
                componentData = {
                  headers: component.data.headers,
                  rows: component.data.rows
                };
                break;
              case 'fileUpload':
                componentData = { fileList: component.data };
                break;
              case 'imageUpload':
                componentData = { imageList: component.data };
                break;
            }
            return {
              type: component.type,
              id: component.id || Math.random().toString(36).substr(2, 9),
              data: componentData
            }
          })
        }
        this.components = newVal ? [...newVal] : [];
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    getUserDisplayText(fieldName, rowData) {
      // 检查是否有对应的userInfo数据
      const userInfoKey = `${fieldName}_userInfo`;
      // 如果没有rowData或fieldName,直接返回空字符串
      if (!rowData || !fieldName) {
        return '';
      }
      // 情况1: 有_userInfo数据
      if (rowData[userInfoKey] && Array.isArray(rowData[userInfoKey])) {
        return rowData[userInfoKey].map(user => user.label || '').join(', ');
      }
      // 情况2: 只有用户ID数组
      if (Array.isArray(rowData[fieldName])) {
        // 使用participants查找用户信息
        return rowData[fieldName].map(userId => {
          const user = this.participants.find(p => p.userId === userId);
          return user ? (user.nickName || user.userName || userId) : userId;
        }).join(', ');
      }
      // 情况3: 已经是字符串(可能是之前格式化过的)
      if (typeof rowData[fieldName] === 'string') {
        return rowData[fieldName];
      }
      // 默认返回空字符串
      return '';
    },
    formatUserNames(userArray) {
      if (!userArray || !Array.isArray(userArray) || userArray.length === 0) {
        return '';
      }
      // 查找参与者列表中的用户,获取昵称并用逗号拼接
      const userNames = userArray.map(userId => {
        const user = this.participants.find(p => p.userId === userId);
        return user ? user.nickName : userId;
      });
      return userNames.join(', ');
    },
    addComponent(type) {
      if (!this.editable) return;
      if (type === "customTable") {
        if (!this.participants || this.participants.length === 0) {
          this.$message.warning('请先选择实验调度');
          this.showAddDialog = false;
          return;
        }
      }
      this.showAddDialog = false;
      const id = Date.now() + Math.random();
      let data = {};
@@ -204,12 +256,88 @@
      if (type === "customTable") data = { headers: [], rows: [] };
      if (type === "fileUpload") data = { fileList: [] };
      if (type === "imageUpload") data = { imageList: [] };
      console.log(type, "111111111111111", this.components);
      this.components.push({ id, type, data });
      this.emitUpdate();
    },
    submit() {
      const data = this.components.map(component => {
        let componentData = null;
        switch (component.type) {
          case 'richText':
            const editorRef = this.$refs[`editor_${component.id}`];
            const editor = Array.isArray(editorRef) ? editorRef[0] : editorRef;
            const content = editor ? editor.getContent() : '';
            componentData = content && content !== '<p></p>' ? content : '';
            break;
          case 'customTable':
            componentData = {
              headers: component.data.headers,
              rows: component.data.rows
            };
            break;
          case 'fileUpload':
            componentData = component.data.fileList;
            break;
          case 'imageUpload':
            componentData = component.data.imageList;
            break;
        }
        return {
          type: component.type,
          data: componentData
        };
      });
      this.$emit('submit', data);
    },
    confirmAddRow(formData) {
      // if (!this.editable) return;
      const { idx, rowIndex, isEdit } = this.rowDialog;
      // 处理formData中的数据,保证用户信息的完整性
      const processedData = { ...formData };
      // 调试输出
      console.log('添加/编辑行数据:', processedData,'isEdit',isEdit);
      if (isEdit) {
        // Vue无法检测到对象或数组深层属性的变化,使用Vue.set来确保响应式
        this.$set(
          this.components[idx].data.rows,
          rowIndex,
          {
            ...processedData,
            updateTime: new Date().toLocaleString()
          }
        );
      } else {
        // 使用数组方法push会被Vue检测到
        this.components[idx].data.rows.push({
          ...processedData,
          updateTime: new Date().toLocaleString()
        });
      }
      console.log('this.components',this.components);
      // 手动触发组件更新
      this.$forceUpdate();
      // 延迟发送事件,确保数据已更新
      this.$nextTick(() => {
        this.emitUpdate();
      });
      this.rowDialog.visible = false;
      this.rowDialog.form = {};
    },
    removeComponent(idx) {
      if (!this.editable) return;
      this.components.splice(idx, 1);
      this.emitUpdate();
    },
    showTableHeaderDialog(idx) {
      this.tableHeaderDialog.visible = true;
@@ -217,14 +345,12 @@
      this.tableHeaderDialog.header = "";
    },
    confirmAddHeader(data) {
      console.log("data", data);
      if (!this.editable) return;
      const { idx } = this.tableHeaderDialog;
      // 添加新表头
      this.components[idx].data.headers.push({ ...data });
      // 为已有行数据添加新表头对应的默认值
      this.components[idx].data.rows.forEach(row => {
        // 如果行数据是对象,直接添加新属性
        if (typeof row === 'object' && row !== null) {
          if (data.type === 'user') {
            this.$set(row, data.name, []);
@@ -232,7 +358,6 @@
            this.$set(row, data.name, '');
          }
        } else {
          // 如果行数据不是对象,转换为对象
          const newRow = {};
          this.components[idx].data.headers.forEach(header => {
            if (header.name === data.name) {
@@ -245,19 +370,18 @@
              newRow[header.name] = row[header.name] || '';
            }
          });
          // 替换原行数据
          const rowIndex = this.components[idx].data.rows.indexOf(row);
          this.components[idx].data.rows.splice(rowIndex, 1, newRow);
        }
      });
      // 关闭弹窗并重置数据
      this.tableHeaderDialog.visible = false;
      this.tableHeaderDialog = {
        visible: false,
        idx: null,
        header: "",
      };
      this.emitUpdate();
    },
    showAddRowDialog(idx, headerList) {
      this.headerList = headerList;
@@ -266,7 +390,6 @@
      this.rowDialog.isEdit = false;
      this.rowDialog.headers = this.components[idx].data.headers;
      this.rowDialog.form = {};
      // 初始化表单数据
      this.rowDialog.headers.forEach((header) => {
        if (header.type === "user") {
          this.rowDialog.form[header.name] = [];
@@ -281,12 +404,13 @@
      this.rowDialog.rowIndex = rowIndex;
      this.rowDialog.isEdit = true;
      this.rowDialog.headers = this.components[idx].data.headers;
      // 深拷贝行数据,避免直接修改原数据
      this.rowDialog.form = JSON.parse(
        JSON.stringify(this.components[idx].data.rows[rowIndex])
      );
    },
    handleDeleteRow(idx, rowIndex) {
      if (!this.editable) return;
      this.$confirm("确认删除该行数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
@@ -295,19 +419,60 @@
        .then(() => {
          this.components[idx].data.rows.splice(rowIndex, 1);
          this.$message.success("删除成功");
          this.emitUpdate();
        })
        .catch(() => {});
        .catch(() => { });
    },
    handleFileChange(idx, fileList) {
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
        }
        if (!file.name) {
          file.name = '默认文件.txt';
        }
        return file;
      });
      this.components[idx].data.fileList = fileList;
      this.emitUpdate();
    },
    handleImageChange(idx, fileList) {
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
        }
        return file;
      });
      this.components[idx].data.imageList = fileList;
      this.emitUpdate();
    },
    handleImageSuccess(res, file, fileList, idx) {
      // 假设后端返回的图片地址在 res.url
      file.url = res.url;
      file.url = 'https://picsum.photos/200/200';
      this.components[idx].data.imageList = fileList;
    },
    beforeImageUpload(file) {
      const isJPG = file.type === 'image/jpeg';
      const isPNG = file.type === 'image/png';
      const isLt2M = file.size / 1024 / 1024 < 2;
      if (!isJPG && !isPNG) {
        this.$message.error('上传图片只能是 JPG 或 PNG 格式!');
        return false;
      }
      if (!isLt2M) {
        this.$message.error('上传图片大小不能超过 2MB!');
        return false;
      }
      return true;
    },
    emitUpdate() {
      // 先创建新对象,这有助于触发更新
      const updatedComponents = JSON.parse(JSON.stringify(this.components));
      this.$emit('update:dataSource', updatedComponents);
    },
  },
};
@@ -319,9 +484,11 @@
  padding: 20px;
  margin-top: 37px;
}
.has-title{
.has-title {
  margin-top: 0px !important;
}
.add-group {
  display: flex;
  align-items: center;
@@ -339,18 +506,47 @@
    margin: 0 32px 0 8px;
  }
}
.dynamic-component {
  background: #ffffff;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
  .delete-icon {
    width: 16px;
    height: 16px;
    cursor: pointer;
  }
}
.image-uploader {
  display: flex;
  ::v-deep .el-upload-list__item {
    width: 104px !important;
    height: 104px !important;
  }
}
.image-upload-container {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px;
}
::v-deep .image-uploader {
  .el-upload--picture-card {
    margin: 0;
  }
  .el-upload-list--picture-card .el-upload-list__item {
    margin: 0 10px 10px 0;
  }
}
::v-deep .el-upload {
  width: 104px;
  height: 104px;
@@ -358,6 +554,7 @@
  align-items: center;
  justify-content: center;
  flex-direction: column;
  .upload-text {
    font-weight: 400;
    font-size: 14px;
@@ -366,6 +563,7 @@
    margin-top: 13px;
  }
}
.uploaf-notice {
  font-weight: 400;
  font-size: 14px;
@@ -373,17 +571,44 @@
  line-height: 22px;
  margin-top: 8px;
}
.table-actions {
  margin-bottom: 10px;
  display: flex;
  gap: 10px;
}
.groupTable {
  width: 100%;
  margin-top: 10px;
  ::v-deep .el-input__inner {
    width: unset !important;
  }
}
.file-list-item {
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}
.image-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.preview-image {
  width: 104px;
  height: 104px;
  object-fit: cover;
  border-radius: 8px;
}
.table-image {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
}
</style>
laboratory/src/components/DynamicComponent/service.js
New file
@@ -0,0 +1,8 @@
import axios from '@/utils/request';
// 获取项目列表 获取用户列表-不分页-根据角色筛选
export const listByRole = (data) => {
    return axios.get('/system/user/listByRole', { params:data })
}
laboratory/src/components/SelectMember/index.vue
@@ -59,7 +59,7 @@
</template>
<script>
import { getRoleList, getUserList } from './service'
import { getRoleList, getUserList, listByRole } from './service'
export default {
    props: {
        projectId: {
@@ -118,7 +118,8 @@
            if (this.projectId) {
                params.projectId = this.projectId;
                // TODO: 这里需要替换为新的接口调用
                // const res = await getProjectUserList(params);
                const res = await listByRole(params);
                this.tableData = res.records;
            } else {
                const res = await getUserList(params);
                this.tableData = res.records;
laboratory/src/components/SelectMember/service.js
@@ -13,4 +13,10 @@
// 角色列表不分页
export const getRoleList = (data) => {
    return axios.post('/system/role/listNotPage', { ...data })
}
}
// 获取项目列表 获取用户列表-不分页-根据角色筛选
export const listByRole = (data) => {
    return axios.get('/system/user/listByRole', { params:data })
}
laboratory/src/components/SelectMemberSimple/index.vue
New file
@@ -0,0 +1,154 @@
<template>
    <el-dialog @open="openDialog" class="select-member" :visible.sync="visible" width="53.33%"
        :close-on-click-modal="false" :show-close="false">
        <template #title>
            <div>选择实验人员</div>
        </template>
        <div class="select-member-content">
            <div class="select-member-content-right">
                <div class="select-member-content-right-header">
                    <div class="select-member-content-right-header-text">人员列表</div>
                    <div class="select-member-content-right-header-search">
                        <el-input clearable v-model="searchKeyword" placeholder="请输入姓名/手机号" />
                    </div>
                </div>
                <Table ref="memberTable" :height="null" :row-key="row => row.userId" :data="filteredTableData"
                    :total="0" @selection-change="handleSelectionChange" :row-class-name="tableRowClassName">
                    <el-table-column type="selection" width="55" />
                    <el-table-column label="角色" prop="roleType" >
                        <template #default="{ row }">
                            {{ row.roleType === 3 ? '工艺工程师' : row.roleType === 4 ? '化验师' : '实验员' }}
                        </template>
                    </el-table-column>
                    <el-table-column label="姓名" prop="nickName" />
                    <el-table-column label="头像" prop="avatar" width="80">
                        <template #default="{ row }">
                            <el-avatar :size="32" :src="row.avatar" />
                        </template>
                    </el-table-column>
                    <el-table-column label="创建时间" prop="signTime" />
                </Table>
            </div>
        </div>
        <div class="select-member-footer">
            <el-button @click="close" type="default">关闭</el-button>
            <el-button type="primary" @click="submit">确认选择</el-button>
        </div>
    </el-dialog>
</template>
<script>
export default {
    props: {
        memberList: {
            type: Array,
            default: () => []
        }
    },
    data() {
        return {
            visible: false,
            searchKeyword: '',
            selectData: [],
            defaultSelected: [] // 默认选中的行
        }
    },
    computed: {
        filteredTableData() {
            if (!this.searchKeyword) {
                return this.memberList;
            }
            const keyword = this.searchKeyword.toLowerCase();
            return this.memberList.filter(item =>
                (item.nickName && item.nickName.toLowerCase().includes(keyword)) ||
                (item.phone && item.phone.includes(keyword))
            );
        }
    },
    methods: {
        setSelection(selected) {
            this.selectData = selected
            this.$nextTick(() => {
                // 设置新选中
                this.memberList.forEach(row => {
                    if (selected.some(i => i.userId === row.userId)) {
                        this.$refs.memberTable.toggleRowSelection(row, true)
                    }
                })
            })
        },
        openDialog() {
            this.setSelection(this.selectData);
        },
        handleSelectionChange(val) {
            this.selectData = val
        },
        open(data = [], defaultSelected = []) {
            this.memberList = data
            this.visible = true
            this.defaultSelected = defaultSelected
            // 在下一个tick中设置选中状态,确保表格已经渲染完成
            this.$nextTick(() => {
                this.setDefaultSelection();
            });
        },
        close() {
            this.visible = false
        },
        submit() {
            this.$emit('submit', this.selectData)
        },
        tableRowClassName({ row, rowIndex }) {
            if (this.selectData.findIndex(item => item.userId === row.userId) != -1) {
                return 'select-row';
            }
            return '';
        },
        setDefaultSelection() {
            if (this.defaultSelected && this.defaultSelected.length > 0) {
                this.defaultSelected.forEach(row => {
                    const targetRow = this.memberList.find(item => item.userId === row.userId);
                    if (targetRow) {
                        this.$refs.memberTable.toggleRowSelection(targetRow, true);
                    }
                });
            }
        }
    }
}
</script>
<style scoped lang="less">
.select-member-content {
    .select-member-content-right {
        margin-bottom: 31px;
        &-header {
            display: flex;
            align-items: center;
            margin-top: 5px;
            justify-content: space-between;
            margin-bottom: 21px;
            &-text {
                flex-shrink: 0;
                font-weight: 500;
                font-size: 16px;
                line-height: 16px;
                color: #222222;
                font-family: 'SourceHanSansCN-Medium';
                margin-right: 20px;
            }
            &-search {
                display: flex;
                align-items: center;
                ::v-deep .el-input {
                    margin-right: 12px;
                }
            }
        }
    }
}
</style>
laboratory/src/components/TableSlot/index.vue
@@ -13,7 +13,9 @@
            </template>
            <template v-if="$slots.table">
                <Table :data="tableData" :total="total" :height="height" :queryForm="queryForm"
                    @handleCurrentChange="handleCurrentChange" @handleSizeChange="handleSizeChange">
                    @handleCurrentChange="handleCurrentChange"
                    @handleSizeChange="handleSizeChange"
                    @selection-change="handleSelectionChange">
                    <slot name="table"></slot>
                </Table>
            </template>
@@ -60,6 +62,9 @@
        },
        handleSizeChange(size) {
            this.$emit('handleSizeChange', size)
        },
        handleSelectionChange(selection) {
            this.$emit('selection-change', selection)
        }
    }
}
laboratory/src/components/approvalProcess/index.vue
@@ -1,44 +1,25 @@
<template>
  <div class="approval-process">
    <el-timeline>
      <el-timeline-item
        v-for="(activity, index) in processData"
        :key="index"
        :type="activity.type"
      >
      <el-timeline-item v-for="(activity, index) in processData" :key="index" :type="activity.type">
        <div v-if="activity.mode === 'card'">
          <div
            class="member-list-card"
            v-for="(group, groupIndex) in activity.groups"
            :key="groupIndex"
          >
          <div class="member-list-card" v-for="(group, groupIndex) in activity.groups" :key="groupIndex">
            <div class="member-item">
              <div class="member-title">
                {{ group.title }}
              </div>
              <div class="flex-over">
                <div
                  class="people-list"
                  v-for="(member, memberIndex) in group.members"
                  :key="memberIndex"
                >
                <div class="people-list" v-for="(member, memberIndex) in group.members" :key="memberIndex">
                  <div class="people-item">
                    <img :src="member.avatar" alt="" class="people-img" />
                    <div class="member-name">{{ member.name }}</div>
                  </div>
                  <div class="member-status">
                    <div
                      class="member-status-text"
                      :class="
                        member.status === 'approved' ? 'success' : 'warning'
                      "
                    >
                      {{ member.status}}
                    <div class="member-status-text" :class="member.status === 'approved' ? 'success' : 'warning'
                      ">
                      {{ member.status }}
                    </div>
                    <div
                      v-if="member.status === '已确认'"
                      class="member-status-time"
                    >
                    <div v-if="member.status === '已确认'" class="member-status-time">
                      {{ member.approveTime }}
                    </div>
                  </div>
@@ -47,18 +28,17 @@
            </div>
          </div>
        </div>
        <div
          v-else
          class="approval-process-item"
          :class="activity.type === 'primary' ? '' : 'approval-process-item1'"
        >
          <div
            v-for="(field, idx) in activity.fields"
            :key="idx"
            style="margin-bottom: 6px"
          >
            <span>{{ field.label }}</span>
            <span>{{ field.value }}</span>
        <div v-else class="approval-process-item" :class="activity.type === 'primary' ? '' : 'approval-process-item1'">
          <div v-for="(field, idx) in activity.fields" :key="idx" style="margin-bottom: 6px">
            <span v-if="field.type == 'img'">
              <el-image style="width: 50px; height: 50px;margin-left: 40px;" v-if="field.value" :src="field.value"
                    :preview-src-list="[field.value]">
                  </el-image>
            </span>
            <span v-else>
              <span>{{ field.label }}</span>
              <span>{{ field.value }}</span>
            </span>
          </div>
        </div>
      </el-timeline-item>
@@ -217,27 +197,21 @@
    // }
    &:nth-child(1) {
      background: linear-gradient(
        to bottom,
        rgba(5, 160, 193, 0.2) 0%,
        rgba(5, 242, 194, 0) 70%
      );
      background: linear-gradient(to bottom,
          rgba(5, 160, 193, 0.2) 0%,
          rgba(5, 242, 194, 0) 70%);
    }
    &:nth-child(2) {
      background: linear-gradient(
        to bottom,
        rgba(255, 77, 79, 0.2) 0%,
        rgba(255, 242, 194, 0) 70%
      );
      background: linear-gradient(to bottom,
          rgba(255, 77, 79, 0.2) 0%,
          rgba(255, 242, 194, 0) 70%);
    }
    &:nth-child(3) {
      background: linear-gradient(
        to bottom,
        rgba(250, 199, 20, 0.21) 0%,
        rgba(255, 242, 194, 0) 70%
      );
      background: linear-gradient(to bottom,
          rgba(250, 199, 20, 0.21) 0%,
          rgba(255, 242, 194, 0) 70%);
    }
    .member-item {
@@ -256,12 +230,14 @@
        text-align: left;
        padding-left: 17px;
      }
      .flex-over {
        margin-top: 12px;
        flex: 1;
        overflow: auto;
        height: 100%;
      }
      .people-list {
        display: flex;
        justify-content: space-between;
@@ -270,6 +246,7 @@
        margin-top: 12px;
        display: flex;
        .people-item {
          display: flex;
          align-items: center;
@@ -281,12 +258,14 @@
          border-radius: 50%;
          margin-right: 10px;
        }
        .member-status {
          align-items: flex-end;
          display: flex;
          flex-direction: column;
          justify-content: space-between;
        }
        .member-status-text {
          width: 52px;
          height: 22px;
@@ -295,6 +274,7 @@
          text-align: center;
          border: 1px solid rgba(0, 0, 0, 0.15);
        }
        .warning {
          background: #fffbe6;
          border-radius: 4px;
@@ -304,12 +284,14 @@
          font-size: 12px;
          color: #faad14;
        }
        .success {
          font-family: PingFangSC, PingFang SC;
          font-weight: 400;
          font-size: 12px;
          color: rgba(0, 0, 0, 0.88);
        }
        .member-status-time {
          font-family: PingFang-SC, PingFang-SC;
          font-weight: 500;
laboratory/src/router/index.js
@@ -388,12 +388,22 @@
            {
                path: "add",
                meta: {
                    title: "新增可行报告",
                    title: "新增可研报告",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityStudy/add.vue"),
            },
            {
                path: "edit",
                meta: {
                    title: "编辑可研报告",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityStudy/add.vue"),
            },
            {
                path: "feasibilityReport",
                meta: {
@@ -403,6 +413,26 @@
                component: () => import("../views/reportLibrary/feasibilityReport/index.vue"),
            },
            {
                path: "addFeasibility",
                meta: {
                    title: "新增可行报告",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityReport/add.vue"),
            },
            {
                path: "editFeasibility",
                meta: {
                    title: "编辑可行报告",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityReport/add.vue"),
            },
            {
                path: "processDevelopment",
                meta: {
                    title: "工艺开发工具",
@@ -410,6 +440,28 @@
                },
                component: () => import("../views/reportLibrary/processDevelopment/index.vue"),
            },
            {
                path: "addProcessDevelopment",
                meta: {
                    title: "新增工艺开发工具",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/processDevelopment/add.vue"),
            },
            {
                path: "editProcessDevelopment",
                meta: {
                    title: "编辑工艺开发工具",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/processDevelopment/add.vue"),
            },
            {
                path: "verificationRelease",
                meta: {
laboratory/src/views/dataManagement/approvalPlan/list.vue
@@ -171,7 +171,7 @@
      ],
      approvalDialogVisible: false,
      approvalDialogType: "approve",
      currentApprovalData: null,
      currentApprovalData: [],
      // 确认弹窗相关数据
      changeStatus: false,
      changeStatusTitle: "",
laboratory/src/views/dataManagement/confirmation-sheet/components/add-test-item.vue
@@ -49,6 +49,10 @@
    dialogVisible: {
      type: Boolean,
      default: false
    },
    editData: {
      type: Object,
      default: () => null
    }
  },
  data() {
@@ -59,7 +63,9 @@
        termType: 1,
        termMethodCode: '',
        termMethod: '',
        sampleRequire: ''
        sampleRequire: '',
        status: 1,
        testId: ''
      },
      rules: {
        termCode: [
@@ -83,17 +89,48 @@
      }
    }
  },
  watch: {
    dialogVisible(val) {
      if (val && this.editData) {
        // 编辑模式,回显数据
        this.$nextTick(() => {
          this.form = {
            ...this.editData,
            termType: Number(this.editData.termType) // 确保termType是数字类型
          }
        })
      } else if (!val) {
        // 关闭对话框时重置表单
        this.resetForm()
      }
    }
  },
  methods: {
    submitForm() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.$emit('confirm', this.form)
          // 提交前确保termType是数字类型
          const submitData = {
            ...this.form,
            termType: Number(this.form.termType)
          }
          this.$emit('confirm', submitData)
          this.resetForm()
        }
      })
    },
    resetForm() {
      this.$refs.form.resetFields()
      this.form = {
        termCode: '',
        termName: '',
        termType: 1,
        termMethodCode: '',
        termMethod: '',
        sampleRequire: '',
        status: 1,
        testId: ''
      }
    }
  }
}
laboratory/src/views/dataManagement/confirmation-sheet/components/add.vue
@@ -6,16 +6,16 @@
      <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
        选择实验调度</el-button>
    </div>
    <Table :tableData="tableData" :total="total" :height="null">
    <Table :data="tableData" :total="total" :height="null">
      <template>
        <el-table-column prop="planCode" label="所属项目课题方案"></el-table-column>
        <el-table-column prop="planName" 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="stage" label="    实验开始时间"></el-table-column>
        <el-table-column prop="stage" 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="testName" label="实验名称"></el-table-column>
        <el-table-column prop="notifyTime" label="通知时间"></el-table-column>
        <el-table-column prop="startTime" label="实验开始时间"></el-table-column>
        <el-table-column prop="endTime" label="实验结束时间"></el-table-column>
        <el-table-column prop="participants" label="参加人员"></el-table-column>
        <el-table-column prop="status" label="状态"></el-table-column>
      </template>
    </Table>
    <div class="header-title-left" style="margin-top: 60px;">
@@ -25,25 +25,35 @@
        新增检测项</el-button>
      <span>【注意:这里有多少个检测项 系统就会自动创建对应数量的《检测项的检验方法及数据记录》】</span>
    </div>
    <Table :tableData="tableData" :total="total" :height="null">
    <Table :data="testItems" :total="testItems.length" :height="null">
      <template>
        <el-table-column prop="planCode" label="序号"></el-table-column>
        <el-table-column prop="planName" 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="stage" label="    检测方法编号"></el-table-column>
        <el-table-column prop="stage" 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 type="index" label="序号" width="80"></el-table-column>
        <el-table-column prop="termName" label="检测项名称"></el-table-column>
        <el-table-column prop="termCode" label="检测项编号"></el-table-column>
        <el-table-column prop="termType" label="定性/定量">
          <template slot-scope="scope">
            {{ scope.row.termType === 1 ? '定性' : '定量' }}
          </template>
        </el-table-column>
        <el-table-column prop="termMethodCode" label="检测方法编号"></el-table-column>
        <el-table-column prop="termMethod" label="检测方法"></el-table-column>
        <el-table-column prop="sampleRequire" label="收样要求"></el-table-column>
        <el-table-column label="操作" width="150">
          <template slot-scope="scope">
            <el-button type="text" @click="handleEditTestItem(scope.row)">编辑</el-button>
            <el-button type="text" @click="handleDeleteTestItem(scope.$index)" class="delete-btn">删除</el-button>
          </template>
        </el-table-column>
      </template>
    </Table>
    <div class="btn_box flex ">
      <el-button type="primary" @click="handleSubmit">提交确认单</el-button>
      <el-button @click="$router.go(-1)">存草稿</el-button>
      <el-button @click="handleSaveDraft">存草稿</el-button>
    </div>
    <experimentalScheduling :show="showScheduling"/>
    <experimentalScheduling :show="showScheduling" @close="showScheduling = false" @submit="setSelectedScheduling"/>
    <add-test-item
      :dialogVisible="testItemDialogVisible"
      :editData="currentTestItem"
      @close="handleTestItemDialogClose"
      @confirm="handleTestItemConfirm"
    />
@@ -60,6 +70,7 @@
import experimentalScheduling from './experimental-scheduling.vue';
import AddTestItem from './add-test-item.vue'
import ConfirmDialog from './confirm-dialog.vue'
import { add, update, getDetail } from '../service'
export default {
  name: 'AddConfirmationSheet',
@@ -93,6 +104,16 @@
        testName: '', // 实验名称
        sampleCode: '' // 取样单编号
      },
      selectedScheduling: null, // 添加选中的实验调度数据
      currentTestItem: null, // 当前编辑的检测项
      formData: {
        id: '', // 确认单ID
        dispatchId: '', // 实验调度ID
        auditStatus: -1, // 审核状态,默认草稿
        testMethodConfirmSheetTerms: [], // 确认单检测项
        confirmSign: '', // 签字
        signTime: '', // 签字时间
      }
    };
  },
  computed: {
@@ -102,10 +123,52 @@
  },
  watch: {},
  created() {
    // 判断是否是编辑模式
    const id = this.$route.query.id
    if (id) {
      this.getDetailData(id)
    }
  },
  mounted() { },
  methods: {
    setSelectedScheduling(data) {
      console.log('data',data)
      if (!data || data.length === 0) return;
      const selectedData = data[0]; // 获取选中的第一条数据
      this.selectedScheduling = selectedData;
      // 更新表格数据
      this.tableData = [{
        planCode: selectedData.projectName || '', // 所属项目课题方案
        planName: selectedData.experimentCode || '', // 实验编号
        testName: selectedData.experimentName || '', // 实验名称
        notifyTime: selectedData.experimentDate || '', // 通知时间
        startTime: selectedData.experimentStartTime || '', // 实验开始时间
        endTime: selectedData.experimentEndTime || '', // 实验结束时间
        participants: selectedData.participantsName || '', // 参加人员
        status: this.getStatusText(selectedData.status) // 状态
      }];
      this.total = this.tableData.length;
      // 更新确认表单数据
      this.confirmFormData = {
        planName: selectedData.projectName || '',
        testCode: selectedData.experimentCode || '',
        testName: selectedData.experimentName || '',
        sampleCode: selectedData.experimentCode || '' // 使用实验编号作为取样单编号
      };
    },
    // 添加状态转换方法
    getStatusText(status) {
      const statusMap = {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      };
      return statusMap[status] || "未知";
    },
    setSelectedIds(arr, selectKeyList) {
      function traverse(item) {
        item.selected = selectKeyList.includes(item.menuId);
@@ -235,37 +298,213 @@
      })
    },
    showAddTestItem() {
      this.currentTestItem = null // 清空当前编辑项
      this.testItemDialogVisible = true
    },
    handleTestItemDialogClose() {
      this.testItemDialogVisible = false
      this.currentTestItem = null
    },
    handleTestItemConfirm(formData) {
      // 将新增的检测项添加到列表中
      this.testItems.push({
        ...formData,
        index: this.testItems.length + 1
      })
      if (this.currentTestItem) {
        // 编辑模式
        const index = this.testItems.findIndex(item => item.termCode === this.currentTestItem.termCode)
        if (index !== -1) {
          this.testItems.splice(index, 1, {
            ...formData,
            id: this.currentTestItem.id // 保留原有ID
          })
        }
      } else {
        // 新增模式
        this.testItems.push({
          ...formData,
          id: this.generateUniqueId() // 生成唯一ID
        })
      }
      this.testItemDialogVisible = false
      this.$message.success('添加成功')
      this.currentTestItem = null
      this.$message.success(this.currentTestItem ? '编辑成功' : '添加成功')
    },
    handleEditTestItem(row) {
      this.currentTestItem = { ...row } // 深拷贝当前行数据
      this.testItemDialogVisible = true
    },
    handleDeleteTestItem(index) {
      this.$confirm('确认删除该检测项吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.testItems.splice(index, 1)
        this.$message.success('删除成功')
      }).catch(() => {})
    },
    generateUniqueId() {
      return 'test_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
    },
    handleSubmit() {
      // 这里可以添加表单验证逻辑
      if (this.testItems.length === 0) {
        this.$message.warning('请至少添加一个检测项')
        return
      }
      if (!this.selectedScheduling) {
        this.$message.warning('请选择实验调度')
        return
      }
      // 设置确认弹窗的数据
      this.confirmFormData = {
        planName: '项目名称示例',
        testCode: '实验编号示例',
        testName: '实验名称示例',
        sampleCode: '取样单编号示例'
        planName: this.selectedScheduling.projectName || '', // 所属项目课题方案
        testCode: this.selectedScheduling.experimentCode || '', // 实验编号
        testName: this.selectedScheduling.experimentName || '', // 实验名称
        sampleCode: this.selectedScheduling.experimentCode || '' // 取样单编号
      }
      this.confirmDialogVisible = true
    },
    handleConfirmSubmit(signatureImage) {
      // 处理最终提交逻辑
      console.log('提交确认单,签名图片:', signatureImage)
      // TODO: 调用提交API
      this.$message.success('提交成功')
      this.$router.push('/dataManagement/confirmation-sheet')
    }
    async handleConfirmSubmit(signatureImage) {
      try {
        // 构建提交数据
        const submitData = {
          dispatchId: this.selectedScheduling.id, // 实验调度ID
          auditStatus: 1, // 待确认状态
          confirmSign:'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'|| signatureImage, // 签字图片
          signTime: new Date().toISOString(), // 签字时间
          testMethodConfirmSheetTerms: this.testItems.map(item => ({
            id: item.id, // 保留原有ID(编辑时使用)
            termCode: item.termCode,
            termName: item.termName,
            termType: item.termType,
            termMethodCode: item.termMethodCode,
            termMethod: item.termMethod,
            sampleRequire: item.sampleRequire,
            status: 2, // 已填写状态
            testId: item.testId
          }))
        }
        // 打印提交参数
        console.log('提交确认单参数:', JSON.stringify(submitData, null, 2))
        // 根据是否有 id 决定调用新增还是更新接口
        if (this.formData.id) {
          submitData.id = this.formData.id
          console.log('调用更新接口,参数:', JSON.stringify(submitData, null, 2))
          await update(submitData)
          this.$message.success('更新成功')
        } else {
          console.log('调用新增接口,参数:', JSON.stringify(submitData, null, 2))
          await add(submitData)
          this.$message.success('提交成功')
        }
        this.confirmDialogVisible = false // 关闭弹窗
        this.$router.go(-1)
      } catch (error) {
        this.$message.error((this.formData.id ? '更新' : '提交') + '失败:' + (error.message || '未知错误'))
      }
    },
    async handleSaveDraft() {
      try {
        if (!this.selectedScheduling) {
          this.$message.warning('请选择实验调度')
          return
        }
        // 构建草稿数据
        const draftData = {
          dispatchId: this.selectedScheduling.id,
          auditStatus: -1, // 草稿箱状态
          testMethodConfirmSheetTerms: this.testItems.map(item => ({
            id: item.id, // 保留原有ID(编辑时使用)
            termCode: item.termCode,
            termName: item.termName,
            termType: item.termType,
            termMethodCode: item.termMethodCode,
            termMethod: item.termMethod,
            sampleRequire: item.sampleRequire,
            status: -1, // 草稿箱状态
            testId: item.testId
          }))
        }
        // 打印草稿参数
        console.log('保存草稿参数:', JSON.stringify(draftData, null, 2))
        // 根据是否有 id 决定调用新增还是更新接口
        if (this.formData.id) {
          draftData.id = this.formData.id
          console.log('调用更新草稿接口,参数:', JSON.stringify(draftData, null, 2))
          await update(draftData)
          this.$message.success('更新草稿成功')
        } else {
          console.log('调用新增草稿接口,参数:', JSON.stringify(draftData, null, 2))
          await add(draftData)
          this.$message.success('保存草稿成功')
        }
        this.$router.push('/dataManagement/confirmation-sheet')
      } catch (error) {
        this.$message.error((this.formData.id ? '更新' : '保存') + '草稿失败:' + (error.message || '未知错误'))
      }
    },
    async getDetailData(id) {
      try {
        const res = await getDetail({ id })
        if (res.data) {
          // 设置实验调度数据
          this.selectedScheduling = {
            id: res.data.dispatchId,
            // ... 其他调度相关字段
          }
          // 设置检测项数据
          this.testItems = res.data.testMethodConfirmSheetTerms.map(item => ({
            ...item,
            id: item.id // 使用后端返回的ID
          }))
          // 设置表单数据
          this.formData = {
            id: res.data.id,
            dispatchId: res.data.dispatchId,
            auditStatus: res.data.auditStatus,
            confirmSign: res.data.confirmSign,
            signTime: res.data.signTime
          }
        }
      } catch (error) {
        this.$message.error('获取详情失败:' + (error.message || '未知错误'))
      }
    },
    async handleEditSubmit(signatureImage) {
      try {
        const submitData = {
          id: this.formData.id,
          dispatchId: this.selectedScheduling.id,
          auditStatus: 1,
          confirmSign: signatureImage,
          signTime: new Date().toISOString(),
          testMethodConfirmSheetTerms: this.testItems.map(item => ({
            id: item.id, // 保留原有ID
            termCode: item.termCode,
            termName: item.termName,
            termType: item.termType,
            termMethodCode: item.termMethodCode,
            termMethod: item.termMethod,
            sampleRequire: item.sampleRequire,
            status: item.status,
            testId: item.testId
          }))
        }
        await update(submitData)
        this.$message.success('更新成功')
        this.$router.push('/dataManagement/confirmation-sheet')
      } catch (error) {
        this.$message.error('更新失败:' + (error.message || '未知错误'))
      }
    },
  },
};
</script>
laboratory/src/views/dataManagement/confirmation-sheet/components/confirm-dialog.vue
@@ -23,19 +23,19 @@
        </div>
      </div>
      <Table :tableData="sampleData" :total="0" :height="null">
      <Table :data="sampleData" :total="0" :height="null">
        <template>
          <el-table-column prop="index" label="序号" width="60" align="center"></el-table-column>
          <el-table-column prop="testName" label="检测项名称"></el-table-column>
          <el-table-column prop="testCode" label="检测项编号"></el-table-column>
          <el-table-column prop="testType" label="定性/定量" align="center">
          <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
          <el-table-column prop="termName" label="检测项名称"></el-table-column>
          <el-table-column prop="termCode" label="检测项编号"></el-table-column>
          <el-table-column prop="termType" label="定性/定量" align="center">
            <template slot-scope="scope">
              {{ scope.row.testType === 1 ? '定性' : '定量' }}
              {{ scope.row.termType === 1 ? '定性' : '定量' }}
            </template>
          </el-table-column>
          <el-table-column prop="methodCode" label="检测方法编号"></el-table-column>
          <el-table-column prop="methodName" label="检测方法"></el-table-column>
          <el-table-column prop="requirements" label="收样要求" show-overflow-tooltip></el-table-column>
          <el-table-column prop="termMethodCode" label="检测方法编号"></el-table-column>
          <el-table-column prop="termMethod" label="检测方法"></el-table-column>
          <el-table-column prop="sampleRequire" label="收样要求" show-overflow-tooltip></el-table-column>
        </template>
      </Table>
@@ -53,7 +53,7 @@
    <div slot="footer" class="dialog-footer">
      <el-button @click="handleClose">取 消</el-button>
      <el-button type="primary" @click="handleConfirm" :disabled="!imgSrc">确 认</el-button>
      <el-button type="primary" @click="handleConfirm">确 认</el-button>
    </div>
    <SignatureCanvas
@@ -115,8 +115,12 @@
    },
    handleConfirm() {
      if (!this.imgSrc) {
        this.$message.warning("请先完成签名确认");
        return;
        this.$message({
          type: 'warning',
          message: '请先完成签名确认',
          duration: 2000
        })
        return
      }
      this.$emit("confirm", this.imgSrc);
      this.handleClose();
laboratory/src/views/dataManagement/confirmation-sheet/components/experimental-scheduling.vue
@@ -1,27 +1,33 @@
<template>
    <div>
        <el-dialog title="选择实验调度" :visible.sync="show" width="80%">
            <TableCustom :queryForm="form" :tableData="tableData" :total="total" :height="null">
        <el-dialog title="选择实验调度" :visible.sync="show" width="80%" @close="$emit('close', false)">
            <TableCustom
            :queryForm="form" :tableData="tableData"
            :total="total" :height="null"
             @handleCurrentChange="handleCurrentChange"
             @selection-change="changeSelectedRows"
             @handleSizeChange="handleSizeChange">
                <template #search>
                    <el-form :model="form" label-width="140px" inline>
                        <el-form-item label="所属项目课题方案:">
                            <el-input v-model="form.planName" placeholder="请输入"></el-input>
                            <el-input v-model="form.projectName" placeholder="请输入"></el-input>
                        </el-form-item>
                        <el-form-item label="实验编号:">
                            <el-input v-model="form.planCode" placeholder="请输入"></el-input>
                            <el-input v-model="form.experimentCode" placeholder="请输入"></el-input>
                        </el-form-item>
                        <el-form-item label="实验名称:">
                            <el-input v-model="form.creator" placeholder="请输入"></el-input>
                            <el-input v-model="form.experimentName" placeholder="请输入"></el-input>
                        </el-form-item>
                        <el-form-item label="通知时间:">
                            <el-date-picker v-model="form.createTime" type="daterange" range-separator="至"
                                start-placeholder="开始日期" end-placeholder="结束日期"
                                value-format="yyyy-MM-dd"></el-date-picker>
                                value-format="yyyy-MM-dd" @change="handleDateChange"></el-date-picker>
                        </el-form-item>
                        <el-form-item label="状态:">
                            <el-select v-model="form.status" placeholder="请选择">
                                <el-option label="待确认" value="待确认"></el-option>
                                <el-option label="已确认" value="已确认"></el-option>
                                <el-option label="全部" value=""></el-option>
                                <el-option label="待确认" value="1"></el-option>
                                <el-option label="已确认" value="2"></el-option>
                            </el-select>
                        </el-form-item>
                        <el-form-item label="" class="search-btn-box">
@@ -32,44 +38,157 @@
                </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="planName" label="实验名称"></el-table-column>
                    <el-table-column prop="stage" label="通知时间"></el-table-column>
                    <el-table-column prop="stage" label="实验开始时间"></el-table-column>
                    <el-table-column prop="stage" 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 type="selection" width="55"></el-table-column>
                    <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
                    <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
                    <el-table-column prop="experimentName" label="实验名称"></el-table-column>
                    <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
                    <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
                    <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
                    <el-table-column prop="participantsName" label="参加人员"></el-table-column>
                    <el-table-column prop="status" label="状态">
                        <template slot-scope="scope">
                            <el-tag :type="getStatusType(scope.row.status)">
                                {{ getStatusText(scope.row.status) }}
                            </el-tag>
                        </template>
                    </el-table-column>
                </template>
            </TableCustom>
            <span slot="footer" class="select-member-footer">
                <el-button @click="show = false">取 消</el-button>
                <el-button type="primary" @click="show = false">确 定</el-button>
                <el-button @click="$emit('close', false)">取 消</el-button>
                <el-button type="primary" @click="handleConfirm">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>
<script>
import { getDispatchList } from "../service";
export default {
    props: ['show'],
    props: {
        show: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            form: {},
            form: {
                projectName: "",
                experimentCode: "",
                experimentName: "",
                startTime: "",
                endTime: "",
                status: "",
                pageNum: 1,
                pageSize: 10
            },
            tableData: [],
            total: 0
            total: 0,
            selectedRows: []
        }
    },
    watch: {
        show(val) {
            if (val) {
                this.getTableData();
            }
        }
    },
    methods: {
        handleDateChange(val) {
            if (val) {
                this.form.startTime = val[0];
                this.form.endTime = val[1];
            } else {
                this.form.startTime = "";
                this.form.endTime = "";
            }
        },
        resetForm() {
            this.form = {
                projectName: "",
                experimentCode: "",
                experimentName: "",
                startTime: "",
                endTime: "",
                status: "",
                pageNum: 1,
                pageSize: 10
            };
            this.getTableData();
        },
        handleSearch() {
            this.form.pageNum = 1;
            this.getTableData();
        },
        getStatusType(status) {
            const statusMap = {
                "-1": "info",
                "1": "warning",
                "2": "success",
                "3": "info"
            };
            return statusMap[status] || "info";
        },
        getStatusText(status) {
            const statusMap = {
                "-1": "草稿箱",
                "1": "待确认",
                "2": "已确认",
                "3": "已封存"
            };
            return statusMap[status] || "未知";
        },
        getTableData() {
            const params = {
                ...this.form
            };
            getDispatchList(params).then(res => {
                console.log('222222222222',res)
                if (res) {
                    this.tableData = res || [];
                    this.total = res.length || 0;
                }
            });
        },
        handleCurrentChange(val) {
            this.form.pageNum = val;
            this.getTableData();
        },
        handleSizeChange(val) {
            this.form.pageSize = val;
            this.form.pageNum = 1;
            this.getTableData();
        },
        changeSelectedRows(val) {
            console.log('val',val)
            this.selectedRows = val;
        },
        handleCancel() {
            this.$emit('update:show', false);
        },
        handleConfirm() {
            if (!this.selectedRows || this.selectedRows.length === 0) {
                this.$message.warning('请至少选择一条数据');
                return;
            }
            if (this.selectedRows.length != 1) {
                this.$message.warning('请选择一条数据');
                return;
            }
            this.$emit('submit', this.selectedRows);
            this.$emit('close', false);
        }
    }
}
</script>
<style lang="less" scoped>
.select-member-footer {
    display: flex;
    justify-content: flex-end;
}
</style>
laboratory/src/views/dataManagement/confirmation-sheet/components/review-dialog.vue
@@ -1,11 +1,6 @@
<template>
  <el-dialog
    title="审核检测方法确认单"
    :visible.sync="visible"
    width="80%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
  <el-dialog title="审核检测方法确认单" :visible="dialogVisible" width="80%" :close-on-click-modal="false" @close="handleClose"
    v-loading="loading">
    <div class="approval-dialog">
      <div class="approval-content">
        <div class="approval-content-card">
@@ -14,39 +9,46 @@
            <div class="basic-info">
              <div class="info-header">
                <div class="info-item">
                  <span class="label">所属项目课题方案:{{ formData.planName }}</span>
                  <span class="label">所属项目课题方案:{{ formData.projectName }}</span>
                </div>
                <div class="info-item">
                  <span class="label">实验名称:{{ formData.testName }}</span>
                  <span class="label">实验名称:{{ formData.experimentName }}</span>
                </div>
                <div class="info-item">
                  <span class="label">所属实验编号:{{ formData.testCode }}</span>
                  <span class="label">所属实验编号:{{ formData.experimentCode }}</span>
                </div>
              </div>
              <div class="info-header">
                <div class="info-item">
                  <span class="label">提交人:{{ formData.submitter }}</span>
                  <span class="label">提交人:{{ formData.createBy }}</span>
                </div>
                <div class="info-item">
                  <span class="label">提交人签名:{{ formData.submitterSignature }}</span>
                  <span class="label">提交人签名:</span>
                  <el-image style="width: 50px; height: 50px" v-if="formData.confirmSign" :src="formData.confirmSign"
                    :preview-src-list="[formData.confirmSign]">
                  </el-image>
                </div>
                <div class="info-item">
                  <span class="label">提交时间:{{ formData.submitTime }}</span>
                  <span class="label">提交时间:{{ formData.signTime }}</span>
                </div>
              </div>
            </div>
            <!-- 检测项表格 -->
            <div class="table-wrapper">
              <Table :tableData="sampleData" :total="0" :height="null">
              <Table :data="formData.testMethodConfirmSheetTerms" :total="0" :height="null">
                <template>
                  <el-table-column prop="index" label="序号" width="60" fixed></el-table-column>
                  <el-table-column prop="processTime" label="检验项名称"></el-table-column>
                  <el-table-column prop="sampleName" label="检验项编号"></el-table-column>
                  <el-table-column prop="sampleCode" label="定性/定量"></el-table-column>
                  <el-table-column prop="temperature" label="检测方法编号"></el-table-column>
                  <el-table-column prop="ph" label="检测方法"></el-table-column>
                  <el-table-column prop="waterAmount" label="收样要求"></el-table-column>
                  <el-table-column type="index" label="序号" width="60" fixed></el-table-column>
                  <el-table-column prop="termName" label="检验项名称"></el-table-column>
                  <el-table-column prop="termCode" label="检验项编号"></el-table-column>
                  <el-table-column prop="termType" label="定性/定量">
                    <template slot-scope="scope">
                      {{ scope.row.termType === 1 ? '定性' : '定量' }}
                    </template>
                  </el-table-column>
                  <el-table-column prop="termMethodCode" label="检测方法编号"></el-table-column>
                  <el-table-column prop="termMethod" label="检测方法"></el-table-column>
                  <el-table-column prop="sampleRequire" label="收样要求"></el-table-column>
                </template>
              </Table>
            </div>
@@ -56,12 +58,7 @@
      <div class="approval-flow">
        <div class="flow-content">
          <approval-process
            :status="formData.status"
            :submit-time="formData.submitTime"
            :approver="formData.approver"
            :approve-time="formData.approveTime"
          />
          <approval-process :processData="formData.processData" />
        </div>
      </div>
    </div>
@@ -72,29 +69,22 @@
        <span>签字确认</span>
        <el-button type="primary" class="el-icon-plus" @click="openSignature">签名</el-button>
      </div>
      <img
        v-if="imgSrc"
        :src="imgSrc"
        alt="签名"
        class="signature-preview"
      />
      <img v-if="imgSrc" :src="imgSrc" alt="签名" class="signature-preview" />
    </div>
    <div slot="footer" class="dialog-footer">
      <el-button @click="handleClose">取 消</el-button>
      <el-button type="primary" @click="handleConfirm" :disabled="!imgSrc">确 认</el-button>
      <el-button type="primary" @click="handleConfirm" :disabled="!imgSrc" v-if="type === 'review'">确 认</el-button>
    </div>
    <SignatureCanvas
      :visible="signatureDialogVisible"
      @confirm="handleSignatureConfirm"
    />
    <SignatureCanvas :visible="signatureDialogVisible" @confirm="handleSignatureConfirm" />
  </el-dialog>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import ApprovalProcess from "@/components/approvalProcess";
import { getDetail, sign } from '../service';
export default {
  name: "ReviewDialog",
@@ -111,63 +101,149 @@
      type: String,
      default: "review",
    },
    formData: {
      type: Object,
      default: () => ({
        planName: '',
        testCode: '',
        testName: '',
        submitter: '',
        submitterSignature: '',
        submitTime: '',
        status: '',
        approver: '',
        approveTime: ''
      }),
    },
    sampleData: {
      type: Array,
      default: () => [],
    },
    id: {
      type: String,
      default: '',
    }
  },
  data() {
    return {
      dialogVisible: false,
      signatureDialogVisible: false,
      imgSrc: "",
      formData: {
        projectName: '',
        experimentCode: '',
        experimentName: '',
        auditPersonId: '',
        auditSign: '',
        auditStatus: '',
        auditTime: '',
        confirmSign: '',
        dispatchId: '',
        signTime: '',
        testMethodConfirmSheetTerms: [],
        processData: []
      },
      loading: false
    };
  },
  watch: {
    visible(val) {
      this.dialogVisible = val;
      if (val && this.id) {
        this.getDetailData();
      }
    }
  },
  methods: {
    async getDetailData() {
      try {
        this.loading = true;
        const res = await getDetail({ id: this.id });
        console.log('res', res)
        if (res) {
          this.formData = res
          // 组装流程数据
          let processData = [];
          // 提交节点
          processData.push({
            type: "primary",
            mode: "list",
            fields: [
              { label: "提交人:", value: res.createBy || "" },
              { label: ' ', value: res.confirmSign || "", type: 'img' },
              { label: "提交时间:", value: res.createTime || "" },
            ],
          });
          if (res.auditStatus == 2) {
            processData.push({
              type: "primary",
              mode: "list",
              fields: [
                { label: "审核人:", value: res.auditPersonName || "" },
                { label: ' ', value: res.auditSign || "", type: 'img' },
                { label: "审核时间:", value: res.auditTime || "" },
              ],
            });
          } else if (res.auditStatus == 1) {
            processData.push({
              type: "warning",
              mode: "list",
              fields: [
                { label: "等待审核" },
              ],
            });
          }
          this.formData.processData = processData
        } else {
          this.$message.error(res.msg || '获取详情失败');
        }
      } catch (error) {
        console.error('获取详情失败:', error);
        this.$message.error('获取详情失败');
      } finally {
        this.loading = false;
      }
    },
    handleClose() {
      this.$emit("update:visible", false);
      this.dialogVisible = false;
      this.$emit("close", false);
      this.imgSrc = "";
      this.formData = {
        projectName: '',
        experimentCode: '',
        experimentName: '',
        auditPersonId: '',
        auditSign: '',
        auditStatus: '',
        auditTime: '',
        confirmSign: '',
        dispatchId: '',
        signTime: '',
        testMethodConfirmSheetTerms: [],
        processData: []
      };
    },
    openSignature() {
      this.signatureDialogVisible = true;
    },
    handleSignatureConfirm(imageData) {
      this.signatureDialogVisible = false;
      this.imgSrc = imageData;
      this.imgSrc = 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg';
    },
    handleConfirm() {
      if (!this.imgSrc) {
        this.$message.warning("请先完成签名确认");
        return;
      }
      this.$emit("confirm", this.imgSrc);
      this.handleClose();
      // 组装签名数据
      const params = {
        testMethodConfirmSheetId: this.id, // 传递当前确认单id
        confirmSign: this.imgSrc, // 签名图片
      };
      sign(params).then(res => {
        if (res && res.code === 200) {
          this.$message.success('签字成功');
          this.handleClose();
        } else {
          this.$message.error(res.msg || '签字失败');
        }
      }).catch(() => {
        this.$message.error('签字失败');
      });
    },
  },
};
</script>
<style lang="less" scoped>
.approval-dialog {
  display: flex;
  min-height: 60vh;
  max-height: 80vh;
  overflow: hidden;
  @media screen and (max-width: 1024px) {
    flex-direction: column;
  }
@@ -176,7 +252,7 @@
    flex: 1;
    margin: 20px;
    background: #ffffff;
    box-shadow: 0px 4px 12px 4px rgba(0,0,0,0.08);
    box-shadow: 0px 4px 12px 4px rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    overflow-y: auto;
@@ -190,11 +266,12 @@
  .approval-flow {
    width: 405px;
    background: #ffffff;
    box-shadow: 0px 4px 12px 4px rgba(0,0,0,0.08);
    box-shadow: 0px 4px 12px 4px rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    padding: 40px 20px;
    margin: 20px;
    margin-left: 0;
    @media screen and (max-width: 1024px) {
      width: 100%;
      padding: 20px;
@@ -209,7 +286,7 @@
.basic-info {
  padding: 28px 20px;
  background: linear-gradient( 180deg, #05A0C1 0%, #05F2C2 100%);
  background: linear-gradient(180deg, #05A0C1 0%, #05F2C2 100%);
  border-radius: 10px 10px 0 0;
  color: #fff;
@@ -231,6 +308,7 @@
    .info-item {
      flex: 1;
      min-width: 250px;
      display: flex;
      @media screen and (max-width: 768px) {
        min-width: 100%;
@@ -238,8 +316,8 @@
      .label {
        font-size: 14px;
        white-space: normal;
        word-break: break-all;
        // white-space: normal;
        // word-break: break-all;
      }
    }
  }
@@ -250,6 +328,7 @@
  background: #ffffff;
  border-radius: 4px;
  overflow-x: auto;
  ::v-deep .el-table {
    width: 100%;
    min-width: 800px;
@@ -302,8 +381,10 @@
  height: calc(100% - 100px) !important;
  box-shadow: none !important;
}
::v-deep .el-dialog__body {
  padding: 0;
  @media screen and (max-width: 768px) {
    padding: 15px;
  }
@@ -322,4 +403,12 @@
    line-height: 1.5;
  }
}
</style>
.signature-image {
  max-width: 200px;
  max-height: 100px;
  margin-top: 8px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}
</style>
laboratory/src/views/dataManagement/confirmation-sheet/index.vue
@@ -1,6 +1,7 @@
<template>
    <div class="list">
        <TableCustom :queryForm="form" :tableData="tableData" :total="total">
        <TableCustom :queryForm="form" :height="null" :tableData="tableData" :total="total"
            @handleCurrentChange="handleCurrentChange" @handleSizeChange="handleSizeChange">
            <template #search>
                <el-form :model="form" label-width="140px" inline>
                    <el-form-item label="所属项目课题方案:">
@@ -14,11 +15,10 @@
                    </el-form-item>
                    <el-form-item label="通知时间:">
                        <el-date-picker v-model="dateRange" type="daterange" range-separator="至"
                            start-placeholder="开始日期" end-placeholder="结束日期"
                            value-format="yyyy-MM-dd"
                            start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd"
                            @change="handleDateChange"></el-date-picker>
                    </el-form-item>
                    <el-form-item label="状态:" v-if="!isDrafts">
                    <el-form-item label="状态:">
                        <el-select v-model="form.auditStatus" placeholder="请选择">
                            <el-option label="待确认" :value="1"></el-option>
                            <el-option label="已通过" :value="2"></el-option>
@@ -36,48 +36,86 @@
            <template #setting>
                <div class="tableTitle">
                    <div class="flex a-center" style="gap: 16px;">
                        <div class="title pointer" :class="{ active: !isDrafts }" @click="switchToDrafts(false)">检验方法确认单列表</div>
                        <div class="drafts pointer" :class="{ active: isDrafts }" @click="switchToDrafts(true)">草稿箱</div>
                        <div class="title pointer" :class="{ active: !isDrafts }" @click="switchToDrafts(false)">
                            检验方法确认单列表</div>
                        <div class="drafts pointer" :class="{ active: isDrafts }" @click="switchToDrafts(true)">草稿箱
                        </div>
                    </div>
                    <el-button @click="handleAddPlan" class="el-icon-plus" type="primary">
                        新增检测方法确认单</el-button>
                </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="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 label="操作" width="150">
                <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
                <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
                <el-table-column prop="experimentName" label="实验名称"></el-table-column>
                <el-table-column prop="createTime" label="提交时间"></el-table-column>
                <el-table-column prop="createBy" label="提交人"></el-table-column>
                <el-table-column label="状态" width="100">
                    <template slot-scope="scope">
                        <el-button type="text" @click="handleReview(scope.row)">审核</el-button>
                        <el-button type="text" @click="handleRevoke(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>
                        <el-tag :type="getStatusType(scope.row.auditStatus)">
                            {{ getStatusText(scope.row.auditStatus) }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="200">
                    <template slot-scope="scope">
                        <!-- 超级管理员(1)和审批人(2) -->
                        <template v-if="userRole == '1' || userRole == '2'">
                            <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
                        </template>
                        <!-- 工艺工程师(3) -->
                        <template v-if="userRole == '3'">
                            <el-button type="text" @click="handleReview(scope.row)"
                                v-if="scope.row.auditStatus == 1">审核</el-button>
                            <el-button type="text" @click="handleDetail(scope.row)"
                                v-if="scope.row.auditStatus == 2">详情</el-button>
                        </template>
                        <!-- 化验师(4) -->
                        <template v-if="userRole == '4'">
                            <el-button type="text" @click="handleRevoke(scope.row)"
                                v-if="scope.row.auditStatus == 1">撤销</el-button>
                            <el-button type="text" @click="handleDetail(scope.row)"
                                v-if="scope.row.auditStatus === 2 || scope.row.auditStatus === 3">详情</el-button>
                            <el-button type="text" @click="handleEdit(scope.row)"
                                v-if="scope.row.auditStatus === 3">编辑</el-button>
                            <el-button type="text" @click="handleDelete(scope.row)"
                                v-if="scope.row.auditStatus === 3">删除</el-button>
                        </template>
                    </template>
                </el-table-column>
            </template>
        </TableCustom>
        <ShowDelConfirm :show="showRevoke" btnType="primary" @close="showRevoke = false" tip="撤销后,工艺工程师将无法收到此审批信息" okText="确定" title="确认要撤销这条确认单吗?"/>
        <review-dialog
            :visible.sync="reviewDialogVisible"
            :type="dialogType"
            :formData="currentRow"
            :sampleData="sampleData"
            @confirm="handleConfirmSubmit"
        />
        <ShowDelConfirm
            :show="showRevoke"
            btnType="primary"
            v-on:close="showRevoke = false"
            tip="撤销后,工艺工程师将无法收到此审批信息"
            okText="确定"
            title="确认要撤销这条确认单吗?"
            v-on:confirm="handleRevokeConfirm" />
        <ShowDelConfirm
            :show="showDelete"
            :title="deleteTitle"
            :tip="deleteTip"
            v-on:close="showDelete = false"
            v-on:confirm="handleDeleteConfirm" />
        <review-dialog :visible.sync="reviewDialogVisible" :type="dialogType" :id="currentRow.id"
             @close="handleClose" />
    </div>
</template>
<script>
import reviewDialog from './components/review-dialog.vue';
import { getList } from './service'
import ShowDelConfirm from "@/components/showDelConfirm/index.vue";
import { getList, deleteById, revokedSheet } from './service'
export default {
    name: "ConfirmationSheet",
    components: {
        reviewDialog
        reviewDialog,
        ShowDelConfirm
    },
    data() {
        return {
@@ -96,34 +134,43 @@
            showRevoke: false,
            tableData: [],
            total: 0,
            editorContent: '',
            reviewDialogVisible: false,
            dialogType: 'review',
            currentRow: {},
            sampleData: [],
            userRole: null,
            showDelete: false,
            deleteTitle: "",
            deleteTip: "",
            currentDeleteRow: null,
        };
    },
    mounted() {
        // 获取用户角色
        const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
        this.userRole = userInfo.roleType;
        this.getTableData();
    },
    methods: {
        async getTableData() {
            try {
                const params = { ...this.form };
                if (this.isDrafts) {
                    params.auditStatus = -1;
                }
                const params = {
                    ...this.form,
                    pageNum: Number(this.form.pageNum),
                    pageSize: Number(this.form.pageSize),
                    auditStatus: this.isDrafts ? -1 : Number(this.form.auditStatus) || ''
                };
                const res = await getList(params);
                if (res.code === 200) {
                    this.tableData = res.data.list || [];
                    this.tableData = res.data.records || [];
                    this.total = res.data.total || 0;
                }
            } catch (error) {
                console.error('获取列表数据失败:', error);
                this.$message.error('获取列表数据失败');
            }
        },
        handleDateChange(val) {
            if (val) {
            if (val && val.length === 2) {
                this.form.startTime = val[0];
                this.form.endTime = val[1];
            } else {
@@ -149,25 +196,37 @@
            this.form.pageNum = 1;
            this.getTableData();
        },
        handleCurrentChange(val) {
            this.form.pageNum = val;
            this.getTableData();
        },
        handleSizeChange(val) {
            this.form.pageSize = val;
            this.getTableData();
        },
        switchToDrafts(isDrafts) {
            this.isDrafts = isDrafts;
            this.resetForm();
        },
        getStatusType(status) {
            const statusMap = {
                pending: "warning",
                rejected: "danger",
                approved: "success",
                archived: "info",
                1: "warning",    // 待确认
                2: "success",    // 已通过
                3: "danger",     // 已驳回
                4: "info",       // 已撤回
                5: "info",       // 已封存
                '-1': "info"     // 草稿箱
            };
            return statusMap[status] || "info";
        },
        getStatusText(status) {
            const statusMap = {
                pending: "待审批",
                rejected: "已驳回",
                approved: "已通过",
                archived: "已封存",
                1: "待确认",
                2: "已通过",
                3: "已驳回",
                4: "已撤回",
                5: "已封存",
                '-1': "草稿箱"
            };
            return statusMap[status] || "未知";
        },
@@ -186,21 +245,52 @@
            this.showRevoke = true;
        },
        handleDetail(row) {
            // 实现查看详情逻辑
            console.log("查看详情:", row);
            this.currentRow = row;
            this.dialogType = 'view';
            this.reviewDialogVisible = true;
        },
        handleEdit(row) {
            // 实现编辑逻辑
            console.log("编辑:", row);
            this.$router.push({
                path: "/dataManagement/confirmation-sheet/edit",
                query: { id: row.id }
            });
        },
        handleDelete(row) {
            // 实现删除逻辑
            console.log("删除:", row);
            this.currentDeleteRow = row;
            this.deleteTitle = "确认要删除该确认单吗?";
            this.deleteTip = "删除后信息无法找回";
            this.showDelete = true;
        },
        handleConfirmSubmit(data) {
            // 处理确认提交后的逻辑
            console.log("确认提交:", data);
        handleDeleteConfirm() {
            if (!this.currentDeleteRow) return;
            deleteById({
                id: this.currentDeleteRow.id
            }).then(() => {
                this.$message.success("删除成功");
                this.showDelete = false;
                this.getTableData();
            }).catch(error => {
                this.$message.error("删除失败");
            });
        },
        handleClose() {
            this.reviewDialogVisible = false;
            this.getTableData();
        },
        async handleRevokeConfirm() {
            if (!this.currentRow) return;
            try {
                await revokedSheet({ id: this.currentRow.id });
                this.$message.success("撤销成功");
                this.showRevoke = false;
                this.getTableData();
            } catch (error) {
                console.error('撤销失败:', error);
                this.$message.error("撤销失败");
            }
        }
    },
};
</script>
@@ -224,7 +314,9 @@
    padding-bottom: 20px;
    justify-content: space-between;
    align-items: center;
    .title, .drafts {
    .title,
    .drafts {
        background: #ffffff;
        border-radius: 8px 8px 0px 0px;
        padding: 16px 29px;
@@ -244,9 +336,8 @@
    .drafts {
        padding: 16px 65px;
        &:not(.active) {
        }
        &:not(.active) {}
    }
}
laboratory/src/views/dataManagement/confirmation-sheet/service.js
@@ -2,5 +2,37 @@
// 列表
export const getList = (data) => {
  return axios.post('/t-experiment-dispatch/api/t-experiment-dispatch/pageList', { ...data })
}
  return axios.post('/api/t-test-method-confirm-sheet/pageList', { ...data })
}
// 详情
export const getDetail = (data) => {
  return axios.get('/open/t-test-method-confirm-sheet/getDetailById', { params:data })
}
//添加
export const add = (data) => {
  return axios.post('/api/t-test-method-confirm-sheet/add', { ...data })
}
//修改
export const update = (data) => {
  return axios.post('/api/t-test-method-confirm-sheet/update', { ...data })
}
//删除
export const deleteById = (data) => {
  return axios.delete('/open/t-test-method-confirm-sheet/deleteById', { params:data })
}
//批量删除
export const deleteByIds = (data) => {
  return axios.delete('/open/t-test-method-confirm-sheet/deleteByIds', { params:data })
}
// 签名
export const sign = (data) => {
  return axios.post('/api/t-test-method-confirm-sheet/sign', { ...data })
}
// 获取实验调度列表
export const getDispatchList = (data) => {
  return axios.get('/open/t-experiment-dispatch/chemistSignList', { params:data })
}
// 撤销
export const revokedSheet = (data) => {
  return axios.get('/open/t-test-method-confirm-sheet/revokedSheet', { params:data })
}
laboratory/src/views/dataManagement/dispatching/addDispatch.vue
@@ -64,134 +64,134 @@
          </el-form-item>
        </div>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <span>二 、实验信息</span>
          </div>
      <div class="header-title" style="margin-bottom: 38px">
        <div class="header-title-left">
          <img src="@/assets/public/headercard.png" />
          <span>二 、实验信息</span>
        </div>
        <div style="padding-left: 25px">
          <el-form-item prop="experimentDate" label="试验日期">
            <el-date-picker
              name="data-test"
              v-model="form.experimentDate"
              type="date"
              placeholder="选择日期"
              @change="handleDateChange"
            ></el-date-picker>
          </el-form-item>
          <el-form-item prop="experimentName" label="实验名称">
            <el-input v-model="form.experimentName" placeholder="请输入" />
          </el-form-item>
          <el-form-item prop="experimentCode" label="实验编号">
            <el-input v-model="form.experimentCode" placeholder="请输入" disabled/>
          </el-form-item>
        </div>
      </div>
      <div style="padding-left: 25px">
        <el-form-item prop="experimentDate" label="试验日期">
          <el-date-picker
            name="data-test"
            v-model="form.experimentDate"
            type="date"
            placeholder="选择日期"
            @change="handleDateChange"
          ></el-date-picker>
        </el-form-item>
        <el-form-item prop="experimentName" label="实验名称">
          <el-input v-model="form.experimentName" placeholder="请输入" />
        </el-form-item>
        <el-form-item prop="experimentCode" label="实验编号">
          <el-input v-model="form.experimentCode" placeholder="请输入" disabled/>
        </el-form-item>
      </div>
        <div class="add-group">
          <span>实验分组</span>
          <el-button type="primary" class="el-icon-plus" @click="handleAddGroup">添加组别</el-button>
        </div>
        <Table
          :data="groupTableData"
          :total="0"
          :height="null"
          class="groupTable"
        >
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="groupName" label="组别"></el-table-column>
          <el-table-column prop="remark" label="备注"></el-table-column>
          <el-table-column label="操作" width="200">
            <template slot-scope="scope">
              <el-button type="text" @click="handleEditGroup(scope.row)">编辑</el-button>
              <el-button type="text" @click="handleDeleteGroup(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </Table>
      <div class="add-group">
        <span>实验分组</span>
        <el-button type="primary" class="el-icon-plus" @click="handleAddGroup">添加组别</el-button>
      </div>
      <Table
        :data="groupTableData"
        :total="0"
        :height="null"
        class="groupTable"
      >
        <el-table-column type="index" label="序号" width="80"></el-table-column>
        <el-table-column prop="groupName" label="组别"></el-table-column>
        <el-table-column prop="remark" label="备注"></el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <el-button type="text" @click="handleEditGroup(scope.row)">编辑</el-button>
            <el-button type="text" @click="handleDeleteGroup(scope.row)">移除</el-button>
          </template>
        </el-table-column>
      </Table>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <span>三 、计划时间及人员</span>
          </div>
      <div class="header-title" style="margin-bottom: 38px">
        <div class="header-title-left">
          <img src="@/assets/public/headercard.png" />
          <span>三 、计划时间及人员</span>
        </div>
        <div style="padding-left: 25px">
          <el-form-item prop="experimentTime" label="试验时间">
            <el-date-picker
              style="width: 100%"
              v-model="form.experimentTime"
              type="datetimerange"
              range-separator="至"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
              value-format="yyyy-MM-dd HH:mm:ss"
              :default-time="['00:00:00', '23:59:59']"
              @change="handleTimeRangeChange"
            ></el-date-picker>
          </el-form-item>
        </div>
        <div class="add-group">
          <div>*</div>
          <span>参加人员</span>
          <el-button type="primary" class="el-icon-plus" @click="addMember">选择参加人员</el-button>
        </div>
        <div class="member-list">
          <div v-for="item in [3,4,5]" :key="item" class="member-list-card">
            <div class="member-item">
              <div class="member-title">
                {{ ["工艺工程师",  "化验师","实验员"][item - 3] }}
              </div>
      </div>
      <div style="padding-left: 25px">
        <el-form-item prop="experimentTime" label="试验时间">
          <el-date-picker
            style="width: 100%"
            v-model="form.experimentTime"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            value-format="yyyy-MM-dd HH:mm:ss"
            :default-time="['00:00:00', '23:59:59']"
            @change="handleTimeRangeChange"
          ></el-date-picker>
        </el-form-item>
      </div>
      <div class="add-group">
        <div>*</div>
        <span>参加人员</span>
        <el-button type="primary" class="el-icon-plus" @click="addMember">选择参加人员</el-button>
      </div>
      <div class="member-list">
        <div v-for="item in [3,4,5]" :key="item" class="member-list-card">
          <div class="member-item">
            <div class="member-title">
              {{ ["工艺工程师",  "化验师","实验员"][item - 3] }}
            </div>
            <div
              :class="item == 3 ? 'member-name-box' : 'flex1'"
            >
              <div
                :class="item == 3 ? 'member-name-box' : 'flex1'"
                :class="
                  item == 3
                    ? 'member-name-box'
                    : 'member-name-box-2'
                "
              >
                <div
                  :class="
                    item == 3
                      ? 'member-name-box'
                      : 'member-name-box-2'
                  "
                  v-for="i in memberList(item)"
                  :key="i.userId"
                  class="member-name"
                >
                  <div
                    v-for="i in memberList(item)"
                    :key="i.userId"
                    class="member-name"
                  >
                    {{ i.nickName }}
                  </div>
                  {{ i.nickName }}
                </div>
              </div>
              <div class="member-change">
                <div class="member-change-btn" @click="editUserList(item)" v-if="item == 4 || item == 5">修改</div>
              </div>
            </div>
            <div class="member-change">
              <div class="member-change-btn" @click="editUserList(item)" v-if="item == 4 || item == 5">修改</div>
            </div>
          </div>
        </div>
      </div>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <span>四 、任务分解</span>
          </div>
          <el-button type="primary" class="el-icon-plus" @click="handleAddTask">新增任务</el-button>
      <div class="header-title" style="margin-bottom: 38px">
        <div class="header-title-left">
          <img src="@/assets/public/headercard.png" />
          <span>四 、任务分解</span>
        </div>
        <el-button type="primary" class="el-icon-plus" @click="handleAddTask">新增任务</el-button>
      </div>
        <Table
          :data="taskTableData"
          :total="0"
          :height="null"
          class="rwuTable"
        >
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="taskName" label="任务名称"></el-table-column>
          <el-table-column prop="personCharge" label="负责人"></el-table-column>
          <el-table-column prop="startTime" label="开始时间"></el-table-column>
          <el-table-column label="操作" width="200">
            <template slot-scope="scope">
              <el-button type="text" @click="handleEditTask(scope.row)">编辑</el-button>
              <el-button type="text" @click="handleDeleteTask(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </Table>
      <Table
        :data="taskTableData"
        :total="0"
        :height="null"
        class="rwuTable"
      >
        <el-table-column type="index" label="序号" width="80"></el-table-column>
        <el-table-column prop="taskName" label="任务名称"></el-table-column>
        <el-table-column prop="personCharge" label="负责人"></el-table-column>
        <el-table-column prop="startTime" label="开始时间"></el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <el-button type="text" @click="handleEditTask(scope.row)">编辑</el-button>
            <el-button type="text" @click="handleDeleteTask(scope.row)">移除</el-button>
          </template>
        </el-table-column>
      </Table>
        <div class="header-title">
          <div class="header-title-left">
@@ -221,7 +221,7 @@
</template>
<script>
import SelectMember from '@/components/SelectMember'
import SelectMember from '@/components/SelectMember/index.vue'
import AddGroupDialog from './components/AddGroupDialog'
import AddTaskDialog from './components/AddTaskDialog'
import AIEditor from '@/components/AiEditor'
laboratory/src/views/dataManagement/dispatching/editDispatch.vue
@@ -469,14 +469,15 @@
    },
    handleClose() {
      this.$emit("close", false);
      this.$emit("update:data", {}); // 触发事件通知父组件更新data
      this.signatureDialogVisible = false;
      this.imgSrc = '';
    },
    handleApprove() {
      // 组装签名数据
      const params = {
        id: this.data.id, // 传递当前调度id
        signImg: this.imgSrc, // 签名图片
        dispatchId: this.data.id, // 传递当前调度id
        confirmSign: this.imgSrc, // 签名图片
      };
      sign(params).then(res => {
        if (res && res.code === 200) {
laboratory/src/views/dataManagement/dispatching/list.vue
@@ -43,22 +43,28 @@
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'list' }"
              :class="{ active: currentType == 'list' }"
              @click="handleTypeChange('list')"
            >
              实验与调度列表
            </div>
            <div
              v-if="userRole == '3'"
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              :class="{ active: currentType == 'draft' }"
              @click="handleTypeChange('draft')"
            >
              草稿箱
            </div>
          </div>
          <el-button @click="handleAddPlan" class="el-icon-plus" type="primary">
            新增实验调度</el-button
          <el-button
            v-if="userRole == '3'"
            @click="handleAddPlan"
            class="el-icon-plus"
            type="primary"
          >
            新增实验调度
          </el-button>
        </div>
      </template>
      <template #table>
@@ -106,7 +112,7 @@
            <template v-if="userRole == '3'">
              <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
              <el-button 
                v-if="scope.row.status === 1"
                v-if="scope.row.status == 1"
                type="text" 
                @click="handleDelete(scope.row)"
              >删除</el-button>
@@ -116,7 +122,7 @@
            <template v-if="userRole == '4' || userRole == '5'">
              <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
              <el-button
                v-if="scope.row.status === 1"
                v-if="scope.row.status == 1"
                type="text"
                @click="handleConfirm(scope.row)"
              >确认</el-button>
@@ -136,6 +142,7 @@
      :type="approvalDialogType"
      :data="currentApprovalData"
      @close="approvalDialogVisible = false"
      @update:data="handleUpdateData"
    />
    <ShowDelConfirm 
      :title="changeStatusTitle" 
@@ -252,19 +259,20 @@
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.form.status = type === 'draft' ? '-1' : '';
      this.form.status = type == 'draft' ? '-1' : '';
      this.form.pageNum = 1;
      this.getTableData();
    },
    getTableData() {
      const params = {
        ...this.form,
        status: this.currentType === 'draft' ? '-1' : this.form.status
        status: this.currentType == 'draft' ? '-1' : this.form.status
      };
      getDispatchList(params).then(res => {
        if (res) {
          this.tableData = res.records || [];
          this.total = res.total || 0;
        console.log('111111111111',res)
        if (res.code==200) {
          this.tableData = res.data.records || [];
          this.total = res.data.total || 0;
        }
      });
    },
@@ -291,6 +299,10 @@
        this.$message.error("删除失败");
      });
    },
    handleUpdateData() {
      this.currentApprovalData = null;
      this.getTableData();
    }
  },
};
</script>
laboratory/src/views/dataManagement/dispatching/service.js
@@ -2,15 +2,15 @@
// 添加项目课题方案
export function addDispatch(data) {
  return axios.post('/t-experiment-dispatch/api/t-experiment-dispatch/add', { ...data })
  return axios.post('/api/t-experiment-dispatch/add', { ...data })
}
//修改方案
export function updateDispatch(data) {
  return axios.post('/t-experiment-dispatch/api/t-experiment-dispatch/update', { ...data })
  return axios.post('/api/t-experiment-dispatch/update', { ...data })
}
//查询方案列表
export function getDispatchList(data) {
  return axios.post('/t-experiment-dispatch/api/t-experiment-dispatch/pageList', { ...data })
  return axios.post('/api/t-experiment-dispatch/pageList', { ...data })
}
//上下架
// export function upAndDown(data) {
@@ -23,18 +23,18 @@
//删除
export function deleteById(data) {
  return axios.delete('/t-experiment-dispatch/open/t-experiment-dispatch/deleteById', { params:data })
  return axios.delete('/open/t-experiment-dispatch/deleteById', { params:data })
}
//批量删除
export function deleteByIds(data) {
  return axios.delete('/t-experiment-dispatch/open/t-experiment-dispatch/deleteByIds', { params:data })
  return axios.delete('/open/t-experiment-dispatch/deleteByIds', { params:data })
}
//根据id查询方案详情
export function getDetailById(data) {
  return axios.get('/t-experiment-dispatch/open/t-experiment-dispatch/getDetailById', {params:data })
  return axios.get('/open/t-experiment-dispatch/getDetailById', {params:data })
}
// 签名
export function sign(data) {
  return axios.post('/t-experiment-dispatch/api/t-experiment-dispatch/sign', { ...data })
  return axios.post('/api/t-experiment-dispatch/sign', { ...data })
}
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue
@@ -2,78 +2,119 @@
  <Card>
    <template style="position: relative">
      <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
        <div class="header-title" style="margin-bottom: 38px; justify-content: space-between">
          <div style="display: flex; align-items: center; gap: 13px">
        <div v-if="!isEdit">
          <div class="header-title" style="margin-bottom: 38px">
            <div style="display: flex; align-items: center; gap: 13px">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>所属实验调度</div>
              </div>
              <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
                选择实验调度</el-button>
            </div>
          </div>
          <!-- //换到详情弹窗 -->
          <!-- <el-button @click="handleStopExperiment" type="danger">
            申请终止实验</el-button> -->
          <Table :data="groupTableData" :total="0" :height="null" class="groupTable">
            <el-table-column type="index" label="序号" width="80"></el-table-column>
            <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
            <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
            <el-table-column prop="experimentName" label="实验名称"></el-table-column>
            <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
            <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
            <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
            <el-table-column prop="participantsName" label="参加人员"></el-table-column>
            <el-table-column prop="status" label="状态">
              <template slot-scope="scope">
                <el-tag :type="getStatusType(scope.row.status)">
                  {{ getStatusText(scope.row.status) }}
                </el-tag>
              </template>
            </el-table-column>
          </Table>
          <div class="header-title" style="margin-bottom: 38px">
            <div class="header-title-left">
              <img src="@/assets/public/headercard.png" />
              <div>所属实验调度</div>
              <span>基础信息</span>
            </div>
            <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
              选择实验调度</el-button>
          </div>
          <el-button @click="handleStopExperiment" type="danger">
            申请终止实验</el-button>
        </div>
        <Table :data="groupTableData" :total="0" :height="null" class="groupTable">
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="groupName" label="组别"></el-table-column>
          <el-table-column prop="remark" label="备注"></el-table-column>
          <el-table-column label="操作" width="200">
            <template slot-scope="scope">
              <el-button type="text" @click="handleEditGroup(scope.row)">编辑</el-button>
              <el-button type="text" @click="handleDeleteGroup(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </Table>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <span>基础信息</span>
          <template v-if="groupData && groupData.length > 0">
            <div class="add-group">
              <span>组别列表</span>
            </div>
            <Table :data="groupData" :total="0" :height="null" class="groupTable">
              <el-table-column type="index" label="序号" width="80"></el-table-column>
              <el-table-column prop="groupName" label="组别"></el-table-column>
              <el-table-column prop="remark" label="备注"></el-table-column>
            </Table>
          </template>
          <div style="padding-left: 25px;margin-top: 28px;">
            <el-form-item prop="experimentDate" label="试验日期">
              <el-date-picker v-model="form.experimentDate" type="datetime" :disabled="isEdit" placeholder="选择日期时间">
              </el-date-picker>
            </el-form-item>
          </div>
        </div>
        <div v-else>
          <div class="header-title" style="margin-bottom: 18px">
            <div class="header-title-left">
              <img src="@/assets/public/headercard.png" />
              <div>所属项目课题方案</div>
            </div>
          </div>
          <div class="content-box">
            <div class="content-box-left">
              <div>项目课题方案名称:{{ groupTableData && groupTableData.length > 0 ? groupTableData[0].projectName : '' }}</div>
              <div>实验编号:{{ groupTableData && groupTableData.length > 0 ? groupTableData[0].experimentCode : '' }}</div>
            </div>
            <div class="content-box-right">
              <div>项目课题方案编号: {{ groupTableData && groupTableData.length > 0 ? groupTableData[0].projectCode : '' }}</div>
              <div>实验名称: {{ groupTableData && groupTableData.length > 0 ? groupTableData[0].experimentName : '' }}</div>
            </div>
          </div>
        </div>
        <div class="add-group">
          <span>组别列表</span>
          <!-- <el-button type="primary" class="el-icon-plus" @click="handleAddGroup">添加组别</el-button> -->
        </div>
        <Table :data="groupTableData" :total="0" :height="null" class="groupTable">
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="groupName" label="组别"></el-table-column>
          <el-table-column prop="remark" label="备注"></el-table-column>
        </Table>
        <div style="padding-left: 25px">
          <el-form-item prop="name" label="试验日期">
            <el-input v-model="form.name" placeholder="请输入" />
          </el-form-item>
        </div>
        <div class="add-group">
        <div class="add-group" v-if="!isEdit">
          <div>*</div>
          <span>参加人员</span>
          <el-button type="primary" class="el-icon-plus" @click="addMember">选择参加人员</el-button>
          <el-button type="primary" class="el-icon-plus" @click="addMember" >选择参加人员</el-button>
        </div>
        <div class="add-group" v-else><span>实验人员</span> </div>
        <div class="member-list">
          <div v-for="item in 3" :key="item" class="member-list-card">
          <div class="member-list-card">
            <div class="member-item">
              <div class="member-title">
                {{ ["工艺工程师", "实验员", "化验师"][item - 1] }}
              </div>
              <div :class="item == 1 || item == 2 ? 'member-name-box' : 'flex1'">
                <div :class="item == 1 || item == 2 ? 'member-name-box': 'member-name-box-2'">
                  <div v-for="i in memberList(item)" :key="i" class="member-name">
                    张三
              <div class="member-title">实验员</div>
              <div class="flex">
                <div class="member-name-box-2">
                  <div v-for="i in selectedParticipants" :key="i.id" class="member-name">
                    {{ i.nickName }}
                  </div>
                </div>
              </div>
              <div class="member-change">
                <div class="member-change-btn">修改</div>
                <div class="member-change-btn" @click="handleEditMember" v-if="!isEdit">修改</div>
              </div>
            </div>
          </div>
        </div>
        <template v-if="groupData && groupData.length > 0 && isEdit">
          <div class="add-group">
            <span>组别列表</span>
          </div>
          <Table :data="groupData" :total="0" :height="null" class="groupTable">
            <el-table-column type="index" label="序号" width="80"></el-table-column>
            <el-table-column prop="groupName" label="组别"></el-table-column>
            <el-table-column prop="remark" label="备注"></el-table-column>
          </Table>
        </template>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
@@ -82,7 +123,8 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor ref="purposeEditor" v-model="editorContents.purpose" height="200px" placeholder="请输入实验目的..." />
          <AiEditor ref="purposeEditor" :readOnly="isEdit" :value="editorContents.purpose" height="200px"
            placeholder="请输入实验目的..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -92,7 +134,8 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor ref="processEditor" v-model="editorContents.process" height="200px" placeholder="请输入工艺参数及路线..." />
          <AiEditor ref="processEditor" :readOnly="isEdit" :value="editorContents.process" height="200px"
            placeholder="请输入工艺参数及路线..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -101,15 +144,17 @@
            <div>三、实验材料及设备</div>
          </div>
        </div>
        <DynamicComponent ref="materialComponent" title="实验材料" @submit="handleMaterialSubmit" />
        <DynamicComponent ref="equipmentComponent" title="实验所用设备" @submit="handleEquipmentSubmit" />
        <DynamicComponent ref="materialComponent" title="实验材料" :participants="participantsData"
          @submit="handleMaterialSubmit" :dataSource="form.experimentMaterial" :editable="!isEdit" />
        <DynamicComponent ref="equipmentComponent" title="实验所用设备" :participants="participantsData"
          @submit="handleEquipmentSubmit" :dataSource="form.experimentDevice" :editable="!isEdit" />
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>四、实验操作步骤记录</div>
          </div>
          <el-button @click="handleAddStep" class="el-icon-plus" type="primary">
          <el-button @click="handleAddStep" class="el-icon-plus" type="primary" v-if="!isEdit">
            添加步骤</el-button>
        </div>
@@ -119,17 +164,18 @@
              步骤{{ idx + 1 }}:{{ item.stepName }}
            </div>
            <div class="step-list-item-control">
              <div class="controlBtn edit" @click="handleEditStep(idx)">
              <div class="controlBtn edit" @click="handleEditStep(idx)" v-if="!isEdit">
                <img src="@/assets/public/edit.png" alt="编辑" class="edit-icon" />
                编辑
              </div>
              <div class="controlBtn delete" @click="handleDeleteStep(idx)">
              <div class="controlBtn delete" @click="handleDeleteStep(idx)" v-if="!isEdit">
                <img src="@/assets/public/delete.png" alt="删除" class="delete-icon" />
                删除
              </div>
            </div>
          </div>
          <DynamicComponent :ref="'stepContent' + idx" @submit="(content) => handleStepContentSubmit(idx, content)" />
          <DynamicComponent :ref="'stepContent' + idx" @submit="(content) => handleStepContentSubmit(idx, content)"
            :dataSource="item.content" :editable="!isEdit" />
        </div>
        <div class="add-project-footer">
@@ -138,23 +184,26 @@
        </div>
      </el-form>
    </template>
    <SelectMember ref="selectMember" />
    <experimentalScheduling :show="showScheduling" />
    <SelectMemberSimple ref="selectMember" @submit="handleMemberSubmit" />
    <experimentalScheduling :show="showScheduling" @submit="handleSchedulingSubmit" @close="handleSchedulingClose" />
    <AddStep ref="addStepDialog" @submit="handleStepSubmit" />
  </Card>
</template>
<script>
import SelectMember from "@/components/SelectMember";
import experimentalScheduling from "../confirmation-sheet/components/experimental-scheduling.vue";
import SelectMemberSimple from "@/components/SelectMemberSimple/index.vue";
import experimentalScheduling from "./components/experimental-scheduling.vue";
import DynamicComponent from "@/components/DynamicComponent";
import AddStep from "./components/add-step.vue";
import AiEditor from "@/components/AiEditor";
import { getGroupByDispatchId, getParticipantsByDispatchId, getDetail } from "./service";
import moment from 'moment';
import { add,update } from "./service";
export default {
  name: "AddProject",
  components: {
    SelectMember,
    SelectMemberSimple,
    experimentalScheduling,
    DynamicComponent,
    AddStep,
@@ -164,8 +213,16 @@
    return {
      showScheduling: false,
      form: {
        material: null,
        equipment: null,
        experimentDate: '', // 实验日期
        experimentMaterial: null,
        experimentDevice: null,
        experimentObjective: '', // 实验目的
        experimentParamRoute: '', // 工艺参数及路线
        experimentStepRecord: [], // 实验步骤记录
        experimentSchemePersons: [], // 实验方案人员
        dispatchId: '', // 实验调度id
        status: -1, // 状态:-1=草稿箱 1=已发送
        commitTime: '', // 提交时间
      },
      editorContents: {
        purpose: "",
@@ -174,158 +231,82 @@
      stepList: [],
      editingStepIndex: -1,
      rules: {
        name: [
          { required: true, message: "请输入项目组名称", trigger: "blur" },
        experimentDate: [
          { required: true, message: "请输入实验日期", trigger: "blur" },
        ],
        description: [
          { required: true, message: "请输入项目组描述", trigger: "blur" },
        ],
        material: [
        experimentMaterial: [
          { required: true, message: "请添加实验材料", trigger: "change" },
        ],
        equipment: [
        experimentDevice: [
          { required: true, message: "请添加实验设备", trigger: "change" },
        ],
      },
      groupTableData: [],
      groupData: [],
      taskTableData: [],
      participantsData: [],
      selectedParticipants: [],
      isEdit: false, // 是否为编辑模式
      editId: null, // 编辑的ID
      viewMaterialData: [], // 查看模式的材料数据
      viewEquipmentData: [], // 查看模式的设备数据
      // 状态映射表
      statusTypeMap: {
        "-1": "info",
        "1": "warning",
        "2": "success",
        "3": "info"
      },
      statusTextMap: {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      }
    };
  },
  async created() {
    // 检查是否为编辑模式
    if (this.$route.query.type === 'edit' && this.$route.query.id) {
      this.isEdit = true;
      this.editId = this.$route.query.id;
      await this.loadEditData();
    }
  },
  methods: {
    submitForm() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          console.log("submit!");
        }
      });
    },
    // ===== 人员相关方法 =====
    addMember() {
      this.$refs.selectMember.open();
      this.$refs.selectMember.open(this.participantsData, []);
    },
    memberList(i) {
      switch (i) {
        case 1:
          return [1];
        case 2:
          return [1];
        case 3:
          return [1, 2, 3, 4, 5, 6, 7, 8];
        case 4:
          return [1, 2, 3, 4, 5, 6, 7, 8];
        default:
          break;
      }
    handleMemberSubmit(selectedMembers) {
      this.selectedParticipants = selectedMembers;
      this.$refs.selectMember.close();
    },
    handleAddGroup() {
      this.$refs.addGroupDialog.open();
    handleEditMember() {
      this.$refs.selectMember.open(this.participantsData, this.selectedParticipants);
    },
    handleEditGroup(row) {
      this.$refs.addGroupDialog.open(row);
    },
    handleDeleteGroup(row) {
      this.$confirm("确认删除该组别吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          const index = this.groupTableData.findIndex((item) => item === row);
          if (index > -1) {
            this.groupTableData.splice(index, 1);
            this.$message.success("删除成功");
          }
        })
        .catch(() => { });
    },
    handleGroupSubmit(form) {
      const index = this.groupTableData.findIndex(
        (item) => item.groupName === form.groupName
      );
      if (index > -1) {
        this.groupTableData.splice(index, 1, form);
      } else {
        this.groupTableData.push(form);
      }
    },
    handleAddTask() {
      this.$refs.addTaskDialog.open();
    },
    handleEditTask(row) {
      this.$refs.addTaskDialog.open(row);
    },
    handleDeleteTask(row) {
      this.$confirm("确认删除该任务吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          const index = this.taskTableData.findIndex((item) => item === row);
          if (index > -1) {
            this.taskTableData.splice(index, 1);
            this.$message.success("删除成功");
          }
        })
        .catch(() => { });
    },
    handleTaskSubmit(form) {
      const index = this.taskTableData.findIndex(
        (item) => item.taskName === form.taskName
      );
      if (index > -1) {
        this.taskTableData.splice(index, 1, form);
      } else {
        this.taskTableData.push(form);
      }
    },
    handleMaterialSubmit(data) {
      this.form.material = data;
    },
    handleEquipmentSubmit(data) {
      this.form.equipment = data;
    },
    handleSave() {
      this.$refs.form.validate((valid) => {
        if (valid && this.validateContent()) {
          this.$refs.materialComponent.submit();
          this.$refs.equipmentComponent.submit();
          const formData = {
            ...this.form,
            ...this.getAllEditorContent(),
            steps: this.stepList,
          };
          console.log("提交的数据:", formData);
          this.$message.success("保存成功");
        }
      });
    },
    handleSaveDraft() {
      this.$refs.materialComponent.submit();
      this.$refs.equipmentComponent.submit();
      const formData = {
        ...this.form,
        ...this.getAllEditorContent(),
        steps: this.stepList,
        status: "draft",
      };
      console.log("草稿数据:", formData);
      this.$message.success("草稿保存成功");
    },
    // ===== 步骤相关方法 =====
    handleAddStep() {
      this.$refs.addStepDialog.open();
    },
    handleStepSubmit(stepData) {
      if (this.editingStepIndex > -1) {
        // 编辑现有步骤
        this.stepList[this.editingStepIndex].stepName = stepData.stepName;
        this.editingStepIndex = -1;
      } else {
        // 添加新步骤
        this.stepList.push({
          stepName: stepData.stepName,
          content: null,
        });
      }
    },
    handleEditStep(index) {
      this.editingStepIndex = index;
      this.$refs.addStepDialog.open(true);
      this.$refs.addStepDialog.setStepName(this.stepList[index].stepName);
    },
    handleDeleteStep(index) {
      this.$confirm("确认删除该步骤吗?删除后步骤内容将无法恢复", "警告", {
@@ -340,13 +321,108 @@
        })
        .catch(() => { });
    },
    handleEditStep(index) {
      this.editingStepIndex = index;
      this.$refs.addStepDialog.open(true);
      this.$refs.addStepDialog.setStepName(this.stepList[index].stepName);
    },
    handleStepContentSubmit(index, content) {
      this.stepList[index].content = content;
    },
    // ===== 材料设备相关方法 =====
    handleMaterialSubmit(data) {
      this.form.experimentMaterial = data;
    },
    handleEquipmentSubmit(data) {
      this.form.experimentDevice = data;
    },
    // ===== 保存提交相关方法 =====
    handleSave() {
      this.submitData(1);
    },
    handleSaveDraft() {
      this.submitData(-1);
    },
    submitData(status) {
      // 先获取所有动态组件的数据
      this.$refs.materialComponent.submit();
      this.$refs.equipmentComponent.submit();
      // 获取所有步骤内容
      const stepContentRefs = Object.keys(this.$refs)
        .filter(key => key.startsWith('stepContent'))
        .map(key => this.$refs[key]);
      stepContentRefs.forEach((ref) => {
        const editor = Array.isArray(ref) ? ref[0] : ref;
        if (editor && typeof editor.submit === 'function') {
          editor.submit();
        }
      });
      // 进行表单校验
      this.$refs.form.validate((valid) => {
        if (valid && this.validateContent()) {
          // 获取富文本编辑器内容
          const experimentObjective = this.$refs.purposeEditor.getContent();
          const experimentParamRoute = this.$refs.processEditor.getContent();
          // 构建实验步骤记录数据
          const experimentStepRecord = this.stepList.map(step => ({
            stepName: step.stepName,
            content: step.content
          }));
          // 构建提交数据
          const formData = {
            ...this.form,
            experimentDate: moment(this.form.experimentDate).format('YYYY-MM-DD HH:mm:ss'),
            dispatchId: this.groupTableData && this.groupTableData.length > 0 ? this.groupTableData[0]?.id : '',
            experimentObjective,
            experimentParamRoute,
            experimentSchemePersons: this.selectedParticipants.map(person => ({
              userId: person.userId,
              nickName: person.nickName,
              roleType: person.roleType,
              commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
            })),
            status,
            commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
          };
          // 统一转换JSON字符串
          formData.experimentStepRecord = JSON.stringify(experimentStepRecord);
          formData.experimentDevice = JSON.stringify(this.form.experimentDevice);
          formData.experimentMaterial = JSON.stringify(this.form.experimentMaterial);
          // 编辑模式下添加id参数
          if (this.isEdit && this.editId) {
            formData.id = this.editId;
          }
          // 根据是否为编辑模式调用不同接口
          const apiCall = this.isEdit ? update(formData) : add(formData);
          apiCall.then(res => {
            if (res.code === 200) {
              this.$message.success(status === 1 ? '保存成功' : '草稿保存成功');
              if (status === 1) {
                this.$router.go(-1);
              }
            } else {
              this.$message.error(res.msg || (status === 1 ? '保存失败' : '草稿保存失败'));
            }
          }).catch(err => {
            this.$message.error(status === 1 ? '保存失败' : '草稿保存失败');
            console.error(status === 1 ? '保存失败:' : '草稿保存失败:', err);
          });
        } else {
          // 获取第一个错误字段并滚动到该位置
          const firstError = this.$refs.form.fields.find(field => field.validateState === 'error');
          if (firstError) {
            this.$nextTick(() => {
              firstError.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            });
          }
        }
      });
    },
    getAllEditorContent() {
      return {
@@ -355,19 +431,221 @@
      };
    },
    validateContent() {
      const contents = this.getAllEditorContent();
      if (!contents.purpose) {
      // 校验实验调度
      if (!this.groupTableData || this.groupTableData.length === 0) {
        this.$message.error("请选择实验调度");
        return false;
      }
      // 校验实验日期
      if (!this.form.experimentDate) {
        this.$message.error("请填写实验日期");
        return false;
      }
      // 校验参与人员
      if (!this.selectedParticipants || this.selectedParticipants.length === 0) {
        this.$message.error("请选择参与人员");
        return false;
      }
      // 校验实验目的
      const purpose = this.$refs.purposeEditor.getContent();
      if (!purpose || purpose === '<p></p>' || purpose.trim() === '<p></p>') {
        this.$message.error("请填写实验目的");
        return false;
      }
      if (!contents.process) {
      // 校验工艺参数及路线
      const process = this.$refs.processEditor.getContent();
      if (!process || process === '<p></p>' || process.trim() === '<p></p>') {
        this.$message.error("请填写工艺参数及路线");
        return false;
      }
      // 校验实验材料
      if (!this.form.experimentMaterial) {
        this.$message.error("请添加实验材料");
        return false;
      }
      // 校验实验设备
      if (!this.form.experimentDevice) {
        this.$message.error("请添加实验设备");
        return false;
      }
      // 校验实验步骤记录
      if (!this.stepList || this.stepList.length === 0) {
        this.$message.error("请添加实验操作步骤");
        return false;
      }
      // 校验每个步骤是否都有内容
      const invalidStep = this.stepList.findIndex(step => !step.content);
      if (invalidStep !== -1) {
        this.$message.error(`请完善第${invalidStep + 1}个步骤的内容`);
        return false;
      }
      return true;
    },
    handleStopExperiment() {
      this.$router.push("/dataManagement/scheme-management/stop-experiment");
    },
    getStatusType(status) {
      return this.statusTypeMap[status] || "info";
    },
    getStatusText(status) {
      return this.statusTextMap[status] || "未知";
    },
    handleSchedulingSubmit(data) {
      this.groupTableData = data || [];
      if (data && data.length > 0 && data[0].id) {
        getGroupByDispatchId({ dispatchId: data[0].id }).then(res => {
          if (res) {
            this.groupData = res || [];
          } else {
            this.$message.error(res.msg || '获取组别列表失败');
          }
        }).catch(err => {
          this.$message.error('获取组别列表失败');
          console.error('获取组别列表失败:', err);
        });
        getParticipantsByDispatchId({ dispatchId: data[0].id }).then(res => {
          console.log("获取参加人员列表:", res);
          if (res) {
            this.participantsData = res || [];
          } else {
            this.$message.error(res.msg || '获取参加人员列表失败');
          }
        }).catch(err => {
          this.$message.error('获取参加人员列表失败');
          console.error('获取参加人员列表失败:', err);
        });
      }
    },
    handleSchedulingClose() {
      this.showScheduling = false;
    },
    // ===== 数据加载方法 =====
    async loadEditData() {
      try {
        const res = await getDetail({ id: this.editId });
        if (!res) {
          this.$message.error('获取详情失败');
          return;
        }
        console.log('编辑数据', res);
        const data = res;
        // 填充基本表单数据
        this.form.experimentDate = data.experimentDate;
        // 填充实验调度信息
        if (data.experimentDispatch?.id) {
          this.form.dispatchId = data.experimentDispatch.id;
          this.groupTableData = [{ ...data.experimentDispatch }];
          // 获取组别信息
          try {
            const groupRes = await getGroupByDispatchId({ dispatchId: data.experimentDispatch.id });
            this.groupData = groupRes || [];
          } catch (err) {
            console.error('获取组别列表失败:', err);
          }
        }
        // 填充参与人员
        this.selectedParticipants = Array.isArray(data.experimentSchemePersons)
          ? data.experimentSchemePersons
          : JSON.parse(data.experimentSchemePersons || '[]');
        // 填充富文本编辑器内容
        this.editorContents.purpose = data.experimentObjective || '';
        this.editorContents.process = data.experimentParamRoute || '';
        // 填充实验材料和设备
        try {
          this.form.experimentMaterial = typeof data.experimentMaterial === 'string'
            ? JSON.parse(data.experimentMaterial)
            : data.experimentMaterial;
        } catch (err) {
          console.error('解析实验材料数据失败:', err);
          this.form.experimentMaterial = [];
        }
        try {
          this.form.experimentDevice = typeof data.experimentDevice === 'string'
            ? JSON.parse(data.experimentDevice)
            : data.experimentDevice;
        } catch (err) {
          console.error('解析实验设备数据失败:', err);
          this.form.experimentDevice = [];
        }
        // 填充实验步骤
        try {
          const stepsData = typeof data.experimentStepRecord === 'string'
            ? JSON.parse(data.experimentStepRecord)
            : data.experimentStepRecord;
          this.stepList = (stepsData || []).map(step => ({
            stepName: step.stepName,
            content: step.content
          }));
        } catch (err) {
          console.error('解析实验步骤数据失败:', err);
          this.stepList = [];
        }
        // 等待组件渲染完成后设置编辑器内容
        this.$nextTick(() => {
          // 设置富文本编辑器内容
          if (this.$refs.purposeEditor) {
            this.$refs.purposeEditor.setContent(this.editorContents.purpose);
          }
          if (this.$refs.processEditor) {
            this.$refs.processEditor.setContent(this.editorContents.process);
          }
          // 设置动态组件的初始数据
          if (!this.isEdit) {
            if (this.$refs.materialComponent && this.form.experimentMaterial) {
              this.$refs.materialComponent.setInitialData(this.form.experimentMaterial);
            }
            if (this.$refs.equipmentComponent && this.form.experimentDevice) {
              this.$refs.equipmentComponent.setInitialData(this.form.experimentDevice);
            }
            // 设置步骤内容的初始数据
            this.stepList.forEach((step, index) => {
              const stepContentRef = this.$refs['stepContent' + index];
              if (stepContentRef && step.content) {
                const editor = Array.isArray(stepContentRef) ? stepContentRef[0] : stepContentRef;
                if (editor?.setInitialData) {
                  editor.setInitialData(step.content);
                }
              }
            });
          }
        });
      } catch (error) {
        this.$message.error('获取详情失败');
        console.error('获取详情失败:', error);
      }
    },
    // 转换数据格式为ViewDynamicComponent需要的格式
    convertToViewFormat(data) {
      if (!data || !Array.isArray(data)) return [];
      return data.map(item => ({
        id: item.id || Math.random().toString(36).substr(2, 9),
        type: item.type,
        data: item.data
      }));
    },
  },
};
@@ -678,7 +956,20 @@
.content-box {
  padding: 0 25px;
  margin-bottom: 30px;
  margin-bottom: 20px;
  width: 65%;
  display: flex;
  .content-box-left{
    flex: 1;
    div{
      padding: 10px 0;
    }
  }
  .content-box-right{
    flex: 1;
    div{
      padding: 10px 0;
    }
  }
}
</style>
Diff truncated after the above file
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue laboratory/src/views/dataManagement/schemeManagement/components/experimental-scheduling.vue laboratory/src/views/dataManagement/schemeManagement/list.vue laboratory/src/views/dataManagement/schemeManagement/service.js laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue laboratory/src/views/dataManagement/suspendExperiment/components/approvalDialog.vue laboratory/src/views/dataManagement/suspendExperiment/list.vue laboratory/src/views/dataManagement/suspendExperiment/service.js laboratory/src/views/reportLibrary/feasibilityReport/add.vue laboratory/src/views/reportLibrary/feasibilityReport/components/approval/index.vue laboratory/src/views/reportLibrary/feasibilityReport/index.vue laboratory/src/views/reportLibrary/feasibilityReport/service.js laboratory/src/views/reportLibrary/feasibilityStudy/add.vue laboratory/src/views/reportLibrary/feasibilityStudy/components/approval/index.vue laboratory/src/views/reportLibrary/feasibilityStudy/index.vue laboratory/src/views/reportLibrary/feasibilityStudy/service.js laboratory/src/views/reportLibrary/processDevelopment/add.vue laboratory/src/views/reportLibrary/processDevelopment/components/approval/index.vue laboratory/src/views/reportLibrary/processDevelopment/index.vue laboratory/src/views/reportLibrary/processDevelopment/service.js