pyt
6 天以前 7615cd178bddab96443504029285d65ea1e7d447
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/leshan-laboratory
29个文件已修改
5个文件已添加
3502 ■■■■■ 已修改文件
culture/src/components/SelectMember/index.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/SelectMember/service.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/Table/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/confirm-storage-dialog/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/layouts/components/HeaderNav.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/router/index.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/detail.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/index.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/deliveryAssessment/projectTeamIntegral/service.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/add.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/addProgenitor.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddAncestor.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/pedigree-chart/progenitorComponents/AddSublevelForm.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/addProject.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/detailProject.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/editProject.vue 285 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/projectList/service.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/components/add-edit.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/components/reset-password.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/views/system/user/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/AiEditor/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/addTableData.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/addTableHeader.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/index.vue 400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/SelectMember/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/SelectMember/service.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/dispatching/list.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue 771 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue 772 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/list.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue 178 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
culture/src/components/SelectMember/index.vue
@@ -1,6 +1,6 @@
<template>
    <el-dialog class="select-member" :visible.sync="visible" width="53.33%" :close-on-click-modal="false"
        :show-close="false">
    <el-dialog @open="openDialog" class="select-member" :visible.sync="visible" width="53.33%"
        :close-on-click-modal="false" :show-close="false">
        <template #title>
            <div>选择参与人员</div>
        </template>
@@ -22,9 +22,10 @@
                        <div class="select-member-content-left-list">
                            <div class="select-member-content-left-list-title">角色列表</div>
                            <div class="select-member-content-left-list-itemBox">
                                <div v-for="item in 10" :key="item.id"
                                    class="select-member-content-left-list-itemBox-item">
                                    实验员
                                <div @click="searchUserList(item.roleId)" v-for="item in roleList" :key="item.roleId"
                                    class="select-member-content-left-list-itemBox-item"
                                    :class="roleId == item.roleId && 'active'">
                                    {{ item.roleName }}
                                </div>
                            </div>
                        </div>
@@ -35,15 +36,15 @@
                        <div class="select-member-content-right-header">
                            <div class="select-member-content-right-header-text">人员列表</div>
                            <div class="select-member-content-right-header-search">
                                <el-input clearable v-model="searchName" placeholder="请输入姓名" />
                                <el-input clearable v-model="nickNameOrPhone" placeholder="请输入姓名/手机号" />
                                <el-button type="primary">搜索</el-button>
                            </div>
                        </div>
                        <Table :data="tableData" :total="0" @selection-change="handleSelectionChange"
                            :row-class-name="tableRowClassName">
                        <Table ref="memberTable" :row-key="row => row.userId" :data="tableData" :total="0"
                            @selection-change="handleSelectionChange" :row-class-name="tableRowClassName">
                            <el-table-column type="selection" width="55" />
                            <el-table-column label="角色" prop="role" />
                            <el-table-column label="姓名" prop="name" />
                            <el-table-column label="角色" prop="roleName" />
                            <el-table-column label="姓名" prop="nickName" />
                            <el-table-column label="创建时间" prop="createTime" />
                        </Table>
                    </div>
@@ -52,40 +53,79 @@
        </div>
        <div class="select-member-footer">
            <el-button @click="close" type="default">关闭</el-button>
            <el-button type="primary">确认选择</el-button>
            <el-button type="primary" @click="submit">确认选择</el-button>
        </div>
    </el-dialog>
</template>
<script>
import { mapState } from 'vuex'
import { getRoleList, getUserList } from './service'
export default {
    props: {
        projectId: {
            type: [String, Number],
            default: null
        }
    },
    data() {
        return {
            visible: false,
            search: '',
            searchName: '',
            tableData: [
                {
                    name: '张三',
                    role: '实验员',
                    createTime: '2025-1-2 15:13:58'
                },
                {
                    name: '李四',
                    role: '实验员',
                    createTime: '2025-1-2 15:13:58'
                },
            ],
            selectData: []
            nickNameOrPhone: '',
            tableData: [],
            selectData: [],
            roleList: [],
            roleId: null,
        }
    },
    computed: {
        ...mapState(['isFold'])
    },
    methods: {
        setSelection(selected) {
            this.selectData = selected
            this.$nextTick(() => {
                // 设置新选中
                this.tableData.forEach(row => {
                    if (selected.some(i => i.userId === row.userId)) {
                        this.$refs.memberTable.toggleRowSelection(row, true)
                    }
                })
            })
        },
        openDialog() {
            // 获取角色列表并根据项目组ID进行过滤
            getRoleList().then(res => {
                if (this.projectId) {
                    // 过滤出实验员和化验师角色
                    this.roleList = res.filter(item => item.roleId == 4 || item.roleId == 5);
                } else {
                    this.roleList = res;
                }
            });
            this.searchUserList(null);
        },
        handleSelectionChange(val) {
            this.selectData = val
        },
        async searchUserList(roleId) {
            this.roleId = roleId
            // 根据是否有项目组ID来决定调用不同的接口
            const params = {
                roleIds: roleId ? [roleId] : [],
                nickNameOrPhone: this.searchName,
                pageSize: 9999,
                pageNum: 1
            };
            if (this.projectId) {
                params.projectId = this.projectId;
                // TODO: 这里需要替换为新的接口调用
                // const res = await getProjectUserList(params);
            } else {
                const res = await getUserList(params);
                this.tableData = res.records;
            }
            // 数据加载完成后重新应用选中状态
            this.setSelection(this.selectData)
        },
        searchRole() {
            this.search = ''
@@ -96,8 +136,11 @@
        close() {
            this.visible = false
        },
        submit() {
            this.$emit('submit', this.selectData)
        },
        tableRowClassName({ row, rowIndex }) {
            if (this.selectData.findIndex(item => item.name === row.name) != -1) {
            if (this.selectData.findIndex(item => item.userId === row.userId) != -1) {
                return 'select-row';
            }
            return '';
@@ -191,6 +234,12 @@
                    &:last-child {
                        margin-bottom: 0;
                    }
                    &:hover,
                    &.active {
                        background: rgba(4, 156, 154, 0.1);
                        color: #049C9A;
                    }
                }
            }
        }
culture/src/components/SelectMember/service.js
New file
@@ -0,0 +1,16 @@
import axios from '@/utils/request';
// 列表
export const getProjectList = (data) => {
    return axios.post('/api/t-project-team/pageList', { ...data })
}
// 用户列表
export const getUserList = (data) => {
    return axios.post('/system/user/list', { ...data })
}
// 角色列表不分页
export const getRoleList = (data) => {
    return axios.post('/system/role/listNotPage', { ...data })
}
culture/src/components/Table/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="table-container" style="width: 100%;">
        <el-table border v-bind="$attrs" v-on="$listeners" :height="height">
        <el-table ref="elTable"  border v-bind="$attrs" v-on="$listeners" :height="height">
            <slot></slot>
        </el-table>
        <div v-if="total > 0">
@@ -42,6 +42,14 @@
        }
    },
    methods: {
        toggleRowSelection(row, selected) {
            this.$refs.elTable.toggleRowSelection(row, selected)
            this.$forceUpdate()
        },
        clearSelection() {
            this.$refs.elTable.clearSelection()
            this.$forceUpdate()
        },
        handleCurrentChange(page) {
            this.$emit('handleCurrentChange', page)
        },
culture/src/components/confirm-storage-dialog/index.vue
@@ -1,7 +1,7 @@
<template>
  <el-dialog
    :visible.sync="visible"
    title=""
    title="签字确认"
    width="520px"
    :close-on-click-modal="false"
    custom-class="record-detail-dialog"
culture/src/layouts/components/HeaderNav.vue
@@ -17,7 +17,7 @@
      </div>
      <div class="user-info">
        <img src="@/assets/public/photo.png" />
        <div class="user-info-text">欢迎您,admin</div>
        <div class="user-info-text">欢迎您,{{ userInfo.nickName }}</div>
        <div class="user-info-line"></div>
        <div @click="outLogin" class="user-info-out">
          <img src="@/assets/public/logOut.png" />
culture/src/router/index.js
@@ -61,6 +61,24 @@
                    keepAlive: true,
                },
                component: () => import("../views/projectList/addProject"),
            },
            {
                path: "editProject",
                name: "EditProject",
                meta: {
                    title: "编辑菌种库项目组",
                    hide: true,
                },
                component: () => import("../views/projectList/editProject"),
            },
            {
                path: "detailProject",
                name: "DetailProject",
                meta: {
                    title: "菌种库项目组详情",
                    hide: true,
                },
                component: () => import("../views/projectList/detailProject"),
            }
        ]
    },
