| | |
| | | </el-button> |
| | | |
| | | <el-button |
| | | type="success" |
| | | @click="handleCompleteReview" |
| | | :disabled="form.status == '3' || form.status == '2'" |
| | | :loading="completeLoading" |
| | | > |
| | | 审查完成 |
| | | </el-button> |
| | | |
| | | <el-button |
| | | type="warning" |
| | | @click="handleSuspendReview" |
| | | :disabled="form.status == '2' || form.status == '3'" |
| | | :loading="suspendLoading" |
| | | > |
| | | 审查中止 |
| | | </el-button> |
| | | |
| | | <el-button |
| | | type="danger" |
| | | @click="handleEndReview" |
| | | :disabled="form.status === '2'" |
| | | :disabled="form.status == '2'" |
| | | :loading="endLoading" |
| | | > |
| | | 结束审查 |
| | | </el-button> |
| | |
| | | <el-input |
| | | v-model="form.initiateTheme" |
| | | placeholder="请输入发起主题" |
| | | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="发起人" prop="initiatePerson"> |
| | | <el-input v-model="form.initiatePerson" /> |
| | | <el-input v-model="form.initiatePerson" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="审查状态" prop="status"> |
| | | <el-select |
| | | v-model="form.status" |
| | | style="width: 100%" |
| | | |
| | | > |
| | | <el-option label="新建" value="0" /> |
| | | <el-option label="审查中" value="1" /> |
| | | <el-option label="结束" value="2" /> |
| | | <el-select v-model="form.status" style="width: 100%"> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_ethical" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="发起时间" prop="startTime"> |
| | | <el-date-picker |
| | | v-model="form.startTime" |
| | | type="datetime" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%" |
| | | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="截止时间" prop="cutOffTime"> |
| | | <el-date-picker |
| | | v-model="form.cutOffTime" |
| | | type="datetime" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%" |
| | | |
| | | > |
| | | </el-date-picker> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 专家相关信息 --> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家姓名" prop="expertName"> |
| | | <el-input v-model="form.expertName" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家编号" prop="expertNo"> |
| | | <el-input v-model="form.expertNo" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家类型" prop="expertType"> |
| | | <el-select |
| | | v-model="form.expertType" |
| | | style="width: 100%" |
| | | |
| | | > |
| | | <el-option label="普通专家" value="normal" /> |
| | | <el-option label="主委专家" value="chief" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家结论" prop="expertConclusion"> |
| | | <el-select |
| | | v-model="form.expertConclusion" |
| | | style="width: 100%" |
| | | |
| | | > |
| | | <el-form-item label="伦理审查结论" prop="expertConclusion"> |
| | | <el-select v-model="form.expertConclusion" style="width: 100%"> |
| | | <el-option label="同意" value="1" /> |
| | | <el-option label="审查中" value="2" /> |
| | | <el-option label="不同意" value="0" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家结论时间" prop="expertTime"> |
| | | <el-form-item label="伦理审查结论时间" prop="expertTime"> |
| | | <el-date-picker |
| | | v-model="form.expertTime" |
| | | type="datetime" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%" |
| | | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="专家排队序号" prop="orderNo"> |
| | | <el-input-number |
| | | v-model="form.orderNo" |
| | | :min="1" |
| | | :max="20" |
| | | style="width: 100%" |
| | | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="专家意见" prop="expertOpinion"> |
| | | <el-form-item label="审查意见" prop="expertOpinion"> |
| | | <el-input |
| | | type="textarea" |
| | | :rows="2" |
| | | v-model="form.expertOpinion" |
| | | placeholder="请输入专家意见" |
| | | |
| | | placeholder="请输入意见" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | :rows="3" |
| | | v-model="form.remark" |
| | | placeholder="请输入备注信息" |
| | | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | </el-form> |
| | | </el-card> |
| | | |
| | |
| | | <el-card class="attachment-card"> |
| | | <div slot="header" class="clearfix"> |
| | | <span class="detail-title">相关附件</span> |
| | | <el-button type="primary" size="mini" @click="handleUploadAttachment"> |
| | | 上传附件 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-table :data="attachments" style="width: 100%"> |
| | | <el-table-column label="文件名称" min-width="200"> |
| | | <template slot-scope="scope"> |
| | | <div class="file-info"> |
| | | <!-- 使用 UploadAttachment 组件 --> |
| | | <UploadAttachment |
| | | ref="uploadAttachment" |
| | | :file-list="attachmentFileList" |
| | | :limit="10" |
| | | accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx" |
| | | @change="handleAttachmentChange" |
| | | @upload-success="handleUploadSuccess" |
| | | @upload-error="handleUploadError" |
| | | @remove="handleAttachmentRemove" |
| | | /> |
| | | |
| | | <!-- 附件列表 --> |
| | | <div |
| | | class="attachment-list" |
| | | v-if="form.annexfilesList && form.annexfilesList.length > 0" |
| | | > |
| | | <div class="list-title"> |
| | | 已上传附件 ({{ form.annexfilesList.length }}) |
| | | </div> |
| | | <el-table :data="form.annexfilesList" style="width: 100%" size="small"> |
| | | <el-table-column label="文件名" min-width="200"> |
| | | <template slot-scope="scope"> |
| | | <i |
| | | class="el-icon-document" |
| | | style="margin-right: 8px; color: #409EFF;" |
| | | ></i> |
| | | <span>{{ scope.row.fileName }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <span class="file-name">{{ scope.row.fileName }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="文件类型" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="small">{{ |
| | | getFileType(scope.row.fileName) |
| | | }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="创建时间" width="160"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ formatDateTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="266"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | size="mini" |
| | | type="primary" |
| | | @click="handlePreview(scope.row)" |
| | | > |
| | | 预览 |
| | | </el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="success" |
| | | @click="handleDownload(scope.row)" |
| | | > |
| | | 下载 |
| | | </el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="danger" |
| | | @click="handleRemoveAttachment(scope.$index)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <el-table-column label="文件类型" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="small">{{ getFileType(scope.row.fileName) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="文件大小" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ formatFileSize(scope.row.fileSize) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="上传时间" width="160" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ parseTime(scope.row.uploadTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="上传人" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ scope.row.uploader }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="操作" width="120" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-view" |
| | | @click="handlePreviewAttachment(scope.row)" |
| | | >预览</el-button |
| | | > |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-download" |
| | | @click="handleDownloadAttachment(scope.row)" |
| | | >下载</el-button |
| | | > |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- 空状态 --> |
| | | <div |
| | | v-if="!form.annexfilesList || form.annexfilesList.length == 0" |
| | | class="empty-attachment" |
| | | > |
| | | <i |
| | | class="el-icon-folder-opened" |
| | | style="font-size: 60px; color: #C0C4CC; margin-bottom: 20px;" |
| | | ></i> |
| | | <p style="color: #909399; font-size: 14px;">暂无附件,请上传相关文件</p> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 专家审查情况 --> |
| | | <el-card class="expert-card"> |
| | | <div slot="header" class="clearfix"> |
| | | <span class="detail-title">专家审查情况 (18位专家 + 1位主委专家)</span> |
| | | <span class="detail-title">专家审查情况</span> |
| | | <div style="float: right;"> |
| | | <el-button size="mini" type="primary" @click="handleAddExpert"> |
| | | 添加专家 |
| | | </el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="primary" |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">专家已同意:</span> |
| | | <span class="stat-value">{{ approvedNormalExperts }}/18</span> |
| | | <span class="stat-label">普通专家:</span> |
| | | <span class="stat-value">{{ normalExpertsCount }}人</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">主委专家状态:</span> |
| | | <span class="stat-value">{{ chiefExpertStatus }}</span> |
| | | <span class="stat-label">主委专家:</span> |
| | | <span class="stat-value">{{ chiefExpertsCount }}人</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">总完成进度:</span> |
| | | <span class="stat-value">{{ completionRate }}%</span> |
| | | <span class="stat-label">已同意:</span> |
| | | <span class="stat-value" |
| | | >{{ approvedExpertsCount }}/{{ totalExpertsCount }}</span |
| | | > |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | |
| | | |
| | | <!-- 专家审查表格 --> |
| | | <el-table |
| | | :data="expertReviews" |
| | | :data="ethicalreviewopinionsList" |
| | | v-loading="expertLoading" |
| | | style="width: 100%" |
| | | height="800" |
| | | height="600" |
| | | :row-class-name="getExpertRowClassName" |
| | | > |
| | | <el-table-column label="序号" width="60" align="center" type="index" /> |
| | |
| | | fixed="left" |
| | | > |
| | | <template slot-scope="scope"> |
| | | <span>{{ scope.row.expertName }}</span> |
| | | <span |
| | | class="expert-name-link" |
| | | @click="handleViewExpertHistory(scope.row)" |
| | | > |
| | | {{ scope.row.expertname }} |
| | | </span> |
| | | <el-tag |
| | | v-if="scope.row.isChief" |
| | | v-if="scope.row.expertType == '1'" |
| | | size="mini" |
| | | type="danger" |
| | | style="margin-left: 5px;" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="专家编号" width="120" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ scope.row.expertNo || "-" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="专家类型" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span :class="scope.row.isChief ? 'chief-expert' : 'normal-expert'"> |
| | | {{ scope.row.isChief ? "主委专家" : "专家" }} |
| | | <span |
| | | :class=" |
| | | scope.row.expertType == '1' ? 'chief-expert' : 'normal-expert' |
| | | " |
| | | > |
| | | {{ getExpertTypeText(scope.row.expertType) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="审查状态" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="statusFilter(scope.row.reviewStatus)" size="small"> |
| | | {{ statusTextFilter(scope.row.reviewStatus) }} |
| | | <el-tag |
| | | :type="getReviewStatusFilter(scope.row.receiveStatus)" |
| | | size="small" |
| | | > |
| | | {{ getReviewStatusText(scope.row.receiveStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <el-table-column label="专家结论" width="120" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag |
| | | v-if="scope.row.expertConclusion" |
| | | :type="conclusionFilter(scope.row.expertConclusion)" |
| | | v-if="scope.row.expertconclusion" |
| | | :type="getConclusionFilter(scope.row.expertconclusion)" |
| | | size="small" |
| | | > |
| | | {{ conclusionTextFilter(scope.row.expertConclusion) }} |
| | | {{ getConclusionText(scope.row.expertconclusion) }} |
| | | </el-tag> |
| | | <span v-else class="no-data">-</span> |
| | | </template> |
| | |
| | | |
| | | <el-table-column label="审查意见" min-width="200" show-overflow-tooltip> |
| | | <template slot-scope="scope"> |
| | | <span :class="{ 'expert-opinion': scope.row.expertOpinion }"> |
| | | {{ scope.row.expertOpinion || "暂无意见" }} |
| | | <span :class="{ 'expert-opinion': scope.row.expertopinion }"> |
| | | {{ scope.row.expertopinion || "暂无意见" }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="结论顺序" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ scope.row.conclusionorder || "-" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="审查时间" width="160" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ |
| | | scope.row.reviewTime ? parseTime(scope.row.reviewTime) : "未审查" |
| | | scope.row.conclusiontime |
| | | ? formatDateTime(scope.row.conclusiontime) |
| | | : "未审查" |
| | | }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="发送时间" width="160" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ |
| | | scope.row.sendTime ? parseTime(scope.row.sendTime) : "未发送" |
| | | scope.row.startTime |
| | | ? formatDateTime(scope.row.startTime) |
| | | : "未发送" |
| | | }}</span> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | type="text" |
| | | icon="el-icon-s-promotion" |
| | | @click="handleSendToExpert(scope.row)" |
| | | :disabled="scope.row.reviewStatus === 'submitted'" |
| | | :class="{ 'sent-button': scope.row.reviewStatus === 'submitted' }" |
| | | :disabled=" |
| | | scope.row.receiveStatus == '2' || |
| | | scope.row.receiveStatus == '3' || |
| | | scope.row.receiveStatus == '4' |
| | | " |
| | | :class="{ |
| | | 'sent-button': |
| | | scope.row.receiveStatus == '2' || |
| | | scope.row.receiveStatus == '3' |
| | | }" |
| | | > |
| | | {{ scope.row.reviewStatus === "submitted" ? "已发送" : "发送" }} |
| | | {{ |
| | | scope.row.receiveStatus == "2" || scope.row.receiveStatus == "3" |
| | | ? "已发送" |
| | | : "发送" |
| | | }} |
| | | </el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-edit" |
| | | @click="handleEditExpertReview(scope.row)" |
| | | :disabled="scope.row.reviewStatus !== 'submitted'" |
| | | :disabled="!['2', '3'].includes(scope.row.receiveStatus)" |
| | | > |
| | | 编辑 |
| | | </el-button> |
| | |
| | | > |
| | | 详情 |
| | | </el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-delete" |
| | | @click="handleDeleteExpertReview(scope.$index)" |
| | | style="color: #f56c6c;" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- 添加专家对话框 --> |
| | | <el-dialog |
| | | title="添加专家" |
| | | :visible.sync="expertDialogVisible" |
| | | width="800px" |
| | | @close="handleExpertDialogClose" |
| | | > |
| | | <div style="margin-bottom: 20px;"> |
| | | <el-input |
| | | v-model="expertSearchQuery" |
| | | placeholder="请输入专家姓名或编号搜索" |
| | | style="width: 300px; margin-right: 10px;" |
| | | @keyup.enter.native="handleSearchExperts" |
| | | > |
| | | <el-button |
| | | slot="append" |
| | | icon="el-icon-search" |
| | | @click="handleSearchExperts" |
| | | ></el-button> |
| | | </el-input> |
| | | <el-select |
| | | v-model="filterExpertType" |
| | | placeholder="专家类型" |
| | | style="width: 150px; margin-right: 10px;" |
| | | @change="handleSearchExperts" |
| | | > |
| | | <el-option label="全部" value=""></el-option> |
| | | <el-option label="普通专家" value="0"></el-option> |
| | | <el-option label="主任委员" value="1"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleSearchExperts">搜索</el-button> |
| | | <el-button @click="handleResetSearch">重置</el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | :data="expertList" |
| | | v-loading="expertListLoading" |
| | | style="width: 100%" |
| | | max-height="400" |
| | | @selection-change="handleExpertSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="55"></el-table-column> |
| | | <el-table-column |
| | | label="专家姓名" |
| | | prop="username" |
| | | width="120" |
| | | ></el-table-column> |
| | | <el-table-column |
| | | label="专家编号" |
| | | prop="userno" |
| | | width="120" |
| | | ></el-table-column> |
| | | <el-table-column label="专家类型" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag |
| | | size="small" |
| | | :type="getIsChiefExpert(scope.row) ? 'danger' : ''" |
| | | > |
| | | {{ getIsChiefExpert(scope.row) ? "主任委员" : "普通专家" }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="单位名称" |
| | | prop="unitname" |
| | | show-overflow-tooltip |
| | | ></el-table-column> |
| | | <el-table-column |
| | | label="职称" |
| | | prop="title" |
| | | width="100" |
| | | ></el-table-column> |
| | | <el-table-column |
| | | label="联系电话" |
| | | prop="telephone" |
| | | width="120" |
| | | ></el-table-column> |
| | | </el-table> |
| | | |
| | | <div style="margin-top: 20px; text-align: center;"> |
| | | <el-pagination |
| | | @size-change="handlePageSizeChange" |
| | | @current-change="handlePageChange" |
| | | :current-page="expertPage.pageNum" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :page-size="expertPage.pageSize" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="expertTotal" |
| | | ></el-pagination> |
| | | </div> |
| | | |
| | | <div slot="footer"> |
| | | <el-button @click="expertDialogVisible = false">取消</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="handleConfirmAddExpert" |
| | | :disabled="selectedExperts.length == 0" |
| | | >确定添加</el-button |
| | | > |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 发送专家对话框 --> |
| | | <el-dialog |
| | | title="发送专家审查" |
| | | :title="sendDialogTitle" |
| | | :visible.sync="sendDialogVisible" |
| | | width="500px" |
| | | > |
| | | <el-form :model="sendForm" ref="sendForm" label-width="100px"> |
| | | <el-form-item label="专家类型" prop="expertType"> |
| | | <el-radio-group v-model="sendForm.expertType"> |
| | | <el-radio label="normal">专家</el-radio> |
| | | <el-radio-group |
| | | v-model="sendForm.expertType" |
| | | @change="handleExpertTypeChange" |
| | | > |
| | | <el-radio label="normal">普通专家</el-radio> |
| | | <el-radio label="chief">主委专家</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="发送时间" prop="startTime" required> |
| | | <el-date-picker |
| | | v-model="sendForm.startTime" |
| | | type="datetime" |
| | | placeholder="请选择发送时间" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="截止时间" |
| | | prop="endTime" |
| | | :required="sendForm.expertType !== 'chief'" |
| | | > |
| | | <el-date-picker |
| | | v-model="sendForm.endTime" |
| | | type="datetime" |
| | | placeholder="请选择截止时间" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%" |
| | | :disabled="sendForm.expertType === 'chief'" |
| | | /> |
| | | <div |
| | | v-if="sendForm.expertType === 'chief'" |
| | | style="font-size: 12px; color: #999; margin-top: 5px;" |
| | | > |
| | | 主委专家无需设置截止时间 |
| | | </div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="发送方式" prop="sendType" required> |
| | | <el-select |
| | | v-model="sendForm.sendType" |
| | | placeholder="请选择发送方式" |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="系统发送" value="0"></el-option> |
| | | <el-option label="邮件发送" value="1"></el-option> |
| | | <el-option label="短信发送" value="2"></el-option> |
| | | <el-option label="其他方式" value="3"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="选择专家" |
| | | prop="expertIds" |
| | | v-if="sendForm.expertType === 'normal'" |
| | | v-if="sendForm.expertType == 'normal'" |
| | | > |
| | | <el-select |
| | | v-model="sendForm.expertIds" |
| | |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="expert in availableExperts" |
| | | :key="expert.id" |
| | | :label="expert.name" |
| | | :value="expert.id" |
| | | v-for="expert in availableNormalExperts" |
| | | :key="getExpertKey(expert)" |
| | | :label=" |
| | | `${expert.expertname}${ |
| | | expert.expertNo ? '(' + expert.expertNo + ')' : '' |
| | | }` |
| | | " |
| | | :value="getExpertKey(expert)" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="发送内容" prop="content"> |
| | | <el-input |
| | | type="textarea" |
| | |
| | | </el-form> |
| | | <div slot="footer"> |
| | | <el-button @click="sendDialogVisible = false">取消</el-button> |
| | | <el-button type="primary" @click="handleSendConfirm" |
| | | <el-button type="primary" @click="handleSendConfirm" :loading="sending" |
| | | >确认发送</el-button |
| | | > |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 上传附件对话框 --> |
| | | <!-- 专家历史审批情况对话框 --> |
| | | <el-dialog |
| | | title="上传附件" |
| | | :visible.sync="uploadDialogVisible" |
| | | width="500px" |
| | | :close-on-click-modal="false" |
| | | title="专家历史审批情况" |
| | | :visible.sync="expertHistoryDialogVisible" |
| | | width="600px" |
| | | > |
| | | <el-upload |
| | | ref="uploadRef" |
| | | class="upload-demo" |
| | | drag |
| | | :action="uploadAction" |
| | | :headers="headers" |
| | | multiple |
| | | :file-list="tempFileList" |
| | | :before-upload="beforeUpload" |
| | | :on-change="handleFileChange" |
| | | :on-remove="handleTempRemove" |
| | | :on-success="handleUploadSuccess" |
| | | :auto-upload="false" |
| | | > |
| | | <i class="el-icon-upload"></i> |
| | | <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> |
| | | <div class="el-upload__tip" slot="tip"> |
| | | 支持上传pdf、jpg、png、doc、docx、xls、xlsx格式文件,单个文件不超过10MB |
| | | <div v-loading="expertHistoryLoading"> |
| | | <div v-if="expertHistoryData" style="padding: 20px;"> |
| | | <el-row :gutter="20" style="margin-bottom: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">专家姓名</div> |
| | | <div class="history-stat-value"> |
| | | {{ currentExpertInfo.expertname || "-" }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">专家编号</div> |
| | | <div class="history-stat-value"> |
| | | {{ currentExpertInfo.expertNo || "-" }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider></el-divider> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">总审批数量</div> |
| | | <div |
| | | class="history-stat-value" |
| | | style="font-size: 24px; color: #409EFF;" |
| | | > |
| | | {{ expertHistoryData.count || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">已接收数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.acceptCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20" style="margin-top: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">未接收数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.notAcceptCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">有意见数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.opinionCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20" style="margin-top: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">无意见数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.notOpinionCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">有附件数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.annexCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20" style="margin-top: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">无附件数量</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.notAnnexCount || 0 }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-upload> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button @click="uploadDialogVisible = false">取消</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="submitUpload" |
| | | :loading="uploadLoading" |
| | | :disabled="tempFileList.length === 0" |
| | | > |
| | | 确认上传 |
| | | </el-button> |
| | | <div v-else style="text-align: center; padding: 40px 0;"> |
| | | <i |
| | | class="el-icon-info" |
| | | style="font-size: 60px; color: #C0C4CC; margin-bottom: 20px;" |
| | | ></i> |
| | | <p style="color: #909399; font-size: 14px;">暂无历史审批数据</p> |
| | | </div> |
| | | </div> |
| | | <div slot="footer"> |
| | | <el-button @click="expertHistoryDialogVisible = false">关闭</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 文件预览弹窗 --> |
| | | <FilePreviewDialog |
| | | :visible="previewVisible" |
| | | :file="currentPreviewFile" |
| | | @close="previewVisible = false" |
| | | @download="handleDownload" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { |
| | | reviewinitiateBaseInfoList, |
| | | ethicalreviewedit, |
| | | ethicalreviewadd |
| | | ethicalreviewadd, |
| | | ethicalreviewInfo, |
| | | ethicalreExpertTotal |
| | | } from "@/api/businessApi"; |
| | | import { listExternalperson } from "@/api/project/externalperson"; |
| | | import CaseBasicInfo from "@/components/CaseBasicInfo"; |
| | | import UploadAttachment from "@/components/UploadAttachment"; |
| | | import FilePreviewDialog from "@/components/FilePreviewDialog"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | export default { |
| | | name: "EthicsReviewDetail", |
| | | components: { CaseBasicInfo }, |
| | | components: { CaseBasicInfo, UploadAttachment, FilePreviewDialog }, |
| | | dicts: ["sys_user_sex", "sys_ethical", "Review_status"], |
| | | |
| | | data() { |
| | | return { |
| | |
| | | isEdit: false, |
| | | // 基本信息 |
| | | infoid: undefined, |
| | | id: undefined, |
| | | caseId: null, |
| | | caseNo: "", |
| | | |
| | |
| | | initiatePerson: "", |
| | | |
| | | // 状态和时间 |
| | | status: "0", // 0:新建, 1:审查中, 2:结束 |
| | | status: "0", // 0:待审查, 1:审查中, 2:审查中止, 3:审查完成 |
| | | startTime: "", |
| | | cutOffTime: "", |
| | | endTime: "", |
| | |
| | | // 专家信息 |
| | | expertName: "", |
| | | expertNo: "", |
| | | expertType: "normal", |
| | | expertType: "0", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | expertTime: "", |
| | |
| | | // 备注 |
| | | remark: "", |
| | | |
| | | // 附件信息 |
| | | annexfilesList: [], |
| | | filePatch: "", |
| | | |
| | | // 专家审查意见列表 |
| | | ethicalreviewopinionsList: [], |
| | | |
| | | // 系统字段 |
| | | createBy: "", |
| | | createTime: "", |
| | |
| | | updateTime: "", |
| | | delFlag: "0" |
| | | }, |
| | | |
| | | // 表单验证规则 |
| | | rules: { |
| | | initiateTheme: [ |
| | | { required: true, message: "发起主题不能为空", trigger: "blur" }, |
| | | { min: 2, max: 100, message: "长度在 2 到 100 个字符", trigger: "blur" } |
| | | ], |
| | | initiatePerson: [ |
| | | { required: true, message: "发起人不能为空", trigger: "blur" } |
| | | ], |
| | | status: [ |
| | | { required: true, message: "审查状态不能为空", trigger: "change" } |
| | | ], |
| | | startTime: [ |
| | | { required: true, message: "发起时间不能为空", trigger: "change" } |
| | | ], |
| | | cutOffTime: [ |
| | | { required: true, message: "截止时间不能为空", trigger: "change" } |
| | | ], |
| | | expertName: [ |
| | | { max: 50, message: "长度不能超过 50 个字符", trigger: "blur" } |
| | | ], |
| | | expertNo: [ |
| | | { max: 50, message: "长度不能超过 50 个字符", trigger: "blur" } |
| | | ], |
| | | expertConclusion: [ |
| | | { max: 2, message: "长度不能超过 2 个字符", trigger: "change" } |
| | | ], |
| | | remark: [ |
| | | { max: 500, message: "长度不能超过 500 个字符", trigger: "blur" } |
| | | ] |
| | | }, |
| | | rules: { |
| | | initiateTheme: [ |
| | | { required: true, message: "发起主题不能为空", trigger: "blur" }, |
| | | { |
| | | min: 2, |
| | | max: 100, |
| | | message: "长度在 2 到 100 个字符", |
| | | trigger: "blur" |
| | | } |
| | | ], |
| | | initiatePerson: [ |
| | | { required: true, message: "发起人不能为空", trigger: "blur" } |
| | | ], |
| | | status: [ |
| | | { required: true, message: "审查状态不能为空", trigger: "change" } |
| | | ], |
| | | expertName: [ |
| | | { max: 50, message: "长度不能超过 50 个字符", trigger: "blur" } |
| | | ], |
| | | expertNo: [ |
| | | { max: 50, message: "长度不能超过 50 个字符", trigger: "blur" } |
| | | ], |
| | | expertConclusion: [ |
| | | { max: 2, message: "长度不能超过 2 个字符", trigger: "change" } |
| | | ], |
| | | remark: [ |
| | | { max: 500, message: "长度不能超过 500 个字符", trigger: "blur" } |
| | | ] |
| | | }, |
| | | |
| | | // 保存加载状态 |
| | | saveLoading: false, |
| | | completeLoading: false, |
| | | suspendLoading: false, |
| | | endLoading: false, |
| | | sending: false, |
| | | |
| | | // 附件数据 |
| | | attachments: [], |
| | | // 附件相关 |
| | | attachmentFileList: [], |
| | | |
| | | // 预览相关 |
| | | previewVisible: false, |
| | | currentPreviewFile: null, |
| | | |
| | | // 专家审查数据 |
| | | expertReviews: [ |
| | | // 专家(18位)- 初始状态为申请中 |
| | | { |
| | | id: 1, |
| | | expertName: "陶昊", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 2, |
| | | expertName: "刘斌", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 3, |
| | | expertName: "于海初 ", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 4, |
| | | expertName: "王红梅", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 5, |
| | | expertName: "王春光", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 6, |
| | | expertName: "王静", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 7, |
| | | expertName: "边文超", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 8, |
| | | expertName: "闫志勇", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 9, |
| | | expertName: "许凤", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 10, |
| | | expertName: "许传屾", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 11, |
| | | expertName: "张红岩", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 12, |
| | | expertName: "杨苏民", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 13, |
| | | expertName: "宋玉强", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 14, |
| | | expertName: "周传利", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 15, |
| | | expertName: "荆凡波", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 16, |
| | | expertName: "矫文捷", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 17, |
| | | expertName: "董震", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | { |
| | | id: 18, |
| | | expertName: "蔡金贞", |
| | | isChief: false, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | }, |
| | | // 主委专家(1位) |
| | | { |
| | | id: 19, |
| | | expertName: "孔心涓", |
| | | isChief: true, |
| | | reviewStatus: "applying", |
| | | expertConclusion: "", |
| | | expertOpinion: "", |
| | | reviewTime: "", |
| | | sendTime: "" |
| | | } |
| | | ], |
| | | expertReviews: [], |
| | | expertLoading: false, |
| | | attachmentLoading: false, |
| | | |
| | | // 添加专家对话框 |
| | | expertDialogVisible: false, |
| | | expertList: [], |
| | | expertListLoading: false, |
| | | expertSearchQuery: "", |
| | | filterExpertType: "", |
| | | expertPage: { |
| | | pageNum: 1, |
| | | pageSize: 50 |
| | | }, |
| | | expertTotal: 0, |
| | | selectedExperts: [], |
| | | |
| | | // 发送对话框 |
| | | sendDialogVisible: false, |
| | | sendForm: { |
| | | expertType: "normal", |
| | | expertIds: [], |
| | | startTime: "", |
| | | endTime: "", |
| | | sendType: "0", |
| | | content: "" |
| | | }, |
| | | |
| | | // 上传相关 |
| | | uploadDialogVisible: false, |
| | | uploadLoading: false, |
| | | tempFileList: [], |
| | | uploadAction: process.env.VUE_APP_BASE_API + "/common/upload", |
| | | headers: { |
| | | Authorization: "Bearer " + getToken() |
| | | }, |
| | | // 专家历史审批情况 |
| | | expertHistoryDialogVisible: false, |
| | | expertHistoryLoading: false, |
| | | expertHistoryData: null, |
| | | currentExpertInfo: {}, |
| | | |
| | | // 可用专家列表 |
| | | availableExperts: [ |
| | | { id: 1, name: "陶昊", type: "normal" }, |
| | | { id: 2, name: "刘斌", type: "normal" }, |
| | | { id: 3, name: "于海初", type: "normal" }, |
| | | { id: 4, name: "王红梅", type: "normal" }, |
| | | { id: 5, name: "王春光", type: "normal" }, |
| | | { id: 6, name: "王静", type: "normal" }, |
| | | { id: 7, name: "边文超", type: "normal" }, |
| | | { id: 8, name: "闫志勇", type: "normal" }, |
| | | { id: 9, name: "许凤", type: "normal" }, |
| | | { id: 10, name: "许传屾", type: "normal" }, |
| | | { id: 11, name: "张红岩", type: "normal" }, |
| | | { id: 12, name: "杨苏民", type: "normal" }, |
| | | { id: 13, name: "宋玉强", type: "normal" }, |
| | | { id: 14, name: "周传利", type: "normal" }, |
| | | { id: 15, name: "荆凡波", type: "normal" }, |
| | | { id: 16, name: "矫文捷", type: "normal" }, |
| | | { id: 17, name: "董震", type: "normal" }, |
| | | { id: 18, name: "蔡金贞", type: "normal" }, |
| | | { id: 19, name: "孔心涓", type: "chief" } |
| | | ] |
| | | // 内部状态跟踪 |
| | | internalExpertList: [] |
| | | }; |
| | | }, |
| | | computed: { |
| | | // 计算属性:专家同意数量 |
| | | approvedNormalExperts() { |
| | | return this.expertReviews.filter( |
| | | expert => !expert.isChief && expert.expertConclusion === "approved" |
| | | // 计算属性:获取专家列表(确保响应式) |
| | | ethicalreviewopinionsList() { |
| | | return this.form.ethicalreviewopinionsList || []; |
| | | }, |
| | | |
| | | // 计算属性:普通专家数量 |
| | | normalExpertsCount() { |
| | | return this.ethicalreviewopinionsList.filter( |
| | | expert => expert.expertType === "0" |
| | | ).length; |
| | | }, |
| | | // 计算属性:主委专家状态 |
| | | chiefExpertStatus() { |
| | | const chiefExpert = this.expertReviews.find(expert => expert.isChief); |
| | | return chiefExpert |
| | | ? this.statusTextFilter(chiefExpert.reviewStatus) |
| | | : "未分配"; |
| | | }, |
| | | // 计算属性:完成进度 |
| | | completionRate() { |
| | | const totalExperts = this.expertReviews.length; |
| | | const completedExperts = this.expertReviews.filter( |
| | | expert => expert.reviewStatus === "submitted" |
| | | |
| | | // 计算属性:主委专家数量 |
| | | chiefExpertsCount() { |
| | | return this.ethicalreviewopinionsList.filter( |
| | | expert => expert.expertType === "1" |
| | | ).length; |
| | | return totalExperts > 0 |
| | | ? Math.round((completedExperts / totalExperts) * 100) |
| | | : 0; |
| | | }, |
| | | |
| | | // 计算属性:总专家数量 |
| | | totalExpertsCount() { |
| | | return this.ethicalreviewopinionsList.length; |
| | | }, |
| | | |
| | | // 计算属性:已同意专家数量 |
| | | approvedExpertsCount() { |
| | | return this.ethicalreviewopinionsList.filter( |
| | | expert => expert.expertconclusion === "1" |
| | | ).length; |
| | | }, |
| | | |
| | | // 计算属性:总体结论 |
| | | overallConclusionText() { |
| | | if (this.approvedNormalExperts >= 12) { |
| | | const total = this.totalExpertsCount; |
| | | const approved = this.approvedExpertsCount; |
| | | |
| | | if (total === 0) return "未审查"; |
| | | if (approved >= Math.ceil(total * 0.7)) { |
| | | // 超过70%同意 |
| | | return "通过"; |
| | | } else if (this.approvedNormalExperts >= 9) { |
| | | } else if (approved >= Math.ceil(total * 0.5)) { |
| | | // 超过50%同意 |
| | | return "修改后通过"; |
| | | } else { |
| | | return "不通过"; |
| | | } |
| | | }, |
| | | |
| | | overallConclusionFilter() { |
| | | if (this.approvedNormalExperts >= 12) { |
| | | const total = this.totalExpertsCount; |
| | | const approved = this.approvedExpertsCount; |
| | | |
| | | if (total === 0) return "info"; |
| | | if (approved >= Math.ceil(total * 0.7)) { |
| | | return "success"; |
| | | } else if (this.approvedNormalExperts >= 9) { |
| | | } else if (approved >= Math.ceil(total * 0.5)) { |
| | | return "warning"; |
| | | } else { |
| | | return "danger"; |
| | | } |
| | | }, |
| | | // 是否可以发送给专家 |
| | | |
| | | // 可发送的普通专家 |
| | | availableNormalExperts() { |
| | | return this.ethicalreviewopinionsList.filter( |
| | | expert => |
| | | expert.expertType === "0" && |
| | | (!expert.receiveStatus || |
| | | expert.receiveStatus === "0" || |
| | | expert.receiveStatus === "1") |
| | | ); |
| | | }, |
| | | |
| | | // 可发送的主委专家 |
| | | availableChiefExperts() { |
| | | return this.ethicalreviewopinionsList.filter( |
| | | expert => |
| | | expert.expertType === "1" && |
| | | (!expert.receiveStatus || |
| | | expert.receiveStatus === "0" || |
| | | expert.receiveStatus === "1") |
| | | ); |
| | | }, |
| | | |
| | | // 是否可以发送给普通专家 |
| | | canSendToNormalExperts() { |
| | | return ( |
| | | this.expertReviews.filter( |
| | | expert => !expert.isChief && expert.reviewStatus === "applying" |
| | | ).length > 0 |
| | | ); |
| | | return this.availableNormalExperts.length > 0; |
| | | }, |
| | | // 是否可以发送给主委专家(需要至少12个专家同意) |
| | | |
| | | // 是否可以发送给主委专家(需要至少12个普通专家同意) |
| | | canSendToChiefExpert() { |
| | | return ( |
| | | this.approvedNormalExperts >= 12 && |
| | | this.expertReviews.filter( |
| | | expert => expert.isChief && expert.reviewStatus === "applying" |
| | | ).length > 0 |
| | | ); |
| | | const normalApprovedCount = this.ethicalreviewopinionsList.filter( |
| | | expert => expert.expertType === "0" && expert.expertconclusion === "1" |
| | | ).length; |
| | | return this.availableChiefExperts.length > 0 && normalApprovedCount >= 12; |
| | | }, |
| | | |
| | | // 是否可以批量发送 |
| | | canBatchSend() { |
| | | return ( |
| | | this.expertReviews.filter(expert => expert.reviewStatus === "applying") |
| | | .length > 0 |
| | | this.availableNormalExperts.length > 0 || |
| | | this.availableChiefExperts.length > 0 |
| | | ); |
| | | }, |
| | | |
| | | // 当前用户信息 |
| | | currentUser() { |
| | | return JSON.parse(sessionStorage.getItem("user") || "{}"); |
| | | }, |
| | | |
| | | // 发送对话框标题 |
| | | sendDialogTitle() { |
| | | if (this.sendForm.expertType === "chief") { |
| | | return "发送主委专家审查"; |
| | | } else if (this.sendForm.expertType === "normal") { |
| | | return "发送普通专家审查"; |
| | | } else { |
| | | return "发送专家审查"; |
| | | } |
| | | } |
| | | }, |
| | | watch: { |
| | | // 监听表单中的专家列表变化 |
| | | "form.ethicalreviewopinionsList": { |
| | | handler(newVal) { |
| | | console.log("专家列表变化:", newVal); |
| | | // 强制触发计算属性更新 |
| | | this.$forceUpdate(); |
| | | }, |
| | | deep: true |
| | | } |
| | | }, |
| | | created() { |
| | | this.infoid = this.$route.query.infoid; |
| | | this.id = this.$route.query.id; |
| | | this.caseId = this.$route.query.infoid; |
| | | // const id = this.$route.query.id; |
| | | this.getDetail(this.infoid); |
| | | |
| | | // if (id && !this.$route.path.includes("/add")) { |
| | | // this.getDetail(this.infoid); |
| | | // } else if (this.$route.path.includes("/add") && this.infoid) { |
| | | // this.initNewData(); |
| | | // } |
| | | this.getDetail(this.infoid, this.id); |
| | | }, |
| | | methods: { |
| | | // 初始化新增数据 |
| | |
| | | }, |
| | | |
| | | // 获取详情 |
| | | async getDetail(infoid) { |
| | | async getDetail(infoid, id) { |
| | | try { |
| | | this.expertLoading = true; |
| | | const response = await reviewinitiateBaseInfoList({ infoid: infoid }); |
| | | let response = {}; |
| | | if (id) { |
| | | response = await ethicalreviewInfo(id); |
| | | } else if (infoid) { |
| | | response = await reviewinitiateBaseInfoList({ infoid: infoid }); |
| | | } |
| | | |
| | | if (response.code === 200) { |
| | | let detailData = {}; |
| | | |
| | | if (response.data) { |
| | | this.form = response.data[0]; |
| | | } |
| | | console.log(this.form, "this.form "); |
| | | if (response.data && id) { |
| | | this.form = response.data; |
| | | // 解析 filePatch 字段 |
| | | this.parseFilePatch(this.form.filePatch); |
| | | this.initAttachmentFileList(); |
| | | |
| | | this.infoid = detailData.infoid || this.infoid; |
| | | this.caseNo = detailData.caseNo || ""; |
| | | // 如果专家审查意见列表不存在,初始化为空数组 |
| | | if (!this.form.ethicalreviewopinionsList) { |
| | | this.$set(this.form, "ethicalreviewopinionsList", []); |
| | | } |
| | | } else if (response.data && infoid) { |
| | | this.form = response.data[0]; |
| | | // 解析 filePatch 字段 |
| | | this.parseFilePatch(this.form.filePatch); |
| | | this.initAttachmentFileList(); |
| | | |
| | | // 如果专家审查意见列表不存在,初始化为空数组 |
| | | if (!this.form.ethicalreviewopinionsList) { |
| | | this.$set(this.form, "ethicalreviewopinionsList", []); |
| | | } |
| | | } |
| | | |
| | | // 设置 expertReviews 用于表格显示 |
| | | this.expertReviews = this.form.ethicalreviewopinionsList; |
| | | |
| | | this.$message.success("数据加载成功"); |
| | | } else { |
| | |
| | | } |
| | | }, |
| | | |
| | | // 获取专家审查列表 |
| | | getExpertReviews(ethicsReviewId) { |
| | | this.expertLoading = true; |
| | | // 模拟数据 - 实际项目中从接口获取 |
| | | setTimeout(() => { |
| | | this.expertLoading = false; |
| | | }, 500); |
| | | // 解析 filePatch 字段 |
| | | parseFilePatch(filePatch) { |
| | | if (!filePatch) { |
| | | this.form.annexfilesList = []; |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | this.form.annexfilesList = JSON.parse(filePatch); |
| | | } catch (error) { |
| | | console.error("解析 filePatch 字段失败:", error); |
| | | this.form.annexfilesList = []; |
| | | } |
| | | }, |
| | | |
| | | // 获取附件列表 |
| | | getAttachments(ethicsReviewId) { |
| | | this.attachmentLoading = true; |
| | | // 模拟获取附件 |
| | | setTimeout(() => { |
| | | this.attachmentLoading = false; |
| | | }, 500); |
| | | // 初始化附件文件列表 |
| | | initAttachmentFileList() { |
| | | if (this.form.annexfilesList && this.form.annexfilesList.length > 0) { |
| | | this.attachmentFileList = this.form.annexfilesList.map(item => ({ |
| | | uid: |
| | | item.id || |
| | | Math.random() |
| | | .toString(36) |
| | | .substr(2, 9), |
| | | name: item.fileName, |
| | | url: item.path || item.fileUrl, |
| | | status: "success" |
| | | })); |
| | | } else { |
| | | this.attachmentFileList = []; |
| | | } |
| | | }, |
| | | |
| | | // 专家行样式 |
| | | getExpertRowClassName({ row }) { |
| | | return row.isChief ? "chief-expert-row" : "normal-expert-row"; |
| | | // 构建 filePatch 字段 |
| | | buildFilePatch() { |
| | | if (!this.form.annexfilesList || this.form.annexfilesList.length === 0) { |
| | | return ""; |
| | | } |
| | | return JSON.stringify(this.form.annexfilesList); |
| | | }, |
| | | |
| | | // 状态过滤器 |
| | | statusFilter(status) { |
| | | // 附件变化处理 |
| | | handleAttachmentChange(fileList) { |
| | | this.attachmentFileList = fileList; |
| | | }, |
| | | |
| | | // 附件移除处理 |
| | | handleAttachmentRemove(file) { |
| | | if (file.url) { |
| | | const index = this.form.annexfilesList.findIndex( |
| | | item => item.path === file.url || item.fileUrl === file.url |
| | | ); |
| | | if (index > -1) { |
| | | this.form.annexfilesList.splice(index, 1); |
| | | } |
| | | } |
| | | }, |
| | | |
| | | // 手动删除附件 |
| | | handleRemoveAttachment(index) { |
| | | this.form.annexfilesList.splice(index, 1); |
| | | this.attachmentFileList.splice(index, 1); |
| | | this.$message.success("附件删除成功"); |
| | | }, |
| | | |
| | | // 上传成功处理 |
| | | handleUploadSuccess({ file, fileList, response }) { |
| | | if (response.code === 200) { |
| | | const attachmentObj = { |
| | | fileName: file.name, |
| | | path: response.data || file.url, |
| | | fileUrl: response.data || file.url, |
| | | type: this.getFileExtension(file.name), |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | infoid: this.infoid, |
| | | delFlag: 0 |
| | | }; |
| | | |
| | | this.form.annexfilesList.push(attachmentObj); |
| | | this.$message.success("文件上传成功"); |
| | | } |
| | | }, |
| | | |
| | | // 上传错误处理 |
| | | handleUploadError({ file, fileList, error }) { |
| | | console.error("附件上传失败:", error); |
| | | this.$message.error("文件上传失败,请重试"); |
| | | }, |
| | | |
| | | // 文件预览 |
| | | handlePreview(file) { |
| | | this.currentPreviewFile = { |
| | | fileName: file.fileName, |
| | | fileUrl: file.path || file.fileUrl, |
| | | fileType: this.getFileType(file.fileName) |
| | | }; |
| | | this.previewVisible = true; |
| | | }, |
| | | |
| | | // 文件下载 |
| | | handleDownload(file) { |
| | | const fileUrl = file.path || file.fileUrl; |
| | | const fileName = file.fileName; |
| | | |
| | | if (fileUrl) { |
| | | const link = document.createElement("a"); |
| | | link.href = fileUrl; |
| | | link.download = fileName; |
| | | link.style.display = "none"; |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | this.$message.success("开始下载文件"); |
| | | } else { |
| | | this.$message.warning("文件路径不存在,无法下载"); |
| | | } |
| | | }, |
| | | |
| | | // 获取文件类型 |
| | | getFileType(fileName) { |
| | | if (!fileName) return "other"; |
| | | |
| | | const extension = fileName |
| | | .split(".") |
| | | .pop() |
| | | .toLowerCase(); |
| | | const imageTypes = ["jpg", "jpeg", "png", "gif", "bmp", "webp"]; |
| | | const pdfTypes = ["pdf"]; |
| | | const officeTypes = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"]; |
| | | |
| | | if (imageTypes.includes(extension)) return "image"; |
| | | if (pdfTypes.includes(extension)) return "pdf"; |
| | | if (officeTypes.includes(extension)) return "office"; |
| | | return "other"; |
| | | }, |
| | | |
| | | // 获取文件扩展名 |
| | | getFileExtension(filename) { |
| | | return filename |
| | | .split(".") |
| | | .pop() |
| | | .toLowerCase(); |
| | | }, |
| | | |
| | | // 日期时间格式化 |
| | | formatDateTime(dateTime) { |
| | | if (!dateTime) return ""; |
| | | |
| | | try { |
| | | const date = new Date(dateTime); |
| | | if (isNaN(date.getTime())) return dateTime; |
| | | |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | const hours = String(date.getHours()).padStart(2, "0"); |
| | | const minutes = String(date.getMinutes()).padStart(2, "0"); |
| | | const seconds = String(date.getSeconds()).padStart(2, "0"); |
| | | |
| | | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
| | | } catch (error) { |
| | | return dateTime; |
| | | } |
| | | }, |
| | | |
| | | // 判断是否为主任委员 |
| | | getIsChiefExpert(expert) { |
| | | // 职称包含"主任委员"或者expertType为"1" |
| | | return ( |
| | | (expert.title && expert.title.includes("主任委员")) || |
| | | expert.expertType === "1" |
| | | ); |
| | | }, |
| | | |
| | | // 专家类型文本转换 |
| | | getExpertTypeText(type) { |
| | | return type === "1" ? "主委专家" : "普通专家"; |
| | | }, |
| | | |
| | | // 审查状态过滤器 |
| | | getReviewStatusFilter(status) { |
| | | const statusMap = { |
| | | applying: "info", |
| | | submitted: "success" |
| | | "0": "info", // 待接收 |
| | | "1": "warning", // 未接收 |
| | | "2": "success", // 已接收 |
| | | "3": "danger", // 超时 |
| | | "4": "danger", // 中止 |
| | | "5": "success" // 完成 |
| | | }; |
| | | return statusMap[status] || "info"; |
| | | }, |
| | | |
| | | statusTextFilter(status) { |
| | | getReviewStatusText(status) { |
| | | const statusMap = { |
| | | applying: "申请中", |
| | | submitted: "已提交" |
| | | "0": "待接收", |
| | | "1": "未接收", |
| | | "2": "已接收", |
| | | "3": "超时", |
| | | "4": "中止", |
| | | "5": "完成" |
| | | }; |
| | | return statusMap[status] || "未知"; |
| | | }, |
| | | |
| | | // 结论过滤器 |
| | | conclusionFilter(conclusion) { |
| | | getConclusionFilter(conclusion) { |
| | | const conclusionMap = { |
| | | approved: "success", |
| | | approved_with_modifications: "warning", |
| | | disapproved: "danger" |
| | | "1": "success", // 同意 |
| | | "2": "warning", // 审查中 |
| | | "0": "danger" // 不同意 |
| | | }; |
| | | return conclusionMap[conclusion] || "info"; |
| | | }, |
| | | |
| | | conclusionTextFilter(conclusion) { |
| | | getConclusionText(conclusion) { |
| | | const conclusionMap = { |
| | | approved: "同意", |
| | | approved_with_modifications: "修改后同意", |
| | | disapproved: "不同意" |
| | | "1": "同意", |
| | | "2": "审查中", |
| | | "0": "不同意" |
| | | }; |
| | | return conclusionMap[conclusion] || "未知"; |
| | | }, |
| | | |
| | | // 专家行样式 |
| | | getExpertRowClassName({ row }) { |
| | | return row.expertType === "1" ? "chief-expert-row" : "normal-expert-row"; |
| | | }, |
| | | |
| | | // 获取专家唯一标识 |
| | | getExpertKey(expert) { |
| | | return expert.id || expert.expertNo || expert.expertname; |
| | | }, |
| | | |
| | | // 专家类型变更处理 |
| | | handleExpertTypeChange() { |
| | | if (this.sendForm.expertType === "chief") { |
| | | // 主委专家无需设置截止时间 |
| | | this.sendForm.endTime = ""; |
| | | } |
| | | }, |
| | | |
| | | // 保存信息 |
| | |
| | | ...this.form, |
| | | // 确保必要字段 |
| | | infoid: this.infoid, |
| | | caseNo: this.caseNo |
| | | caseNo: this.caseNo, |
| | | // 构建 filePatch 字段 |
| | | filePatch: this.buildFilePatch() |
| | | }; |
| | | |
| | | let response = null; |
| | |
| | | }); |
| | | }, |
| | | |
| | | // 审查完成 |
| | | async handleCompleteReview() { |
| | | this.$confirm("确定要将本次伦理审查状态设为完成吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }) |
| | | .then(async () => { |
| | | this.completeLoading = true; |
| | | try { |
| | | const updateData = { |
| | | ...this.form, |
| | | status: "3", // 审查完成 |
| | | endTime: new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19) |
| | | }; |
| | | |
| | | const response = await ethicalreviewedit(updateData); |
| | | |
| | | if (response.code === 200) { |
| | | this.$message.success("审查状态已更新为完成"); |
| | | this.form.status = "3"; |
| | | this.form.endTime = updateData.endTime; |
| | | } else { |
| | | this.$message.error("操作失败:" + (response.msg || "未知错误")); |
| | | } |
| | | } catch (error) { |
| | | console.error("更新审查状态失败:", error); |
| | | this.$message.error("更新审查状态失败"); |
| | | } finally { |
| | | this.completeLoading = false; |
| | | } |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | |
| | | // 审查中止 |
| | | async handleSuspendReview() { |
| | | this.$confirm( |
| | | "确定要中止本次伦理审查吗?所有专家的审查状态将变为中止。", |
| | | "提示", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | } |
| | | ) |
| | | .then(async () => { |
| | | this.suspendLoading = true; |
| | | try { |
| | | // 更新所有专家的接收状态为中止 |
| | | if ( |
| | | this.form.ethicalreviewopinionsList && |
| | | this.form.ethicalreviewopinionsList.length > 0 |
| | | ) { |
| | | this.form.ethicalreviewopinionsList.forEach(expert => { |
| | | expert.receiveStatus = "4"; // 中止状态 |
| | | expert.updateTime = new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19); |
| | | }); |
| | | } |
| | | |
| | | const updateData = { |
| | | ...this.form, |
| | | status: "2" // 审查中止 |
| | | }; |
| | | |
| | | const response = await ethicalreviewedit(updateData); |
| | | |
| | | if (response.code === 200) { |
| | | this.$message.success("审查已中止,所有专家状态已更新"); |
| | | this.form.status = "2"; |
| | | |
| | | // 触发计算属性更新 |
| | | this.$forceUpdate(); |
| | | } else { |
| | | this.$message.error("操作失败:" + (response.msg || "未知错误")); |
| | | } |
| | | } catch (error) { |
| | | console.error("中止审查失败:", error); |
| | | this.$message.error("中止审查失败"); |
| | | } finally { |
| | | this.suspendLoading = false; |
| | | } |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | |
| | | // 结束审查 |
| | | async handleEndReview() { |
| | | this.$confirm( |
| | |
| | | } |
| | | ) |
| | | .then(async () => { |
| | | this.endLoading = true; |
| | | try { |
| | | const updateData = { |
| | | ...this.form, |
| | | status: "2", |
| | | status: "2", // 审查中止 |
| | | endTime: new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | |
| | | } catch (error) { |
| | | console.error("结束审查失败:", error); |
| | | this.$message.error("结束审查失败"); |
| | | } finally { |
| | | this.endLoading = false; |
| | | } |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | |
| | | // 发送给专家 |
| | | handleSendToNormalExperts() { |
| | | const normalExperts = this.expertReviews.filter( |
| | | expert => !expert.isChief && expert.reviewStatus === "applying" |
| | | // 打开添加专家对话框 |
| | | handleAddExpert() { |
| | | this.expertDialogVisible = true; |
| | | this.loadExperts(); |
| | | }, |
| | | |
| | | // 加载专家列表 |
| | | async loadExperts() { |
| | | try { |
| | | this.expertListLoading = true; |
| | | const params = { |
| | | usertype: "伦理专家", // 伦理专家 |
| | | pageNum: this.expertPage.pageNum, |
| | | pageSize: this.expertPage.pageSize |
| | | }; |
| | | |
| | | if (this.expertSearchQuery) { |
| | | params.username = this.expertSearchQuery; |
| | | } |
| | | |
| | | const response = await listExternalperson(params); |
| | | if (response.code === 200) { |
| | | this.expertList = response.rows; |
| | | this.expertTotal = response.total; |
| | | } else { |
| | | this.$message.error( |
| | | "加载专家列表失败:" + (response.msg || "未知错误") |
| | | ); |
| | | } |
| | | } catch (error) { |
| | | console.error("加载专家列表失败:", error); |
| | | this.$message.error("加载专家列表失败"); |
| | | } finally { |
| | | this.expertListLoading = false; |
| | | } |
| | | }, |
| | | |
| | | // 搜索专家 |
| | | handleSearchExperts() { |
| | | this.expertPage.pageNum = 1; |
| | | this.loadExperts(); |
| | | }, |
| | | |
| | | // 重置搜索 |
| | | handleResetSearch() { |
| | | this.expertSearchQuery = ""; |
| | | this.filterExpertType = ""; |
| | | this.expertPage.pageNum = 1; |
| | | this.loadExperts(); |
| | | }, |
| | | |
| | | // 专家选择变化 |
| | | handleExpertSelectionChange(selection) { |
| | | this.selectedExperts = selection; |
| | | }, |
| | | |
| | | // 确认添加专家 |
| | | handleConfirmAddExpert() { |
| | | if (this.selectedExperts.length === 0) { |
| | | this.$message.warning("请选择要添加的专家"); |
| | | return; |
| | | } |
| | | |
| | | // 确保ethicalreviewopinionsList存在 |
| | | if (!this.form.ethicalreviewopinionsList) { |
| | | this.$set(this.form, "ethicalreviewopinionsList", []); |
| | | } |
| | | |
| | | // 过滤已存在的专家 |
| | | const existingExpertIds = this.form.ethicalreviewopinionsList.map( |
| | | expert => expert.expertNo || expert.expertname |
| | | ); |
| | | this.sendForm.expertIds = normalExperts.map(expert => expert.id); |
| | | const newExperts = this.selectedExperts.filter(expert => { |
| | | return !existingExpertIds.includes(expert.userno || expert.username); |
| | | }); |
| | | |
| | | if (newExperts.length === 0) { |
| | | this.$message.warning("选择的专家已存在"); |
| | | return; |
| | | } |
| | | |
| | | // 添加专家到列表 |
| | | newExperts.forEach(expert => { |
| | | // 判断是否为主任委员 |
| | | const isChief = this.getIsChiefExpert(expert); |
| | | |
| | | const expertReview = { |
| | | id: undefined, |
| | | infoid: this.infoid, |
| | | nitiateId: this.form.id || undefined, |
| | | caseNo: this.form.caseNo, |
| | | expertname: expert.username, |
| | | expertNo: expert.userno, |
| | | expertType: isChief ? "1" : "0", // 主任委员设置为主委专家 |
| | | deptName: expert.unitname || "", |
| | | title: expert.title || "", |
| | | telephone: expert.telephone || "", |
| | | receiveStatus: "0", // 待接收 |
| | | expertconclusion: "", |
| | | expertopinion: "", |
| | | conclusionannex: "", |
| | | conclusionorder: this.form.ethicalreviewopinionsList.length + 1, |
| | | conclusiontime: "", |
| | | startTime: "", |
| | | endTime: "", |
| | | sendType: "", |
| | | createBy: this.currentUser.username || "admin", |
| | | createTime: new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19), |
| | | updateBy: this.currentUser.username || "admin", |
| | | updateTime: new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19), |
| | | delFlag: "0" |
| | | }; |
| | | |
| | | // 使用Vue.set确保响应式 |
| | | this.form.ethicalreviewopinionsList.push(expertReview); |
| | | }); |
| | | |
| | | // 触发计算属性更新 |
| | | this.$forceUpdate(); |
| | | |
| | | console.log( |
| | | "添加专家后,当前专家列表:", |
| | | this.form.ethicalreviewopinionsList |
| | | ); |
| | | console.log("普通专家数量:", this.normalExpertsCount); |
| | | console.log("主委专家数量:", this.chiefExpertsCount); |
| | | console.log("可发送普通专家:", this.availableNormalExperts); |
| | | |
| | | this.$message.success(`成功添加 ${newExperts.length} 位专家`); |
| | | this.expertDialogVisible = false; |
| | | this.selectedExperts = []; |
| | | }, |
| | | |
| | | // 添加专家对话框关闭 |
| | | handleExpertDialogClose() { |
| | | this.selectedExperts = []; |
| | | this.expertSearchQuery = ""; |
| | | this.filterExpertType = ""; |
| | | this.expertPage.pageNum = 1; |
| | | }, |
| | | |
| | | // 页码变化 |
| | | handlePageChange(pageNum) { |
| | | this.expertPage.pageNum = pageNum; |
| | | this.loadExperts(); |
| | | }, |
| | | |
| | | // 每页条数变化 |
| | | handlePageSizeChange(pageSize) { |
| | | this.expertPage.pageSize = pageSize; |
| | | this.expertPage.pageNum = 1; |
| | | this.loadExperts(); |
| | | }, |
| | | |
| | | // 发送给普通专家 |
| | | handleSendToNormalExperts() { |
| | | this.sendForm.expertIds = this.availableNormalExperts.map(expert => |
| | | this.getExpertKey(expert) |
| | | ); |
| | | this.sendForm.expertType = "normal"; |
| | | this.sendForm.endTime = ""; // 重置截止时间 |
| | | this.sendDialogVisible = true; |
| | | }, |
| | | |
| | | // 发送给主委专家 |
| | | handleSendToChiefExpert() { |
| | | const chiefExpert = this.expertReviews.find( |
| | | expert => expert.isChief && expert.reviewStatus === "applying" |
| | | this.sendForm.expertIds = this.availableChiefExperts.map(expert => |
| | | this.getExpertKey(expert) |
| | | ); |
| | | if (chiefExpert) { |
| | | this.sendForm.expertIds = [chiefExpert.id]; |
| | | this.sendForm.expertType = "chief"; |
| | | this.sendDialogVisible = true; |
| | | } |
| | | this.sendForm.expertType = "chief"; |
| | | this.sendForm.endTime = ""; // 主委专家无需截止时间 |
| | | this.sendDialogVisible = true; |
| | | }, |
| | | |
| | | // 批量发送 |
| | | handleBatchSend() { |
| | | const applyingExperts = this.expertReviews.filter( |
| | | expert => expert.reviewStatus === "applying" |
| | | const allAvailableExperts = [ |
| | | ...this.availableNormalExperts, |
| | | ...this.availableChiefExperts |
| | | ]; |
| | | this.sendForm.expertIds = allAvailableExperts.map(expert => |
| | | this.getExpertKey(expert) |
| | | ); |
| | | this.sendForm.expertIds = applyingExperts.map(expert => expert.id); |
| | | this.sendForm.expertType = "batch"; |
| | | this.sendForm.endTime = ""; // 重置截止时间 |
| | | this.sendDialogVisible = true; |
| | | }, |
| | | |
| | | // 发送给单个专家 |
| | | handleSendToExpert(expert) { |
| | | this.sendForm.expertIds = [expert.id]; |
| | | this.sendForm.expertType = expert.isChief ? "chief" : "normal"; |
| | | this.sendForm.expertIds = [this.getExpertKey(expert)]; |
| | | this.sendForm.expertType = expert.expertType === "1" ? "chief" : "normal"; |
| | | this.sendForm.endTime = expert.expertType === "1" ? "" : ""; // 主委专家无需截止时间 |
| | | this.sendDialogVisible = true; |
| | | }, |
| | | |
| | | // 确认发送 |
| | | handleSendConfirm() { |
| | | async handleSendConfirm() { |
| | | if (!this.sendForm.startTime) { |
| | | this.$message.warning("请选择发送时间"); |
| | | return; |
| | | } |
| | | |
| | | // 普通专家需要截止时间,主委专家不需要 |
| | | if (this.sendForm.expertType !== "chief" && !this.sendForm.endTime) { |
| | | this.$message.warning("请选择截止时间"); |
| | | return; |
| | | } |
| | | |
| | | if (!this.sendForm.sendType) { |
| | | this.$message.warning("请选择发送方式"); |
| | | return; |
| | | } |
| | | |
| | | if (this.sendForm.expertIds.length === 0) { |
| | | this.$message.warning("请选择要发送的专家"); |
| | | return; |
| | | } |
| | | |
| | | // 模拟发送 |
| | | this.$message.success("发送成功"); |
| | | this.sendDialogVisible = false; |
| | | this.sending = true; |
| | | try { |
| | | // 模拟发送过程 |
| | | await new Promise(resolve => setTimeout(resolve, 1000)); |
| | | |
| | | // 更新专家状态 |
| | | this.sendForm.expertIds.forEach(expertId => { |
| | | const index = this.expertReviews.findIndex( |
| | | expert => expert.id === expertId |
| | | ); |
| | | if (index !== -1) { |
| | | this.expertReviews[index].reviewStatus = "submitted"; |
| | | this.expertReviews[index].sendTime = new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19); |
| | | } |
| | | }); |
| | | // 更新专家状态 |
| | | this.sendForm.expertIds.forEach(expertKey => { |
| | | const index = this.form.ethicalreviewopinionsList.findIndex( |
| | | expert => this.getExpertKey(expert) === expertKey |
| | | ); |
| | | if (index !== -1) { |
| | | this.form.ethicalreviewopinionsList[index].receiveStatus = "2"; // 已接收 |
| | | this.form.ethicalreviewopinionsList[ |
| | | index |
| | | ].startTime = this.sendForm.startTime; |
| | | this.form.ethicalreviewopinionsList[ |
| | | index |
| | | ].endTime = this.sendForm.endTime; |
| | | this.form.ethicalreviewopinionsList[ |
| | | index |
| | | ].sendType = this.sendForm.sendType; |
| | | this.form.ethicalreviewopinionsList[index].updateTime = new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19); |
| | | |
| | | this.sendForm = { |
| | | expertType: "normal", |
| | | expertIds: [], |
| | | content: "" |
| | | }; |
| | | // 使用Vue.set确保响应式 |
| | | this.$set( |
| | | this.form.ethicalreviewopinionsList, |
| | | index, |
| | | this.form.ethicalreviewopinionsList[index] |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | this.$message.success("发送成功"); |
| | | this.sendDialogVisible = false; |
| | | this.sendForm = { |
| | | expertType: "normal", |
| | | expertIds: [], |
| | | startTime: "", |
| | | endTime: "", |
| | | sendType: "0", |
| | | content: "" |
| | | }; |
| | | |
| | | // 触发计算属性更新 |
| | | this.$forceUpdate(); |
| | | } catch (error) { |
| | | console.error("发送失败:", error); |
| | | this.$message.error("发送失败,请重试"); |
| | | } finally { |
| | | this.sending = false; |
| | | } |
| | | }, |
| | | |
| | | // 编辑专家审查 |
| | |
| | | this.$prompt("请输入审查意见", "编辑专家审查", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | inputValue: expert.expertOpinion || "", |
| | | inputValue: expert.expertopinion || "", |
| | | inputPlaceholder: "请输入审查意见", |
| | | inputValidator: value => { |
| | | if (!value || value.trim() === "") { |
| | | return "审查意见不能为空"; |
| | |
| | | } |
| | | }) |
| | | .then(({ value }) => { |
| | | // 模拟更新专家审查 |
| | | const index = this.expertReviews.findIndex(e => e.id === expert.id); |
| | | const index = this.form.ethicalreviewopinionsList.findIndex( |
| | | e => e.id === expert.id || e.expertNo === expert.expertNo |
| | | ); |
| | | if (index !== -1) { |
| | | this.expertReviews[index].expertOpinion = value; |
| | | this.form.ethicalreviewopinionsList[index].expertopinion = value; |
| | | this.form.ethicalreviewopinionsList[index].updateTime = new Date() |
| | | .toISOString() |
| | | .replace("T", " ") |
| | | .substring(0, 19); |
| | | |
| | | // 使用Vue.set确保响应式 |
| | | this.$set( |
| | | this.form.ethicalreviewopinionsList, |
| | | index, |
| | | this.form.ethicalreviewopinionsList[index] |
| | | ); |
| | | |
| | | this.$message.success("审查意见已更新"); |
| | | } |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | |
| | | // 查看专家历史审批情况 |
| | | async handleViewExpertHistory(expert) { |
| | | console.log(12); |
| | | |
| | | if (!expert.expertNo) { |
| | | this.$message.warning("该专家没有编号,无法查询历史审批情况"); |
| | | return; |
| | | } |
| | | |
| | | this.currentExpertInfo = expert; |
| | | this.expertHistoryLoading = true; |
| | | this.expertHistoryDialogVisible = true; |
| | | |
| | | try { |
| | | const params = { |
| | | expertNo: expert.expertNo |
| | | }; |
| | | console.log(11); |
| | | |
| | | const response = await ethicalreExpertTotal(params); |
| | | console.log(response); |
| | | |
| | | if (response) { |
| | | this.expertHistoryData = response[0]; |
| | | } else { |
| | | this.$message.error( |
| | | "查询专家历史审批情况失败:" + (response.msg || "未知错误") |
| | | ); |
| | | this.expertHistoryData = null; |
| | | } |
| | | } catch (error) { |
| | | console.error("查询专家历史审批情况失败:", error); |
| | | this.$message.error("查询专家历史审批情况失败"); |
| | | this.expertHistoryData = null; |
| | | } finally { |
| | | this.expertHistoryLoading = false; |
| | | } |
| | | }, |
| | | |
| | | // 查看专家审查详情 |
| | | handleViewExpertReview(expert) { |
| | | this.$alert( |
| | | ` |
| | | <div> |
| | | <p><strong>专家姓名:</strong>${expert.expertName}</p> |
| | | <p><strong>专家类型:</strong>${ |
| | | expert.isChief ? "主委专家" : "专家" |
| | | }</p> |
| | | <p><strong>审查状态:</strong>${this.statusTextFilter( |
| | | expert.reviewStatus |
| | | <div style="line-height: 1.6;"> |
| | | <p><strong>专家姓名:</strong>${expert.expertname}</p> |
| | | <p><strong>专家编号:</strong>${expert.expertNo || "-"}</p> |
| | | <p><strong>专家类型:</strong>${this.getExpertTypeText( |
| | | expert.expertType |
| | | )}</p> |
| | | <p><strong>科室名称:</strong>${expert.deptName || "-"}</p> |
| | | <p><strong>职称:</strong>${expert.title || "-"}</p> |
| | | <p><strong>联系电话:</strong>${expert.telephone || "-"}</p> |
| | | <p><strong>审查状态:</strong>${this.getReviewStatusText( |
| | | expert.receiveStatus |
| | | )}</p> |
| | | <p><strong>专家结论:</strong>${ |
| | | expert.expertConclusion |
| | | ? this.conclusionTextFilter(expert.expertConclusion) |
| | | expert.expertconclusion |
| | | ? this.getConclusionText(expert.expertconclusion) |
| | | : "未提交" |
| | | }</p> |
| | | <p><strong>审查意见:</strong>${expert.expertOpinion || "无"}</p> |
| | | <p><strong>审查时间:</strong>${expert.reviewTime || "未审查"}</p> |
| | | <p><strong>审查意见:</strong>${expert.expertopinion || "无"}</p> |
| | | <p><strong>结论顺序:</strong>${expert.conclusionorder || "-"}</p> |
| | | <p><strong>审查时间:</strong>${expert.conclusiontime || "未审查"}</p> |
| | | <p><strong>发送时间:</strong>${expert.startTime || "未发送"}</p> |
| | | <p><strong>截止时间:</strong>${expert.endTime || "-"}</p> |
| | | <p><strong>发送方式:</strong>${ |
| | | expert.sendType |
| | | ? expert.sendType === "0" |
| | | ? "系统发送" |
| | | : expert.sendType === "1" |
| | | ? "邮件发送" |
| | | : expert.sendType === "2" |
| | | ? "短信发送" |
| | | : "其他方式" |
| | | : "-" |
| | | }</p> |
| | | </div> |
| | | `, |
| | | "专家审查详情", |
| | | { |
| | | dangerouslyUseHTMLString: true, |
| | | customClass: "expert-review-detail-dialog" |
| | | customClass: "expert-review-detail-dialog", |
| | | showConfirmButton: false, |
| | | showCancelButton: true, |
| | | cancelButtonText: "关闭" |
| | | } |
| | | ); |
| | | }, |
| | | |
| | | // 上传附件相关方法 |
| | | handleUploadAttachment() { |
| | | this.uploadDialogVisible = true; |
| | | }, |
| | | // 删除专家审查 |
| | | handleDeleteExpertReview(index) { |
| | | this.$confirm("确定要删除该专家的审查记录吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }) |
| | | .then(() => { |
| | | this.form.ethicalreviewopinionsList.splice(index, 1); |
| | | |
| | | // 上传前校验 |
| | | beforeUpload(file) { |
| | | const allowedTypes = [ |
| | | "application/pdf", |
| | | "image/jpeg", |
| | | "image/png", |
| | | "application/msword", |
| | | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", |
| | | "application/vnd.ms-excel", |
| | | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| | | ]; |
| | | // 触发计算属性更新 |
| | | this.$forceUpdate(); |
| | | |
| | | const maxSize = 10 * 1024 * 1024; |
| | | |
| | | const isTypeOk = |
| | | allowedTypes.includes(file.type) || |
| | | file.name.endsWith(".pdf") || |
| | | file.name.endsWith(".jpg") || |
| | | file.name.endsWith(".jpeg") || |
| | | file.name.endsWith(".png") || |
| | | file.name.endsWith(".doc") || |
| | | file.name.endsWith(".docx") || |
| | | file.name.endsWith(".xls") || |
| | | file.name.endsWith(".xlsx"); |
| | | |
| | | if (!isTypeOk) { |
| | | this.$message.error("文件格式不支持"); |
| | | return false; |
| | | } |
| | | |
| | | if (file.size > maxSize) { |
| | | this.$message.error("文件大小不能超过10MB"); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }, |
| | | |
| | | // 文件选择变化 |
| | | handleFileChange(file, fileList) { |
| | | this.tempFileList = fileList; |
| | | }, |
| | | |
| | | // 移除临时文件 |
| | | handleTempRemove(file, fileList) { |
| | | this.tempFileList = fileList; |
| | | }, |
| | | |
| | | // 上传成功处理 |
| | | handleUploadSuccess(response, file, fileList) { |
| | | if (response.code === 200) { |
| | | this.$message.success("文件上传成功"); |
| | | this.uploadDialogVisible = false; |
| | | this.tempFileList = []; |
| | | } else { |
| | | this.$message.error(response.msg || "文件上传失败"); |
| | | } |
| | | }, |
| | | |
| | | // 提交上传 |
| | | async submitUpload() { |
| | | if (this.tempFileList.length === 0) { |
| | | this.$message.warning("请先选择要上传的文件"); |
| | | return; |
| | | } |
| | | this.$refs.uploadRef.submit(); |
| | | this.uploadLoading = true; |
| | | }, |
| | | |
| | | // 预览附件 |
| | | handlePreviewAttachment(attachment) { |
| | | if (attachment.fileName.endsWith(".pdf")) { |
| | | window.open(attachment.fileUrl, "_blank"); |
| | | } else if (attachment.fileName.match(/\.(jpg|jpeg|png)$/i)) { |
| | | this.$alert( |
| | | `<img src="${attachment.fileUrl}" style="max-width: 100%;" alt="${attachment.fileName}">`, |
| | | "图片预览", |
| | | { |
| | | dangerouslyUseHTMLString: true, |
| | | customClass: "image-preview-dialog" |
| | | } |
| | | ); |
| | | } else { |
| | | this.$message.info("该文件类型暂不支持在线预览,请下载后查看"); |
| | | } |
| | | }, |
| | | |
| | | // 下载附件 |
| | | handleDownloadAttachment(attachment) { |
| | | const link = document.createElement("a"); |
| | | link.href = attachment.fileUrl; |
| | | link.download = attachment.fileName; |
| | | link.click(); |
| | | this.$message.success(`开始下载: ${attachment.fileName}`); |
| | | }, |
| | | |
| | | // 获取文件类型 |
| | | getFileType(fileName) { |
| | | const ext = fileName |
| | | .split(".") |
| | | .pop() |
| | | .toLowerCase(); |
| | | const typeMap = { |
| | | pdf: "PDF", |
| | | doc: "DOC", |
| | | docx: "DOCX", |
| | | xls: "XLS", |
| | | xlsx: "XLSX", |
| | | jpg: "JPG", |
| | | jpeg: "JPEG", |
| | | png: "PNG" |
| | | }; |
| | | return typeMap[ext] || ext.toUpperCase(); |
| | | }, |
| | | |
| | | // 文件大小格式化 |
| | | formatFileSize(size) { |
| | | if (size === 0) return "0 B"; |
| | | const k = 1024; |
| | | const sizes = ["B", "KB", "MB", "GB"]; |
| | | const i = Math.floor(Math.log(size) / Math.log(k)); |
| | | return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; |
| | | this.$message.success("专家审查记录已删除"); |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | |
| | | // 时间格式化 |
| | |
| | | color: #67c23a !important; |
| | | } |
| | | |
| | | /* 专家姓名链接样式 */ |
| | | .expert-name-link { |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | text-decoration: none; |
| | | transition: color 0.3s; |
| | | } |
| | | |
| | | .expert-name-link:hover { |
| | | color: #66b1ff; |
| | | text-decoration: underline; |
| | | } |
| | | |
| | | /* 历史统计样式 */ |
| | | .history-stat-item { |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | background-color: #f5f7fa; |
| | | text-align: center; |
| | | } |
| | | |
| | | .history-stat-label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .history-stat-value { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | padding: 20px 0 0; |
| | | } |
| | | |
| | | .attachment-section { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .attachment-header { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 16px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .attachment-title { |
| | | font-weight: bold; |
| | | margin: 0 8px; |
| | | } |
| | | |
| | | .attachment-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .attachment-list { |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | .list-title { |
| | | font-weight: bold; |
| | | margin-bottom: 12px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | /* 案例信息展示样式 */ |
| | | .selected-case-info { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .case-info-card { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 768px) { |
| | | .ethics-review-detail { |