13404089107
2025-05-08 b7ec20b3ec22c858f2db3d9285c5e9d38bd8a48f
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/leshan-laboratory
2 文件已重命名
26个文件已修改
39个文件已删除
9个文件已添加
14640 ■■■■ 已修改文件
culture/package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/public/index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/layouts/components/HeaderNav.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/layouts/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/main.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/router/index.js 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/utils/baseurl.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/utils/encryption.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/utils/request.js 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/utils/sm4.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/SampleDeliveryRecord/components/receiveConfirmDialog.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/SampleDeliveryRecord/deliveryRecord.vue 440 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/SampleDeliveryRecord/list.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/approvalPlan/addPlan.vue 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/approvalPlan/components/approvalDialog.vue 402 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/components/add-test-item.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/components/add.vue 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/components/confirm-dialog.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/components/experimental-scheduling.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/components/review-dialog.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/confirmation-sheet/index.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/dispatching/addDispatch.vue 504 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/dispatching/components/AddGroupDialog.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/dispatching/components/AddTaskDialog.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/dispatching/editDispatch.vue 673 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/dispatching/list.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/inspectionReport/components/addDialog.vue 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/inspectionReport/detail.vue 312 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/inspectionReport/list.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/originalRecordTest/components/addDialog.vue 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/originalRecordTest/detail.vue 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/originalRecordTest/list.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleManage/addSample.vue 487 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleManage/components/addTime.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleManage/components/experimental-scheduling.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleManage/components/receiveConfirmDialog.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleManage/list.vue 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleRecordList/changeRecord.vue 327 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleRecordList/components/confirmDialog.vue 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleRecordList/components/sampleDialog.vue 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleRecordList/list.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleSubmissionList/components/receiveConfirmDialog.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleSubmissionList/list.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/sampleSubmissionList/submission.vue 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/testResultReport/components/checkout-result.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/testResultReport/components/evaluation-dialog.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/testResultReport/components/experimental-scheduling.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/testResultReport/detail.vue 783 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/dataManagement/testResultReport/list.vue 344 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/login/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/login/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/add.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/service.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/strain-library/strain-flow-chart/index.vue 694 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/vue.config.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/ViewDynamicComponent.vue 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/index.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/router/index.js 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/utils/baseurl.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/utils/encryption.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/utils/request.js 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/utils/sm4.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/approvalPlan/list.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/approvalPlan/service.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue 175 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue 789 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/list.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/suspendExperiment/components/approvalDialog.vue 127 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/suspendExperiment/list.vue 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/login/index.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/login/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/vue.config.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/package.json
@@ -10,12 +10,14 @@
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@antv/g6": "^4.8.24",
    "@tinymce/tinymce-vue": "^3.2.8",
    "aieditor": "^1.3.6",
    "axios": "^0.24.0",
    "core-js": "^3.41.0",
    "element-ui": "^2.15.6",
    "moment": "^2.30.1",
    "sm-crypto": "^0.3.13",
    "vue": "^2.7.16",
    "vue-router": "^3.6.5",
    "vuex": "^3.6.2"
culture/public/index.html
@@ -7,7 +7,7 @@
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>logo.jpg">
  <!-- <link rel="icon" href="data:,"> -->
  <title>实验室流程</title>
  <title>菌种库</title>