@@ -244,7 +262,7 @@
            {
                path: 'breeding-record',
                name: 'BreedingRecord',
                meta: {
                meta: {
                    title: "菌种选育保藏记录",
                },
                component: () => import("../views/strain-library/breeding-record"),
@@ -417,21 +435,21 @@
    // 登录验证
    // 排除登录页的校验
    // if (to.path === "/login") {
    //     if (sessionStorage.getItem('token')) {
    //         next('/projectList');  // 已登录状态访问登录页时重定向到系统首页
    //         return;
    //     }
    //     next();
    //     return;
    // }
    if (to.path === "/login") {
        if (sessionStorage.getItem('token')) {
            next('/projectList');  // 已登录状态访问登录页时重定向到系统首页
            return;
        }
        next();
        return;
    }
    // // 登录状态校验
    // const isAuthenticated = sessionStorage.getItem('token');
    // if (!isAuthenticated) {
    //     next('/login');  // 未登录用户重定向到登录页
    //     return;
    // }
    // 登录状态校验
    const isAuthenticated = sessionStorage.getItem('token');
    if (!isAuthenticated) {
        next('/login');  // 未登录用户重定向到登录页
        return;
    }
    // 判断是否拥有要跳转菜单权限
    let menus = store.state.menus
culture/src/views/deliveryAssessment/projectTeamIntegral/detail.vue
@@ -1,20 +1,23 @@
<template>
    <div>
    <div v-if="Object.keys(detailData).length">
        <div class="top-box-header">
            <div class="top-box-header-title">
                <div>项目组总积分表</div>
                <div class="top-box-header-time">
                    <div>评定开始时间:2024-02-09</div>
                    <div>评定结束始时间:2024-02-09</div>
                    <div>评定开始时间:{{ detailData.startTime }}</div>
                    <div>评定结束始时间:{{ detailData.endTime }}</div>
                </div>
            </div>
            <div class="top-box-integral">
                <div :style="{ backgroundColor: ['rgba(232, 250, 246, 1)', 'rgba(254, 237, 220, 1)', 'rgba(239, 248, 255, 1)', 'rgba(255, 237, 238, 1)'][item - 1] }"
                    v-for="item in 4" :key="item" class="top-box-integral-card">
                    <div class="top-box-integral-card-title">{{ ['项目组总积分', '化验师积分', '实验员积分', '实验终止次数'][item -
                    <div class="top-box-integral-card-title">{{ ['项目组总积分', '菌种工程师积分', '菌种实验员积分', '菌种实验失败次数'][item -
                        1] }}</div>
                    <div :style="{ color: ['rgba(4, 156, 154, 1)', 'rgba(255, 147, 0, 1)', 'rgba(23, 119, 213, 1)', 'rgba(255, 73, 85, 1)'][item - 1] }"
                        class="top-box-integral-card-num">99.9</div>
                        class="top-box-integral-card-num">{{
                            detailData[['teamIntegral', 'engineerIntegral', 'experimenterIntegral', 'failCount'][item - 1]]
                        }}
                    </div>
                </div>
            </div>
        </div>
@@ -27,8 +30,13 @@
                    </div>
                </div>
                <div class="integral-content-box-right">
                    <div v-show="actionsLeftTab != 1" @wheel.prevent="handleWheel" class="integral-content-box-right-nameTab">
                        <div @click="changeActiveName(item)" :class="activeNameTab == item && 'activeName'" class="integral-content-box-right-nameTab-name" v-for="item in 8" :key="item">张三</div>
                    <div v-show="actionsLeftTab != 1" @wheel.prevent="handleWheel"
                        class="integral-content-box-right-nameTab">
                        <div @click="changeActiveName(item.userName)"
                            :class="activeNameTab == item.userName && 'activeName'"
                            class="integral-content-box-right-nameTab-name"
                            v-for="{ item, index } in detailData.detailExperimentVOS" :key="index">{{ item.userName }}
                        </div>
                    </div>
                    <div class="integral-content-box-right-thead">
                        <div>评定项</div>
@@ -37,14 +45,31 @@
                        <div>结束时间</div>
                    </div>
                    <div class="integral-content-box-right-body">
                        <div v-for="item in itemList" :key="item" class="integral-content-box-right-body-item">
                        <div v-for="(item, index) in itemList" :key="index"
                            class="integral-content-box-right-body-item">
                            <div>{{ item.gainer }}</div>
                            <div>
                                <div v-if="item.situationOne">{{ item.situationOne }}</div>
                                <div v-if="item.situationTwo">{{ item.situationTwo }}</div>
                                <div>{{ item.situationOne }}{{
                                    actionsLeftTab === 2 ?
                                        (selectedExperimenter || {})[item.keys[0]] :
                                        detailData[item.keys[0]]
                                }}</div>
                                <div>{{ item.situationTwo }}{{
                                    actionsLeftTab === 2 ?
                                        (selectedExperimenter || {})[item.keys[1]] :
                                        detailData[item.keys[1]]
                                }}</div>
                            </div>
                            <div></div>
                            <div></div>
                            <div>{{
                                actionsLeftTab === 2 ?
                                    (selectedExperimenter || {})[item.keys[2]] :
                                    detailData[item.keys[2]]
                            }}</div>
                            <div>{{
                                actionsLeftTab === 2 ?
                                    (selectedExperimenter || {})[item.keys[3]] :
                                    detailData[item.keys[3]]
                            }}</div>
                        </div>
                    </div>
                </div>
@@ -54,27 +79,30 @@
</template>
<script>
import { getDetailData } from './service'
export default {
    data() {
        return {
            actionsLeftTab: 1,
            activeNameTab: 1,
            actionspPersonnel: null,
            activeNameTab: '',
            craftList: [
                {
                    gainer: '1、创新型课题',
                    situationOne: '课题数:',
                    situationTwo: '积分数:',
                    keys: ['innovateCount', 'innovateIntegral', 'innovateStartTime', 'innovateEndTime']
                },
                {
                    gainer: '2、规程型课题',
                    situationOne: '课题数:',
                    situationTwo: '积分数:',
                    keys: ['regulationCount', 'regulationIntegral', 'regulationStartTime', 'regulationEndTime']
                },
                {
                    gainer: '3、实验操作评定',
                    situationOne: '考核数:',
                    situationTwo: '积分数:',
                    keys: ['handleCount', 'handleIntegral', 'handleStartTime', 'handleEndTime']
                },
            ],//菌种工程师
            assayList: [
@@ -82,8 +110,11 @@
                    gainer: '1、实验操作评定',
                    situationOne: '考核数:',
                    situationTwo: '积分数:',
                    keys: ['handleCount', 'handleIntegral', 'handleStartTime', 'handleEndTime']
                },
            ],//菌种实验员
            detailData: {},
            selectedExperimenter: null
        }
    },
    computed: {
@@ -99,14 +130,29 @@
        }
    },
    created() {
        getDetailData(this.$route.query.id).then(res => {
            this.detailData = res;
            // 确保detailExperimentVOS存在且是数组
            this.detailData.detailExperimentVOS = this.detailData.detailExperimentVOS || [];
            if (this.detailData.detailExperimentVOS.length) {
                this.selectedExperimenter = this.detailData.detailExperimentVOS[0];
                this.activeNameTab = this.selectedExperimenter.userName;
            } else {
                // 设置空对象保护
                this.selectedExperimenter = {};
                this.activeNameTab = '';
            }
        })
    },
    methods: {
        changeActiveItem(item) {
            this.actionsLeftTab = item
        },
        changeActiveName(item) {
            this.activeNameTab = item
        changeActiveName(userName) {
            this.activeNameTab = userName;
            this.selectedExperimenter = this.detailData.detailExperimentVOS.find(
                item => item.userName === userName
            );
        },
        handleWheel(e) {
            if (this.scrollTimer) {
culture/src/views/deliveryAssessment/projectTeamIntegral/index.vue
@@ -5,34 +5,38 @@
            <template #search>
                <el-form :model="form" label-width="140px" inline>
                    <el-form-item label="项目组名称:">
                        <el-input v-model="form.name" placeholder="请输入"></el-input>
                        <el-input v-model="form.teamName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="创建日期:">
                        <el-date-picker v-model="value1" type="daterange" range-separator="至" start-placeholder="开始日期"
                            end-placeholder="结束日期">
                        <el-date-picker v-model="form.date" type="daterange" range-separator="至"
                            start-placeholder="开始日期" end-placeholder="结束日期">
                        </el-date-picker>
                    </el-form-item>
                    <el-form-item label="状态:">
                        <el-select placeholder="请选择"></el-select>
                        <el-select placeholder="请选择" v-model="form.status">
                            <el-option label="待审核" :value="1"></el-option>
                            <el-option label="待评定" :value="2"></el-option>
                            <el-option label="已评定" :value="3"></el-option>
                            <el-option label="已驳回" :value="4"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item class="search-btn-box">
                        <el-button>重置</el-button>
                        <el-button type="primary">查询</el-button>
                        <el-button @click="reset">重置</el-button>
                        <el-button type="primary" @click="search">查询</el-button>
                    </el-form-item>
                </el-form>
            </template>
            <template #table>
                <el-table-column prop="name" label="项目组名称" />
                <el-table-column prop="age" label="项目组总积分" />
                <el-table-column prop="age" label="工艺工程师积分" />
                <el-table-column prop="age" label="化验师积分" />
                <el-table-column prop="age" label="实验员积分" />
                <el-table-column prop="age" label="评定开始时间" />
                <el-table-column prop="age" label="评定结束时间" />
                <el-table-column prop="age" label="状态" />
                <el-table-column prop="age" label="操作">
                <el-table-column prop="teamName" label="所属项目组" />
                <el-table-column prop="teamIntegral" label="菌种项目组总积分" />
                <el-table-column prop="engineerIntegral" label="菌种工程师积分" />
                <el-table-column prop="experimenterIntegral" label="菌种实验员积分" />
                <el-table-column prop="failCount" label="菌种实验员失败次数" />
                <el-table-column prop="startTime" label="评定开始时间" />
                <el-table-column prop="endTime" label="评定结束时间" />
                <el-table-column label="操作">
                    <template #default="{ row }">
                        <el-button @click="goDetail" type="text">详情</el-button>
                        <el-button @click="goDetail(row.projectId)" type="text">详情</el-button>
                    </template>
                </el-table-column>
            </template>
@@ -41,12 +45,12 @@
</template>
<script>
import { getListData } from './service'
export default {
    name: 'ProjectTeamIntegral',
    data() {
        return {
            form: {
            },
            form: {},
            tableData: [],
            queryForm: {
                pageSize: 10,
@@ -55,10 +59,13 @@
            total: 0
        }
    },
    created() {
        this.getList()
    },
    methods: {
        goDetail() {
        goDetail(id) {
            this.$router.push({
                path: '/projectList/addProject'
                path: `/deliveryAssessment/projectTeamIntegral-detail?id=${id}`,
            })
        },
        handleCurrentChange(page) {
@@ -70,7 +77,28 @@
            this.getList()
        },
        getList() {
            let obj = {
                ...this.queryForm
            }
            if (obj.date) {
                obj.startTime = moment(obj.date[0]).format('YYYY-MM-DD')
                obj.endTime = moment(obj.date[1]).format('YYYY-MM-DD')
                delete obj.date
            }
            getListData(obj).then(res => {
                this.tableData = res.data.records
                this.total = res.data.total
            })
        },
        reset() {
            this.queryForm = {
                pageSize: 10,
                pageNum: 1
            }
            this.getList()
        },
        search() {
            this.getList()
        }
    }
}
culture/src/views/deliveryAssessment/projectTeamIntegral/service.js
New file
@@ -0,0 +1,11 @@
import axios from '@/utils/request';
// 列表
export const getListData = (data) => {
    return axios.post('/api/t-strain-report/pageListProject', { ...data })
}
// 详情
export const getDetailData = (id) => {
    return axios.get(`/open/t-strain-report/getDetailByIdProject?id=${id}`)
}
culture/src/views/pedigree-chart/add.vue
@@ -60,34 +60,36 @@
        <el-button @click="handleDraft">存草稿</el-button>
        <el-button @click="handleCancel">取消</el-button>
      </div>
      <!-- 签字确认组件 -->
      <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </el-form>
    <ParentForm ref="parentForm" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
    <AddSublevelPlan ref="addSublevelPlan" @addNodeSign="addNodeSign" />
    <ConfirmStorageDialog name="接种操作人签字" :visible.sync="confirmStorageDialogVisible"
      @confirm="handleSignatureConfirm" />
    <!-- 菌种工程师 -->
    <ConfirmStorageDialog name="菌种保藏人签字" text="是否确认该项菌种信息入库" :visible.sync="storageVisible"
      @confirm="handleSignatureConfirm" />
  </div>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import G6 from '@antv/g6';
import ParentForm from "./components/ParentForm.vue";
import PlanForm from "./components/PlanForm.vue";
import AddSublevelForm from "./components/AddSublevelForm.vue";
import AddSublevelPlan from "./components/AddSublevelPlan.vue";
import ConfirmStorageDialog from "@/components/confirm-storage-dialog";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
    ParentForm,
    PlanForm,
    AddSublevelForm,
    AddSublevelPlan
    AddSublevelPlan,
    ConfirmStorageDialog
  },
  data() {
    return {
@@ -127,6 +129,9 @@
      isAddingNode: false,
      nodeData: {},
      nodeType: '',//1母代 2计划数 3子孙代
      tableData: [],
      confirmStorageDialogVisible: false,
      storageVisible: false
    };
  },
  computed: {
@@ -154,12 +159,12 @@
    addNodeSign(value, type) {
      this.nodeData = value
      this.nodeType = type
      this.signatureVisible = true;
      this.confirmStorageDialogVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.signatureVisible = true;
          this.confirmStorageDialogVisible = true;
        }
      });
    },
@@ -171,7 +176,7 @@
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      this.confirmStorageDialogVisible = false;
      console.log("submit form with signature:", signatureImage);
      if (this.nodeType === 1) {
        this.handleAddParent(this.nodeData)
culture/src/views/pedigree-chart/addProgenitor.vue
@@ -47,31 +47,33 @@
        <el-button @click="handleDraft">存草稿</el-button>
        <el-button @click="handleCancel">取消</el-button>
      </div>
      <!-- 签字确认组件 -->
      <SignatureCanvas :visible.sync="signatureVisible" @confirm="handleSignatureConfirm" />
    </el-form>
    <AddAncestor ref="addAncestor" @addNodeSign="addNodeSign" />
    <PlanForm ref="planForm" @addNodeSign="addNodeSign" />
    <AddSublevelForm ref="addSublevelForm" @addNodeSign="addNodeSign" />
    <ConfirmStorageDialog name="接种操作人签字" :visible.sync="confirmStorageDialogVisible"
      @confirm="handleSignatureConfirm" />
    <!-- 菌种工程师 -->
    <ConfirmStorageDialog name="菌种保藏人签字" text="是否确认该项菌种信息入库" :visible.sync="storageVisible"
      @confirm="handleSignatureConfirm" />
  </div>
</template>
<script>
import SignatureCanvas from "@/components/SignatureCanvas.vue";
import G6 from '@antv/g6';
import PlanForm from "./progenitorComponents/PlanForm.vue";
import AddAncestor from "./progenitorComponents/AddAncestor.vue";
import AddSublevelForm from "./progenitorComponents/AddSublevelForm.vue";
import ConfirmStorageDialog from "@/components/confirm-storage-dialog";
export default {
  name: "AddPedigree",
  components: {
    SignatureCanvas,
    PlanForm,
    AddAncestor,
    AddSublevelForm
    AddSublevelForm,
    ConfirmStorageDialog
  },
  data() {
    return {
@@ -111,6 +113,9 @@
      isAddingNode: false,
      nodeData: {},
      nodeType: '',//1祖代 2计划数 3母代
      tableData: [],
      confirmStorageDialogVisible: false,
      storageVisible: false
    };
  },
  computed: {
@@ -138,12 +143,12 @@
    addNodeSign(value, type) {
      this.nodeData = value
      this.nodeType = type
      this.signatureVisible = true;
      this.confirmStorageDialogVisible = true;
    },
    handleSubmit() {
      this.$refs.pedigreeForm.validate((valid) => {
        if (valid) {
          this.signatureVisible = true;
          this.confirmStorageDialogVisible = true;
        }
      });
    },
@@ -155,7 +160,7 @@
      this.$router.back();
    },
    handleSignatureConfirm(signatureImage) {
      this.signatureVisible = false;
      this.confirmStorageDialogVisible = false;
      console.log("submit form with signature:", signatureImage);
      if (this.nodeType === 1) {
        this.handleAddParent(this.nodeData)
@@ -173,7 +178,7 @@
          strainName: this.form.strainName,
          strainNo: this.form.strainNo,
          status: 'add',
          activeType: null,
          activeType: 1,
          isDiscarded: true,
        });
        return
@@ -204,9 +209,8 @@
        this.$refs.addSublevelForm.openInitData({
          title: '新增菌种传代项',
          form: {
            strainName: this.form.strainName,
            strainNo: this.form.strainNo,
            isDiscarded: true
            isDiscarded: true,
            ...nodeModel.data
          }
        })
      } else {
culture/src/views/pedigree-chart/progenitorComponents/AddAncestor.vue
@@ -22,8 +22,14 @@
                        </div>
                    </el-form-item>
                </el-col>
                <el-col :span="20">
                <el-col :span="20" v-if="planForm.activeType == 1">
                    <el-form-item label="来源获得" prop="source">
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.source"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10" v-else>
                    <el-form-item label="菌落编号" prop="source">
                        <el-input :disabled="planForm.status != 'add'" v-model="planForm.source"
                            placeholder="请输入"></el-input>
                    </el-form-item>
@@ -93,11 +99,7 @@
    data() {
        return {
            planDialogVisible: false,
            planForm: {
                status: 'add',
                activeType: 1,
                isDiscarded: true,
            },
            planForm: {},
            planRules: {
                inoculateNo: [
                    { required: true, message: '请输入菌种编号', trigger: 'blur' }
culture/src/views/pedigree-chart/progenitorComponents/AddSublevelForm.vue
@@ -15,23 +15,23 @@
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种编号" prop="strainNo">
                        <el-input disabled v-model="form.strainNo"></el-input>
                        <el-input disabled v-model="form.inoculateNo"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="传代菌种名称" prop="strainName">
                        <el-input disabled v-model="form.strainName"></el-input>
                        <el-input disabled v-model="form.inoculateName"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种编号" prop="inoculateNo">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateNo"
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.num"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
                <el-col :span="10">
                    <el-form-item label="接种菌种名称" prop="inoculateName">
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.inoculateName"
                        <el-input :disabled="!dialogTitle.includes('新增')" v-model="form.name"
                            placeholder="请输入"></el-input>
                    </el-form-item>
                </el-col>
culture/src/views/projectList/addProject.vue
@@ -2,11 +2,11 @@
    <Card>
        <template>
            <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="name" label="项目组名称">
                    <el-input v-model="form.name" placeholder="请输入" />
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="description" label="项目负责人">
                    <el-input v-model="form.description" placeholder="请输入" />
                <el-form-item prop="personCharge" label="项目负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
@@ -17,41 +17,73 @@
                <el-button class="el-icon-plus" type="primary" @click="addMember"> 添加项目组成员</el-button>
            </div>
            <div class="member-list">
                <div v-for="(item,index) in ['审批人', '菌种工程师', '菌种实验员']" :key="item" class="member-list-card">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ item }}</div>
                        <div :class="index == 0 || index == 1 ? 'member-name-box' : 'member-name-box-2'">
                            <div v-for="i in memberList(index+1)" :key="i" class="member-name">张三</div>
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                        <div class="member-edit">修改</div>
                        <div class="member-edit" v-if="memberList(item).length != 0" @click="editUserList">修改</div>
                    </div>
                </div>
            </div>
            <div class="add-project-footer">
                <el-button type="primary">保存</el-button>
                <el-button @click="submitForm" type="primary">保存</el-button>
            </div>
        </template>
        <SelectMember ref="selectMember" />
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { addProject } from './service'
export default {
    name: 'AddProject',
    data() {
        return {
            form: {},
            rules: {
                name: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                description: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            }
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            // 角色配置常量
            ROLE_CONFIG: {
                1: { key: 'approver', limit: 1, label: '菌种审批人' },
                2: { key: 'engineer', limit: 1, label: '菌种工程师' },
            },
        }
    },
    methods: {
        submitForm() {
            this.$refs.form.validate((valid) => {
                if (valid) {
                    console.log('submit!')
                    if (this.selectMemberData.length == 0) {
                        this.$message.error('请选择项目组成员')
                        return
                    }
                    const ROLE_NAME_TO_TYPE = {
                        '菌种审批人': 1,
                        '菌种工程师': 2,
                        '菌种实验员': 3,
                    };
                    const data = {
                        teamName: this.form.teamName,
                        personCharge: this.form.personCharge,
                        staffs: this.selectMemberData.map(member => ({
                            userId: member.userId,
                            roleType: ROLE_NAME_TO_TYPE[member.roleName]
                        }))
                    }
                    addProject(data).then(res => {
                        if (res.code == 200) {
                            this.$message.success('添加成功')
                            this.$router.push({ name: 'ProjectList' })
                        }
                    })
                }
            })
        },
@@ -61,16 +93,31 @@
        memberList(i) {
            switch (i) {
                case 1:
                    return [1]
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return [1]
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return [1, 2, 3, 4, 5, 6, 7, 8]
                case 4:
                    return [1, 2, 3, 4, 5, 6, 7, 8]
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
        selectUser(data) {
            for (const [roleId, config] of Object.entries(this.ROLE_CONFIG)) {
                const members = data.filter(item => item.roleName === config.label);
                if (members.length > config.limit) {
                    this.$message.error(`${config.label}最多只能选择${config.limit}个`);
                    return
                }
            }
            this.selectMemberData = data;
            this.$refs.selectMember.close();
        },
        editUserList() {
            this.$refs.selectMember.open();
            this.$nextTick(() => {
                this.$refs.selectMember.setSelection(this.selectMemberData);
            });
        }
    }
}
@@ -188,6 +235,10 @@
                font-weight: 500;
                font-size: 16px;
                color: #FFFFFF;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
culture/src/views/projectList/detailProject.vue
New file
@@ -0,0 +1,233 @@
<template>
    <Card>
        <template>
            <el-form disabled ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="personCharge" label="项目组负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
                <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>项目组成员</div>
                </div>
            </div>
            <div class="member-list">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                    </div>
                </div>
            </div>
        </template>
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { getProjectDetail } from './service'
export default {
    name: 'EddProject',
    data() {
        return {
            form: {},
            rules: {
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            ROLE_NAME_TO_TYPE: {
                '菌种审批人': 1,
                '菌种工程师': 2,
                '菌种实验员': 3,
            }
        }
    },
    created() {
        getProjectDetail({ id: this.$route.query.id }).then(res => {
            this.form = {
                teamName: res.teamName,
                personCharge: res.personCharge
            }
            this.selectMemberData = res.staffs.map(item => ({
                userId: Number(item.userId),
                roleName: ['菌种审批人', '菌种工程师', '菌种实验员'][item.roleType - 1],
                nickName: item.nickName
            }))
        })
    },
    methods: {
        addMember() {
            this.$refs.selectMember.open()
        },
        memberList(i) {
            switch (i) {
                case 1:
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
    }
}
</script>
<style scoped lang="less">
.el-form--inline .el-form-item {
    margin-right: 83px;
}
.header-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 13px;
    .header-title-left {
        display: flex;
        align-items: center;
        gap: 13px;
        img {
            width: 12px;
            height: 19px;
        }
        div {
            flex-shrink: 0;
            font-weight: bold;
            font-size: 18px;
            color: #222222;
            line-height: 27px;
            font-family: 'Source Han Sans CN Bold Bold';
            &:before {
                content: '*';
                color: #F56C6C;
                margin-right: 4px;
            }
        }
    }
}
.member-list {
    margin-top: 18px;
    display: flex;
    flex-wrap: wrap;
    gap: 28px;
    .member-list-card {
        position: relative;
        width: 340px;
        height: 400px;
        border-radius: 8px;
        border: 1px solid #DCDFE6;
        &:nth-child(1) {
            background: linear-gradient(to bottom, rgba(4, 156, 154, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(2) {
            background: linear-gradient(to bottom, rgba(5, 160, 193, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(3) {
            background: linear-gradient(to bottom, rgba(255, 77, 79, 0.20) 0%, rgba(255, 242, 194, 0) 70%);
        }
        &:nth-child(4) {
            background: linear-gradient(to bottom, rgba(250, 199, 20, 0.21) 0%, rgba(255, 242, 194, 0) 70%);
        }
        .member-item {
            height: 100%;
            display: flex;
            flex-direction: column;
            .member-title {
                margin-top: 20px;
                width: 100%;
                font-family: 'Source Han Sans CN Bold Bold';
                font-weight: bold;
                font-size: 16px;
                color: rgba(0, 0, 0, 0.8);
                line-height: 16px;
                text-align: center;
            }
            .member-name-box {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .member-name-box-2 {
                padding: 0 20px;
                padding-top: 40px;
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                align-items: flex-start;
                flex-wrap: wrap;
                gap: 20px;
                justify-content: center;
            }
            .member-name {
                width: 60px;
                height: 60px;
                background: #7D8B79;
                border-radius: 50%;
                text-align: center;
                line-height: 60px;
                font-weight: 500;
                font-size: 16px;
                color: #FFFFFF;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
                cursor: pointer;
                position: absolute;
                bottom: 10px;
                left: 50%;
                transform: translateX(-50%);
                font-weight: 400;
                font-size: 12px;
                color: #FF4D4F;
                line-height: 22px;
                width: 40px;
                background: #FFF1F0;
                border-radius: 4px;
                border: 1px solid #FFCCC7;
                text-align: center;
            }
        }
    }
}
.add-project-footer {
    margin-top: 43px;
    button {
        width: 220px;
    }
}
</style>
culture/src/views/projectList/editProject.vue
New file
@@ -0,0 +1,285 @@
<template>
    <Card>
        <template>
            <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
                <el-form-item prop="teamName" label="项目组名称">
                    <el-input v-model="form.teamName" placeholder="请输入" />
                </el-form-item>
                <el-form-item prop="personCharge" label="项目组负责人">
                    <el-input v-model="form.personCharge" placeholder="请输入" />
                </el-form-item>
            </el-form>
            <div class="header-title">
                <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>项目组成员</div>
                </div>
                <el-button class="el-icon-plus" type="primary" @click="addMember"> 添加项目组成员</el-button>
            </div>
            <div class="member-list">
                <div v-for="item in 3" :key="item" class="member-list-card">
                    <div class="member-item">
                        <div class="member-title">{{ ['菌种审批人', '菌种工程师', '菌种实验员'][item - 1] }}</div>
                        <div :class="item == 1 || item == 2 ? 'member-name-box' : 'member-name-box-2'">
                            <el-tooltip v-for="i in memberList(item)" :key="i.userId" class="member-name" effect="dark"
                                :content="i.nickName" placement="top">
                                <span>{{ i.nickName }}</span>
                            </el-tooltip>
                        </div>
                        <div class="member-edit" v-if="memberList(item).length != 0" @click="editUserList">修改</div>
                    </div>
                </div>
            </div>
            <div class="add-project-footer">
                <el-button @click="submitForm" type="primary">保存</el-button>
            </div>
        </template>
        <SelectMember ref="selectMember" @submit="selectUser" />
    </Card>
</template>
<script>
import { getProjectDetail, editProject } from './service'
export default {
    name: 'EddProject',
    data() {
        return {
            form: {},
            rules: {
                teamName: [{ required: true, message: '请输入项目组名称', trigger: 'blur' }],
                personCharge: [{ required: true, message: '请输入项目组描述', trigger: 'blur' }]
            },
            selectMemberData: [],
            // 角色配置常量
            ROLE_CONFIG: {
                1: { key: 'approver', limit: 1, label: '菌种审批人' },
                2: { key: 'engineer', limit: 1, label: '菌种工程师' },
            },
            ROLE_NAME_TO_TYPE: {
                '菌种审批人': 1,
                '菌种工程师': 2,
                '菌种实验员': 3,
            }
        }
    },
    created() {
        getProjectDetail({ id: this.$route.query.id }).then(res => {
            this.form = {
                teamName: res.teamName,
                personCharge: res.personCharge
            }
            this.selectMemberData = res.staffs.map(item => ({
                userId: Number(item.userId),
                roleName: ['菌种审批人', '菌种工程师', '菌种实验员'][item.roleType - 1],
                nickName: item.nickName
            }))
        })
    },
    methods: {
        submitForm() {
            this.$refs.form.validate((valid) => {
                if (valid) {
                    if (this.selectMemberData.length == 0) {
                        this.$message.error('请选择项目组成员')
                        return
                    }
                    const data = {
                        id: this.$route.query.id,
                        teamName: this.form.teamName,
                        personCharge: this.form.personCharge,
                        staffs: this.selectMemberData.map(member => ({
                            userId: member.userId,
                            roleType: this.ROLE_NAME_TO_TYPE[member.roleName]
                        }))
                    }
                    editProject(data).then(res => {
                        if (res.code == 200) {
                            this.$message.success('添加成功')
                            this.$router.push({ name: 'ProjectList' })
                        }
                    })
                }
            })
        },
        addMember() {
            this.$refs.selectMember.open()
        },
        memberList(i) {
            switch (i) {
                case 1:
                    return this.selectMemberData.filter(item => item.roleName == '菌种审批人')
                case 2:
                    return this.selectMemberData.filter(item => item.roleName == '菌种工程师')
                case 3:
                    return this.selectMemberData.filter(item => item.roleName == '菌种实验员')
                default:
                    break;
            }
        },
        selectUser(data) {
            for (const [roleId, config] of Object.entries(this.ROLE_CONFIG)) {
                const members = data.filter(item => item.roleName === config.label);
                if (members.length > config.limit) {
                    this.$message.error(`${config.label}最多只能选择${config.limit}个`);
                    return
                }
            }
            this.selectMemberData = data;
            this.$refs.selectMember.close();
        },
        editUserList() {
            this.$refs.selectMember.open();
            this.$nextTick(() => {
                this.$refs.selectMember.setSelection(this.selectMemberData);
            });
        }
    }
}
</script>
<style scoped lang="less">
.el-form--inline .el-form-item {
    margin-right: 83px;
}
.header-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 13px;
    .header-title-left {
        display: flex;
        align-items: center;
        gap: 13px;
        img {
            width: 12px;
            height: 19px;
        }
        div {
            flex-shrink: 0;
            font-weight: bold;
            font-size: 18px;
            color: #222222;
            line-height: 27px;
            font-family: 'Source Han Sans CN Bold Bold';
            &:before {
                content: '*';
                color: #F56C6C;
                margin-right: 4px;
            }
        }
    }
}
.member-list {
    margin-top: 18px;
    display: flex;
    flex-wrap: wrap;
    gap: 28px;
    .member-list-card {
        position: relative;
        width: 340px;
        height: 400px;
        border-radius: 8px;
        border: 1px solid #DCDFE6;
        &:nth-child(1) {
            background: linear-gradient(to bottom, rgba(4, 156, 154, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(2) {
            background: linear-gradient(to bottom, rgba(5, 160, 193, 0.2) 0%, rgba(5, 242, 194, 0) 70%);
        }
        &:nth-child(3) {
            background: linear-gradient(to bottom, rgba(255, 77, 79, 0.20) 0%, rgba(255, 242, 194, 0) 70%);
        }
        &:nth-child(4) {
            background: linear-gradient(to bottom, rgba(250, 199, 20, 0.21) 0%, rgba(255, 242, 194, 0) 70%);
        }
        .member-item {
            height: 100%;
            display: flex;
            flex-direction: column;
            .member-title {
                margin-top: 20px;
                width: 100%;
                font-family: 'Source Han Sans CN Bold Bold';
                font-weight: bold;
                font-size: 16px;
                color: rgba(0, 0, 0, 0.8);
                line-height: 16px;
                text-align: center;
            }
            .member-name-box {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .member-name-box-2 {
                padding: 0 20px;
                padding-top: 40px;
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                align-items: flex-start;
                flex-wrap: wrap;
                gap: 20px;
                justify-content: center;
            }
            .member-name {
                width: 60px;
                height: 60px;
                background: #7D8B79;
                border-radius: 50%;
                text-align: center;
                line-height: 60px;
                font-weight: 500;
                font-size: 16px;
                color: #FFFFFF;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                box-sizing: border-box;
            }
            .member-edit {
                cursor: pointer;
                position: absolute;
                bottom: 10px;
                left: 50%;
                transform: translateX(-50%);
                font-weight: 400;
                font-size: 12px;
                color: #FF4D4F;
                line-height: 22px;
                width: 40px;
                background: #FFF1F0;
                border-radius: 4px;
                border: 1px solid #FFCCC7;
                text-align: center;
            }
        }
    }
}
.add-project-footer {
    margin-top: 43px;
    button {
        width: 220px;
    }
}
</style>
culture/src/views/projectList/service.js
@@ -2,30 +2,30 @@
// 列表
export const getProjectList = (data) => {
    return axios.post('/t_project_team/api/pageList', { ...data })
    return axios.post('/api/t_project_team/pageList', { ...data })
}
// 新增
export const addProject = (data) => {
    return axios.post('/t_project_team/api/add', { ...data })
    return axios.post('/api/t_project_team/add', { ...data })
}
// 编辑
export const editProject = (data) => {
    return axios.post('/t_project_team/api/update', { ...data })
    return axios.post('/api/t_project_team/update', { ...data })
}
// 详情
export const getProjectDetail = (data) => {
    return axios.get(`/t_project_team/open/getDetailById?id=${data.id}`)
    return axios.get(`/open/t_project_team/getDetailById?id=${data.id}`)
}
// 修改项目组状态
export const changeStatus = (data) => {
    return axios.post('/t_project_team/api/upAndDown', { ...data })
    return axios.post('/api/t_project_team/upAndDown', { ...data })
}
// 删除项目组
export const deleteProject = (data) => {
    return axios.delete(`/t_project_team/open/deleteById?id=${data.id}`)
    return axios.delete(`/open/t_project_team/deleteById?id=${data.id}`)
}
culture/src/views/system/user/components/add-edit.vue
@@ -20,7 +20,7 @@
              :value="item.roleId"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="登录状态" prop="status">
        <el-form-item label="启动状态" prop="status">
          <el-switch v-model="form.status"></el-switch>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
@@ -56,33 +56,51 @@
    },
  },
  data() {
    var validatePhone = (rule, value, callback) => {
      if (!value) {
        // The 'required' rule will handle empty value, so we can make this optional here
        // or keep it for a more specific message if needed.
        // For now, let's assume 'required' handles the empty case.
        callback();
        return;
      }
      const phoneRegex = new RegExp(/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,'g'); // Regex for 11-digit Chinese mobile numbers
      if (!phoneRegex.test(value)) {
        callback(new Error('请输入有效的11位手机号码'));
      } else {
        callback();
      }
    };
    return {
      form: { status: true },
      userDeptId: '',
      rules: {
        nickName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
        phonenumber: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
        roleId: [{ required: true, message: '请选择角色', trigger: 'blur' }],
        phonenumber: [
          { required: true, message: '请输入联系电话', trigger: 'blur' },
          { validator: validatePhone, trigger: 'blur' }
        ],
        roleId: [{ required: true, message: '请选择角色', trigger: 'change' }],
        userName: [{ required: true, message: '请输入登陆账号', trigger: 'blur' }],
        status: [{ required: true, message: '请选择启动状态', trigger: 'blur' }],
      },
    }
  },
  created() {
    this.form = { status: true }
    if (Object.keys(this.row).length) {
    if (this.row && this.row.userId) {
      this.form = {
        userId: this.row.userId,
        nickName: this.row.nickName,
        phonenumber: this.row.phonenumber,
        roleId: this.row.roleId,
        userName: this.row.userName,
        remark: this.row.remark,
        ...this.row,
        status: this.row.status == 0 ? true : false,
      }
    } else {
      this.form = {}
      this.form = {
        nickName: '',
        phonenumber: '',
        userName: '',
        roleId: '',
        status: true,
        remark: '',
      }
    }
  },
  mounted() { },
culture/src/views/system/user/components/reset-password.vue
@@ -3,16 +3,16 @@
    <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="重置密码" width="30%">
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="姓名" prop="nickName">
          <el-input :disabled="true" v-model="form.nickName" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input :disabled="true" v-model="form.nickName" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="登陆账号" prop="account">
          <el-input :disabled="true" v-model="form.account" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input :disabled="true" v-model="form.account" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="新密码" prop="password">
          <el-input v-model="form.password" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input v-model="form.password" type="password" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input v-model="form.confirmPassword" placeholder="请输入" style="width: 50%;"></el-input>
          <el-input v-model="form.confirmPassword" type="password" placeholder="请输入" style="width: 95%;"></el-input>
        </el-form-item>
      </el-form>
      <div class="select-member-footer">
culture/src/views/system/user/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="list">
    <TableCustom :queryForm="pagination" :tableData="data" :total="pagination.total"
      @currentChange="handleCurrentChange" @sizeChange="handleSizeChange">
      @handleCurrentChange="handleCurrentChange" @handleSizeChange="handleSizeChange">
      <template #search>
        <el-form label-width="100px" inline>
          <el-form-item label="人员搜索">
@@ -24,7 +24,7 @@
          </el-form-item>
          <el-form-item style="margin-left: 63px;">
            <el-button @click="reset">重置</el-button>
            <el-button type="primary" @click="onSubmit">查询</el-button>
            <el-button type="primary" @click="onSubmit" style="margin-left: 10px;">查询</el-button>
          </el-form-item>
        </el-form>
      </template>
@@ -45,10 +45,6 @@
            <div class="status_class">
              <div :class="row.status == 0 ? 'green' : 'red'"></div>
              <div>{{ row.status == 0 ? '正常' : '禁用' }}</div>
              <div v-if="row.status == 1" style="cursor: pointer"
                @click="; (dialogVisibleView = true), (rowView = row), $forceUpdate()">
                <i class="el-icon-warning"></i>
              </div>
            </div>
          </template>
        </el-table-column>
@@ -56,28 +52,26 @@
        <el-table-column label="操作" width="300">
          <template slot-scope="{ row }">
            <div>
              <el-button type="text" @click="edit(row)">编辑</el-button>
              <el-button type="text" @click="edit(row)">账号继承</el-button>
              <el-button v-if="row.status != 0" type="text" @click="updateStatus(row, true)">
              <el-button type="text" @click="edit(row)" class="action-button">编辑</el-button>
              <el-button type="text" @click="inherit(row)" class="action-button">账号继承</el-button>
              <el-button v-if="row.status != 0" type="text" @click="updateStatus(row, true)" class="action-button">
                启用
              </el-button>
              <el-button v-if="row.status == 0" type="text" @click="updateStatus(row, false)">
              <el-button v-if="row.status == 0" type="text" @click="updateStatus(row, false)" class="action-button">
                禁用
              </el-button>
              <el-button type="text" @click="detail(row)">重置密码</el-button>
              <el-button type="text" @click="del(row)">删除</el-button>
              <el-button type="text" @click="detail(row)" class="action-button">重置密码</el-button>
              <el-button type="text" @click="del(row)" class="action-button">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </template>
    </TableCustom>
    <AddEdit v-if="dialogVisible" :row="row" :deptList="deptList" :deptType="deptTypeList" :roleList="roleList"
    <AddEdit v-if="dialogVisible" :row="row" :deptType="deptTypeList" :roleList="roleList"
      :dialogVisible="dialogVisible" @close="dialogVisible = false, row = {}" @confirm="confirm" />
    <ResetPassword v-if="passwordVisible" :row="row" :dialogVisible="passwordVisible"
      @close="passwordVisible = false, row = {}" @confirm="passwordConfirm" />
    <ViewData v-if="dialogVisibleView" :row="rowView" :dialogVisible="dialogVisibleView"
      @close="dialogVisibleView = false, rowView = {}" />
    <Disb v-if="disbDialogVisible" :row="disbRow" :dialogVisible="disbDialogVisible"
      @close="disbDialogVisible = false, disbRow = {}" @confirm="disbConfirm" />
    <ShowDelConfirm :show="delShow" @close="delShow = false" @confirm="delConfirm" title="确认要删除该人员吗?"
@@ -90,7 +84,6 @@
<script>
import { getList, add, edit, delDept, roleList, updatePwd, changeStatus, typeList } from './service'
import AddEdit from './components/add-edit.vue'
import ViewData from './components/view-data.vue'
import Disb from './components/disb.vue'
import ResetPassword from './components/reset-password.vue'
import Inherit from './components/inherit.vue'
@@ -98,7 +91,6 @@
  name: 'User',
  components: {
    AddEdit,
    ViewData,
    Disb,
    ResetPassword,
    Inherit,
@@ -113,7 +105,6 @@
      inheritDialogVisible: false,//账号继承
      data: [],//列表数据
      nickNameOrPhone: '',//人员搜索
      deptId: [],//部门
      roleId: [],//角色
      status: '',//状态
      roleList: [],//角色列表
@@ -137,9 +128,8 @@
  },
  watch: {},
  created() {
    // this.getRoleList()
    // this.getListData()
    // this.getTypeList()
    this.getRoleList()
    this.getListData()
  },
  mounted() { },
  methods: {
@@ -150,7 +140,7 @@
    },
    getRoleList() {
      roleList().then((res) => {
        this.roleList = res.data.data
        this.roleList = res
      })
    },
    delConfirm() {
@@ -201,6 +191,10 @@
      this.row = row
      this.dialogVisible = true
    },
    inherit(row) {
      this.inheritRow = row
      this.inheritDialogVisible = true
    },
    updateStatus(row, type) {
      if (type) {
        changeStatus({ ...row, status: 0 }).then(() => {
@@ -220,18 +214,14 @@
    },
    async getListData() {
      let obj = {
        ...this.pagination,
        pageNum: this.pagination.pageNum,
        pageSize: this.pagination.pageSize,
        nickNameOrPhone: this.nickNameOrPhone,
        deptIds: this.deptId,
        roleIds: this.roleId,
        status: this.status,
      }
      this.listLoading = true
      const {
        data: {
          data: { records, total },
        },
      } = await getList(obj)
      const { records,total } = await getList(obj)
      this.data = records
      this.pagination.total = total
      this.timeOutID = setTimeout(() => {
@@ -241,7 +231,6 @@
    reset() {
      this.nickNameOrPhone = ''
      this.roleId = []
      this.deptId = []
      this.status = ''
      this.pagination.pageNum = 1
      this.getListData()
@@ -267,6 +256,10 @@
  height: 100%;
}
.action-button {
  margin-right: 8px;
}
.green {
  background-color: green;
}
culture/src/views/system/user/service.js
@@ -36,6 +36,6 @@
export const updatePwd = (data) => {
  return axios.post(`/system/user/resetPwd`, { ...data })
}
export const typeList = (data) => {
export const typeList = () => {
  return axios.get(`/t-business-dept/list/type?type=1`,)
}
laboratory/src/components/AiEditor/index.vue
@@ -72,7 +72,6 @@
  },
  mounted() {
    this.initEditor()
    console.log('editor instance:', this.editor)
  },
  beforeDestroy() {
    this.destroyEditor()
laboratory/src/components/DynamicComponent/addTableData.vue
@@ -62,6 +62,7 @@
                    :disabled="!checkEditPermission(header)"
                  >
                    <i class="el-icon-plus"></i>
                    <!-- <div slot="tip" class="el-upload__tip">暂未连接服务器,使用默认图片</div> -->
                  </el-upload>
                </el-form-item>
                <el-form-item
@@ -143,6 +144,8 @@
</template>
  
<script>
import { listByRole } from './service';
export default {
  name: "AddDialog",
  props: {
@@ -171,13 +174,8 @@
      rules: {},
      photoList: [],
      spectrumList: [],
      userOptions: [
        { value: '1', label: '用户1' },
        { value: '2', label: '用户2' },
        { value: '3', label: '用户3' },
        { value: '4', label: '用户4' },
        { value: '5', label: '用户5' }
      ]
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
      userOptions: []
    };
  },
  computed: {
@@ -198,7 +196,6 @@
    visible: {
      handler(newVal) {
        if (newVal) {
          console.log('弹窗打开,初始化数据');
          this.showHeaderList = JSON.parse(JSON.stringify(this.headerList));
          this.$forceUpdate();
          if (this.isEdit && this.editData) {
@@ -209,8 +206,6 @@
            this.initFormData();
          }
          this.initRules();
          console.log('初始化后的表单数据:', this.form);
          console.log('初始化后的校验规则:', this.rules);
        }
      },
    },
@@ -218,7 +213,6 @@
      immediate: true,
      handler(newVal) {
        if (newVal && newVal.length) {
          console.log('headerList变化,重新初始化');
          if (this.isEdit && this.editData) {
            this.setFormData(this.editData);
          } else {
@@ -228,18 +222,22 @@
        }
      },
    },
    showHeaderList: {
      immediate: true,
      handler(newVal) {
        if (newVal ) {
          console.log("222222222222222222", JSON.stringify(newVal));
        }
      },
    },
  },
  methods: {
    getUserOptions() {
      listByRole().then(res => {
        if (res) {
          this.userOptions = res.map(user => ({
            value: user.userId,
            label: user.nickName || user.userName
          }));
        } else {
          this.$message.error('获取用户列表失败');
        }
      }).catch(err => {
        console.error('获取用户列表失败', err);
      });
    },
    checkEditPermission(header) {
      if (!header.role || !Array.isArray(header.role)) {
        return true;
@@ -262,7 +260,6 @@
          }
        });
      }
      console.log('生成的校验规则:', rules);
      this.rules = rules;
    },
    initFormData() {
@@ -286,8 +283,6 @@
      Object.keys(formData).forEach(key => {
        this.$set(this.form, key, formData[key]);
      });
      console.log('初始化后的表单数据:', this.form);
    },
    setFormData(data) {
      // 设置基础表单数据
@@ -355,23 +350,32 @@
      this.initFormData();
    },
    handleSubmit() {
      console.log('开始提交表单');
      console.log('表单数据:', this.form);
      console.log('校验规则:', this.rules);
      
      this.$refs.form.validate((valid) => {
        console.log('表单验证结果:', valid);
        if (valid) {
          const submitData = {
            ...this.form,
            photos: this.photoList,
            spectrums: this.spectrumList,
          };
          console.log('提交数据:', submitData);
          // 为用户类型字段添加用户完整信息
          if (this.headerList && this.headerList.length) {
            this.headerList.forEach(header => {
              if (header.type === 'user' && Array.isArray(submitData[header.name])) {
                // 为每个用户类型字段添加userInfo属性,包含用户完整信息
                submitData[`${header.name}_userInfo`] = submitData[header.name].map(userId => {
                  const userInfo = this.userOptions.find(user => user.value === userId);
                  return userInfo ? userInfo : { value: userId, label: userId };
                });
              }
            });
          }
          console.log(submitData,'修改的数据')
          this.$emit("success", submitData);
          this.handleClose();
        } else {
          console.log('表单验证失败');
          this.$message.error('请填写必填项');
        }
      });
@@ -381,20 +385,31 @@
      this.$refs.form.validateField("photos");
    },
    handleSpectrumChange(file, fileList) {
      this.spectrumList = fileList;
      // 使用默认图片替代实际上传
      this.spectrumList = [{
        name: '默认图片.jpg',
        url: this.defaultImageUrl,
        status: 'success'
      }];
      // 同时更新form中对应的字段值以通过表单验证
      const imageHeader = this.headerList.find(h => h.type === 'image');
      if (imageHeader && imageHeader.name) {
        // 保存图片URL,这样在表格中可以直接使用
        this.$set(this.form, imageHeader.name, this.defaultImageUrl);
        console.log('设置图片字段:', imageHeader.name, this.defaultImageUrl);
      }
      this.$refs.form.validateField("spectrums");
    },
    handleSpectrumRemove(file) {
      // 处理文件移除逻辑
      this.spectrumList = [];
    },
    handleIPadSpectrum() {
      // TODO: 调用 iPad 选择图谱功能
      console.log("调用 iPad 选择图谱功能");
    },
  },
  mounted() {
    console.log("初始headerList:", this.headerList);
    // 获取用户列表数据
    this.getUserOptions();
  },
};
</script>
@@ -431,7 +446,7 @@
    .form-content {
      flex: 1;
      overflow-y: auto;
      padding: 0 10px;
      padding: 10px 10px;
      max-height: calc(90vh - 250px); // 设置内容区域最大高度
      &::-webkit-scrollbar {
@@ -447,6 +462,9 @@
        background: #f5f7fa;
      }
    }
    .el-form-item::after{
      height: 10px !important;
    }
  }
}
laboratory/src/components/DynamicComponent/addTableHeader.vue
@@ -1,11 +1,5 @@
<template>
  <el-dialog
    title="新增表头"
    :visible.sync="dialogVisible"
    width="30%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
  <el-dialog title="新增表头" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false" @close="handleClose">
    <div class="sample-dialog">
      <div class="sample-content">
        <div class="form-content">
@@ -13,43 +7,30 @@
            <el-row :gutter="24">
              <el-col :span="24">
                <el-form-item label="表头名称" prop="name">
                  <el-input
                    v-model="form.name"
                    style="width: 100%"
                    placeholder="请输入表头名称"
                  />
                  <el-input v-model="form.name" style="width: 100%" placeholder="请输入表头名称" />
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="表头类型" prop="type">
                  <el-radio-group v-model="form.type" style="width: 100%">
                    <el-radio-button  label="text">文本框</el-radio-button>
                    <el-radio-button label="text">文本框</el-radio-button>
                    <el-radio-button label="image">图片上传</el-radio-button>
                    <el-radio-button  label="date">日期选择</el-radio-button>
                    <el-radio-button  label="user">人员选择</el-radio-button>
                    <el-radio-button label="date">日期选择</el-radio-button>
                    <el-radio-button label="user">人员选择</el-radio-button>
                  </el-radio-group>
                </el-form-item>
              </el-col>
              <el-col :span="24">
                <el-form-item label="操作权限" prop="role">
                  <el-select v-model="form.role" placeholder="请选择" style="width: 100%" multiple>
                    <el-option
                      v-for="item in options"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                    >
                    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
                    </el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="24" v-if="['text', 'date', 'user'].includes(form.type)">
                <el-form-item label="提示文案" prop="message">
                  <el-input
                    v-model="form.message"
                    style="width: 100%"
                    placeholder="请输入提示文案"
                  />
                  <el-input v-model="form.message" style="width: 100%" placeholder="请输入提示文案" />
                </el-form-item>
              </el-col>
              <el-col :span="24">
@@ -71,7 +52,7 @@
    </div>
  </el-dialog>
</template>
<script>
export default {
  name: "AddDialog",
@@ -139,15 +120,18 @@
    },
    options() {
      // 将participants转换为select组件需要的格式
      return this.participants.map(participant => ({
        value: participant.userId ,
      let userId = JSON.parse(sessionStorage.getItem('userInfo'))?.userId
      let nickName = JSON.parse(sessionStorage.getItem('userInfo'))?.nickName
      let newList = JSON.parse(JSON.stringify(this.participants))
      newList.push({ userId, nickName })
      return newList.map(participant => ({
        value: participant.userId,
        label: participant.nickName
      }));
    }
  },
  mounted() {
    // 组件挂载时的初始化逻辑
    console.log('组件已挂载');
  },
  methods: {
    setFormData(data) {
@@ -179,7 +163,7 @@
        this.$message.error('请输入提示文案');
        return;
      }
      this.$refs.form.validate((valid) => {
        if (valid) {
          const submitData = {
@@ -192,7 +176,7 @@
  },
};
</script>
<style scoped lang="less">
::v-deep .el-dialog__body {
  padding: 0;
@@ -335,12 +319,15 @@
    .el-upload-list {
      margin-top: 10px;
    }
    .el-upload-list__item {
      transition: all 0.3s;
      &:hover {
        background-color: #f5f7fa;
      }
    }
    .el-upload__tip {
      color: #909399;
      font-size: 12px;
@@ -348,4 +335,4 @@
    }
  }
}
</style>
</style>
laboratory/src/components/DynamicComponent/index.vue
@@ -4,155 +4,96 @@
      <div class="add-group">
        <div v-if="title">*</div>
        <span v-if="title">{{ title }}</span>
        <el-button
          @click="showAddDialog = true"
          class="el-icon-plus"
          type="primary"
        >
          添加组件</el-button
        >
        <el-button v-if="editable" @click="showAddDialog = true" class="el-icon-plus" type="primary">
          添加组件</el-button>
      </div>
      <!-- 选择组件弹窗 -->
      <AddComponentDialog
        :visible="showAddDialog"
        @confirm="addComponent"
        @close="showAddDialog = false"
      />
      <AddComponentDialog v-if="editable" :visible="showAddDialog" @confirm="addComponent"
        @close="showAddDialog = false" />
      <!-- 动态渲染组件 -->
      <div
        v-for="(item, idx) in components"
        :key="item.id"
        class="dynamic-component"
      >
      <div v-for="(item, idx) in components" :key="item.id" class="dynamic-component">
        <!-- 富文本 -->
        <div v-if="item.type == 'richText'">
          <AiEditor
            :ref="`editor_${item.id}`"
           :value="item.data.content"
            height="200px"
            placeholder="请输入内容..."
          />
          <AiEditor :ref="`editor_${item.id}`" :value="item.data.content" height="200px" :readOnly="!editable" placeholder="请输入内容..."
            :disabled="!editable" />
        </div>
        <!-- 自定义表格 -->
        <div v-else-if="item.type == 'customTable'" style="flex: 1">
          <div class="table-actions">
            <el-button size="mini" @click="showTableHeaderDialog(idx)"
              >添加表头</el-button
            >
            <el-button
              size="mini"
              type="primary"
              @click="showAddRowDialog(idx, item.data.headers)"
              >添加数据</el-button
            >
          <div v-if="editable" class="table-actions">
            <el-button size="mini"  @click="showTableHeaderDialog(idx)">添加表头</el-button>
            <el-button size="mini" type="primary" @click="showAddRowDialog(idx, item.data.headers)">添加数据</el-button>
          </div>
          <Table
            :data="item.data.rows"
            :total="null"
            :height="null"
            class="groupTable"
          >
            <!-- <el-table-column
              type="index"
              label="序号"
              width="80"
            ></el-table-column> -->
            <el-table-column
              v-for="(header, hidx) in item.data.headers"
              :key="hidx"
              :label="header.name"
              :prop="header.name"
            />
            <el-table-column
              label="更新时间"
              prop="updateTime"
              min-width="180"
            ></el-table-column>
            <el-table-column label="操作" min-width="200">
          <Table :data="item.data.rows" :total="null" :height="null" class="groupTable" :key="item.id + '_' + JSON.stringify(item.data.rows).length">
            <el-table-column v-for="(header, hidx) in item.data.headers" :key="hidx" :label="header.name"
              :prop="header.name">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)"
                  >编辑</el-button
                >
                <el-button
                  type="text"
                  @click="handleDeleteRow(idx, scope.$index)"
                  >删除</el-button
                >
                <!-- 用户类型显示 -->
                <template v-if="header.type === 'user'">
                  {{ getUserDisplayText(header.name, scope.row) }}
                </template>
                <!-- 图片类型显示 -->
                <template v-else-if="header.type === 'image'">
                  <img v-if="scope.row[header.name]"
                       :src="scope.row[header.name]"
                       alt="头像"
                       class="table-image" />
                </template>
                <!-- 其他类型 -->
                <template v-else>
                  {{ scope.row[header.name] }}
                </template>
              </template>
            </el-table-column>
            <el-table-column label="更新时间" prop="updateTime" min-width="180"></el-table-column>
            <el-table-column  label="操作" min-width="200" v-if="dialogCanEdit">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)" >编辑</el-button>
                <el-button type="text" v-if="editable" @click="handleDeleteRow(idx, scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </Table>
        </div>
        <!-- 文件上传 -->
        <div v-else-if="item.type == 'fileUpload'">
          <el-upload
            action="#"
            :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)"
            list-type="text"
          >
          <el-upload v-if="editable" action="#" :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)" list-type="text">
            <el-button size="small" icon="el-icon-upload2">点击上传</el-button>
          </el-upload>
          <div v-else>
            <div v-for="file in item.data.fileList" :key="file.uid" class="file-list-item">
              {{ file.name }}
            </div>
          </div>
        </div>
        <!-- 图片上传 -->
        <div v-else-if="item.type == 'imageUpload'">
          <div class="image-upload-container">
            <el-upload
              action="#"
              :file-list="item.data.imageList"
            <el-upload v-if="editable" action="#" :file-list="item.data.imageList"
              :on-change="(file, fileList) => handleImageChange(idx, fileList)"
              :on-success="(res, file, fileList) => handleImageSuccess(res, file, fileList, idx)"
              :auto-upload="true"
              :http-request="() => {}"
              :before-upload="beforeImageUpload"
              list-type="picture-card"
              class="image-uploader"
            >
              :on-success="(res, file, fileList) => handleImageSuccess(res, file, fileList, idx)" :auto-upload="true"
              :http-request="() => { }" :before-upload="beforeImageUpload" list-type="picture-card"
              class="image-uploader">
              <i class="el-icon-plus"></i>
              <div class="upload-text">上传图片</div>
            </el-upload>
            <div v-else class="image-preview">
              <img v-for="image in item.data.imageList" :key="image.uid" :src="image.url" class="preview-image" />
            </div>
            <div class="uploaf-notice">支持.jpg .png格式</div>
          </div>
        </div>
        <img
          src="@/assets/public/delete.png"
          @click="removeComponent(idx)"
          alt="删除"
          class="delete-icon"
        />
        <img v-if="editable" src="@/assets/public/delete.png" @click="removeComponent(idx)" alt="删除"
          class="delete-icon" />
      </div>
    </div>
    <!-- 添加表头弹窗 -->
    <el-dialog
      :visible.sync="tableHeaderDialog.visible"
      title="添加表头"
      width="300px"
    >
      <el-input
        v-model="tableHeaderDialog.header"
        placeholder="请输入表头"
      ></el-input>
      <span slot="footer" class="dialog-footer">
        <el-button @click="tableHeaderDialog.visible = false">取消</el-button>
        <el-button type="primary" @click="confirmAddHeader">确定</el-button>
      </span>
    </el-dialog>
    <addTableHeader
      :visible.sync="tableHeaderDialog.visible"
      :participants="participants"
      @confirm="confirmAddHeader"
    ></addTableHeader>
    <addTableData
      :visible.sync="rowDialog.visible"
      :headerList="rowDialog.headers"
      :editData="rowDialog.form"
      :isEdit="rowDialog.isEdit"
      @success="confirmAddRow"
    >
    <addTableHeader  :visible.sync="tableHeaderDialog.visible" :participants="participants"
      @confirm="confirmAddHeader"></addTableHeader>
    <addTableData  :visible.sync="rowDialog.visible" :headerList="rowDialog.headers"
      :editData="rowDialog.form" :isEdit="rowDialog.isEdit" @success="confirmAddRow">
    </addTableData>
  </div>
</template>
@@ -181,6 +122,18 @@
    participants: {
      type: Array,
      default: () => []
    },
    dataSource: {
      type: Array,
      default: () => []
    },
    editable: {
      type: Boolean,
      default: true
    },
    dialogCanEdit: {
      type: Boolean,
      default: true
    }
  },
  data() {
@@ -200,12 +153,94 @@
        headers: [],
        form: {},
      },
      headerList: [], //编辑的表头列表
      headerList: [],
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
    };
  },
  watch: {
    dataSource: {
      handler(newVal) {
        if (newVal) {
          newVal = newVal.map(component => {
            let componentData = null;
            switch (component.type) {
              case 'richText':
                componentData = { content: component.data }
                break;
              case 'customTable':
                componentData = {
                  headers: component.data.headers,
                  rows: component.data.rows
                };
                break;
              case 'fileUpload':
                componentData = { fileList: component.data };
                break;
              case 'imageUpload':
                componentData = { imageList: component.data };
                break;
            }
            return {
              type: component.type,
              id: component.id || Math.random().toString(36).substr(2, 9),
              data: componentData
            }
          })
        }
        this.components = newVal ? [...newVal] : [];
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    getUserDisplayText(fieldName, rowData) {
      // 检查是否有对应的userInfo数据
      const userInfoKey = `${fieldName}_userInfo`;
      // 如果没有rowData或fieldName,直接返回空字符串
      if (!rowData || !fieldName) {
        return '';
      }
      // 情况1: 有_userInfo数据
      if (rowData[userInfoKey] && Array.isArray(rowData[userInfoKey])) {
        return rowData[userInfoKey].map(user => user.label || '').join(', ');
      }
      // 情况2: 只有用户ID数组
      if (Array.isArray(rowData[fieldName])) {
        // 使用participants查找用户信息
        return rowData[fieldName].map(userId => {
          const user = this.participants.find(p => p.userId === userId);
          return user ? (user.nickName || user.userName || userId) : userId;
        }).join(', ');
      }
      // 情况3: 已经是字符串(可能是之前格式化过的)
      if (typeof rowData[fieldName] === 'string') {
        return rowData[fieldName];
      }
      // 默认返回空字符串
      return '';
    },
    formatUserNames(userArray) {
      if (!userArray || !Array.isArray(userArray) || userArray.length === 0) {
        return '';
      }
      // 查找参与者列表中的用户,获取昵称并用逗号拼接
      const userNames = userArray.map(userId => {
        const user = this.participants.find(p => p.userId === userId);
        return user ? user.nickName : userId;
      });
      return userNames.join(', ');
    },
    addComponent(type) {
      // 如果是添加自定义表格,需要检查是否已选择实验调度
      if (!this.editable) return;
      if (type === "customTable") {
        if (!this.participants || this.participants.length === 0) {
          this.$message.warning('请先选择实验调度');
@@ -213,7 +248,7 @@
          return;
        }
      }
      this.showAddDialog = false;
      const id = Date.now() + Math.random();
      let data = {};
@@ -221,69 +256,88 @@
      if (type === "customTable") data = { headers: [], rows: [] };
      if (type === "fileUpload") data = { fileList: [] };
      if (type === "imageUpload") data = { imageList: [] };
      console.log(type, "111111111111111", this.components);
      this.components.push({ id, type, data });
      this.emitUpdate();
    },
    submit() {
      // 获取所有组件的数据
      const data = this.components.map(component => {
        let componentData = null;
        switch (component.type) {
          case 'richText':
            const editorRef = this.$refs[`editor_${component.id}`];
            const editor = Array.isArray(editorRef) ? editorRef[0] : editorRef;
            console.log('editor ref:', editor);
            const content = editor ? editor.getContent() : '';
            componentData = content && content !== '<p></p>' ? content : '';
            break;
          case 'customTable':
            // 获取表格数据
            componentData = {
              headers: component.data.headers,
              rows: component.data.rows
            };
            break;
          case 'fileUpload':
            // 获取文件列表
            componentData = component.data.fileList;
            break;
          case 'imageUpload':
            // 获取图片列表
            componentData = component.data.imageList;
            break;
        }
        return {
          type: component.type,
          data: componentData
        };
      });
      // 触发 submit 事件,将数据传递给父组件
      this.$emit('submit', data);
    },
    confirmAddRow(formData) {
      // if (!this.editable) return;
      const { idx, rowIndex, isEdit } = this.rowDialog;
      // 处理formData中的数据,保证用户信息的完整性
      const processedData = { ...formData };
      // 调试输出
      console.log('添加/编辑行数据:', processedData,'isEdit',isEdit);
      if (isEdit) {
        // 编辑模式
        this.components[idx].data.rows[rowIndex] = {
          ...formData,
          updateTime: new Date().toLocaleString()
        };
        // Vue无法检测到对象或数组深层属性的变化,使用Vue.set来确保响应式
        this.$set(
          this.components[idx].data.rows,
          rowIndex,
          {
            ...processedData,
            updateTime: new Date().toLocaleString()
          }
        );
      } else {
        // 新增模式
        // 使用数组方法push会被Vue检测到
        this.components[idx].data.rows.push({
          ...formData,
          ...processedData,
          updateTime: new Date().toLocaleString()
        });
      }
      console.log('this.components',this.components);
      // 手动触发组件更新
      this.$forceUpdate();
      // 延迟发送事件,确保数据已更新
      this.$nextTick(() => {
        this.emitUpdate();
      });
      this.rowDialog.visible = false;
      this.rowDialog.form = {};
    },
    removeComponent(idx) {
      if (!this.editable) return;
      this.components.splice(idx, 1);
      this.emitUpdate();
    },
    showTableHeaderDialog(idx) {
      this.tableHeaderDialog.visible = true;
@@ -291,14 +345,12 @@
      this.tableHeaderDialog.header = "";
    },
    confirmAddHeader(data) {
      console.log("data", data);
      if (!this.editable) return;
      const { idx } = this.tableHeaderDialog;
      // 添加新表头
      this.components[idx].data.headers.push({ ...data });
      // 为已有行数据添加新表头对应的默认值
      this.components[idx].data.rows.forEach(row => {
        // 如果行数据是对象,直接添加新属性
        if (typeof row === 'object' && row !== null) {
          if (data.type === 'user') {
            this.$set(row, data.name, []);
@@ -306,7 +358,6 @@
            this.$set(row, data.name, '');
          }
        } else {
          // 如果行数据不是对象,转换为对象
          const newRow = {};
          this.components[idx].data.headers.forEach(header => {
            if (header.name === data.name) {
@@ -319,19 +370,18 @@
              newRow[header.name] = row[header.name] || '';
            }
          });
          // 替换原行数据
          const rowIndex = this.components[idx].data.rows.indexOf(row);
          this.components[idx].data.rows.splice(rowIndex, 1, newRow);
        }
      });
      // 关闭弹窗并重置数据
      this.tableHeaderDialog.visible = false;
      this.tableHeaderDialog = {
        visible: false,
        idx: null,
        header: "",
      };
      this.emitUpdate();
    },
    showAddRowDialog(idx, headerList) {
      this.headerList = headerList;
@@ -340,7 +390,6 @@
      this.rowDialog.isEdit = false;
      this.rowDialog.headers = this.components[idx].data.headers;
      this.rowDialog.form = {};
      // 初始化表单数据
      this.rowDialog.headers.forEach((header) => {
        if (header.type === "user") {
          this.rowDialog.form[header.name] = [];
@@ -355,12 +404,13 @@
      this.rowDialog.rowIndex = rowIndex;
      this.rowDialog.isEdit = true;
      this.rowDialog.headers = this.components[idx].data.headers;
      // 深拷贝行数据,避免直接修改原数据
      this.rowDialog.form = JSON.parse(
        JSON.stringify(this.components[idx].data.rows[rowIndex])
      );
    },
    handleDeleteRow(idx, rowIndex) {
      if (!this.editable) return;
      this.$confirm("确认删除该行数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
@@ -369,11 +419,13 @@
        .then(() => {
          this.components[idx].data.rows.splice(rowIndex, 1);
          this.$message.success("删除成功");
          this.emitUpdate();
        })
        .catch(() => {});
        .catch(() => { });
    },
    handleFileChange(idx, fileList) {
      // 为每个文件添加默认的URL和名称
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
@@ -384,9 +436,11 @@
        return file;
      });
      this.components[idx].data.fileList = fileList;
      this.emitUpdate();
    },
    handleImageChange(idx, fileList) {
      // 为每个文件添加默认的URL
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
@@ -394,9 +448,9 @@
        return file;
      });
      this.components[idx].data.imageList = fileList;
      this.emitUpdate();
    },
    handleImageSuccess(res, file, fileList, idx) {
      // 使用默认图片URL
      file.url = 'https://picsum.photos/200/200';
      this.components[idx].data.imageList = fileList;
    },
@@ -415,6 +469,11 @@
      }
      return true;
    },
    emitUpdate() {
      // 先创建新对象,这有助于触发更新
      const updatedComponents = JSON.parse(JSON.stringify(this.components));
      this.$emit('update:dataSource', updatedComponents);
    },
  },
};
</script>
@@ -425,9 +484,11 @@
  padding: 20px;
  margin-top: 37px;
}
.has-title{
.has-title {
  margin-top: 0px !important;
}
.add-group {
  display: flex;
  align-items: center;
@@ -445,25 +506,30 @@
    margin: 0 32px 0 8px;
  }
}
.dynamic-component {
  background: #ffffff;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
  .delete-icon {
    width: 16px;
    height: 16px;
    cursor: pointer;
  }
}
.image-uploader{
.image-uploader {
  display: flex;
 ::v-deep .el-upload-list__item{
width: 104px !important;
height: 104px !important;
  ::v-deep .el-upload-list__item {
    width: 104px !important;
    height: 104px !important;
  }
}
.image-upload-container {
  display: flex;
  flex-wrap: wrap;
@@ -475,6 +541,7 @@
  .el-upload--picture-card {
    margin: 0;
  }
  .el-upload-list--picture-card .el-upload-list__item {
    margin: 0 10px 10px 0;
  }
@@ -487,6 +554,7 @@
  align-items: center;
  justify-content: center;
  flex-direction: column;
  .upload-text {
    font-weight: 400;
    font-size: 14px;
@@ -495,6 +563,7 @@
    margin-top: 13px;
  }
}
.uploaf-notice {
  font-weight: 400;
  font-size: 14px;
@@ -502,17 +571,44 @@
  line-height: 22px;
  margin-top: 8px;
}
.table-actions {
  margin-bottom: 10px;
  display: flex;
  gap: 10px;
}
.groupTable {
  width: 100%;
  margin-top: 10px;
  ::v-deep .el-input__inner {
    width: unset !important;
  }
}
.file-list-item {
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}
.image-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.preview-image {
  width: 104px;
  height: 104px;
  object-fit: cover;
  border-radius: 8px;
}
.table-image {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
}
</style>
laboratory/src/components/DynamicComponent/service.js
New file
@@ -0,0 +1,8 @@
import axios from '@/utils/request';
// 获取项目列表 获取用户列表-不分页-根据角色筛选
export const listByRole = (data) => {
    return axios.get('/system/user/listByRole', { params:data })
}
laboratory/src/components/SelectMember/index.vue
@@ -59,7 +59,7 @@
</template>
<script>
import { getRoleList, getUserList } from './service'
import { getRoleList, getUserList, listByRole } from './service'
export default {
    props: {
        projectId: {
@@ -118,7 +118,8 @@
            if (this.projectId) {
                params.projectId = this.projectId;
                // TODO: 这里需要替换为新的接口调用
                // const res = await getProjectUserList(params);
                const res = await listByRole(params);
                this.tableData = res.records;
            } else {
                const res = await getUserList(params);
                this.tableData = res.records;
laboratory/src/components/SelectMember/service.js
@@ -13,4 +13,10 @@
// 角色列表不分页
export const getRoleList = (data) => {
    return axios.post('/system/role/listNotPage', { ...data })
}
}
// 获取项目列表 获取用户列表-不分页-根据角色筛选
export const listByRole = (data) => {
    return axios.get('/system/user/listByRole', { params:data })
}
laboratory/src/views/dataManagement/dispatching/list.vue
@@ -43,22 +43,22 @@
          <div class="flex a-center">
            <div
              class="title"
              :class="{ active: currentType === 'list' }"
              :class="{ active: currentType == 'list' }"
              @click="handleTypeChange('list')"
            >
              实验与调度列表
            </div>
            <div
              v-if="userRole === '3'"
              v-if="userRole == '3'"
              class="drafts"
              :class="{ active: currentType === 'draft' }"
              :class="{ active: currentType == 'draft' }"
              @click="handleTypeChange('draft')"
            >
              草稿箱
            </div>
          </div>
          <el-button 
            v-if="userRole === '3'"
            v-if="userRole == '3'"
            @click="handleAddPlan" 
            class="el-icon-plus" 
            type="primary"
@@ -112,7 +112,7 @@
            <template v-if="userRole == '3'">
              <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
              <el-button 
                v-if="scope.row.status === 1"
                v-if="scope.row.status == 1"
                type="text" 
                @click="handleDelete(scope.row)"
              >删除</el-button>
@@ -122,7 +122,7 @@
            <template v-if="userRole == '4' || userRole == '5'">
              <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
              <el-button
                v-if="scope.row.status === 1"
                v-if="scope.row.status == 1"
                type="text"
                @click="handleConfirm(scope.row)"
              >确认</el-button>
@@ -259,14 +259,14 @@
    },
    handleTypeChange(type) {
      this.currentType = type;
      this.form.status = type === 'draft' ? '-1' : '';
      this.form.status = type == 'draft' ? '-1' : '';
      this.form.pageNum = 1;
      this.getTableData();
    },
    getTableData() {
      const params = {
        ...this.form,
        status: this.currentType === 'draft' ? '-1' : this.form.status
        status: this.currentType == 'draft' ? '-1' : this.form.status
      };
      getDispatchList(params).then(res => {
        console.log('111111111111',res)
laboratory/src/views/dataManagement/schemeManagement/addPlan.vue
@@ -2,70 +2,90 @@
  <Card>
    <template style="position: relative">
      <el-form ref="form" :model="form" :rules="rules" inline label-position="top">
        <div class="header-title" style="margin-bottom: 38px; justify-content: space-between">
          <div style="display: flex; align-items: center; gap: 13px">
        <div v-if="!isEdit">
          <div class="header-title" style="margin-bottom: 38px">
            <div style="display: flex; align-items: center; gap: 13px">
              <div class="header-title-left">
                <img src="@/assets/public/headercard.png" />
                <div>所属实验调度</div>
              </div>
              <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
                选择实验调度</el-button>
            </div>
          </div>
          <!-- //换到详情弹窗 -->
          <!-- <el-button @click="handleStopExperiment" type="danger">
            申请终止实验</el-button> -->
          <Table :data="groupTableData" :total="0" :height="null" class="groupTable">
            <el-table-column type="index" label="序号" width="80"></el-table-column>
            <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
            <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
            <el-table-column prop="experimentName" label="实验名称"></el-table-column>
            <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
            <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
            <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
            <el-table-column prop="participantsName" label="参加人员"></el-table-column>
            <el-table-column prop="status" label="状态">
              <template slot-scope="scope">
                <el-tag :type="getStatusType(scope.row.status)">
                  {{ getStatusText(scope.row.status) }}
                </el-tag>
              </template>
            </el-table-column>
          </Table>
          <div class="header-title" style="margin-bottom: 38px">
            <div class="header-title-left">
              <img src="@/assets/public/headercard.png" />
              <div>所属实验调度</div>
              <span>基础信息</span>
            </div>
            <el-button @click="showScheduling = true" class="el-icon-plus" type="primary">
              选择实验调度</el-button>
          </div>
          <el-button @click="handleStopExperiment" type="danger">
            申请终止实验</el-button>
        </div>
        <Table :data="groupTableData" :total="0" :height="null" class="groupTable">
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
          <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
          <el-table-column prop="experimentName" label="实验名称"></el-table-column>
          <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
          <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
          <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
          <el-table-column prop="participantsName" label="参加人员"></el-table-column>
          <el-table-column prop="status" label="状态">
            <template slot-scope="scope">
              <el-tag :type="getStatusType(scope.row.status)">
                {{ getStatusText(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
        </Table>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <span>基础信息</span>
          <template v-if="groupData && groupData.length > 0">
            <div class="add-group">
              <span>组别列表</span>
            </div>
            <Table :data="groupData" :total="0" :height="null" class="groupTable">
              <el-table-column type="index" label="序号" width="80"></el-table-column>
              <el-table-column prop="groupName" label="组别"></el-table-column>
              <el-table-column prop="remark" label="备注"></el-table-column>
            </Table>
          </template>
          <div style="padding-left: 25px;margin-top: 28px;">
            <el-form-item prop="experimentDate" label="试验日期">
              <el-date-picker v-model="form.experimentDate" type="datetime" :disabled="isEdit" placeholder="选择日期时间">
              </el-date-picker>
            </el-form-item>
          </div>
        </div>
        <div v-else>
          <div class="header-title" style="margin-bottom: 18px">
            <div class="header-title-left">
              <img src="@/assets/public/headercard.png" />
              <div>所属项目课题方案</div>
            </div>
          </div>
          <div class="content-box">
            <div class="content-box-left">
              <div>项目课题方案名称:{{ groupTableData && groupTableData.length > 0 ? groupTableData[0].projectName : '' }}</div>
              <div>实验编号:{{ groupTableData && groupTableData.length > 0 ? groupTableData[0].experimentCode : '' }}</div>
            </div>
            <div class="content-box-right">
              <div>项目课题方案编号: {{ groupTableData && groupTableData.length > 0 ? groupTableData[0].projectCode : '' }}</div>
              <div>实验名称: {{ groupTableData && groupTableData.length > 0 ? groupTableData[0].experimentName : '' }}</div>
            </div>
          </div>
        </div>
        <div class="add-group">
          <span>组别列表</span>
          <!-- <el-button type="primary" class="el-icon-plus" @click="handleAddGroup">添加组别</el-button> -->
        </div>
        <Table :data="groupData" :total="0" :height="null" class="groupTable">
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="groupName" label="组别"></el-table-column>
          <el-table-column prop="remark" label="备注"></el-table-column>
        </Table>
        <div style="padding-left: 25px;margin-top: 28px;">
          <el-form-item prop="experimentDate" label="试验日期">
            <el-date-picker
      v-model="form.experimentDate"
      type="datetime"
      placeholder="选择日期时间">
    </el-date-picker>
          </el-form-item>
        </div>
        <div class="add-group">
        <div class="add-group" v-if="!isEdit">
          <div>*</div>
          <span>参加人员</span>
          <el-button type="primary" class="el-icon-plus" @click="addMember">选择参加人员</el-button>
          <el-button type="primary" class="el-icon-plus" @click="addMember" >选择参加人员</el-button>
        </div>
        <div class="add-group" v-else><span>实验人员</span> </div>
        <div class="member-list">
          <div class="member-list-card">
            <div class="member-item">
@@ -78,11 +98,23 @@
                </div>
              </div>
              <div class="member-change">
                <div class="member-change-btn" @click="handleEditMember">修改</div>
                <div class="member-change-btn" @click="handleEditMember" v-if="!isEdit">修改</div>
              </div>
            </div>
          </div>
        </div>
        <template v-if="groupData && groupData.length > 0 && isEdit">
          <div class="add-group">
            <span>组别列表</span>
          </div>
          <Table :data="groupData" :total="0" :height="null" class="groupTable">
            <el-table-column type="index" label="序号" width="80"></el-table-column>
            <el-table-column prop="groupName" label="组别"></el-table-column>
            <el-table-column prop="remark" label="备注"></el-table-column>
          </Table>
        </template>
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
@@ -91,7 +123,8 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor ref="purposeEditor" :value="editorContents.purpose" height="200px" placeholder="请输入实验目的..." />
          <AiEditor ref="purposeEditor" :readOnly="isEdit" :value="editorContents.purpose" height="200px"
            placeholder="请输入实验目的..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -101,7 +134,8 @@
          </div>
        </div>
        <div class="content-box">
          <AiEditor ref="processEditor" :value="editorContents.process" height="200px" placeholder="请输入工艺参数及路线..." />
          <AiEditor ref="processEditor" :readOnly="isEdit" :value="editorContents.process" height="200px"
            placeholder="请输入工艺参数及路线..." />
        </div>
        <div class="header-title" style="margin-bottom: 38px">
@@ -110,15 +144,17 @@
            <div>三、实验材料及设备</div>
          </div>
        </div>
        <DynamicComponent ref="materialComponent" title="实验材料" :participants="participantsData" @submit="handleMaterialSubmit" />
        <DynamicComponent ref="equipmentComponent" title="实验所用设备" :participants="participantsData" @submit="handleEquipmentSubmit" />
        <DynamicComponent ref="materialComponent" title="实验材料" :participants="participantsData"
          @submit="handleMaterialSubmit" :dataSource="form.experimentMaterial" :editable="!isEdit" />
        <DynamicComponent ref="equipmentComponent" title="实验所用设备" :participants="participantsData"
          @submit="handleEquipmentSubmit" :dataSource="form.experimentDevice" :editable="!isEdit" />
        <div class="header-title" style="margin-bottom: 38px">
          <div class="header-title-left">
            <img src="@/assets/public/headercard.png" />
            <div>四、实验操作步骤记录</div>
          </div>
          <el-button @click="handleAddStep" class="el-icon-plus" type="primary">
          <el-button @click="handleAddStep" class="el-icon-plus" type="primary" v-if="!isEdit">
            添加步骤</el-button>
        </div>
@@ -128,17 +164,18 @@
              步骤{{ idx + 1 }}:{{ item.stepName }}
            </div>
            <div class="step-list-item-control">
              <div class="controlBtn edit" @click="handleEditStep(idx)">
              <div class="controlBtn edit" @click="handleEditStep(idx)" v-if="!isEdit">
                <img src="@/assets/public/edit.png" alt="编辑" class="edit-icon" />
                编辑
              </div>
              <div class="controlBtn delete" @click="handleDeleteStep(idx)">
              <div class="controlBtn delete" @click="handleDeleteStep(idx)" v-if="!isEdit">
                <img src="@/assets/public/delete.png" alt="删除" class="delete-icon" />
                删除
              </div>
            </div>
          </div>
          <DynamicComponent :ref="'stepContent' + idx" @submit="(content) => handleStepContentSubmit(idx, content)" />
          <DynamicComponent :ref="'stepContent' + idx" @submit="(content) => handleStepContentSubmit(idx, content)"
            :dataSource="item.content" :editable="!isEdit" />
        </div>
        <div class="add-project-footer">
@@ -159,9 +196,9 @@
import DynamicComponent from "@/components/DynamicComponent";
import AddStep from "./components/add-step.vue";
import AiEditor from "@/components/AiEditor";
import { getGroupByDispatchId,getParticipantsByDispatchId } from "./service";
import { getGroupByDispatchId, getParticipantsByDispatchId, getDetail } from "./service";
import moment from 'moment';
import { add } from "./service";
import { add,update } from "./service";
export default {
  name: "AddProject",
@@ -209,267 +246,67 @@
      taskTableData: [],
      participantsData: [],
      selectedParticipants: [],
      isEdit: false, // 是否为编辑模式
      editId: null, // 编辑的ID
      viewMaterialData: [], // 查看模式的材料数据
      viewEquipmentData: [], // 查看模式的设备数据
      // 状态映射表
      statusTypeMap: {
        "-1": "info",
        "1": "warning",
        "2": "success",
        "3": "info"
      },
      statusTextMap: {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      }
    };
  },
  async created() {
    // 检查是否为编辑模式
    if (this.$route.query.type === 'edit' && this.$route.query.id) {
      this.isEdit = true;
      this.editId = this.$route.query.id;
      await this.loadEditData();
    }
  },
  methods: {
    confirmAddRow() {
      // 处理添加行的逻辑
      console.log('添加行');
    },
    submitForm() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          console.log("submit!");
        }
      });
    },
    // ===== 人员相关方法 =====
    addMember() {
      this.$refs.selectMember.open(this.participantsData, []);
    },
    memberList(i) {
      const roleTypes = {
        1: '工艺工程师',
        2: '化验师',
        3: '实验员'
      };
      return this.selectedParticipants.filter(member => member.roleType === i) || [];
    handleMemberSubmit(selectedMembers) {
      this.selectedParticipants = selectedMembers;
      this.$refs.selectMember.close();
    },
    handleAddGroup() {
      this.$refs.addGroupDialog.open();
    handleEditMember() {
      this.$refs.selectMember.open(this.participantsData, this.selectedParticipants);
    },
    handleEditGroup(row) {
      this.$refs.addGroupDialog.open(row);
    },
    handleDeleteGroup(row) {
      this.$confirm("确认删除该组别吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          const index = this.groupTableData.findIndex((item) => item === row);
          if (index > -1) {
            this.groupTableData.splice(index, 1);
            this.$message.success("删除成功");
          }
        })
        .catch(() => { });
    },
    handleGroupSubmit(form) {
      const index = this.groupTableData.findIndex(
        (item) => item.groupName === form.groupName
      );
      if (index > -1) {
        this.groupTableData.splice(index, 1, form);
      } else {
        this.groupTableData.push(form);
      }
    },
    handleAddTask() {
      this.$refs.addTaskDialog.open();
    },
    handleEditTask(row) {
      this.$refs.addTaskDialog.open(row);
    },
    handleDeleteTask(row) {
      this.$confirm("确认删除该任务吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          const index = this.taskTableData.findIndex((item) => item === row);
          if (index > -1) {
            this.taskTableData.splice(index, 1);
            this.$message.success("删除成功");
          }
        })
        .catch(() => { });
    },
    handleTaskSubmit(form) {
      const index = this.taskTableData.findIndex(
        (item) => item.taskName === form.taskName
      );
      if (index > -1) {
        this.taskTableData.splice(index, 1, form);
      } else {
        this.taskTableData.push(form);
      }
    },
    handleMaterialSubmit(data) {
      // 处理实验材料数据
      this.form.experimentMaterial = data;
    },
    handleEquipmentSubmit(data) {
      // 处理实验设备数据
      this.form.experimentDevice = data;
    },
    handleSave() {
      // 先获取所有动态组件的数据
      this.$refs.materialComponent.submit();
      this.$refs.equipmentComponent.submit();
      // 获取所有步骤内容 - 添加错误检查
      this.stepList.forEach((step, index) => {
        const stepContentRef = this.$refs['stepContent' + index];
        console.log('stepContentRef',stepContentRef)
        const editor = Array.isArray(stepContentRef) ? stepContentRef[0] : stepContentRef;
        if (editor && typeof editor.submit === 'function') {
          editor.submit();
        }
      });
      console.log('材料数据',this.form.experimentMaterial)
      console.log('设备数据',this.form.experimentDevice)
      console.log('步骤数据',this.form.stepList)
      // 然后进行表单校验
      this.$refs.form.validate((valid) => {
        if (valid && this.validateContent()) {
          // 构建提交数据
          const formData = {
            ...this.form,
            // 实验日期,使用 moment 格式化为指定格式
            experimentDate: moment(this.form.experimentDate).format('YYYY-MM-DD HH:mm:ss'),
            // 实验调度ID,从选中的调度数据中获取
            dispatchId: this.groupTableData[0]?.id,
            // 实验设备,转换为JSON字符串
            experimentDevice: this.form.experimentDevice,
            // 实验材料,转换为JSON字符串
            experimentMaterial: this.form.experimentMaterial,
            // 实验目的,从富文本编辑器获取内容
            experimentObjective: this.$refs.purposeEditor.getContent(),
            // 工艺参数及路线,从富文本编辑器获取内容
            experimentParamRoute: this.$refs.processEditor.getContent(),
            // 实验方案人员,包含用户ID、昵称和角色类型
            experimentSchemePersons: this.selectedParticipants.map(person => ({
              userId: person.userId,    // 用户ID
              nickName: person.nickName, // 用户昵称
              roleType: person.roleType,  // 角色类型
              commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
            })),
            // 实验步骤记录,转换为JSON字符串,包含步骤名称和内容
            experimentStepRecord: this.stepList.map(step => ({
              stepName: step.stepName,  // 步骤名称
              content: step.content     // 步骤内容
            })),
            // 状态:1=已发送
            status: 1,
            // 提交时间,使用 moment 格式化为指定格式
            commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
          };
          console.log('formData 提交数据',formData)
          // 调用添加接口
          add(formData).then(res => {
            if (res.code === 200) {
              this.$message.success('保存成功');
              this.$router.go(-1)
              // 可以在这里添加跳转逻辑
            } else {
              this.$message.error(res.msg || '保存失败');
            }
          }).catch(err => {
            this.$message.error('保存失败');
            console.error('保存失败:', err);
          });
        } else {
          // 获取第一个错误字段
          const firstError = this.$refs.form.fields.find(field => field.validateState === 'error');
          if (firstError) {
            // 滚动到错误字段
            this.$nextTick(() => {
              firstError.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            });
          }
        }
      });
    },
    handleSaveDraft() {
      // 先获取所有动态组件的数据
      this.$refs.materialComponent.submit();
      this.$refs.equipmentComponent.submit();
      // 获取所有步骤内容 - 添加错误检查
      this.stepList.forEach((step, index) => {
        const stepContentRef = this.$refs['stepContent' + index];
        if (stepContentRef && typeof stepContentRef.submit === 'function') {
          stepContentRef.submit();
        }
      });
      // 然后进行表单校验
      this.$refs.form.validate((valid) => {
        if (valid && this.validateContent()) {
          // 构建草稿数据
          const formData = {
            ...this.form,
            // 实验日期,使用 moment 格式化为指定格式
            experimentDate: moment(this.form.experimentDate).format('YYYY-MM-DD HH:mm:ss'),
            // 实验调度ID,从选中的调度数据中获取
            dispatchId: this.groupTableData[0]?.id,
            // 实验设备,转换为JSON字符串
            experimentDevice: JSON.stringify(this.form.experimentDevice),
            // 实验材料,转换为JSON字符串
            experimentMaterial: JSON.stringify(this.form.experimentMaterial),
            // 实验目的,从富文本编辑器获取内容
            experimentObjective: this.$refs.purposeEditor.getContent(),
            // 工艺参数及路线,从富文本编辑器获取内容
            experimentParamRoute: this.$refs.processEditor.getContent(),
            // 实验方案人员,包含用户ID、昵称和角色类型
            experimentSchemePersons: this.selectedParticipants.map(person => ({
              userId: person.userId,    // 用户ID
              nickName: person.nickName, // 用户昵称
              roleType: person.roleType  // 角色类型
            })),
            // 实验步骤记录,转换为JSON字符串,包含步骤名称和内容
            experimentStepRecord: JSON.stringify(this.stepList.map(step => ({
              stepName: step.stepName,  // 步骤名称
              content: step.content     // 步骤内容
            }))),
            // 状态:-1=草稿箱
            status: -1,
            // 提交时间,使用 moment 格式化为指定格式
            commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
          };
          // 调用添加接口
          add(formData).then(res => {
            if (res.code === 200) {
              this.$message.success('草稿保存成功');
              // 可以在这里添加跳转逻辑
            } else {
              this.$message.error(res.msg || '草稿保存失败');
            }
          }).catch(err => {
            this.$message.error('草稿保存失败');
            console.error('草稿保存失败:', err);
          });
        } else {
          // 获取第一个错误字段
          const firstError = this.$refs.form.fields.find(field => field.validateState === 'error');
          if (firstError) {
            // 滚动到错误字段
            this.$nextTick(() => {
              firstError.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            });
          }
        }
      });
    },
    // ===== 步骤相关方法 =====
    handleAddStep() {
      this.$refs.addStepDialog.open();
    },
    handleStepSubmit(stepData) {
      if (this.editingStepIndex > -1) {
        // 编辑现有步骤
        this.stepList[this.editingStepIndex].stepName = stepData.stepName;
        this.editingStepIndex = -1;
      } else {
        // 添加新步骤
        this.stepList.push({
          stepName: stepData.stepName,
          content: null,
        });
      }
    },
    handleEditStep(index) {
      this.editingStepIndex = index;
      this.$refs.addStepDialog.open(true);
      this.$refs.addStepDialog.setStepName(this.stepList[index].stepName);
    },
    handleDeleteStep(index) {
      this.$confirm("确认删除该步骤吗?删除后步骤内容将无法恢复", "警告", {
@@ -484,14 +321,108 @@
        })
        .catch(() => { });
    },
    handleEditStep(index) {
      this.editingStepIndex = index;
      this.$refs.addStepDialog.open(true);
      this.$refs.addStepDialog.setStepName(this.stepList[index].stepName);
    },
    handleStepContentSubmit(index, content) {
      console.log('步骤内容',content)
      this.stepList[index].content = content;
    },
    // ===== 材料设备相关方法 =====
    handleMaterialSubmit(data) {
      this.form.experimentMaterial = data;
    },
    handleEquipmentSubmit(data) {
      this.form.experimentDevice = data;
    },
    // ===== 保存提交相关方法 =====
    handleSave() {
      this.submitData(1);
    },
    handleSaveDraft() {
      this.submitData(-1);
    },
    submitData(status) {
      // 先获取所有动态组件的数据
      this.$refs.materialComponent.submit();
      this.$refs.equipmentComponent.submit();
      // 获取所有步骤内容
      const stepContentRefs = Object.keys(this.$refs)
        .filter(key => key.startsWith('stepContent'))
        .map(key => this.$refs[key]);
      stepContentRefs.forEach((ref) => {
        const editor = Array.isArray(ref) ? ref[0] : ref;
        if (editor && typeof editor.submit === 'function') {
          editor.submit();
        }
      });
      // 进行表单校验
      this.$refs.form.validate((valid) => {
        if (valid && this.validateContent()) {
          // 获取富文本编辑器内容
          const experimentObjective = this.$refs.purposeEditor.getContent();
          const experimentParamRoute = this.$refs.processEditor.getContent();
          // 构建实验步骤记录数据
          const experimentStepRecord = this.stepList.map(step => ({
            stepName: step.stepName,
            content: step.content
          }));
          // 构建提交数据
          const formData = {
            ...this.form,
            experimentDate: moment(this.form.experimentDate).format('YYYY-MM-DD HH:mm:ss'),
            dispatchId: this.groupTableData && this.groupTableData.length > 0 ? this.groupTableData[0]?.id : '',
            experimentObjective,
            experimentParamRoute,
            experimentSchemePersons: this.selectedParticipants.map(person => ({
              userId: person.userId,
              nickName: person.nickName,
              roleType: person.roleType,
              commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
            })),
            status,
            commitTime: moment().format('YYYY-MM-DD HH:mm:ss'),
          };
          // 统一转换JSON字符串
          formData.experimentStepRecord = JSON.stringify(experimentStepRecord);
          formData.experimentDevice = JSON.stringify(this.form.experimentDevice);
          formData.experimentMaterial = JSON.stringify(this.form.experimentMaterial);
          // 编辑模式下添加id参数
          if (this.isEdit && this.editId) {
            formData.id = this.editId;
          }
          // 根据是否为编辑模式调用不同接口
          const apiCall = this.isEdit ? update(formData) : add(formData);
          apiCall.then(res => {
            if (res.code === 200) {
              this.$message.success(status === 1 ? '保存成功' : '草稿保存成功');
              if (status === 1) {
                this.$router.go(-1);
              }
            } else {
              this.$message.error(res.msg || (status === 1 ? '保存失败' : '草稿保存失败'));
            }
          }).catch(err => {
            this.$message.error(status === 1 ? '保存失败' : '草稿保存失败');
            console.error(status === 1 ? '保存失败:' : '草稿保存失败:', err);
          });
        } else {
          // 获取第一个错误字段并滚动到该位置
          const firstError = this.$refs.form.fields.find(field => field.validateState === 'error');
          if (firstError) {
            this.$nextTick(() => {
              firstError.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            });
          }
        }
      });
    },
    getAllEditorContent() {
      return {
@@ -500,37 +431,37 @@
      };
    },
    validateContent() {
      // // 校验实验调度
      // if (!this.groupTableData || this.groupTableData.length === 0) {
      //   this.$message.error("请选择实验调度");
      //   return false;
      // }
      // 校验实验调度
      if (!this.groupTableData || this.groupTableData.length === 0) {
        this.$message.error("请选择实验调度");
        return false;
      }
      // // 校验实验日期
      // if (!this.form.experimentDate) {
      //   this.$message.error("请填写实验日期");
      //   return false;
      // }
      // 校验实验日期
      if (!this.form.experimentDate) {
        this.$message.error("请填写实验日期");
        return false;
      }
      // // 校验参与人员
      // if (!this.selectedParticipants || this.selectedParticipants.length === 0) {
      //   this.$message.error("请选择参与人员");
      //   return false;
      // }
      // 校验参与人员
      if (!this.selectedParticipants || this.selectedParticipants.length === 0) {
        this.$message.error("请选择参与人员");
        return false;
      }
      // // 校验实验目的
      // const purpose = this.$refs.purposeEditor.getContent();
      // if (!purpose || purpose === '<p></p>' || purpose.trim() === '<p></p>') {
      //   this.$message.error("请填写实验目的");
      //   return false;
      // }
      // 校验实验目的
      const purpose = this.$refs.purposeEditor.getContent();
      if (!purpose || purpose === '<p></p>' || purpose.trim() === '<p></p>') {
        this.$message.error("请填写实验目的");
        return false;
      }
      // // 校验工艺参数及路线
      // const process = this.$refs.processEditor.getContent();
      // if (!process || process === '<p></p>' || process.trim() === '<p></p>') {
      //   this.$message.error("请填写工艺参数及路线");
      //   return false;
      // }
      // 校验工艺参数及路线
      const process = this.$refs.processEditor.getContent();
      if (!process || process === '<p></p>' || process.trim() === '<p></p>') {
        this.$message.error("请填写工艺参数及路线");
        return false;
      }
      // 校验实验材料
      if (!this.form.experimentMaterial) {
@@ -551,11 +482,10 @@
      }
      // 校验每个步骤是否都有内容
      for (let i = 0; i < this.stepList.length; i++) {
        if (!this.stepList[i].content) {
          this.$message.error(`请完善第${i + 1}个步骤的内容`);
          return false;
        }
      const invalidStep = this.stepList.findIndex(step => !step.content);
      if (invalidStep !== -1) {
        this.$message.error(`请完善第${invalidStep + 1}个步骤的内容`);
        return false;
      }
      return true;
@@ -564,26 +494,14 @@
      this.$router.push("/dataManagement/scheme-management/stop-experiment");
    },
    getStatusType(status) {
      const statusMap = {
        "-1": "info",
        "1": "warning",
        "2": "success",
        "3": "info"
      };
      return statusMap[status] || "info";
      return this.statusTypeMap[status] || "info";
    },
    getStatusText(status) {
      const statusMap = {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      };
      return statusMap[status] || "未知";
      return this.statusTextMap[status] || "未知";
    },
    handleSchedulingSubmit(data) {
      this.groupTableData = data;
      if (data && data.length > 0) {
      this.groupTableData = data || [];
      if (data && data.length > 0 && data[0].id) {
        getGroupByDispatchId({ dispatchId: data[0].id }).then(res => {
          if (res) {
            this.groupData = res || [];
@@ -610,12 +528,124 @@
    handleSchedulingClose() {
      this.showScheduling = false;
    },
    handleMemberSubmit(selectedMembers) {
      this.selectedParticipants = selectedMembers;
      this.$refs.selectMember.close();
    // ===== 数据加载方法 =====
    async loadEditData() {
      try {
        const res = await getDetail({ id: this.editId });
        if (!res) {
          this.$message.error('获取详情失败');
          return;
        }
        console.log('编辑数据', res);
        const data = res;
        // 填充基本表单数据
        this.form.experimentDate = data.experimentDate;
        // 填充实验调度信息
        if (data.experimentDispatch?.id) {
          this.form.dispatchId = data.experimentDispatch.id;
          this.groupTableData = [{ ...data.experimentDispatch }];
          // 获取组别信息
          try {
            const groupRes = await getGroupByDispatchId({ dispatchId: data.experimentDispatch.id });
            this.groupData = groupRes || [];
          } catch (err) {
            console.error('获取组别列表失败:', err);
          }
        }
        // 填充参与人员
        this.selectedParticipants = Array.isArray(data.experimentSchemePersons)
          ? data.experimentSchemePersons
          : JSON.parse(data.experimentSchemePersons || '[]');
        // 填充富文本编辑器内容
        this.editorContents.purpose = data.experimentObjective || '';
        this.editorContents.process = data.experimentParamRoute || '';
        // 填充实验材料和设备
        try {
          this.form.experimentMaterial = typeof data.experimentMaterial === 'string'
            ? JSON.parse(data.experimentMaterial)
            : data.experimentMaterial;
        } catch (err) {
          console.error('解析实验材料数据失败:', err);
          this.form.experimentMaterial = [];
        }
        try {
          this.form.experimentDevice = typeof data.experimentDevice === 'string'
            ? JSON.parse(data.experimentDevice)
            : data.experimentDevice;
        } catch (err) {
          console.error('解析实验设备数据失败:', err);
          this.form.experimentDevice = [];
        }
        // 填充实验步骤
        try {
          const stepsData = typeof data.experimentStepRecord === 'string'
            ? JSON.parse(data.experimentStepRecord)
            : data.experimentStepRecord;
          this.stepList = (stepsData || []).map(step => ({
            stepName: step.stepName,
            content: step.content
          }));
        } catch (err) {
          console.error('解析实验步骤数据失败:', err);
          this.stepList = [];
        }
        // 等待组件渲染完成后设置编辑器内容
        this.$nextTick(() => {
          // 设置富文本编辑器内容
          if (this.$refs.purposeEditor) {
            this.$refs.purposeEditor.setContent(this.editorContents.purpose);
          }
          if (this.$refs.processEditor) {
            this.$refs.processEditor.setContent(this.editorContents.process);
          }
          // 设置动态组件的初始数据
          if (!this.isEdit) {
            if (this.$refs.materialComponent && this.form.experimentMaterial) {
              this.$refs.materialComponent.setInitialData(this.form.experimentMaterial);
            }
            if (this.$refs.equipmentComponent && this.form.experimentDevice) {
              this.$refs.equipmentComponent.setInitialData(this.form.experimentDevice);
            }
            // 设置步骤内容的初始数据
            this.stepList.forEach((step, index) => {
              const stepContentRef = this.$refs['stepContent' + index];
              if (stepContentRef && step.content) {
                const editor = Array.isArray(stepContentRef) ? stepContentRef[0] : stepContentRef;
                if (editor?.setInitialData) {
                  editor.setInitialData(step.content);
                }
              }
            });
          }
        });
      } catch (error) {
        this.$message.error('获取详情失败');
        console.error('获取详情失败:', error);
      }
    },
    handleEditMember() {
      this.$refs.selectMember.open(this.participantsData, this.selectedParticipants);
    // 转换数据格式为ViewDynamicComponent需要的格式
    convertToViewFormat(data) {
      if (!data || !Array.isArray(data)) return [];
      return data.map(item => ({
        id: item.id || Math.random().toString(36).substr(2, 9),
        type: item.type,
        data: item.data
      }));
    },
  },
};
@@ -926,7 +956,20 @@
.content-box {
  padding: 0 25px;
  margin-bottom: 30px;
  margin-bottom: 20px;
  width: 65%;
  display: flex;
  .content-box-left{
    flex: 1;
    div{
      padding: 10px 0;
    }
  }
  .content-box-right{
    flex: 1;
    div{
      padding: 10px 0;
    }
  }
}
</style>
laboratory/src/views/dataManagement/schemeManagement/components/approvalDialog.vue
@@ -18,25 +18,34 @@
                :rules="rules"
                inline
                label-position="top"
                :disabled="type === 'view'"
              >
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
                    <img src="@/assets/public/headercard.png" />
                    <div>所属实验调度</div>
                  <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="handleStopExperiment" type="danger">
                      申请终止实验</el-button>
                  </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 :data="dispatchData" :total="0" :height="null" class="groupTable">
                  <el-table-column type="index" label="序号" width="80"></el-table-column>
                  <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
                  <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
                  <el-table-column prop="experimentName" label="实验名称"></el-table-column>
                  <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
                  <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
                  <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
                  <el-table-column prop="participantsName" label="参加人员"></el-table-column>
                  <el-table-column prop="status" label="状态">
                    <template slot-scope="scope">
                      <el-tag :type="getStatusType(scope.row.status)">
                        {{ getStatusText(scope.row.status) }}
                      </el-tag>
                    </template>
                  </el-table-column>
                </Table>
                <div class="header-title" style="margin-bottom: 38px">
@@ -46,72 +55,37 @@
                  </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>
                <template v-if="groupData && groupData.length > 0">
                  <div class="add-group">
                    <span>组别列表</span>
                  </div>
                  <Table :data="groupData" :total="0" :height="null" class="groupTable">
                    <el-table-column type="index" label="序号" width="80"></el-table-column>
                    <el-table-column prop="groupName" label="组别"></el-table-column>
                    <el-table-column prop="remark" label="备注"></el-table-column>
                  </Table>
                </template>
                <div style="padding-left: 25px; margin-top: 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"
                    />
                <div style="padding-left: 25px;margin-top: 28px;">
                  <el-form-item prop="experimentDate" label="试验日期">
                    <el-date-picker  v-model="form.experimentDate" type="datetime" :disabled="true" placeholder="选择日期时间">
                    </el-date-picker>
                  </el-form-item>
                </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-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 class="member-title">实验员</div>
                      <div class="flex">
                        <div class="member-name-box-2">
                          <div v-for="i in selectedParticipants" :key="i.id" class="member-name">
                            {{ i.nickName }}
                          </div>
                        </div>
                      </div>
                      <div class="member-change" v-if="type !== 'view'">
                        <div class="member-change-btn">修改</div>
                      </div>
                    </div>
                  </div>
@@ -123,12 +97,15 @@
                    <div>一、实验目的</div>
                  </div>
                </div>
                <AiEditor
                  ref="purposeEditor"
                  v-model="form.purpose"
                  height="200px"
                  placeholder="请输入实验目的..."
                />
                <div class="content-box">
                  <AiEditor
                    ref="purposeEditor"
                    :readOnly="true"
                    :value="form.experimentObjective"
                    height="200px"
                    placeholder="请输入实验目的..."
                  />
                </div>
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
@@ -136,12 +113,15 @@
                    <div>二、工艺参数及路线</div>
                  </div>
                </div>
                <AiEditor
                  ref="processEditor"
                  v-model="form.process"
                  height="200px"
                  placeholder="请输入工艺参数及路线..."
                />
                <div class="content-box">
                  <AiEditor
                    ref="processEditor"
                    :readOnly="true"
                    :value="form.experimentParamRoute"
                    height="200px"
                    placeholder="请输入工艺参数及路线..."
                  />
                </div>
                <div class="header-title" style="margin-bottom: 38px">
                  <div class="header-title-left">
@@ -149,13 +129,19 @@
                    <div>三、实验材料及设备</div>
                  </div>
                </div>
                <ViewDynamicComponent
                <DynamicComponent
                  ref="materialComponent"
                  title="实验材料"
                  :components="form.materialsAndEquipment || []"
                  :dialogCanEdit="false"
                  :dataSource="form.experimentMaterial"
                  :editable="false"
                />
                <ViewDynamicComponent
                <DynamicComponent
                  ref="equipmentComponent"
                  title="实验所用设备"
                  :components="form.materialsAndEquipment || []"
                  :dialogCanEdit="false"
                  :dataSource="form.experimentDevice"
                  :editable="false"
                />
                <div class="header-title" style="margin-bottom: 38px">
@@ -165,15 +151,17 @@
                  </div>
                </div>
                <div class="step-list" v-for="(item, idx) in form.operationSteps" :key="idx">
                <div class="step-list" v-for="(item, idx) in stepList" :key="idx">
                  <div class="step-list-item">
                    <div class="step-list-item-title">
                      步骤{{ idx + 1 }}:{{ item.stepName }}
                    </div>
                  </div>
                  <ViewDynamicComponent
                  <DynamicComponent
                  :dialogCanEdit="false"
                    :ref="'stepContent' + idx"
                    :components="[item]"
                    :dataSource="item.content"
                    :editable="false"
                  />
                </div>
              </el-form>
@@ -181,13 +169,10 @@
          </Card>
        </div>
        <!-- 右侧审批流程 -->
        <div class="approval-flow" v-if="type === 'view'">
        <div class="approval-flow" v-if="showApprovalFlow">
          <div class="flow-content">
            <approval-process
              :status="form.status"
              :submit-time="form.createTime"
              :approver="form.approver"
              :approve-time="form.approveTime"
              :processData="approvalProcessData"
            />
          </div>
        </div>
@@ -203,15 +188,16 @@
<script>
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";
import DynamicComponent from "@/components/DynamicComponent";
import AiEditor from "@/components/AiEditor";
import { getDetail, getGroupByDispatchId } from "../service";
export default {
  name: "ApprovalDialog",
  components: {
    ApprovalProcess,
    SignatureCanvas,
    ViewDynamicComponent,
    DynamicComponent,
    AiEditor,
  },
  props: {
@@ -231,187 +217,43 @@
  data() {
    return {
      form: {
        planName: "",
        planCode: "",
        stage: "",
        testDate: "",
        testName: "",
        testCode: "",
        testTime: "",
        creator: "",
        createTime: "",
        approvalComment: "",
        status: "approved",
        approver: "",
        approveTime: "",
        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: "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",
                },
              ],
            },
          },
        ],
        projectName: "", // 项目课题方案名称
        projectCode: "", // 项目课题方案编号
        experimentCode: "", // 实验编号
        experimentName: "", // 实验名称
        experimentDate: "", // 实验日期
        experimentMaterial: [], // 实验材料
        experimentDevice: [], // 实验设备
        experimentObjective: "", // 实验目的
        experimentParamRoute: "", // 工艺参数及路线
        createBy: "", // 创建人
        createTime: "", // 创建时间
        status: "", // 状态
        approver: "", // 审批人
        approveTime: "", // 审批时间
      },
      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: [],
      groupData: [],
      dispatchData: [], // 实验调度数据
      stepList: [],
      selectedParticipants: [], // 实验参与人员
      showApprovalFlow: false,
      approvalProcessData: [],
    };
  },
  computed: {
    dialogTitle() {
      return this.type === "approve" ? "确认实验调度" : "实验调度详情";
    },
  },
  watch: {
    data: {
      handler(val) {
        if (val) {
          // 深拷贝数据,避免直接修改props
          this.form = JSON.parse(
            JSON.stringify({
              ...this.form,
              ...val,
              // 确保这些字段存在,如果不存在则使用默认值
              materialsAndEquipment: val.materialsAndEquipment || [],
              operationSteps: val.operationSteps || [],
            })
          );
          console.log("接收到的数据:", this.form);
        if (val && val.id) {
          // 当接收到数据且有ID时,调用获取详情接口
          this.getPlanDetail(val.id);
        }
      },
      immediate: true,
@@ -419,28 +261,35 @@
    },
    visible: {
      handler(val) {
        if (val && this.type === "view") {
          // 当弹窗打开且是查看模式时,获取详情数据
          this.getPlanDetail();
        if (val && this.data && this.data.id) {
          // 弹窗打开时,确保数据已获取
          this.getPlanDetail(this.data.id);
        }
      },
      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;
      }
    handleStopExperiment() {
      this.$router.push("/dataManagement/scheme-management/stop-experiment?id=" + this.data.id);
    },
    getStatusType(status) {
      const statusMap = {
        "-1": "info",
        "1": "warning",
        "2": "success",
        "3": "info"
      };
      return statusMap[status] || "info";
    },
    getStatusText(status) {
      const statusMap = {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      };
      return statusMap[status] || "未知";
    },
    handleClose() {
      this.$emit("update:visible", false);
@@ -466,9 +315,6 @@
        status: "rejected",
      });
    },
    memberList(item) {
      return item === 1 ? 2 : item === 2 ? 3 : 1;
    },
    openSignature() {
      this.signatureDialogVisible = true;
    },
@@ -476,129 +322,190 @@
      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() {
    async getPlanDetail(id) {
      try {
        // TODO: 替换为实际的接口调用
        // const { data } = await this.$api.getPlanDetail({ planCode: this.data.planCode });
        const res = await getDetail({ id });
        if (!res) {
          this.$message.error('获取方案详情失败');
          this.handleClose();
          return;
        }
        if(res.stopReason){
          this.showApprovalFlow = true;
          //中止实验申请
        let processData = [];
         processData.push({
              type: "primary",
              mode: "list",
              fields: [
                { label: "提交人:", value: res.updateBy || "" },
                { label: "提交时间:", value: res.createTime || "" },
              ],
            });
            if(res.status==4||res.status==3){
              processData.push({
                type:
                  res.auditStatus === 2
                    ? "primary"
                    : res.auditStatus === 3
                    ? "danger"
                    : "warning",
                mode: "list",
                fields: [
                  {
                    label: "审核结果:",
                    value:
                      res.auditStatus === 2
                        ? "通过"
                        : res.auditStatus === 3
                        ? "驳回"
                        : "待审批",
                  },
                  { label: "审批意见:", value: res.auditRemark || "" },
                  { label: "审核人:", value: res.auditPersonName || "" },
                  { label: "审核时间:", value: res.auditTime || "" },
                ],
              });
            }else{
              processData.push({
                type: "warning",
                mode: "list",
                fields: [
                  { label: "等待审核"},
                ],
              });
            }
            this.approvalProcessData = processData;
        }
        // 模拟接口返回数据
        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,
          projectName: res.projectName,
          projectCode: res.projectCode,
          experimentCode: res.experimentCode,
          experimentName: res.experimentName,
          experimentDate: res.experimentDate,
          createBy: res.createBy,
          createTime: res.createTime,
          status: res.status,
          experimentObjective: res.experimentObjective || '',
          experimentParamRoute: res.experimentParamRoute || '',
        };
        // 构建实验调度数据
        if (res.experimentDispatch) {
          this.dispatchData = [res.experimentDispatch];
        }
        // 填充组别数据
        if (res.dispatchId) {
          try {
            const groupRes = await getGroupByDispatchId({ dispatchId: res.dispatchId });
            if (groupRes) {
              this.groupData = groupRes || [];
            }
          } catch (err) {
            console.error('获取组别列表失败:', err);
          }
        }
        // 填充实验材料和设备
        if (res.experimentMaterial) {
          try {
            const materialData = typeof res.experimentMaterial === 'string'
              ? JSON.parse(res.experimentMaterial)
              : res.experimentMaterial;
            this.form.experimentMaterial = materialData;
            // 为DynamicComponent设置初始数据
            // this.$nextTick(() => {
            //   if (this.$refs.materialComponent) {
            //     this.$refs.materialComponent.setInitialData(materialData);
            //   }
            // });
          } catch (err) {
            console.error('解析实验材料数据失败:', err);
          }
        }
        if (res.experimentDevice) {
          try {
            const deviceData = typeof res.experimentDevice === 'string'
              ? JSON.parse(res.experimentDevice)
              : res.experimentDevice;
            this.form.experimentDevice = deviceData;
            // 为DynamicComponent设置初始数据
            this.$nextTick(() => {
              // if (this.$refs.equipmentComponent) {
              //   this.$refs.equipmentComponent.setInitialData(deviceData);
              // }
            });
          } catch (err) {
            console.error('解析实验设备数据失败:', err);
          }
        }
        // 填充实验步骤
        if (res.experimentStepRecord) {
          try {
            const stepsData = typeof res.experimentStepRecord === 'string'
              ? JSON.parse(res.experimentStepRecord)
              : res.experimentStepRecord;
            this.stepList = (stepsData || []).map(step => ({
              stepName: step.stepName,
              content: step.content
            }));
            // 设置步骤内容的初始数据
            this.$nextTick(() => {
              // this.stepList.forEach((step, index) => {
              //   const stepContentRef = this.$refs['stepContent' + index];
              //   if (stepContentRef && step.content) {
              //     const editor = Array.isArray(stepContentRef) ? stepContentRef[0] : stepContentRef;
              //     if (editor && typeof editor.setInitialData === 'function') {
              //       editor.setInitialData(step.content);
              //     }
              //   }
              // });
            });
          } catch (err) {
            console.error('解析实验步骤数据失败:', err);
            this.stepList = [];
          }
        }
        // 设置实验人员
        if (res.experimentSchemePersons) {
          try {
            const participantsData = typeof res.experimentSchemePersons === 'string'
              ? JSON.parse(res.experimentSchemePersons)
              : res.experimentSchemePersons;
            this.selectedParticipants = participantsData || [];
          } catch (err) {
            console.error('解析实验人员数据失败:', err);
            this.selectedParticipants = [];
          }
        }
        // 更新编辑器内容
        this.$nextTick(() => {
          if (this.$refs.purposeEditor) {
            this.$refs.purposeEditor.setContent(this.form.experimentObjective);
          }
          if (this.$refs.processEditor) {
            this.$refs.processEditor.setContent(this.form.experimentParamRoute);
          }
        });
      } catch (error) {
        console.error("获取方案详情失败:", error);
        this.$message.error("获取方案详情失败");
@@ -614,24 +521,54 @@
  border-bottom: 1px solid #e4e7ed;
}
::v-deep .el-dialog__body {
  padding: 20px;
  max-height: 80vh;
  overflow: hidden;
}
@media screen and (max-width: 1200px) {
  ::v-deep .el-dialog__body {
    max-height: none;
    overflow: auto;
  }
}
.approval-dialog {
  display: flex;
  height: 60vh;
  padding:20px;
  overflow: hidden;
  @media screen and (max-width: 1200px) {
    flex-direction: column;
    height: auto;
    .approval-content, .approval-flow {
      width: 100%;
      margin-right: 0;
      margin-bottom: 20px;
      height: 50vh;
    }
  }
  .approval-content {
    flex: 1;
    flex: 7;
    margin-right: 20px;
    background: #ffffff;
    box-shadow: 0px 4px 12px 4px rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    overflow-y: auto;
  }
  .approval-flow {
    flex: 3;
    min-width: 350px;
    padding: 40px 20px;
    width: 405px;
    background: #ffffff;
    box-shadow: 0px 4px 12px 4px rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    overflow-y: auto;
    .flow-title {
      font-size: 16px;
@@ -762,7 +699,7 @@
}
.groupTable {
  width: 65%;
  width: 85%;
  padding-left: 40px;
}
@@ -779,8 +716,8 @@
  margin-left: 38px;
  .member-list-card {
    width: 280px;
    height: 300px;
    width: 340px;
    height: 400px;
    border-radius: 8px;
    border: 1px solid #dcdfe6;
@@ -891,11 +828,13 @@
.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;
@@ -907,13 +846,24 @@
  }
}
.dialog-footer {
  align-items: center;
.content-box {
  padding: 0 25px;
  margin-bottom: 20px;
  width: 65%;
  display: flex;
  justify-content: center;
  button {
    width: 150px;
  .content-box-left {
    flex: 1;
    div {
      padding: 10px 0;
    }
  }
  .content-box-right {
    flex: 1;
    div {
      padding: 10px 0;
    }
  }
}
</style>
laboratory/src/views/dataManagement/schemeManagement/list.vue
@@ -23,6 +23,7 @@
          <el-form-item label="状态:">
            <el-select v-model="form.status" placeholder="请选择">
              <el-option label="全部" value=""></el-option>
              <el-option label="草稿" :value="-1"></el-option>
              <el-option label="已发送" :value="1"></el-option>
              <el-option label="申请中止待审核" :value="2"></el-option>
              <el-option label="申请中止已通过" :value="3"></el-option>
@@ -89,13 +90,13 @@
            <!-- 工艺工程师(3) -->
            <template v-if="userRole == '3'">
              <el-button type="text" @click="handleEdit(scope.row)" v-if="scope.row.status == '4'">编辑</el-button>
              <!-- <el-button type="text" @click="handleEdit(scope.row)" >编辑</el-button> -->
              <el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
            </template>
            <!-- 实验员(5) -->
            <template v-if="userRole == '5'">
              <el-button type="text" @click="handleEdit(scope.row)" v-if="scope.row.status == '4'">编辑</el-button>
              <el-button type="text" @click="handleEdit(scope.row)" v-if="scope.row.status == 1">编辑</el-button>
            </template>
          </template>
        </el-table-column>
@@ -156,11 +157,19 @@
  methods: {
    handlePageChange(page) {
      this.form.pageNum = page;
      // 当处于草稿箱模式时,强制将状态设置为-1
      if (this.currentType === 'draft') {
        this.form.status = -1;
      }
      this.getTableData();
    },
    handleSizeChange(size) {
      this.form.pageSize = size;
      this.form.pageNum = 1;
      // 当处于草稿箱模式时,强制将状态设置为-1
      if (this.currentType === 'draft') {
        this.form.status = -1;
      }
      this.getTableData();
    },
    resetForm() {
@@ -174,10 +183,18 @@
        pageNum: 1,
        pageSize: 10
      };
      // 当处于草稿箱模式时,强制将状态设置为-1
      if (this.currentType === 'draft') {
        this.form.status = -1;
      }
      this.getTableData();
    },
    handleSearch() {
      this.form.pageNum = 1;
      // 当处于草稿箱模式时,强制将状态设置为-1
      if (this.currentType === 'draft') {
        this.form.status = -1;
      }
      this.getTableData();
    },
    getStatusType(status) {
@@ -187,7 +204,8 @@
        '2': "warning",
        '3': "success",
        '4': "danger",
        '5': "info"
        '5': "info",
        '6':'success'
      };
      return statusMap[status] || "info";
    },
@@ -198,7 +216,8 @@
        '2': "申请中止待审核",
        '3': "申请中止已通过",
        '4': "申请中止已驳回",
        '5': "已封存"
        '5': "已封存",
        '6':'实验员已提交'
      };
      return statusMap[status] || "未知";
    },
@@ -219,6 +238,10 @@
    },
    async getTableData() {
      try {
        // 当处于草稿箱模式时,强制将状态设置为-1
        if (this.currentType === 'draft') {
          this.form.status = -1;
        }
        const { data } = await getList(this.form);
        this.tableData = data.records || [];
        this.total = data.total || 0;
@@ -244,17 +267,7 @@
    handleDetail(row) {
      this.approvalDialogType = 'view';
      this.approvalDialogVisible = true;
      this.getPlanDetail(row.id);
    },
    async getPlanDetail(id) {
      try {
        const { data } = await this.$api.getDetail({ id });
        this.currentApprovalData = data;
      } catch (error) {
        console.error('获取方案详情失败:', error);
        this.$message.error('获取方案详情失败');
        this.approvalDialogVisible = false;
      }
      this.currentApprovalData = row;
    },
    handleApproveSubmit(data) {
      this.approvalDialogVisible = false;
laboratory/src/views/dataManagement/schemeManagement/service.js
@@ -14,7 +14,7 @@
}
//修改
export const update = (data) => {
  return axios.post('/api/t-experiment-scheme/update', { ...data })
  return axios.post('/api/t-experiment-scheme/updateTester', { ...data })
}
//删除
export const deleteById = (data) => {
laboratory/src/views/dataManagement/schemeManagement/stop-experiment.vue
@@ -4,33 +4,41 @@
      <div class="header-title">
        <div class="header-title-left">
          <img src="@/assets/public/headercard.png" />
          <span>所属实验调度</span>
          <div>所属实验调度</div>
        </div>
      </div>
      <div class="table-container">
        <el-table :data="experimentData" border style="width: 100%">
          <el-table-column prop="name" label="所属项目课题方案" />
          <el-table-column prop="code" label="实验编号" />
          <el-table-column prop="title" label="实验主题" />
          <el-table-column prop="startTime" label="实验开始时间" />
          <el-table-column prop="endTime" label="实验结束时间" />
          <el-table-column prop="participants" label="参加人员" />
          <el-table-column prop="status" label="状态" />
          <el-table-column type="index" label="序号" width="80"></el-table-column>
          <el-table-column prop="projectName" label="所属项目课题方案"></el-table-column>
          <el-table-column prop="experimentCode" label="实验编号"></el-table-column>
          <el-table-column prop="experimentName" label="实验名称"></el-table-column>
          <el-table-column prop="experimentDate" label="通知时间"></el-table-column>
          <el-table-column prop="experimentStartTime" label="实验开始时间"></el-table-column>
          <el-table-column prop="experimentEndTime" label="实验结束时间"></el-table-column>
          <el-table-column prop="participantsName" label="参加人员"></el-table-column>
          <el-table-column prop="status" label="状态">
            <template slot-scope="scope">
              <el-tag :type="getStatusType(scope.row.status)">
                {{ getStatusText(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <div class="header-title">
        <div class="header-title-left">
          <img src="@/assets/public/headercard.png" />
          <span>申请说明</span>
          <div>中止原因说明</div>
        </div>
      </div>
      <div class="content-box">
        <AiEditor
          ref="reasonEditor"
          v-model="editorContent"
          :value="editorContent"
          height="200px"
          placeholder="请输入申请说明..."
        />
@@ -46,6 +54,9 @@
          :auto-upload="false"
          :on-change="handleFileChange"
          :file-list="fileList"
          :on-remove="handleFileRemove"
          multiple
          :limit="5"
        >
          <el-button size="small" type="primary">选择文件</el-button>
          <div slot="tip" class="el-upload__tip">支持格式:.rar .zip .doc .docx .pdf .jpg...</div>
@@ -54,7 +65,7 @@
      <div class="footer-section">
        <div class="footer-content">
          <el-button type="primary" @click="openSignatureDialog" :disabled="!agreement">提交</el-button>
          <el-button type="primary" @click="openSignatureDialog" >提交</el-button>
          <el-checkbox v-model="agreement">我确认,已仔细实验说明,准确描述本次实验中止全部情况及原因,特此申请中止本次实验。</el-checkbox>
        </div>
      </div>
@@ -98,6 +109,7 @@
<script>
import AiEditor from '@/components/AiEditor'
import SignatureCanvas from "@/components/SignatureCanvas.vue"
import { getDetail, applicationTermination } from './service'
export default {
  name: 'StopExperiment',
@@ -107,25 +119,54 @@
  },
  data() {
    return {
      experimentData: [{
        name: '金标准研究项',
        code: 'DD-EX001',
        title: '金标准研究项',
        startTime: '2025-1-2 14:50:19',
        endTime: '2025-02-27',
        participants: '范兵, 李天霸, 张三, 李四',
        status: '已确认'
      }],
      id: null,
      experimentData: [],
      editorContent: '',
      fileList: [],
      agreement: false,
      signatureDialogVisible: false,
      signatureCanvasVisible: false,
      imgSrc: "",
      loading: false
    }
  },
  created() {
    this.id = this.$route.query.id
    if (this.id) {
      this.getExperimentDetail()
    } else {
      this.$message.error('参数错误,缺少实验ID')
    }
  },
  methods: {
    // 获取实验详情
    async getExperimentDetail() {
      try {
        this.loading = true
        const res = await getDetail({ id: this.id })
        if (res) {
          const data = res
          this.experimentData = [{...data.experimentDispatch}]
        } else {
          this.$message.warning('未获取到实验详情')
        }
      } catch (error) {
        console.error('获取实验详情失败:', error)
        this.$message.error('获取实验详情失败')
      } finally {
        this.loading = false
      }
    },
    handleFileChange(file, fileList) {
      // this.fileList = fileList
      this.fileList = [{uid: Date.now(),
         name: '实验中止申请表.txt',
         raw: file,
         size: file.size,
         url:'https://example.com/files/default-stop-application.pdf',
         status: 'success'}]
    },
    handleFileRemove(file, fileList) {
      this.fileList = fileList
    },
    getEditorContent() {
@@ -133,17 +174,18 @@
    },
    validateForm() {
      const content = this.getEditorContent()
      if (!content) {
        this.$message.error('请填写申请说明')
      if (!content || content === '<p></p>' || content.trim() === '<p></p>') {
        this.$message.error('请填写中止原因说明')
        return false
      }
      if (!this.agreement) {
        this.$message.error('请确认申请说明')
        this.$message.error('请先勾选确认申请说明')
        return false
      }
      return true
    },
    openSignatureDialog() {
      if (!this.validateForm()) return
      this.signatureDialogVisible = true
    },
    handleDialogClose() {
@@ -155,24 +197,70 @@
    },
    handleSignatureConfirm(imageData) {
      this.signatureCanvasVisible = false
      this.imgSrc = imageData
      // this.imgSrc = imageData
      this.imgSrc = 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
    },
    handleConfirm() {
    async handleConfirm() {
      if (!this.imgSrc) {
        this.$message.warning("请先完成签名确认")
        return
      }
      if (this.validateForm()) {
        const formData = {
          reason: this.getEditorContent(),
          files: this.fileList,
          experimentInfo: this.experimentData[0],
          signature: this.imgSrc
        try {
          this.loading = true
          // 处理多个文件,将文件路径和名称通过逗号拼接
          let filePaths = '';
          let fileNames = '';
          if (this.fileList.length > 0) {
            // 模拟文件路径
            filePaths = this.fileList.map(file => file.url).join(',')
            // 文件名称通过逗号拼接
            fileNames = this.fileList.map(file => file.name).join(',')
          }
          const formData = {
            id: this.id,                     // 实验方案id
            stopReason: this.getEditorContent(), // 中止原因
            commitSign: this.imgSrc,         // 提交签字
            stopFile: filePaths,             // 中止文件路径,多个文件路径通过逗号拼接
            stopFileName: fileNames          // 中止文件名称,多个文件名称通过逗号拼接
          }
          console.log('提交的数据:', formData)
          await applicationTermination(formData)
          this.$message.success('提交成功')
          this.handleDialogClose()
          // 提交成功后返回列表页
          this.$router.go(-1)
        } catch (error) {
          console.error('提交失败:', error)
          this.$message.error('提交失败')
        } finally {
          this.loading = false
        }
        console.log('提交的数据:', formData)
        this.$message.success('提交成功')
        this.handleDialogClose()
      }
    },
    getStatusType(status) {
      const statusMap = {
        "-1": "info",
        "1": "warning",
        "2": "success",
        "3": "info"
      };
      return statusMap[status] || "info";
    },
    getStatusText(status) {
      const statusMap = {
        "-1": "草稿箱",
        "1": "待确认",
        "2": "已确认",
        "3": "已封存"
      };
      return statusMap[status] || "未知";
    }
  }
}
@@ -203,6 +291,20 @@
      line-height: 27px;
      font-family: "Source Han Sans CN Bold Bold";
    }
    div {
      flex-shrink: 0;
      font-weight: bold;
      font-size: 18px;
      color: #222222;
      line-height: 27px;
      font-family: "Source Han Sans CN Bold Bold";
      &:before {
        content: "*";
        color: #f56c6c;
        margin-right: 4px;
      }
    }
  }
}
.header-title:first-child {
@@ -228,11 +330,11 @@
    span {
      font-size: 14px;
      color: #222222;
      &::before {
        content: "*";
        color: #f56c6c;
        margin-right: 4px;
      }
      // &::before {
      //   content: "*";
      //   color: #f56c6c;
      //   margin-right: 4px;
      // }
    }
  }