</head>
<body>
culture/src/layouts/components/HeaderNav.vue
@@ -57,7 +57,7 @@
    // 退出登录
    outLogin() {
      sessionStorage.clear()
      this.$router.replace({ path: "/" });
      this.$router.replace({ path: "/login" });
    },
    // 关闭标签
    closeTag(tag) {
culture/src/layouts/index.vue
@@ -9,7 +9,7 @@
            <div class="image">
              <img src="../assets/logo.jpg" alt="" srcset="" />
            </div>
            <div class="title">实验室流程</div>
            <div class="title">菌种库</div>
          </div>
          <!-- 左侧菜单 -->
          <div v-if="!isFold" class="sidebar-container">
culture/src/main.js
@@ -14,6 +14,7 @@
import "aieditor/dist/style.css"
import './assets/tailwind.css'
import './styles/element-variables.less'
import { fetchEncryptionKey } from './utils/encryption'
Vue.config.productionTip = false;
Vue.use(ElementUI, { size: 'small' })
@@ -62,6 +63,9 @@
  return height
}
// 获取加密密钥
// fetchEncryptionKey()
new Vue({
  router,
  store,
culture/src/router/index.js
@@ -23,7 +23,8 @@
    }
 */
const routes = [{
const routes = [
    {
        path: "/",
        redirect: "/projectList/list",
    },
@@ -42,7 +43,8 @@
            title: "项目组管理",
        },
        component: Layouts,
        children: [{
        children: [
            {
                path: "list",
                name: "ProjectList",
                meta: {
@@ -68,7 +70,8 @@
            title: "系统管理",
        },
        component: Layouts,
        children: [{
        children: [
            {
                path: "user",
                name: "User",
                meta: {
@@ -126,13 +129,15 @@
        meta: {
            title: "菌种库",
        },
        children: [{
        children: [
            {
                path: "/strain-library",
                component: Parent,
                meta: {
                    title: "菌种库管理",
                },
                children: [{
                children: [
                    {
                        path: "strain-library-manage",
                        name: "StrainLibraryManage",
                        meta: {
@@ -208,259 +213,24 @@
                    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: "/dataManagement",
        component: Layouts,
        meta: {
            title: "实验室数据管理",
        },
        children: [{
                path: "approvalPlan",
                meta: {
                    title: "项目课题方案审批",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/approvalPlan/list.vue"),
            },
            {
                path: "addPlan",
                name: "addPlan",
                meta: {
                    title: "新增项目课题方案",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/approvalPlan/addPlan"),
            },
            {
                path: "dispatching",
                meta: {
                    title: "实验调度管理",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/dispatching/list.vue"),
            },
            {
                path: "addDispatch",
                meta: {
                    title: "新增实验调度",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/dispatching/addDispatch"),
            },
            {
                path: "confirmation-sheet",
                name: "ConfirmationSheet",
                meta: {
                    title: "检验方法确认单",
                },
                component: () => import("../views/dataManagement/confirmation-sheet"),
            },
            {
                path: "confirmation-sheet/add",
                name: "AddConfirmationSheet",
                meta: {
                    title: "检验方法确认单",
                },
                component: () => import("../views/dataManagement/confirmation-sheet/components/add.vue"),
            },
            {
                path: "/sampleManage",
                meta: {
                    title: "样品管理",
                    // keepAlive: true,
                },
                component: Parent,
                children: [{
                        path: "manage",
                        meta: {
                            title: "样品管理",
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleManage/list.vue"),
                    },
                    {
                        path: "addSample",
                        name: "addPaddSamplelan",
                        meta: {
                            title: "新增样品",
                            hide: true,
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleManage/addSample"),
                    }, {
                        path: "record",
                        meta: {
                            title: "取样操作记录列表",
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleRecordList/list.vue"),
                    },
                    {
                        path: "changeRecord",
                        name: "changeRecord",
                        meta: {
                            title: "填写取样操作记录",
                            hide: true,
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleRecordList/changeRecord"),
                    }, {
                        path: "submissionList",
                        meta: {
                            title: "送样单列表",
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleSubmissionList/list.vue"),
                    },
                    {
                        path: "submission",
                        name: "submission",
                        meta: {
                            title: "送样单详情",
                            hide: true,
                            keepAlive: true,
                        },
                        component: () => import("../views/dataManagement/sampleSubmissionList/submission"),
                    },
                ]
            },
            {
                path: "deliveryRecord",
                meta: {
                    title: "取样送样记录",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/SampleDeliveryRecord/list.vue"),
            },
            {
                path: "delivery",
                name: "delivery",
                meta: {
                    title: "送样详情",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/SampleDeliveryRecord/deliveryRecord"),
            },
            {
                path: "originalRecordTest",
                meta: {
                    title: "原始检验记录",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/originalRecordTest/list.vue"),
            },
            {
                path: "originalRecordTest/detail",
                name: "OriginalRecordTestDetail",
                meta: {
                    title: "原始检验记录详情",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/originalRecordTest/detail.vue"),
            },
            {
                path: "inspectionReport",
                meta: {
                    title: "检验报告管理",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/inspectionReport/list.vue"),
            },
            {
                path: "inspectionReport/detail",
                name: "inspectionReportDetail",
                meta: {
                    title: "检验报告详情",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/inspectionReport/detail.vue"),
            },
            {
                path: "testResultReport",
                meta: {
                    title: "实验结果汇报",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/testResultReport/list.vue"),
            },
            {
                path: "testResultReport/detail",
                name: "testResultReportDetail",
                meta: {
                    title: "实验结果详情",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/testResultReport/detail.vue"),
            },
        ],
    },
    {
        path: "/reportLibrary",
        component: Layouts,
        meta: {
            title: "专业报告库审批",
        },
        children: [{
                path: "feasibilityStudy",
                meta: {
                    title: "可研报告库",
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityStudy/index.vue"),
            },
            {
                path: "add",
                meta: {
                    title: "新增可行报告",
                    hide: true,
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityStudy/add.vue"),
            },
            {
                path: "feasibilityReport",
                meta: {
                    title: "可行报告库",
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/feasibilityReport/index.vue"),
            },
            {
                path: "processDevelopment",
                meta: {
                    title: "工艺开发工具",
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/processDevelopment/index.vue"),
            },
            {
                path: "verificationRelease",
                meta: {
                    title: "验证与发布",
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/verificationRelease/index.vue"),
            },
            {
                path: "projectProposalLibrary",
                meta: {
                    title: "立项报告库",
                    keepAlive: true,
                },
                component: () => import("../views/reportLibrary/projectProposalLibrary/index.vue"),
            },
        ],
    }, {
        path: "/strainReportLibrary",
        component: Layouts,
culture/src/utils/baseurl.js
@@ -1,7 +1,7 @@
const apiConfig = {
    // 开发环境
    development: {
        baseURL: "",
        baseURL: "http://192.168.110.34:8081",
        imgUrl: "",
    },
    // 生产环境
culture/src/utils/encryption.js
New file
@@ -0,0 +1,34 @@
import axios from 'axios'
import apiConfig from './baseurl'
let encryptionKey = '2022lab02ora12to' // 默认密钥
// 从接口获取密钥
export const fetchEncryptionKey = async () => {
    try {
        const response = await axios.get(`${apiConfig.baseURL}/api/system/getEncryptionKey`)
        if (response.data && response.data.code === 200) {
            // 转换为Buffer并验证字节长度
            const keyBuffer = Buffer.from(response.data.data, 'utf-8')
            if (keyBuffer.length !== 16) {
                console.warn('无效密钥长度,使用默认密钥')
                return encryptionKey // 保持原有密钥
            }
            // 存储原始字符串和Buffer两种格式
            encryptionKey = response.data.data
            return encryptionKey
        }
    } catch (error) {
        console.error('获取加密密钥失败:', error)
    }
}
// 新增方法获取Buffer格式的密钥
export const getEncryptionKeyBuffer = () => {
    return Buffer.from(encryptionKey, 'utf-8')
}
// 获取当前密钥(保持字符串格式)
export const getEncryptionKey = () => encryptionKey
culture/src/utils/request.js
@@ -1,13 +1,10 @@
import axios from 'axios'
import apiConfig from './baseurl'
import {
  Message
} from 'element-ui'
import { Message } from 'element-ui'
import { encryptBySM4, decryptBySM4 } from './sm4'  // 添加decryptBySM4
const service = axios.create({
  baseURL: apiConfig.baseURL,
  // baseURL: apiConfig.baseURL,
  withCredentials: false, // 当跨域请求时发送cookie
  timeout: 30000, // request timeout
})
@@ -16,6 +13,10 @@
service.interceptors.request.use(
  config => {
    config['headers']['Authorization'] = `${sessionStorage.getItem('token')}`
    // 判断是否需要加密(只对/api开头的请求进行加密)
    const needEncrypt = config.url.startsWith('/api');
    if (config.method == 'get') {
      if (!config.params) config.params = {};
      config.params = {
@@ -24,8 +25,8 @@
    }
    if (config.method == 'post') {
      if (!config.data) config.data = {};
      config.data = {
        ...config.data,
      if (needEncrypt) {
        config.data = { param: encryptBySM4(config.data) };
      }
    }
    return config
@@ -42,11 +43,24 @@
      return
    }
    const res = response;
    // 新增解密处理:仅处理/api路径的POST响应
    if (res.config.method === 'post' && res.config.url.startsWith('/api')) {
      try {
        if (res.data && res.data.data) {
          // 这里假设使用decryptBySM4进行解密
          res.data.data = decryptBySM4(res.data.data);
        }
      } catch (e) {
        console.error('数据解密失败:', e);
      }
    }
    if (res.data.code == 200) {
      if (!res.data.data) {
      if (!res.data) {
        return Promise.resolve({})
      }
      return Promise.resolve(res.data.data)
      return Promise.resolve(res.data)
    } else {
      if (res.data.code == 103 || res.data.code == 401) {
        Message({
culture/src/utils/sm4.js
New file
@@ -0,0 +1,14 @@
import { sm4 } from 'sm-crypto';
import { getEncryptionKey, fetchEncryptionKey, getEncryptionKeyBuffer } from './encryption';
// SM4加密函数
export const encryptBySM4 = (data) => {
    const key = getEncryptionKeyBuffer(); // 获取当前密钥
    return sm4.encrypt(JSON.stringify(data), key);
};
// SM4解密函数
export const decryptBySM4 = (data) => {
    const key = getEncryptionKeyBuffer(); // 获取当前密钥
    return JSON.parse(sm4.decrypt(data, key));
};
culture/src/views/dataManagement/SampleDeliveryRecord/components/receiveConfirmDialog.vue
File was deleted
culture/src/views/dataManagement/SampleDeliveryRecord/deliveryRecord.vue
File was deleted
culture/src/views/dataManagement/SampleDeliveryRecord/list.vue
File was deleted
culture/src/views/dataManagement/approvalPlan/addPlan.vue
File was deleted
culture/src/views/dataManagement/approvalPlan/components/approvalDialog.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/components/add-test-item.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/components/add.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/components/confirm-dialog.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/components/experimental-scheduling.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/components/review-dialog.vue
File was deleted
culture/src/views/dataManagement/confirmation-sheet/index.vue
File was deleted
culture/src/views/dataManagement/dispatching/addDispatch.vue
File was deleted
culture/src/views/dataManagement/dispatching/components/AddGroupDialog.vue
File was deleted
culture/src/views/dataManagement/dispatching/components/AddTaskDialog.vue
File was deleted
culture/src/views/dataManagement/dispatching/editDispatch.vue
File was deleted
culture/src/views/dataManagement/dispatching/list.vue
File was deleted
culture/src/views/dataManagement/inspectionReport/components/addDialog.vue
File was deleted
culture/src/views/dataManagement/inspectionReport/detail.vue
File was deleted
culture/src/views/dataManagement/inspectionReport/list.vue
File was deleted
culture/src/views/dataManagement/originalRecordTest/components/addDialog.vue
File was deleted
culture/src/views/dataManagement/originalRecordTest/detail.vue
File was deleted
culture/src/views/dataManagement/originalRecordTest/list.vue
File was deleted
culture/src/views/dataManagement/sampleManage/addSample.vue
File was deleted
culture/src/views/dataManagement/sampleManage/components/addTime.vue
File was deleted
culture/src/views/dataManagement/sampleManage/components/experimental-scheduling.vue
File was deleted
culture/src/views/dataManagement/sampleManage/components/receiveConfirmDialog.vue
File was deleted
culture/src/views/dataManagement/sampleManage/list.vue
File was deleted
culture/src/views/dataManagement/sampleRecordList/changeRecord.vue
File was deleted
culture/src/views/dataManagement/sampleRecordList/components/confirmDialog.vue
File was deleted
culture/src/views/dataManagement/sampleRecordList/components/sampleDialog.vue
File was deleted
culture/src/views/dataManagement/sampleRecordList/list.vue
File was deleted
culture/src/views/dataManagement/sampleSubmissionList/components/receiveConfirmDialog.vue
File was deleted
culture/src/views/dataManagement/sampleSubmissionList/list.vue
File was deleted
culture/src/views/dataManagement/sampleSubmissionList/submission.vue
File was deleted
culture/src/views/dataManagement/testResultReport/components/checkout-result.vue
File was deleted
culture/src/views/dataManagement/testResultReport/components/evaluation-dialog.vue
File was deleted
culture/src/views/dataManagement/testResultReport/components/experimental-scheduling.vue
File was deleted
culture/src/views/dataManagement/testResultReport/detail.vue
File was deleted
culture/src/views/dataManagement/testResultReport/list.vue
File was deleted
culture/src/views/login/index.vue
@@ -11,7 +11,7 @@
      <div class="login-form">
        <div class="form-item flex">
          <img class="form-item-icon" :src="require('../../assets/login/account@2x.png')" alt="">
          <el-input v-model="loginForm.account" placeholder="请输入账号"></el-input>
          <el-input v-model="loginForm.username" placeholder="请输入账号"></el-input>
        </div>
        <div class="form-item flex mt-40">
@@ -28,6 +28,7 @@
  </div>
</template>
<script>
import { loginReq } from './service'
export default {
  name: 'Login',
  data() {
@@ -35,7 +36,7 @@
      windowWidth: window.innerWidth,
      loginForm: {
        account: '',
        username: '',
        password: ''
      },
      viewWidth: '',
@@ -78,8 +79,19 @@
      console.log(this.viewWidth)
    },
    login() {
      if (this.loginForm.username == '') {
        this.$message.warning('请输入账号')
        return
      }
      if (this.loginForm.password == '') {
        this.$message.warning('请输入密码')
        return
      }
      loginReq(this.loginForm).then(res => {
        sessionStorage.setItem('token', res.token)
        sessionStorage.setItem('userInfo', JSON.stringify(res.userInfo.user))
      this.$router.push('/')
      console.log(this.loginForm)
      })
    }
  }
}
culture/src/views/login/service.js
@@ -1,6 +1,6 @@
import axios from '@/utils/request';
// 登录
export const login = (data) => {
export const loginReq = (data) => {
    return axios.post('/login', { ...data })
}
culture/src/views/pedigree-chart/add.vue
New file
@@ -0,0 +1,256 @@
<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>
            <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>
        </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>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
  },
  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" },
        ],
      },
    };
  },
  methods: {
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.signatureVisible = true;
        }
      });
    },
    handleDraft() {
      // 实现存草稿逻辑
      console.log("save draft", this.form);
    },
    handleCancel() {
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      // 处理提交逻辑
      console.log("submit form with signature:", this.form, signatureImage);
      this.$router.back();
    },
  },
};
</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;
    justify-content: space-between;
  }
  @media (max-width: 1199px) {
    flex-direction: column;
    align-items: flex-start;
  }
}
.flex-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  @media (max-width: 768px) {
    flex-direction: column;
    align-items: flex-start;
    width: 100%;
  }
}
.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%;
    }
  }
}
.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;
  }
}
.end-btn {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
</style>
culture/src/views/pedigree-chart/index.vue
@@ -177,6 +177,11 @@
        status: "",
      };
    },
    handleNewStrain() {
      this.$router.push({
        path: "/strain/add-pedigree",
      });
    },
    handleSearch() {
      // 实现查询逻辑
      console.log("查询条件:", this.form);
culture/src/views/projectList/index.vue
@@ -3,16 +3,16 @@
        <TableCustom :queryForm="queryForm" :tableData="tableData" :total="total" @currentChange="handleCurrentChange"
            @sizeChange="handleSizeChange">
            <template #search>
                <el-form :model="form" label-width="140px" inline>
                <el-form label-width="140px" inline>
                    <el-form-item label="项目组名称:">
                        <el-input v-model="form.name" placeholder="请输入"></el-input>
                        <el-input v-model="queryForm.name" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="项目负责人:">
                        <el-input v-model="form.name" placeholder="请输入"></el-input>
                        <el-input v-model="queryForm.name" 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="queryForm.createdDate" type="daterange" range-separator="至"
                            start-placeholder="开始日期" end-placeholder="结束日期">
                        </el-date-picker>
                    </el-form-item>
                    <el-form-item class="search-btn-box">
@@ -54,13 +54,11 @@
</template>
<script>
import { getProjectList } from './service'
export default {
    name: 'ProjectList',
    data() {
        return {
            form: {
                name: ''
            },
            showDelConfirm: false,
            rowId: '',
            changeStatus: false,
@@ -73,6 +71,9 @@
            },
            total: 0
        }
    },
    created() {
        this.getList()
    },
    methods: {
        handleAddProject() {
@@ -113,7 +114,9 @@
            this.getList()
        },
        getList() {
            getProjectList(this.queryForm).then(res => {
                console.log(res);
            })
        }
    }
}
culture/src/views/projectList/service.js
New file
@@ -0,0 +1,6 @@
import axios from '@/utils/request';
// 列表
export const getProjectList = (data) => {
    return axios.post('/api/t-project-team/pageList', { ...data })
}
culture/src/views/strain-library/strain-flow-chart/index.vue
New file
@@ -0,0 +1,694 @@
<template>
  <div class="strain-flow-chart">
    <div class="toolbar">
      <el-button type="primary" size="small" @click="addNode" :disabled="!canAddNode">新增</el-button>
      <el-button size="small" @click="setGenerationPlan" :disabled="!selectedNode">设置传代计划数</el-button>
      <el-button size="small" @click="showDetail" :disabled="!selectedNode">详情</el-button>
    </div>
    <div id="mountNode"></div>
    <!-- 节点详情弹窗 -->
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" @close="handleDialogClose"
      :close-on-click-modal="false">
      <el-form :model="form" :rules="rules" ref="form" label-width="120px">
        <el-form-item :label="formLabel" prop="value">
          <el-input v-model="form.value" :type="inputType"></el-input>
        </el-form-item>
        <el-form-item label="废弃状态" prop="isDiscarded" v-if="showDiscarded">
          <el-switch v-model="form.isDiscarded"></el-switch>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleSubmit">确 定</el-button>
      </div>
    </el-dialog>
    <!-- 新增母代弹窗 -->
    <el-dialog title="新增母代" :visible.sync="addParentDialogVisible" width="500px" :close-on-click-modal="false">
      <el-form :model="parentForm" :rules="parentRules" ref="parentForm" label-width="120px">
        <el-form-item label="代传菌种编号" prop="number">
          <el-input v-model="parentForm.number"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="addParentDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleAddParent">确 定</el-button>
      </div>
    </el-dialog>
    <!-- 设置传代计划数弹窗 -->
    <el-dialog title="设置传代计划数" :visible.sync="planDialogVisible" width="500px" :close-on-click-modal="false">
      <el-form :model="planForm" :rules="planRules" ref="planForm" label-width="120px">
        <el-form-item label="计划数" prop="count">
          <el-input v-model="planForm.count" type="number" :min="1"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="planDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleSetPlan">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import G6 from '@antv/g6';
import dagre from 'dagre';
export default {
  name: 'StrainFlowChart',
  data() {
    return {
      graph: null,
      nodeCount: 0,
      selectedNode: null,
      graphData: {
        nodes: [],
        edges: []
      },
      // 弹窗相关数据
      dialogVisible: false,
      dialogTitle: '',
      formLabel: '',
      inputType: 'text',
      showDiscarded: false,
      isAddingNode: false,
      form: {
        value: '',
        isDiscarded: false
      },
      rules: {
        value: [
          { required: true, message: '不能为空', trigger: 'blur' }
        ]
      },
      // 新增母代弹窗数据
      addParentDialogVisible: false,
      parentForm: {
        number: ''
      },
      parentRules: {
        number: [
          { required: true, message: '菌种编号不能为空', trigger: 'blur' }
        ]
      },
      // 设置传代计划数弹窗数据
      planDialogVisible: false,
      planForm: {
        count: 1
      },
      planRules: {
        count: [
          { required: true, message: '计划数不能为空', trigger: 'blur' }
        ]
      }
    };
  },
  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: {
    initGraph() {
      const container = document.getElementById('mountNode');
      const width = container.scrollWidth;
      const height = container.scrollHeight || 600;
      // 自定义节点
      G6.registerNode('custom-node', {
        draw(cfg, group) {
          const width = 120;
          const titleHeight = 30;
          const contentHeight = 40;
          const gap = 4;
          const totalHeight = titleHeight + gap + contentHeight;
          // 根据节点状态设置颜色
          const isDiscarded = cfg.isDiscarded;
          const titleFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'l(0) 0:#0ACBCA 1:#049C9A' : 'l(0) 0:#0ACBCA 1:#049C9A');
          const contentFill = isDiscarded ? 'rgba(245, 248, 250, 1)' : (cfg.selected ? 'rgba(4,156,154,0.2)' : 'rgba(4,156,154,0.1)');
          const textFill = isDiscarded ? 'rgba(144, 147, 153, 1)' : '#049C9A';
          const stroke = isDiscarded ? '#DCDFE6' : (cfg.selected ? '#049C9A' : 'transparent');
          // 创建渐变
          const gradient = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2,
              width: width,
              height: titleHeight,
              radius: 20,
              fill: titleFill,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'title-box',
          });
          // 下部分 - 内容背景
          const contentBox = group.addShape('rect', {
            attrs: {
              x: -width / 2,
              y: -totalHeight / 2 + titleHeight + gap,
              width: width,
              height: contentHeight,
              fill: contentFill,
              radius: 15,
              cursor: 'move',
              stroke: stroke,
              lineWidth: isDiscarded ? 1 : (cfg.selected ? 2 : 0),
            },
            name: 'content-box',
          });
          // 标题文本
          if (cfg.label) {
            group.addShape('text', {
              attrs: {
                text: cfg.label,
                x: 0,
                y: -totalHeight / 2 + titleHeight / 2,
                fill: isDiscarded ? 'rgba(144, 147, 153, 1)' : '#fff',
                fontSize: 12,
                textAlign: 'center',
                textBaseline: 'middle',
                fontWeight: 'bold',
                cursor: 'move',
              },
              name: 'title-text',
            });
          }
          // 内容文本
          let content = '';
          if (cfg.label === '传代计划数') {
            content = `${cfg.planCount || 0}`;
          } else if (cfg.number) {
            content = cfg.label === '母代' ? `代传菌种编号:${cfg.number}` : `接种菌种编号:${cfg.number}`;
          }
          if (content) {
            group.addShape('text', {
              attrs: {
                text: content,
                x: 0,
                y: -totalHeight / 2 + titleHeight + gap + contentHeight / 2,
                fill: textFill,
                fontSize: 10,
                textAlign: 'center',
                textBaseline: 'middle',
                cursor: 'move',
              },
              name: 'content-text',
            });
          }
          return gradient;
        },
        getAnchorPoints() {
          return [
            [0.5, 0], // 上
            [1, 0.5], // 右
            [0.5, 1], // 下
            [0, 0.5], // 左
          ];
        },
        setState(name, value, item) {
          // 移除悬浮效果,保持节点样式始终一致
        },
      });
      this.graph = new G6.Graph({
        container: 'mountNode',
        width,
        height,
        fitView: true,
        fitViewPadding: 30,
        animate: false,
        enabledStack: false,
        renderer: 'canvas',
        minZoom: 0.3,
        maxZoom: 2,
        defaultZoom: 1,
        layout: {
          type: 'dagre',
          rankdir: 'LR',
          align: 'UL',
          nodesep: 30,  // 减小节点间距
          ranksep: 50,  // 减小层级间距
          controlPoints: true,
        },
        modes: {
          default: [
            {
              type: 'drag-canvas',
              enableOptimize: true,
              direction: 'both',
              scalableRange: 0.1,
              dragTimesOfScale: 0.1,
              onlyChangeComputeZoom: true,
            },
            {
              type: 'zoom-canvas',
              sensitivity: 1.5,
              enableOptimize: true,
            },
            {
              type: 'drag-node',
              enableDelegate: true,
              delegateStyle: {
                fill: '#f3f3f3',
                stroke: '#ccc',
                opacity: 0.5,
              },
              updateEdge: false,
              enableOptimize: true,
              optimizeZoom: 0.7,
              damping: 0.1,
            }
          ]
        },
        defaultNode: {
          type: 'custom-node',
          style: {
            fill: 'l(0) 0:#0ACBCA 1:#049C9A',
          },
        },
        defaultEdge: {
          type: 'cubic-horizontal',
          style: {
            stroke: 'rgba(4, 156, 154, 1)',
            lineWidth: 1,
            opacity: 0.5,
            endArrow: {
              path: G6.Arrow.triangle(6, 6),
              fill: 'rgba(4, 156, 154, 1)',
              stroke: 'rgba(4, 156, 154, 1)',
            },
          },
        },
        optimizeEdge: true,
        optimizeLayoutAnimation: true,
      });
      const canvas = this.graph.get('canvas');
      canvas.set('localRefresh', false);
      canvas.set('autoDraw', true);
      canvas.set('animating', false);
      let throttleTimer = null;
      const throttleInterval = 16;
      this.graph.on('node:dragstart', () => {
        canvas.set('localRefresh', false);
        this.graph.get('canvas').draw();
      });
      this.graph.on('node:drag', (e) => {
        if (throttleTimer) return;
        throttleTimer = setTimeout(() => {
          const model = e.item.get('model');
          const edges = this.graph.getEdges().filter(edge => {
            const source = edge.getSource();
            const target = edge.getTarget();
            return source.get('id') === model.id || target.get('id') === model.id;
          });
          edges.forEach(edge => {
            this.graph.refreshItem(edge);
          });
          throttleTimer = null;
        }, throttleInterval);
      });
      this.graph.on('node:dragend', (e) => {
        if (throttleTimer) {
          clearTimeout(throttleTimer);
          throttleTimer = null;
        }
        const model = e.item.get('model');
        const edges = this.graph.getEdges().filter(edge => {
          const source = edge.getSource();
          const target = edge.getTarget();
          return source.get('id') === model.id || target.get('id') === model.id;
        });
        edges.forEach(edge => {
          this.graph.refreshItem(edge);
        });
        canvas.set('localRefresh', true);
        this.graph.get('canvas').draw();
      });
      this.graph.data(this.graphData);
      this.graph.render();
      let debounceTimer = null;
      this.graph.on('afterchange', () => {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
          if (!canvas.get('destroyed')) {
            canvas.draw();
          }
        }, 16);
      });
    },
    initEvents() {
      // 监听窗口大小变化
      window.addEventListener('resize', this.handleResize);
      // 节点点击事件
      this.graph.on('node:click', (evt) => {
        const node = evt.item;
        const nodeModel = node.getModel();
        // 如果节点已废弃,不允许任何操作
        if (nodeModel.isDiscarded) {
          this.$message.warning('该节点已废弃,不能进行操作');
          return;
        }
        // 更新选中节点
        this.selectedNode = nodeModel;
        // 更新节点选中状态
        this.graphData.nodes.forEach(n => {
          n.selected = n.id === nodeModel.id;
        });
        this.graph.changeData(this.graphData);
      });
      // 画布点击事件,取消选中节点
      this.graph.on('canvas:click', () => {
        this.selectedNode = null;
        this.graphData.nodes.forEach(n => {
          n.selected = false;
        });
        this.graph.changeData(this.graphData);
      });
    },
    handleResize() {
      if (this.graph) {
        const container = document.getElementById('mountNode');
        const width = container.scrollWidth;
        const height = container.scrollHeight || 600;
        this.graph.changeSize(width, height);
      }
    },
    addNode() {
      // 如果没有节点,新增母代
      if (this.graphData.nodes.length === 0) {
        this.addParentDialogVisible = true;
        this.parentForm.number = '';
        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.dialogVisible = true;
        this.dialogTitle = `新增${nextLevel}`;
        this.formLabel = '接种菌种编号';
        this.form.value = '';
        this.form.isDiscarded = false;
        this.showDiscarded = true;
        this.inputType = 'text';
        this.isAddingNode = true;
      }
    },
    handleAddParent() {
      this.$refs.parentForm.validate((valid) => {
        if (valid) {
          const parentId = `parent-${++this.nodeCount}`;
          const container = document.getElementById('mountNode');
          const height = container.scrollHeight || 600;
          this.graphData.nodes.push({
            id: parentId,
            label: '母代',
            number: this.parentForm.number.trim(),
            x: 200,
            y: 200,
            style: {
              fill: '#00B5AA',
            },
          });
          this.graph.changeData(this.graphData);
          this.$message.success('母代节点添加成功');
          this.addParentDialogVisible = false;
        }
      });
    },
    handleDialogClose() {
      this.$refs.form.resetFields();
    },
    handleSubmit() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          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: this.form.value.trim(),
              isDiscarded: this.form.isDiscarded,
              style: {
                fill: this.form.isDiscarded ? '#999' : '#00B5AA',
                opacity: this.form.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.isAddingNode = false;
            this.dialogVisible = 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(this.form.value);
              } else {
                this.graphData.nodes[nodeIndex].number = this.form.value.trim();
                if (this.showDiscarded) {
                  this.graphData.nodes[nodeIndex].isDiscarded = this.form.isDiscarded;
                  // 如果设置为废弃状态,同时废弃所有子节点
                  if (this.form.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.planDialogVisible = true;
      this.planForm.count = 1;
    },
    handleSetPlan() {
      this.$refs.planForm.validate((valid) => {
        if (valid) {
          const nodeModel = this.selectedNode;
          const planCount = this.planForm.count;
          const generationId = `generation-${++this.nodeCount}`;
          this.graphData.nodes.push({
            id: generationId,
            label: '传代计划数',
            planCount: planCount,
            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.planDialogVisible = false;
        }
      });
    },
    showDetail() {
      if (!this.selectedNode) {
        this.$message.warning('请先选择节点');
        return;
      }
      const nodeModel = this.selectedNode;
      if (nodeModel.label === '母代') {
        this.dialogTitle = '母代详情';
        this.formLabel = '代传菌种编号';
        this.form.value = nodeModel.number || '';
        this.showDiscarded = false;
        this.inputType = 'text';
      } else if (nodeModel.label === '子代' || nodeModel.label === '孙代') {
        this.dialogTitle = `${nodeModel.label}详情`;
        this.formLabel = '接种菌种编号';
        this.form.value = nodeModel.number || '';
        this.form.isDiscarded = nodeModel.isDiscarded || false;
        this.showDiscarded = true;
        this.inputType = 'text';
      } else if (nodeModel.label === '传代计划数') {
        this.dialogTitle = '传代计划数详情';
        this.formLabel = '计划数';
        this.form.value = nodeModel.planCount || '';
        this.showDiscarded = false;
        this.inputType = 'number';
      }
      this.dialogVisible = true;
    }
  },
};
</script>
<style lang="less" scoped>
.strain-flow-chart {
  width: 100%;
  height: 100%;
  min-height: 600px;
  background-color: #fff;
  padding: 24px;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  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: 1px solid #eee;
    border-radius: 4px;
  }
}
</style>
culture/vue.config.js
@@ -3,18 +3,18 @@
    return path.join(__dirname, dir)
}
module.exports = {
    outputDir: 'laboratory', // 配置打包后的文件夹名称
    outputDir: 'culture', // 配置打包后的文件夹名称
    lintOnSave: false,
    publicPath: '/',
    devServer: {
        disableHostCheck: true, //禁用主机检查 
        proxy: {
            "/api": { // 设置以什么前缀开头的请求用来代理
                target: "http://localhost:8080", //要访问的跨域的域名
                target: "http://192.168.110.34:8081", //要访问的跨域的域名
                secure: false, // 使用的是http协议则设置为false,https协议则设置为true
                changOrigin: true, //开启代理
                pathRewrite: {
                    "^/api": "",
                    "^/api": "/api",
                },
            },
        },
laboratory/package.json
@@ -3,7 +3,7 @@
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve:dev": "cross-env VUE_APP_ENV=development vue-cli-service serve --port 8087",
    "serve:dev": "cross-env VUE_APP_ENV=development vue-cli-service serve --port 8090",
    "serve:prod": "cross-env VUE_APP_ENV=production vue-cli-service serve",
    "build:dev": "cross-env VUE_APP_ENV=development vue-cli-service build",
    "build:prod": "cross-env VUE_APP_ENV=production vue-cli-service build",
@@ -16,6 +16,7 @@
    "core-js": "^3.41.0",
    "element-ui": "^2.15.6",
    "moment": "^2.30.1",
    "sm-crypto": "^0.3.13",
    "vue": "^2.7.16",
    "vue-router": "^3.6.5",
    "vuex": "^3.6.2"
laboratory/src/components/DynamicComponent/ViewDynamicComponent.vue
New file
@@ -0,0 +1,230 @@
<template>
  <div>
    <div class="choose-material" :class="title ? '' : 'has-title'">
      <div class="add-group" v-if="title">
        <div>*</div>
        <span>{{ title }}</span>
      </div>
      <!-- 动态渲染组件 -->
      <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="请输入内容..."
          />
        </div>
        <!-- 自定义表格 -->
        <div v-else-if="item.type == 'customTable'" style="flex: 1">
          <Table
            :data="item.data.rows"
            :total="null"
            :height="null"
            class="groupTable"
          >
            <el-table-column
              v-for="(header, hidx) in item.data.headers"
              :key="hidx"
              :label="header.name"
              :prop="header.name"
            >
              <template slot-scope="scope">
                <!-- 文本类型 -->
                <span v-if="header.type === 'text'">{{ scope.row[header.name] }}</span>
                <!-- 图片类型 -->
                <div v-else-if="header.type === 'image'" class="image-preview">
                  <el-image
                    v-for="(img, imgIndex) in scope.row[header.name]"
                    :key="imgIndex"
                    :src="img.url"
                    :preview-src-list="[img.url]"
                    fit="cover"
                    class="preview-image"
                  />
                </div>
                <!-- 日期类型 -->
                <span v-else-if="header.type === 'date'">{{ scope.row[header.name] }}</span>
                <!-- 用户类型 -->
                <div v-else-if="header.type === 'user'" class="user-tags">
                  <el-tag
                    v-for="user in scope.row[header.name]"
                    :key="user"
                    class="user-tag"
                  >
                    {{ getUserName(user) }}
                  </el-tag>
                </div>
              </template>
            </el-table-column>
            <el-table-column
              label="更新时间"
              prop="updateTime"
              min-width="180"
            ></el-table-column>
          </Table>
        </div>
        <!-- 文件上传 -->
        <div v-else-if="item.type == 'fileUpload'">
          <el-upload
            action="#"
            :file-list="item.data.fileList"
            :disabled="true"
            list-type="text"
          >
            <el-button style="display: none">点击上传</el-button>
          </el-upload>
        </div>
        <!-- 图片上传 -->
        <div v-else-if="item.type == 'imageUpload'">
          <el-image
            v-for="(img, imgIndex) in item.data.images"
            :key="imgIndex"
            :src="img.url"
            :preview-src-list="[img.url]"
            fit="cover"
            class="preview-image"
          />
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import Table from "../Table/index.vue";
import AiEditor from "../AiEditor/index.vue";
export default {
  name: "ViewDynamicComponent",
  components: {
    Table,
    AiEditor
  },
  props: {
    title: {
      type: String,
      default: "",
    },
    components: {
      type: Array,
      default: () => [],
    }
  },
  data() {
    return {
      userOptions: [
        { value: '1', label: '用户1' },
        { value: '2', label: '用户2' },
        { value: '3', label: '用户3' },
        { value: '4', label: '用户4' },
        { value: '5', label: '用户5' }
      ]
    };
  },
  methods: {
    getUserName(userId) {
      const user = this.userOptions.find(u => u.value === userId);
      return user ? user.label : userId;
    }
  }
};
</script>
<style scoped lang="less">
.preview-image{
  width: 120px;
  height: 120px;
  border-radius: 4px;
  object-fit: cover;
  margin-right: 20px;
}
.choose-material {
  background: #eff8fa;
  padding: 20px;
  margin-top: 37px;
}
.has-title{
  margin-top: 0px !important;
}
.add-group {
  display: flex;
  align-items: center;
  margin-bottom: 19px;
  div {
    color: #f56c6c;
  }
  span {
    font-weight: 500;
    font-size: 14px;
    color: #222222;
    line-height: 21px;
    margin: 0 32px 0 8px;
  }
}
.dynamic-component {
  background: #ffffff;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
}
.rich-text-content {
  width: 100%;
  min-height: 200px;
  padding: 10px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  background-color: #f5f7fa;
}
.image-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  .preview-image {
    width: 120px;
    height: 120px;
    border-radius: 4px;
    object-fit: cover;
  }
}
.user-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.user-tag {
  margin-right: 4px;
  margin-bottom: 4px;
}
.uploaf-notice {
  font-weight: 400;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.85);
  line-height: 22px;
  margin-top: 8px;
}
.groupTable {
  width: 100%;
  margin-top: 10px;
  ::v-deep .el-input__inner {
    width: unset !important;
  }
}
</style>
laboratory/src/components/DynamicComponent/index.vue
@@ -298,26 +298,6 @@
        })
        .catch(() => {});
    },
    confirmAddRow(formData) {
      const { idx, rowIndex, isEdit } = this.rowDialog;
      if (isEdit) {
        // 编辑模式:替换原有行数据
        this.components[idx].data.rows.splice(rowIndex, 1, formData);
      } else {
        // 新增模式:添加新行数据
        this.components[idx].data.rows.push(formData);
      }
      this.rowDialog.visible = false;
      // 重置对话框数据
      this.rowDialog = {
        visible: false,
        idx: null,
        rowIndex: null,
        isEdit: false,
        headers: [],
        form: {},
      };
    },
    handleFileChange(idx, fileList) {
      this.components[idx].data.fileList = fileList;
    },
@@ -329,70 +309,6 @@
      file.url = res.url;
      this.components[idx].data.imageList = fileList;
    },
    // 获取所有组件数据
    getComponentsData() {
      // 整理数据,图片只保留url
      const submitData = this.components.map((item) => {
        if (item.type === "richText") {
          // 获取富文本编辑器的内容
          const editorRef = this.$refs[`editor_${item.id}`];
          return {
            ...item,
            data: {
              content: editorRef ? editorRef.getContent() : item.data.content
            }
          };
        }
        if (item.type === "imageUpload") {
          return {
            ...item,
            data: {
              imageList: item.data.imageList.map((img) => ({ url: img.url })),
            },
          };
        }
        return item;
      });
      return submitData;
    },
    // 验证所有组件数据
    validateComponents() {
      // 验证富文本编辑器
      const richTextValid = this.components.every(item => {
        if (item.type === 'richText') {
          const editorRef = this.$refs[`editor_${item.id}`];
          return editorRef && editorRef.getContent().trim() !== '';
        }
        return true;
      });
      if (!richTextValid) {
        this.$message.error('请填写所有富文本内容');
        return false;
      }
      // 验证表格数据
      const tableValid = this.components.every(item => {
        if (item.type === 'customTable') {
          return item.data.rows.length > 0;
        }
        return true;
      });
      if (!tableValid) {
        this.$message.error('请至少添加一行表格数据');
        return false;
      }
      return true;
    },
    // 提交数据
    submit() {
      if (this.validateComponents()) {
        const data = this.getComponentsData();
        this.$emit('submit', data);
      }
    }
  },
};
</script>
laboratory/src/router/index.js
@@ -26,7 +26,7 @@
const routes = [
    {
        path: "/",
        redirect: "/projectList/list",
        redirect: "/login",
    },
    {
        path: "/login",
@@ -341,6 +341,14 @@
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/testResultReport/detail.vue"),
            },
            {
                path: "suspendExperiment",
                meta: {
                    title: "实验中止审批",
                    keepAlive: true,
                },
                component: () => import("../views/dataManagement/suspendExperiment/list.vue"),
            },
        ],
    },
@@ -660,17 +668,28 @@
    document.title = to.meta.title || '实验室流程';
    // 登录验证
    // if (to.path === "/login") {
    //     sessionStorage.removeItem('userInfo')
    //     next()
    // } else if (!sessionStorage.getItem('userInfo')) {
    //     next('/login')
    // } else {
    //     // 判断是否拥有要跳转菜单权限
    //     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('/system');  // 已登录状态访问登录页时重定向到系统首页
            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.meta.hide || !to.meta.oneself) {
@@ -703,7 +722,6 @@
    }
    next()
    // }
});
export default router;
laboratory/src/utils/baseurl.js
@@ -1,7 +1,7 @@
const apiConfig = {
    // 开发环境
    development: {
        baseURL: "",
        baseURL: "http://192.168.110.34:8081",
        imgUrl: "",
    },
    // 生产环境
laboratory/src/utils/encryption.js
New file
@@ -0,0 +1,34 @@
import axios from 'axios'
import apiConfig from './baseurl'
let encryptionKey = '2022lab02ora12to' // 默认密钥
// 从接口获取密钥
export const fetchEncryptionKey = async () => {
    try {
        const response = await axios.get(`${apiConfig.baseURL}/api/system/getEncryptionKey`)
        if (response.data && response.data.code === 200) {
            // 转换为Buffer并验证字节长度
            const keyBuffer = Buffer.from(response.data.data, 'utf-8')
            if (keyBuffer.length !== 16) {
                console.warn('无效密钥长度,使用默认密钥')
                return encryptionKey // 保持原有密钥
            }
            // 存储原始字符串和Buffer两种格式
            encryptionKey = response.data.data
            return encryptionKey
        }
    } catch (error) {
        console.error('获取加密密钥失败:', error)
    }
}
// 新增方法获取Buffer格式的密钥
export const getEncryptionKeyBuffer = () => {
    return Buffer.from(encryptionKey, 'utf-8')
}
// 获取当前密钥(保持字符串格式)
export const getEncryptionKey = () => encryptionKey
laboratory/src/utils/request.js
@@ -1,13 +1,10 @@
import axios from 'axios'
import apiConfig from './baseurl'
import {
  Message
} from 'element-ui'
import { Message } from 'element-ui'
import { encryptBySM4, decryptBySM4 } from './sm4'  // 添加decryptBySM4
const service = axios.create({
  baseURL: apiConfig.baseURL,
  // baseURL: apiConfig.baseURL,
  withCredentials: false, // 当跨域请求时发送cookie
  timeout: 30000, // request timeout
})
@@ -16,6 +13,10 @@
service.interceptors.request.use(
  config => {
    config['headers']['Authorization'] = `${sessionStorage.getItem('token')}`
    // 判断是否需要加密(只对/api开头的请求进行加密)
    const needEncrypt = config.url.startsWith('/api');
    if (config.method == 'get') {
      if (!config.params) config.params = {};
      config.params = {
@@ -24,8 +25,8 @@
    }
    if (config.method == 'post') {
      if (!config.data) config.data = {};
      config.data = {
        ...config.data,
      if (needEncrypt) {
        config.data = { param: encryptBySM4(config.data) };
      }
    }
    return config
@@ -42,11 +43,24 @@
      return
    }
    const res = response;
    // 新增解密处理:仅处理/api路径的POST响应
    if (res.config.method === 'post' && res.config.url.startsWith('/api')) {
      try {
        if (res.data && res.data.data) {
          // 这里假设使用decryptBySM4进行解密
          res.data.data = decryptBySM4(res.data.data);
        }
      } catch (e) {
        console.error('数据解密失败:', e);
      }
    }
    if (res.data.code == 200) {
      if (!res.data.data) {
      if (!res.data) {
        return Promise.resolve({})
      }
      return Promise.resolve(res.data.data)
      return Promise.resolve(res.data)
    } else {
      if (res.data.code == 103 || res.data.code == 401) {
        Message({
laboratory/src/utils/sm4.js
New file
@@ -0,0 +1,14 @@
import { sm4 } from 'sm-crypto';
import { getEncryptionKey, fetchEncryptionKey, getEncryptionKeyBuffer } from './encryption';
// SM4加密函数
export const encryptBySM4 = (data) => {
    const key = getEncryptionKeyBuffer(); // 获取当前密钥
    return sm4.encrypt(JSON.stringify(data), key);
};
// SM4解密函数
export const decryptBySM4 = (data) => {
    const key = getEncryptionKeyBuffer(); // 获取当前密钥
    return JSON.parse(sm4.decrypt(data, key));
};
laboratory/src/views/dataManagement/approvalPlan/list.vue
@@ -27,7 +27,7 @@
          </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 type="primary" @click="handleSearch" style="margin-left: 20px;">查询</el-button>
          </el-form-item>
        </el-form>
      </template>
@@ -117,6 +117,7 @@
<script>
import ApprovalDialog from './components/approvalDialog.vue'
import { getProposalList } from './service'
export default {
  name: "ProjectList",
@@ -133,6 +134,8 @@
        createTime: [],
        approver: "",
        status: "",
        pageNum: 1,
        pageSize: 10,
      },
      tableData: [],
      total: 0,
@@ -208,6 +211,8 @@
        createTime: [],
        approver: "",
        status: "",
        pageNum: 1,
        pageSize: 10,
      };
    },
    handleSearch() {
@@ -282,6 +287,9 @@
    getTableData() {
      // 根据currentType请求不同的数据
      if (this.currentType === 'list') {
        getProposalList(this.form).then(res => {
          console.log(res,'1111111111111111111111111')
        })
        this.tableData = this.mockListData;
        this.total = this.mockListData.length;
      } else {
laboratory/src/views/dataManagement/approvalPlan/service.js
New file
@@ -0,0 +1,36 @@
import axios from '@/utils/request';
// 添加项目课题方案
export function addProposal(data) {
  return axios.post('/api/t-project-proposal/add', { ...data })
}
//修改方案
export function updateProposal(data) {
  return axios.post('/api/t-project-proposal/update', { ...data })
}
//查询方案列表
export function getProposalList(data) {
  return axios.post('/api/t-project-proposal/pageList', { ...data })
}
//上下架
export function upAndDown(data) {
  return axios.post('/api/t-project-proposal/upAndDown', { ...data })
}
//审批
export function audit(data) {
  return axios.post('/api/t-project-proposal/audit', { ...data })
}
//删除
export function deleteById(data) {
  return axios.delete('/open/t-project-proposal/deleteById', { ...data })
}
//批量删除
export function deleteByIds(data) {
  return axios.delete('/open/t-project-proposal/deleteByIds', { ...data })
}
//根据id查询方案详情
export function getDetailById(data) {
  return axios.get('/open/t-project-proposal/getDetailById', { ...data })
}
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue
@@ -1,55 +1,27 @@
<template>
  <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;">
      <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 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
            >
            <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
              选择实验调度</el-button>
          </div>
          <el-button
              @click="handleStopExperiment"
              type="danger"
            >
              申请终止实验</el-button
            >
          <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>
        <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
              >
              <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>
@@ -66,17 +38,8 @@
          <!-- <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>
        <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>
@@ -90,9 +53,7 @@
        <div class="add-group">
          <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="member-list">
          <div v-for="item in 3" :key="item" class="member-list-card">
@@ -100,21 +61,9 @@
              <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="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>
                </div>
@@ -133,12 +82,7 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor
            ref="purposeEditor"
            v-model="editorContents.purpose"
            height="200px"
            placeholder="请输入实验目的..."
          />
          <AiEditor ref="purposeEditor" v-model="editorContents.purpose" height="200px" placeholder="请输入实验目的..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -148,12 +92,7 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor
            ref="processEditor"
            v-model="editorContents.process"
            height="200px"
            placeholder="请输入工艺参数及路线..."
          />
          <AiEditor ref="processEditor" v-model="editorContents.process" height="200px" placeholder="请输入工艺参数及路线..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -162,16 +101,8 @@
            <div>三、实验材料及设备</div>
          </div>
        </div>
        <DynamicComponent
          ref="materialComponent"
          title="实验材料"
          @submit="handleMaterialSubmit"
        />
        <DynamicComponent
          ref="equipmentComponent"
          title="实验所用设备"
          @submit="handleEquipmentSubmit"
        />
        <DynamicComponent ref="materialComponent" title="实验材料" @submit="handleMaterialSubmit" />
        <DynamicComponent ref="equipmentComponent" title="实验所用设备" @submit="handleEquipmentSubmit" />
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
@@ -179,8 +110,7 @@
            <div>四、实验操作步骤记录</div>
          </div>
          <el-button @click="handleAddStep" class="el-icon-plus" type="primary">
            添加步骤</el-button
          >
            添加步骤</el-button>
        </div>
        <div class="step-list" v-for="(item, idx) in stepList" :key="idx">
@@ -190,33 +120,20 @@
            </div>
            <div class="step-list-item-control">
              <div class="controlBtn edit" @click="handleEditStep(idx)">
                <img
                  src="@/assets/public/edit.png"
                  alt="编辑"
                  class="edit-icon"
                />
                <img src="@/assets/public/edit.png" alt="编辑" class="edit-icon" />
                编辑
              </div>
              <div class="controlBtn delete" @click="handleDeleteStep(idx)">
                <img
                  src="@/assets/public/delete.png"
                  alt="删除"
                  class="delete-icon"
                />
                <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)" />
        </div>
        <div class="add-project-footer">
          <el-button type="primary" class="save-btn" @click="handleSave"
            >发送</el-button
          >
          <el-button type="primary" class="save-btn" @click="handleSave">发送</el-button>
          <el-button @click="handleSaveDraft">存草稿</el-button>
        </div>
      </el-form>
@@ -450,7 +367,7 @@
      return true;
    },
    handleStopExperiment() {
      this.$router.push('/dataManagement/scheme-management/stop-experiment')
      this.$router.push("/dataManagement/scheme-management/stop-experiment");
    },
  },
};
@@ -467,6 +384,7 @@
  flex-wrap: wrap;
  gap: 13px;
  margin-top: 38px;
  .header-title-left {
    display: flex;
    align-items: center;
@@ -529,6 +447,7 @@
.header-title:first-child {
  margin-top: 0px;
  .header-title-left {
    margin-top: 0;
  }
@@ -558,6 +477,7 @@
  width: 65%;
  padding-left: 40px;
}
.rwuTable {
  width: 85%;
  padding-left: 40px;
@@ -577,35 +497,27 @@
    border: 1px solid #dcdfe6;
    &:nth-child(1) {
      background: linear-gradient(
        to bottom,
      background: linear-gradient(to bottom,
        rgba(4, 156, 154, 0.2) 0%,
        rgba(5, 242, 194, 0) 70%
      );
          rgba(5, 242, 194, 0) 70%);
    }
    &:nth-child(2) {
      background: linear-gradient(
        to bottom,
      background: linear-gradient(to bottom,
        rgba(5, 160, 193, 0.2) 0%,
        rgba(5, 242, 194, 0) 70%
      );
          rgba(5, 242, 194, 0) 70%);
    }
    &:nth-child(3) {
      background: linear-gradient(
        to bottom,
      background: linear-gradient(to bottom,
        rgba(255, 77, 79, 0.2) 0%,
        rgba(255, 242, 194, 0) 70%
      );
          rgba(255, 242, 194, 0) 70%);
    }
    &:nth-child(4) {
      background: linear-gradient(
        to bottom,
      background: linear-gradient(to bottom,
        rgba(250, 199, 20, 0.21) 0%,
        rgba(255, 242, 194, 0) 70%
      );
          rgba(255, 242, 194, 0) 70%);
    }
    .member-item {
@@ -623,6 +535,7 @@
        line-height: 16px;
        text-align: center;
      }
      .flex1 {
        flex: 1;
      }
@@ -664,6 +577,7 @@
        padding: 10px 0;
        margin-top: auto;
        cursor: pointer;
        .member-change-btn {
          background: #fff1f0;
          border-radius: 4px;
@@ -695,14 +609,17 @@
  padding: 20px;
  margin-top: 37px;
}
.step-list {
  background: #eff8fa;
  padding: 20px;
  .step-list-item {
    display: flex;
    justify-content: space-between;
    padding: 25px;
    background: #ffffff;
    .step-list-item-title {
      font-weight: 500;
      font-size: 14px;
@@ -711,9 +628,11 @@
      flex-wrap: wrap;
      flex: 1;
    }
    .step-list-item-control {
      display: flex;
      align-items: center;
      .controlBtn {
        height: 24px;
        background: #ffffff;
@@ -722,6 +641,7 @@
        display: flex;
        align-items: center;
      }
      .edit {
        border: 1px solid #44be09;
        font-family: PingFangSC, PingFang SC;
@@ -731,6 +651,7 @@
        line-height: 24px;
        margin-right: 50px;
      }
      .delete {
        border: 1px solid #ff4d4f;
        font-family: PingFangSC, PingFang SC;
@@ -739,11 +660,13 @@
        color: #ff4d4f;
        line-height: 24px;
      }
      .edit-icon {
        width: 14px;
        height: 14px;
        margin-right: 8px;
      }
      .delete-icon {
        width: 13px;
        height: 13px;
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue
@@ -1,6 +1,7 @@
<template>
  <div>
  <el-dialog
    :title="dialogTitle"
      title="实验方案详情"
    :visible.sync="visible"
    width="80%"
    :close-on-click-modal="false"
@@ -11,85 +12,176 @@
      <div class="approval-content">
        <Card class="approval-content-card">
          <template style="position: relative">
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <span>项目课题方案信息</span>
              </div>
            </div>
            <el-form
              ref="form"
              :model="form"
              :rules="rules"
              inline
              label-position="top"
              style="margin-top: 38px"
                :disabled="type === 'view'"
            >
              <el-form-item prop="name" label="项目课题方案名称">
                <el-input v-model="form.name" placeholder="请输入" />
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>所属实验调度</div>
                  </div>
                </div>
                <Table :data="groupTableData" :total="0" :height="null">
                  <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 class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <span>基础信息</span>
                  </div>
                </div>
                <div class="add-group">
                  <span>组别列表</span>
                </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; margin-top: 20px">
                  <el-form-item prop="testTime" label="试验时间">
                    <el-date-picker
                      v-model="form.testTime"
                      type="datetime"
                      placeholder="选择日期时间"
                      value-format="yyyy-MM-dd HH:mm:ss"
                    />
              </el-form-item>
              <el-form-item prop="description" label="项目阶段">
                <el-input v-model="form.description" placeholder="请输入" />
              </el-form-item>
              <el-form-item prop="description" label="项目课题方案编号">
                <el-input v-model="form.description" placeholder="请输入" />
              </el-form-item>
            </el-form>
            <div class="header-title">
                </div>
                <div class="add-group">
                  <div>*</div>
                  <span>实验人员</span>
                </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 || item == 3
                            ? 'member-name-box'
                            : 'flex1'
                        "
                      >
                        <div
                          :class="
                            item == 1 || item == 2 || item == 3
                              ? 'member-name-box'
                              : 'member-name-box-2'
                          "
                        >
                          <div
                            v-for="i in memberList(item)"
                            :key="i"
                            class="member-name"
                          >
                            张三
                          </div>
                        </div>
                      </div>
                      <div class="member-change" v-if="type !== 'view'">
                        <div class="member-change-btn">修改</div>
                      </div>
                    </div>
                  </div>
                </div>
                <div class="header-title" style="margin-bottom: 38px">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>一 、实验目的</div>
              </div>
            </div>
            <div class="header-title">
                <AiEditor
                  ref="purposeEditor"
                  v-model="form.purpose"
                  height="200px"
                  placeholder="请输入实验目的..."
                />
                <div class="header-title" style="margin-bottom: 38px">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>二 、实验拆料和设备</div>
                    <div>二、工艺参数及路线</div>
              </div>
            </div>
            <div class="item-title">
              <span>1.实验材料</span>
                <AiEditor
                  ref="processEditor"
                  v-model="form.process"
                  height="200px"
                  placeholder="请输入工艺参数及路线..."
                />
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>三、实验材料及设备</div>
            </div>
            <div class="item-title">
              <span>2.实验设备</span>
                </div>
                <ViewDynamicComponent
                  title="实验材料"
                  :components="form.materialsAndEquipment || []"
                />
                <ViewDynamicComponent
                  title="实验所用设备"
                  :components="form.materialsAndEquipment || []"
                />
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>四、实验操作步骤记录</div>
                  </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>三 、检测方法及开发</div>
                <div class="step-list" v-for="(item, idx) in form.operationSteps" :key="idx">
                  <div class="step-list-item">
                    <div class="step-list-item-title">
                      步骤{{ idx + 1 }}:{{ item.stepName }}
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>四 、实验步骤</div>
                  <ViewDynamicComponent
                    :ref="'stepContent' + idx"
                    :components="[item]"
                  />
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>五 、数据采集及分析</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>六 、结果评估</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <span>注意事项</span>
              </div>
            </div>
              </el-form>
          </template>
          <SelectMember ref="selectMember" />
        </Card>
      </div>
      <!-- 右侧审批流程 -->
      <div class="approval-flow">
        <div class="approval-flow" v-if="type === 'view'">
        <div class="flow-content">
          <approval-process
            :status="form.status"
@@ -100,47 +192,27 @@
        </div>
      </div>
    </div>
    <div class="approval-dialog-approve">
      <div class="status">
        <div class="status-title">审批结果</div>
        <div class="status-content">
          <div
            class="resolve"
            :class="status == '1' && 'activeStatus'"
            @click.stop="status = 1"
          >
            通过
          </div>
          <div
            class="reject"
            :class="status == '2' && 'activeStatus'"
            @click.stop="status = 2"
          >
            驳回
          </div>
        </div>
      </div>
      <div class="remark">
        <div class="remark-title">审批意见</div>
        <el-input type="textarea" v-model="remark" placeholder="请输入审批意见"   />
      </div>
    </div>
    <div slot="footer" class="dialog-footer">
      <el-button @click="handleClose">取 消</el-button>
      <el-button type="primary" @click="handleApprove" v-if="type === 'approve'"
        >通过</el-button
      >
    </div>
  </el-dialog>
    <SignatureCanvas
      :visible="signatureDialogVisible"
      @confirm="handleSignatureConfirm"
    />
  </div>
</template>
<script>
import ApprovalProcess from '@/components/approvalProcess'
import ApprovalProcess from "@/components/approvalProcess";
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import ViewDynamicComponent from "@/components/DynamicComponent/ViewDynamicComponent.vue";
import AiEditor from "@/components/AiEditor/index.vue";
export default {
  name: "ApprovalDialog",
  components: {
    ApprovalProcess
    ApprovalProcess,
    SignatureCanvas,
    ViewDynamicComponent,
    AiEditor,
  },
  props: {
    visible: {
@@ -162,35 +234,214 @@
        planName: "",
        planCode: "",
        stage: "",
        testDate: "",
        testName: "",
        testCode: "",
        testTime: "",
        creator: "",
        createTime: "",
        approvalComment: "",
        status: "pending",
        status: "approved",
        approver: "",
        approveTime: ""
        approveTime: "",
        materialsAndEquipment: [
          {
            id: 1,
            type: "richText",
            data: {
              content:
                "<p>1. 实验材料说明</p><p>2. 设备使用说明</p><p>3. 安全注意事项</p>",
      },
      radio1: 1,
      rules: {},
          },
          {
            id: 2,
            type: "customTable",
            data: {
              headers: [
                { name: "材料名称", type: "text" },
                { name: "规格", type: "text" },
                { name: "数量", type: "text" },
                { name: "用途", type: "text" },
              ],
              rows: [
                {
                  材料名称: "催化剂A",
                  规格: "工业级",
                  数量: "100g",
                  用途: "反应催化剂",
                  updateTime: "2024-01-01 12:00:00",
                },
                {
                  材料名称: "溶剂B",
                  规格: "分析纯",
                  数量: "500ml",
                  用途: "反应溶剂",
                  updateTime: "2024-01-01 12:00:00",
                },
              ],
            },
          },
          {
            id: 3,
            type: "fileUpload",
            data: {
              fileList: [
                {
                  name: "材料安全说明书.pdf",
                  url: "https://example.com/msds.pdf",
                },
                {
                  name: "设备操作手册.docx",
                  url: "https://example.com/manual.docx",
                },
              ],
            },
          },
          {
            id: 4,
            type: "imageGallery",
            data: {
              images: [
                {
                  url: "https://example.com/equipment1.jpg",
                  title: "实验设备1",
                  description: "主要反应设备",
                },
                {
                  url: "https://example.com/equipment2.jpg",
                  title: "实验设备2",
                  description: "辅助设备",
                },
              ],
            },
          },
        ],
        operationSteps: [
          {
            id: 7,
            type: "richText",
            data: {
              content:
                "<p>1. 准备工作</p><p>2. 设备检查</p><p>3. 实验操作</p><p>4. 数据记录</p>",
            },
          },
          {
            id: 8,
            type: "customTable",
            data: {
              headers: [
                { name: "步骤", type: "text" },
                { name: "操作内容", type: "text" },
                { name: "操作人", type: "user" },
                { name: "操作图片", type: "image" },
              ],
              rows: [
                {
                  步骤: "步骤1",
                  操作内容: "称取催化剂",
                  操作人: ["1"],
                  操作图片: [{ url: "https://example.com/step1.jpg" }],
                  updateTime: "2024-01-01 12:00:00",
                },
                {
                  步骤: "步骤2",
                  操作内容: "加入溶剂",
                  操作人: ["2"],
                  操作图片: [{ url: "https://example.com/step2.jpg" }],
                  updateTime: "2024-01-01 12:00:00",
                },
              ],
            },
          },
        ],
      },
      rules: {
        planName: [
          {
            required: true,
            message: "请输入项目课题方案名称",
            trigger: "blur",
          },
        ],
        planCode: [
          {
            required: true,
            message: "请输入项目课题方案编号",
            trigger: "blur",
          },
        ],
        stage: [{ required: true, message: "请输入项目阶段", trigger: "blur" }],
        testDate: [
          { required: true, message: "请选择试验日期", trigger: "change" },
        ],
        testName: [
          { required: true, message: "请输入实验名称", trigger: "blur" },
        ],
        testCode: [
          { required: true, message: "请输入实验编号", trigger: "blur" },
        ],
        testTime: [
          { required: true, message: "请选择试验时间", trigger: "change" },
        ],
      },
      imgSrc: "",
      signatureDialogVisible: false,
      status: "1",
      remark: "",
      groupTableData: [],
      taskTableData: [],
    };
  },
  computed: {
    dialogTitle() {
      return this.type === "approve" ? "审批" : "审批详情";
      return this.type === "approve" ? "确认实验调度" : "实验调度详情";
    },
  },
  watch: {
    data: {
      handler(val) {
        if (val) {
          this.form = { ...val };
          // 深拷贝数据,避免直接修改props
          this.form = JSON.parse(
            JSON.stringify({
              ...this.form,
              ...val,
              // 确保这些字段存在,如果不存在则使用默认值
              materialsAndEquipment: val.materialsAndEquipment || [],
              operationSteps: val.operationSteps || [],
            })
          );
          console.log("接收到的数据:", this.form);
        }
      },
      immediate: true,
      deep: true,
    },
    visible: {
      handler(val) {
        if (val && this.type === "view") {
          // 当弹窗打开且是查看模式时,获取详情数据
          this.getPlanDetail();
        }
      },
      immediate: true,
    },
  },
  methods: {
    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;
      }
    },
    handleClose() {
      this.$emit("update:visible", false);
      this.form.approvalComment = "";
@@ -215,6 +466,145 @@
        status: "rejected",
      });
    },
    memberList(item) {
      return item === 1 ? 2 : item === 2 ? 3 : 1;
    },
    openSignature() {
      this.signatureDialogVisible = true;
    },
    handleSignatureConfirm(imageData) {
      console.log("imageData imageData", imageData);
      this.signatureDialogVisible = false;
      this.imgSrc = imageData;
      // 这里处理签名确认后的逻辑
      // this.$confirm('确认该实验调度吗?', '提示', {
      //   confirmButtonText: '确定',
      //   cancelButtonText: '取消',
      //   type: 'warning'
      // }).then(() => {
      //   // 这里可以将签名图片数据(imageData)连同其他数据一起提交到后端
      //   this.$message.success('确认成功');
      //   this.signatureDialogVisible = false;
      //   this.getTableData();
      // }).catch(() => {
      //   this.signatureDialogVisible = false;
      // });
    },
    // 获取方案详情
    async getPlanDetail() {
      try {
        // TODO: 替换为实际的接口调用
        // const { data } = await this.$api.getPlanDetail({ planCode: this.data.planCode });
        // 模拟接口返回数据
        const mockDetailData = {
          planCode: this.data.planCode,
          planName: "2024年度实验室设备升级方案",
          stage: "设备升级实验",
          testDate: "2024-03-15",
          testTime: "2024-03-15 14:00:00",
          tester: "张三",
          creator: "张三",
          createTime: "2024-03-15",
          status: "pending",
          approver: "李四",
          approveTime: "2024-03-16",
          materialsAndEquipment: [
            {
              id: 1,
              type: "richText",
              data: {
                content:
                  "<p>1. 实验材料说明</p><p>2. 设备使用说明</p><p>3. 安全注意事项</p>",
              },
            },
            {
              id: 2,
              type: "customTable",
              data: {
                headers: [
                  { name: "材料名称", type: "text" },
                  { name: "规格", type: "text" },
                  { name: "数量", type: "text" },
                  { name: "用途", type: "text" },
                ],
                rows: [
                  {
                    材料名称: "催化剂A",
                    规格: "工业级",
                    数量: "100g",
                    用途: "反应催化剂",
                    updateTime: "2024-01-01 12:00:00",
                  },
                  {
                    材料名称: "溶剂B",
                    规格: "分析纯",
                    数量: "500ml",
                    用途: "反应溶剂",
                    updateTime: "2024-01-01 12:00:00",
                  },
                ],
              },
            },
            {
              id: 3,
              type: "fileUpload",
              data: {
                fileList: [
                  {
                    name: "材料安全说明书.pdf",
                    url: "https://example.com/msds.pdf",
                  },
                  {
                    name: "设备操作手册.docx",
                    url: "https://example.com/manual.docx",
                  },
                ],
              },
            },
            {
              id: 4,
              type: "imageUpload",
              data: {
                images: [
                  {
                    url: "https://example.com/equipment1.jpg",
                    title: "实验设备1",
                    description: "主要反应设备",
                  },
                  {
                    url: "https://example.com/equipment2.jpg",
                    title: "实验设备2",
                    description: "辅助设备",
                  },
                ],
              },
            },
          ],
          operationSteps: [
            {
              id: 4,
              type: "richText",
              data: {
                content:
                  "<p>1. 准备工作</p><p>2. 设备检查</p><p>3. 实验操作</p><p>4. 数据记录</p>",
              },
            },
          ],
        };
        // 更新表单数据
        this.form = {
          ...this.form,
          ...mockDetailData,
        };
      } catch (error) {
        console.error("获取方案详情失败:", error);
        this.$message.error("获取方案详情失败");
        this.handleClose();
      }
    },
  },
};
</script>
@@ -226,7 +616,7 @@
.approval-dialog {
  display: flex;
  height: 300px;
  height: 60vh;
  .approval-content {
    flex: 1;
@@ -261,6 +651,10 @@
  }
}
.approval-dialog-approve {
  margin-top: 26px;
}
.approval-content-card {
  height: calc(100% - 100px) !important;
  box-shadow: none !important;
@@ -271,12 +665,12 @@
  align-items: center;
  flex-wrap: wrap;
  gap: 13px;
  margin-top: 38px;
  .header-title-left {
    display: flex;
    align-items: center;
    gap: 13px;
    margin-top: 38px;
    img {
      width: 12px;
@@ -314,6 +708,8 @@
}
.header-title:first-child {
  margin-top: 0 !important;
  .header-title-left {
    margin-top: 0;
  }
@@ -340,54 +736,174 @@
}
.approval-dialog-approve {
  padding: 38px 20px;
  display: flex;
  align-content: center;
  .status {
    margin-right: 40px;
  img {
    border: 2px dashed #049c9a;
  }
  //   align-items: center;
  .status-title {
    color: #222222;
    font-family: "SourceHanSansCN-Medium";
    line-height: 14px;
    margin-bottom: 16px;
  }
  .status-content {
.add-group {
  padding-left: 25px;
  margin-top: 14px;
    display: flex;
    align-items: center;
    gap: 16px;
    background: #ffffff;
    border-radius: 10px;
    border: 1px solid rgba(4, 156, 154, 0.5);
    .resolve {
      border-radius: 10px;
      font-size: 16px;
      padding: 5px 55px;
      font-weight: 400;
      color: #333333;
      cursor: pointer;
  margin-bottom: 19px;
  div {
    color: #f56c6c;
    }
    .reject {
      border-radius: 10px;
      font-size: 16px;
      padding: 5px 55px;
      font-weight: 400;
      color: #333333;
      cursor: pointer;
    }
    .activeStatus {
      background: #ebfefd;
      color: #049c9a;
      box-shadow: 0px 0px 6px 0px rgba(10, 109, 108, 0.25);
      border-radius: 10px;
    }
  }
  .remark-title {
  span {
    font-weight: 500;
    font-size: 14px;
    color: #222222;
    font-family: "SourceHanSansCN-Medium";
    line-height: 14px;
    margin-bottom: 16px;
    line-height: 21px;
    margin: 0 32px 0 8px;
  }
}
.groupTable {
  width: 65%;
  padding-left: 40px;
}
.rwuTable {
  width: 85%;
  padding-left: 40px;
}
.member-list {
  margin-top: 18px;
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin-left: 38px;
  .member-list-card {
    width: 280px;
    height: 300px;
    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.2) 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;
      }
      .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;
        }
      }
    }
  }
}
.step-list {
  background: #eff8fa;
  padding: 20px;
  .step-list-item {
    display: flex;
    justify-content: space-between;
    padding: 25px;
    background: #ffffff;
    .step-list-item-title {
      font-weight: 500;
      font-size: 14px;
      color: rgba(0, 0, 0, 0.8);
      line-height: 20px;
      flex-wrap: wrap;
      flex: 1;
    }
  }
}
@@ -395,6 +911,7 @@
    align-items: center;
    display: flex;
    justify-content: center;
    button{
        width: 150px;
    }
laboratory/src/views/dataManagement/schemeManagement/list.vue
@@ -127,7 +127,37 @@
          createTime: '2024-03-15',
          status: 'pending',
          approver: '李四',
          approveTime: '2024-03-16'
          approveTime: '2024-03-16',
          purpose: '<p>1. 研究新型催化剂的性能</p><p>2. 优化反应条件</p><p>3. 提高产品收率</p>',
          processParameters: [
            {
              '工艺参数': '反应温度',
              '参数值': '25℃',
              '操作人员': ['1', '2'],
              updateTime: '2024-01-01 12:00:00'
            },
            {
              '工艺参数': '反应压力',
              '参数值': '1.0MPa',
              '操作人员': ['3'],
              updateTime: '2024-01-01 12:00:00'
            }
          ],
          materials: [
            {
              '材料名称': '催化剂A',
              '规格': '工业级',
              '数量': '100g',
              updateTime: '2024-01-01 12:00:00'
            },
            {
              '材料名称': '溶剂B',
              '规格': '分析纯',
              '数量': '500ml',
              updateTime: '2024-01-01 12:00:00'
            }
          ],
          steps: '<p>1. 准备工作</p><p>2. 设备检查</p><p>3. 实验操作</p><p>4. 数据记录</p>'
        },
        {
          planCode: 'PLAN-2024-002',
@@ -139,7 +169,25 @@
          createTime: '2024-03-14',
          status: 'approved',
          approver: '赵六',
          approveTime: '2024-03-15'
          approveTime: '2024-03-15',
          purpose: '<p>1. 评估现有安全管理制度</p><p>2. 制定新的安全规范</p><p>3. 进行安全培训</p>',
          processParameters: [
            {
              '工艺参数': '培训时间',
              '参数值': '2小时',
              '操作人员': ['1', '2', '3'],
              updateTime: '2024-01-01 12:00:00'
            }
          ],
          materials: [
            {
              '材料名称': '培训材料',
              '规格': 'A4',
              '数量': '50份',
              updateTime: '2024-01-01 12:00:00'
            }
          ],
          steps: '<p>1. 安全评估</p><p>2. 制度更新</p><p>3. 人员培训</p><p>4. 效果评估</p>'
        },
        {
          planCode: 'PLAN-2024-003',
@@ -261,9 +309,113 @@
      console.log("删除数据:", row);
    },
    handleDetail(row) {
      this.currentApprovalData = row;
      // 打开弹窗
      this.approvalDialogType = 'view';
      this.approvalDialogVisible = true;
      // 调用获取详情接口
      this.getPlanDetail(row.planCode);
    },
    // 获取方案详情
    async getPlanDetail(planCode) {
      try {
        // TODO: 替换为实际的接口调用
        // const { data } = await this.$api.getPlanDetail({ planCode });
        // 模拟接口返回数据
        const mockDetailData = {
          planCode: planCode,
          planName: '2024年度实验室设备升级方案',
          stage: '设备升级实验',
          testDate: '2024-03-15',
          testTime: '2024-03-15 14:00:00',
          tester: '张三',
          creator: '张三',
          createTime: '2024-03-15',
          status: 'pending',
          approver: '李四',
          approveTime: '2024-03-16',
          experimentPurpose: [
            {
              id: 1,
              type: 'richText',
              data: {
                content: '<p>1. 研究新型催化剂的性能</p><p>2. 优化反应条件</p><p>3. 提高产品收率</p>'
              }
            }
          ],
          processParameters: [
            {
              id: 2,
              type: 'customTable',
              data: {
                headers: [
                  { name: '工艺参数', type: 'text' },
                  { name: '参数值', type: 'text' },
                  { name: '操作人员', type: 'user' }
                ],
                rows: [
                  {
                    '工艺参数': '反应温度',
                    '参数值': '25℃',
                    '操作人员': ['1', '2'],
                    updateTime: '2024-01-01 12:00:00'
                  },
                  {
                    '工艺参数': '反应压力',
                    '参数值': '1.0MPa',
                    '操作人员': ['3'],
                    updateTime: '2024-01-01 12:00:00'
                  }
                ]
              }
            }
          ],
          materialsAndEquipment: [
            {
              id: 3,
              type: 'customTable',
              data: {
                headers: [
                  { name: '材料名称', type: 'text' },
                  { name: '规格', type: 'text' },
                  { name: '数量', type: 'text' }
                ],
                rows: [
                  {
                    '材料名称': '催化剂A',
                    '规格': '工业级',
                    '数量': '100g',
                    updateTime: '2024-01-01 12:00:00'
                  },
                  {
                    '材料名称': '溶剂B',
                    '规格': '分析纯',
                    '数量': '500ml',
                    updateTime: '2024-01-01 12:00:00'
                  }
                ]
              }
            }
          ],
          operationSteps: [
            {
              id: 4,
              type: 'richText',
              data: {
                content: '<p>1. 准备工作</p><p>2. 设备检查</p><p>3. 实验操作</p><p>4. 数据记录</p>'
              }
            }
          ]
        };
        // 更新弹窗数据
        this.currentApprovalData = mockDetailData;
      } catch (error) {
        console.error('获取方案详情失败:', error);
        this.$message.error('获取方案详情失败');
        this.approvalDialogVisible = false;
      }
    },
    handleTypeChange(type) {
      this.currentType = type;
laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue
@@ -84,7 +84,7 @@
      <div slot="footer" class="dialog-footer">
        <el-button @click="handleDialogClose">取 消</el-button>
        <el-button type="primary" @click="handleConfirm" :disabled="!imgSrc">确 认</el-button>
        <el-button type="primary" @click="handleConfirm">确 认</el-button>
      </div>
    </el-dialog>
laboratory/src/views/dataManagement/suspendExperiment/components/approvalDialog.vue
File was renamed from culture/src/views/dataManagement/sampleManage/components/approvalDialog.vue
@@ -1,11 +1,6 @@
<template>
  <el-dialog
    :title="dialogTitle"
    :visible.sync="visible"
    width="80%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
  <el-dialog :title="dialogTitle" :visible.sync="visible" width="80%" :close-on-click-modal="false"
    @close="handleClose">
    <div class="approval-dialog">
      <!-- 左侧审批内容 -->
      <div class="approval-content">
@@ -14,89 +9,37 @@
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <span>项目课题方案信息</span>
                <div>所属实验调度</div>
              </div>
            </div>
            <el-form
              ref="form"
              :model="form"
              :rules="rules"
              inline
              label-position="top"
              style="margin-top: 38px"
            >
              <el-form-item prop="name" label="项目课题方案名称">
                <el-input v-model="form.name" placeholder="请输入" />
              </el-form-item>
              <el-form-item prop="description" label="项目阶段">
                <el-input v-model="form.description" placeholder="请输入" />
              </el-form-item>
              <el-form-item prop="description" label="项目课题方案编号">
                <el-input v-model="form.description" 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="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>二 、实验拆料和设备</div>
              </div>
            </div>
            <div class="item-title">
              <span>1.实验材料</span>
            </div>
            <div class="item-title">
              <span>2.实验设备</span>
            </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="stage" 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="approveTime" label="参加人员"></el-table-column>
              <el-table-column prop="status" label="状态">
              </el-table-column>
            </Table>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>三 、检测方法及开发</div>
                <div>中止原因说明</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>四 、实验步骤</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>五 、数据采集及分析</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>六 、结果评估</div>
              </div>
            </div>
            <div class="header-title">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <span>注意事项</span>
              </div>
            </div>
            <AiEditor ref="purposeEditor" v-model="form.purpose" height="200px" placeholder="请输入文字" />
          </template>
          <SelectMember ref="selectMember" />
        </Card>
      </div>
      <!-- 右侧审批流程 -->
      <div class="approval-flow">
        <div class="flow-content">
          <approval-process
            :status="form.status"
            :submit-time="form.createTime"
            :approver="form.approver"
            :approve-time="form.approveTime"
          />
          <approval-process :status="form.status" :submit-time="form.createTime" :approver="form.approver"
            :approve-time="form.approveTime" />
        </div>
      </div>
    </div>
@@ -104,18 +47,10 @@
      <div class="status">
        <div class="status-title">审批结果</div>
        <div class="status-content">
          <div
            class="resolve"
            :class="status == '1' && 'activeStatus'"
            @click.stop="status = 1"
          >
          <div class="resolve" :class="status == '1' && 'activeStatus'" @click.stop="status = 1">
            通过
          </div>
          <div
            class="reject"
            :class="status == '2' && 'activeStatus'"
            @click.stop="status = 2"
          >
          <div class="reject" :class="status == '2' && 'activeStatus'" @click.stop="status = 2">
            驳回
          </div>
        </div>
@@ -125,22 +60,21 @@
        <el-input type="textarea" v-model="remark" placeholder="请输入审批意见"   />
      </div>
    </div>
    <div slot="footer" class="dialog-footer select-member-footer">
    <div slot="footer" class="dialog-footer">
      <el-button @click="handleClose">取 消</el-button>
      <el-button type="primary" @click="handleApprove" v-if="type === 'approve'"
        >通过</el-button
      >
        style="margin-left: 20px;">通过</el-button>
    </div>
  </el-dialog>
</template>
<script>
import ApprovalProcess from '@/components/approvalProcess'
import AiEditor from '@/components/AiEditor'
export default {
  name: "ApprovalDialog",
  components: {
    ApprovalProcess
    ApprovalProcess, AiEditor
  },
  props: {
    visible: {
@@ -177,7 +111,7 @@
  },
  computed: {
    dialogTitle() {
      return this.type === "approve" ? "审批" : "审批详情";
      return this.type === "approve" ? "审批实验中止申请" : "实验中止申请审批详情";
    },
  },
  watch: {
@@ -271,6 +205,7 @@
  align-items: center;
  flex-wrap: wrap;
  gap: 13px;
  margin-bottom: 38px;
  .header-title-left {
    display: flex;
@@ -343,9 +278,11 @@
  padding: 38px 20px;
  display: flex;
  align-content: center;
  .status {
    margin-right: 40px;
  }
  //   align-items: center;
  .status-title {
    color: #222222;
@@ -353,6 +290,7 @@
    line-height: 14px;
    margin-bottom: 16px;
  }
  .status-content {
    display: flex;
    align-items: center;
@@ -360,6 +298,7 @@
    background: #ffffff;
    border-radius: 10px;
    border: 1px solid rgba(4, 156, 154, 0.5);
    .resolve {
      border-radius: 10px;
      font-size: 16px;
@@ -368,6 +307,7 @@
      color: #333333;
      cursor: pointer;
    }
    .reject {
      border-radius: 10px;
      font-size: 16px;
@@ -376,6 +316,7 @@
      color: #333333;
      cursor: pointer;
    }
    .activeStatus {
      background: #ebfefd;
      color: #049c9a;
@@ -383,6 +324,7 @@
      border-radius: 10px;
    }
  }
  .remark-title {
    color: #222222;
    font-family: "SourceHanSansCN-Medium";
@@ -395,6 +337,7 @@
    align-items: center;
    display: flex;
    justify-content: center;
    button{
        width: 150px;
    }
laboratory/src/views/dataManagement/suspendExperiment/list.vue
File was renamed from culture/src/views/dataManagement/approvalPlan/list.vue
@@ -3,115 +3,62 @@
    <TableCustom :queryForm="form" :tableData="tableData" :total="total">
      <template #search>
        <el-form :model="form" labelWidth="auto" inline>
          <el-form-item label="项目课题方案名称:">
          <el-form-item label="所属项目课题方案:">
            <el-input v-model="form.planName" placeholder="请输入"></el-input>
          </el-form-item>
          <el-form-item label="项目课题方案编号:">
          <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-date-picker
              v-model="form.createTime"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="yyyy-MM-dd"
            ></el-date-picker>
            <el-date-picker v-model="form.createTime" type="daterange" range-separator="至" start-placeholder="开始日期"
              end-placeholder="结束日期" value-format="yyyy-MM-dd"></el-date-picker>
          </el-form-item>
          <el-form-item label="审批人:">
          <el-form-item label="状态">
            <el-input v-model="form.approver" placeholder="请输入"></el-input>
          </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 type="primary" @click="handleSearch" style="margin-left: 20px;">查询</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 class="title" :class="{ active: currentType === 'list' }" @click="handleTypeChange('list')">申请中止试验方案列表
          </div>
          <el-button @click="handleAddPlan" 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="stage" 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="status" label="审批状态">
        <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>
        <el-table-column prop="approver" label="提交时间"></el-table-column>
        <el-table-column prop="approveTime" 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>
        <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
              v-if="scope.row.status === 'pending'"
              type="text"
              @click="handleApprove(scope.row)"
              >审批</el-button
            >
            <el-button
              v-if="scope.row.status === 'approved'"
              type="text"
              @click="handleRevokeApprove(scope.row)"
              >撤销审批</el-button
            >
            <el-button
              v-if="scope.row.status === 'rejected'"
              type="text"
              @click="handleEdit(scope.row)"
              >编辑</el-button
            >
            <el-button
              v-if="scope.row.status === 'rejected'"
              type="text"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
            <el-button type="text" @click="handleDetail(scope.row)"
              >详情</el-button
            >
            <el-button v-if="scope.row.status === 'pending'" type="text"
              @click="handleApprove(scope.row)">审批</el-button>
            <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <!-- 审批弹窗 -->
    <approval-dialog
      :visible.sync="approvalDialogVisible"
      :type="approvalDialogType"
      :data="currentApprovalData"
      @approve="handleApproveSubmit"
      @reject="handleRejectSubmit"
    />
    <approval-dialog :visible.sync="approvalDialogVisible" :type="approvalDialogType" :data="currentApprovalData"
      @approve="handleApproveSubmit" @reject="handleRejectSubmit" />
  </div>
</template>
@@ -297,15 +244,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;
@@ -317,6 +267,7 @@
    width: unset;
    cursor: pointer;
  }
  .drafts {
    padding: 16px 65px;
    background: #fafafc;
@@ -328,6 +279,7 @@
    margin-left: 16px;
    cursor: pointer;
  }
  .active{
    color: #049c9a;
    background: #ffffff;
laboratory/src/views/login/index.vue
@@ -11,7 +11,7 @@
      <div class="login-form">
        <div class="form-item flex">
          <img class="form-item-icon" :src="require('../../assets/login/account@2x.png')" alt="">
          <el-input v-model="loginForm.account" placeholder="请输入账号"></el-input>
          <el-input v-model="loginForm.username" placeholder="请输入账号"></el-input>
        </div>
        <div class="form-item flex mt-40">
@@ -28,6 +28,7 @@
  </div>
</template>
<script>
import { loginReq } from './service'
export default {
  name: 'Login',
  data() {
@@ -35,7 +36,7 @@
      windowWidth: window.innerWidth,
      loginForm: {
        account: '',
        username: '',
        password: ''
      },
      viewWidth: '',
@@ -75,11 +76,21 @@
    // 添加处理窗口大小变化的方法
    handleResize() {
      this.viewWidth = window.innerWidth
      console.log(this.viewWidth)
    },
    login() {
      this.$router.push('/')
      console.log(this.loginForm)
      if (this.loginForm.username == '') {
        this.$message.warning('请输入账号')
        return
      }
      if (this.loginForm.password == '') {
        this.$message.warning('请输入密码')
        return
      }
      loginReq(this.loginForm).then(res => {
        sessionStorage.setItem('token', res.token)
        sessionStorage.setItem('userInfo', JSON.stringify(res.userInfo.user))
        this.$router.push('/system')
      })
    }
  }
}
laboratory/src/views/login/service.js
@@ -1,6 +1,6 @@
import axios from '@/utils/request';
// 登录
export const login = (data) => {
export const loginReq = (data) => {
    return axios.post('/login', { ...data })
}
laboratory/vue.config.js
@@ -10,11 +10,19 @@
        disableHostCheck: true, //禁用主机检查 
        proxy: {
            "/api": { // 设置以什么前缀开头的请求用来代理
                target: "http://localhost:8080", //要访问的跨域的域名
                target: "http://192.168.110.34:8081", //要访问的跨域的域名
                secure: false, // 使用的是http协议则设置为false,https协议则设置为true
                changOrigin: true, //开启代理
                pathRewrite: {
                    "^/api": "",
                    "^/api": "/api",
                },
            },
            "/": { // 设置以什么前缀开头的请求用来代理
                target: "http://192.168.110.34:8081", //要访问的跨域的域名
                secure: false, // 使用的是http协议则设置为false,https协议则设置为true
                changOrigin: true, //开启代理
                pathRewrite: {
                    "^/": "/",
                },
            },
        },