| | |
| | | <!-- 统计卡片 --> |
| | | <view class="stats-card"> |
| | | <view class="stat-item"> |
| | | <text class="count">{{ stats.totalReviews }}</text> |
| | | <text class="count">{{ stats.count }}</text> |
| | | <text class="label">总审查量</text> |
| | | </view> |
| | | <view class="divider"></view> |
| | | <view class="stat-item"> |
| | | <text class="count">{{ stats.approvedReviews }}</text> |
| | | <text class="count">{{ stats.throughCount }}</text> |
| | | <text class="label">审查通过</text> |
| | | </view> |
| | | <view class="divider"></view> |
| | | <view class="stat-item"> |
| | | <text class="count">{{ stats.rejectedReviews }}</text> |
| | | <text class="count">{{ stats.rejectCount }}</text> |
| | | <text class="label">审查驳回</text> |
| | | </view> |
| | | <view class="divider"></view> |
| | | <view class="stat-item"> |
| | | <text class="count">{{ stats.abandonedReviews }}</text> |
| | | <text class="count">{{ stats.waiveCount }}</text> |
| | | <text class="label">已放弃</text> |
| | | </view> |
| | | </view> |
| | |
| | | <text |
| | | v-for="status in statusOptions" |
| | | :key="status.value" |
| | | :class="{ active: currentStatus === status.value }" |
| | | :class="{ active: currentStatus == status.value }" |
| | | @tap="selectStatus(status.value)" |
| | | > |
| | | {{ status.label }} |
| | |
| | | @scrolltolower="onLoadMore" |
| | | > |
| | | <view |
| | | v-for="(review, index) in filteredReviews" |
| | | :key="review.id" |
| | | v-for="(review, index) in reviewList" |
| | | :key="review.id || index" |
| | | class="review-item card" |
| | | @tap="viewDetail(review)" |
| | | > |
| | |
| | | <u-icon name="order" size="16" color="#fff" /> |
| | | </view> |
| | | <view class="info-content"> |
| | | <text class="donor-name">{{ review.donorName }}</text> |
| | | <text class="hospital-no">{{ review.hospitalNo }}</text> |
| | | <text class="expert-type" v-if="review.expertType">{{ review.expertType }}</text> |
| | | <text class="donor-name">{{ review.name || "未填写姓名" }}</text> |
| | | <text class="hospital-no">{{ |
| | | review.inpatientno || "无住院号" |
| | | }}</text> |
| | | <text class="expert-type" v-if="review.expertname"> |
| | | 专家: {{ review.expertname }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view class="status-tag" :class="review.status"> |
| | | {{ getStatusText(review.status) }} |
| | | <view class="status-tag" :class="getReviewStatusClass(review)"> |
| | | {{ getReviewStatusText(review) }} |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | <view class="info-row"> |
| | | <view class="info-col"> |
| | | <text class="info-label">性别/年龄</text> |
| | | <text class="info-value">{{ review.gender }}/{{ review.age }}岁</text> |
| | | <text class="info-value" |
| | | >{{ review.sex == 1 ? "男" : "女" }}/{{ |
| | | getAgeWithUnit(review) |
| | | }}</text |
| | | > |
| | | </view> |
| | | <view class="info-col"> |
| | | <text class="info-label">血型</text> |
| | | <text class="info-value">{{ review.bloodType }}</text> |
| | | <text class="info-value">{{ |
| | | getDictLabel("sys_BloodType", review.bloodtype) || "未知" |
| | | }}</text> |
| | | </view> |
| | | <view class="info-col"> |
| | | <text class="info-label">疾病诊断</text> |
| | | <text class="info-value">{{ review.diagnosis }}</text> |
| | | <text class="info-value">{{ |
| | | review.diagnosisname || "未填写" |
| | | }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 审查详情 --> |
| | | <view class="review-details"> |
| | | <view class="detail-item"> |
| | | <view class="detail-item" v-if="review.createTime"> |
| | | <u-icon name="clock" size="14" color="#909399" /> |
| | | <text class="detail-text">提交时间:{{ review.submitTime }}</text> |
| | | <text class="detail-text" |
| | | >创建时间:{{ formatDate(review.createTime) }}</text |
| | | > |
| | | </view> |
| | | <view class="detail-item" v-if="review.reviewTime"> |
| | | <view class="detail-item" v-if="review.conclusiontime"> |
| | | <u-icon name="checkmark-circle" size="14" color="#909399" /> |
| | | <text class="detail-text">审查时间:{{ review.reviewTime }}</text> |
| | | <text class="detail-text" |
| | | >审查时间:{{ formatDate(review.conclusiontime) }}</text |
| | | > |
| | | </view> |
| | | <view class="detail-item" v-if="review.reviewer"> |
| | | <view class="detail-item" v-if="review.expertname"> |
| | | <u-icon name="account" size="14" color="#909399" /> |
| | | <text class="detail-text">审查人:{{ review.reviewer }}</text> |
| | | <text class="detail-text">审查人:{{ review.expertname }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 审查结论 --> |
| | | <view class="conclusion-section" v-if="review.status !== 'abandoned'"> |
| | | <view |
| | | class="conclusion-section" |
| | | v-if="review.expertconclusion && review.expertconclusion !== 2" |
| | | > |
| | | <text class="conclusion-label">审查结论:</text> |
| | | <text class="conclusion-content">{{ review.conclusion || '暂无结论' }}</text> |
| | | <text class="conclusion-content">{{ |
| | | getConclusionText(review.expertconclusion) |
| | | }}</text> |
| | | </view> |
| | | |
| | | <!-- 放弃原因 --> |
| | | <view class="abandon-reason" v-if="review.status === 'abandoned'"> |
| | | <text class="reason-label">放弃原因:</text> |
| | | <text class="reason-content">{{ review.abandonReason || '用户主动放弃' }}</text> |
| | | <!-- 专家意见 --> |
| | | <view class="opinion-section" v-if="review.expertopinion"> |
| | | <text class="opinion-label">专家意见:</text> |
| | | <text class="opinion-content">{{ review.expertopinion }}</text> |
| | | </view> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="action-buttons"> |
| | | <button |
| | | class="action-btn detail-btn" |
| | | @tap.stop="viewDetail(review)" |
| | | > |
| | | <button class="action-btn detail-btn" @tap.stop="viewDetail(review)"> |
| | | <u-icon name="eye" size="14" color="#747CF9" /> |
| | | <text>查看详情</text> |
| | | </button> |
| | | |
| | | <button |
| | | v-if="review.status === 'approved'" |
| | | v-if="review.expertconclusion == 1" |
| | | class="action-btn download-btn" |
| | | @tap.stop="downloadReport(review)" |
| | | > |
| | |
| | | </button> |
| | | |
| | | <button |
| | | v-if="review.status === 'rejected'" |
| | | v-if="review.expertconclusion == 2" |
| | | class="action-btn appeal-btn" |
| | | @tap.stop="submitAppeal(review)" |
| | | > |
| | |
| | | </button> |
| | | |
| | | <button |
| | | v-if="review.status === 'abandoned'" |
| | | v-if="review.expertconclusion == 2 || review.expertconclusion == 3" |
| | | class="action-btn restart-btn" |
| | | @tap.stop="restartReview(review)" |
| | | > |
| | |
| | | </view> |
| | | |
| | | <!-- 加载状态 --> |
| | | <!-- <view class="load-more" v-if="hasMore"> |
| | | <u-loading size="24" color="#747CF9"></u-loading> |
| | | <text>加载更多...</text> |
| | | </view> --> |
| | | <u-loading-icon :show="hasMore" text="提交中..."></u-loading-icon> |
| | | |
| | | <u-loading-icon :show="loading" text="加载中..."></u-loading-icon> |
| | | |
| | | <!-- 空状态 --> |
| | | <view class="empty-state" v-if="!loading && filteredReviews.length === 0"> |
| | | <view class="empty-state" v-if="!loading && reviewList.length == 0"> |
| | | <view> {{ loading }}-{{ reviewList.length }} </view> |
| | | <u-icon name="file-remove" size="80" color="#C0C4CC" /> |
| | | <text class="empty-text">暂无审查记录</text> |
| | | <text class="empty-desc">当前筛选条件下没有找到相关记录</text> |
| | |
| | | <text>重置筛选条件</text> |
| | | </button> |
| | | </view> |
| | | |
| | | <!-- 加载完成提示 --> |
| | | <view class="load-complete" v-if="!hasMore && reviewList.length > 0"> |
| | | <text>已加载全部数据</text> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from 'vue' |
| | | import { onLoad, onShow } from '@dcloudio/uni-app' |
| | | import { ref, computed, onMounted, watch } from "vue"; |
| | | import { |
| | | onLoad, |
| | | onShow, |
| | | onPullDownRefresh, |
| | | onReachBottom, |
| | | } from "@dcloudio/uni-app"; |
| | | import { useDict } from "@/utils/dict"; |
| | | |
| | | // 响应式数据 |
| | | const loading = ref(false) |
| | | const refreshing = ref(false) |
| | | const hasMore = ref(true) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const loading = ref(false); |
| | | const refreshing = ref(false); |
| | | const hasMore = ref(true); |
| | | const pageNum = ref(1); |
| | | const pageSize = ref(10); |
| | | const dict = ref({}); |
| | | |
| | | // 筛选条件 |
| | | const currentStatus = ref('all') |
| | | const searchKeyword = ref('') |
| | | const currentStatus = ref("all"); |
| | | const searchKeyword = ref(""); |
| | | |
| | | // 统计数据 |
| | | const stats = ref({ |
| | | totalReviews: 0, |
| | | approvedReviews: 0, |
| | | rejectedReviews: 0, |
| | | abandonedReviews: 0 |
| | | }) |
| | | count: 0, |
| | | throughCount: 0, |
| | | rejectCount: 0, |
| | | waiveCount: 0, |
| | | }); |
| | | |
| | | // 状态选项 - 根据您的要求设置 |
| | | // 审查列表数据 |
| | | const reviewList = ref([]); |
| | | const total = ref(0); |
| | | |
| | | // 状态选项 |
| | | const statusOptions = ref([ |
| | | { label: '全部', value: 'all' }, |
| | | { label: '审查通过', value: 'approved' }, |
| | | { label: '审查驳回', value: 'rejected' }, |
| | | { label: '放弃', value: 'abandoned' } |
| | | ]) |
| | | { label: "全部", value: "all" }, |
| | | { label: "待审查", value: "pending" }, |
| | | { label: "审查通过", value: "approved" }, |
| | | { label: "审查驳回", value: "rejected" }, |
| | | { label: "已放弃", value: "abandoned" }, |
| | | ]); |
| | | |
| | | // 模拟数据 |
| | | const reviews = ref([ |
| | | { |
| | | id: 1, |
| | | hospitalNo: 'D230415', |
| | | donorName: '张某某', |
| | | gender: '男', |
| | | age: 45, |
| | | bloodType: 'A型', |
| | | diagnosis: '终末期肝病', |
| | | status: 'approved', |
| | | expertType: '主委专家', |
| | | submitTime: '2025-12-01 10:30', |
| | | reviewTime: '2025-12-02 14:20', |
| | | reviewer: '孔心涓', |
| | | conclusion: '符合伦理要求,同意开展器官捐献工作' |
| | | }, |
| | | { |
| | | id: 2, |
| | | hospitalNo: 'D230416', |
| | | donorName: '李某某', |
| | | gender: '女', |
| | | age: 38, |
| | | bloodType: 'O型', |
| | | diagnosis: '终末期肾病', |
| | | status: 'rejected', |
| | | expertType: '专家', |
| | | submitTime: '2025-12-01 14:20', |
| | | reviewTime: '2025-12-03 09:15', |
| | | reviewer: '陶昊', |
| | | conclusion: '风险评估不足,需要补充材料后重新审查' |
| | | }, |
| | | { |
| | | id: 3, |
| | | hospitalNo: 'D230417', |
| | | donorName: '王某某', |
| | | gender: '男', |
| | | age: 52, |
| | | bloodType: 'B型', |
| | | diagnosis: '终末期心脏病', |
| | | status: 'abandoned', |
| | | expertType: '专家', |
| | | submitTime: '2025-11-30 16:45', |
| | | abandonReason: '家属要求停止审查流程', |
| | | reviewer: '刘斌' |
| | | }, |
| | | { |
| | | id: 4, |
| | | hospitalNo: 'D230418', |
| | | donorName: '赵某某', |
| | | gender: '女', |
| | | age: 29, |
| | | bloodType: 'AB型', |
| | | diagnosis: '急性肝功能衰竭', |
| | | status: 'approved', |
| | | expertType: '主委专家', |
| | | submitTime: '2025-12-02 08:15', |
| | | reviewTime: '2025-12-03 16:30', |
| | | reviewer: '孔心涓', |
| | | conclusion: '紧急情况处理得当,同意立即开展捐献程序' |
| | | } |
| | | ]) |
| | | // 字典映射 |
| | | const statusDict = { |
| | | pending: "待审查", |
| | | approved: "审查通过", |
| | | rejected: "审查驳回", |
| | | abandoned: "已放弃", |
| | | }; |
| | | |
| | | // 计算属性 |
| | | const filteredReviews = computed(() => { |
| | | let result = reviews.value |
| | | // 结论映射 |
| | | const conclusionDict = { |
| | | 0: "未审核", |
| | | 1: "审查通过", |
| | | 2: "审查驳回", |
| | | 3: "放弃", |
| | | 4: "修改后同意", |
| | | }; |
| | | |
| | | // 状态筛选 |
| | | if (currentStatus.value !== 'all') { |
| | | result = result.filter(review => review.status === currentStatus.value) |
| | | } |
| | | // 获取字典标签 |
| | | const getDictLabel = (dictType, dictValue) => { |
| | | if (!dict.value[dictType] || !dictValue) return ""; |
| | | const dictItem = dict.value[dictType].find( |
| | | (item) => item.dictValue == String(dictValue), |
| | | ); |
| | | return dictItem ? dictItem.dictLabel : dictValue; |
| | | }; |
| | | |
| | | // 关键词搜索 |
| | | if (searchKeyword.value) { |
| | | const keyword = searchKeyword.value.toLowerCase() |
| | | result = result.filter(review => |
| | | review.donorName.toLowerCase().includes(keyword) || |
| | | review.hospitalNo.toLowerCase().includes(keyword) || |
| | | review.diagnosis.toLowerCase().includes(keyword) || |
| | | (review.reviewer && review.reviewer.toLowerCase().includes(keyword)) |
| | | ) |
| | | } |
| | | // 获取年龄和单位 |
| | | const getAgeWithUnit = (review) => { |
| | | if (!review.age) return "未知"; |
| | | const unit = getDictLabel("sys_AgeUnit", review.ageunit) || "岁"; |
| | | return `${review.age}${unit}`; |
| | | }; |
| | | |
| | | return result |
| | | }) |
| | | // 格式化日期 |
| | | const formatDate = (dateString) => { |
| | | if (!dateString) return ""; |
| | | const date = new Date(dateString); |
| | | return `${date.getFullYear()}-${(date.getMonth() + 1) |
| | | .toString() |
| | | .padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")} ${date |
| | | .getHours() |
| | | .toString() |
| | | .padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; |
| | | }; |
| | | |
| | | // 获取审查状态文本 |
| | | const getReviewStatusText = (review) => { |
| | | if (review.receiveStatus == 0) return "待推送"; |
| | | if (review.receiveStatus == 1) return "未阅读"; |
| | | if (review.receiveStatus == 2) return "已阅读"; |
| | | if (review.receiveStatus == 3) return "超时"; |
| | | if (review.receiveStatus == 4) return "中止"; |
| | | if (review.receiveStatus == 5) return "完成"; |
| | | if (!review.receiveStatus && review.receiveStatus !== 0) return "待推送"; |
| | | return "待推送"; |
| | | }; |
| | | |
| | | // 获取审查状态类名 |
| | | const getReviewStatusClass = (review) => { |
| | | if (review.receiveStatus == 0) return "abandoned"; |
| | | if (review.receiveStatus == 1) return "abandoned"; |
| | | if (review.receiveStatus == 2) return "pending"; |
| | | if (review.receiveStatus == 3) return "unknown"; |
| | | if (review.receiveStatus == 4) return "unknown"; |
| | | if (review.receiveStatus == 5) return "approved"; |
| | | if (!review.receiveStatus && review.receiveStatus !== 0) return "pending"; |
| | | return "unknown"; |
| | | }; |
| | | |
| | | // 获取结论文本 |
| | | const getConclusionText = (conclusion) => { |
| | | return conclusionDict[conclusion] || "未知结论"; |
| | | }; |
| | | |
| | | // 方法 |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | approved: '审查通过', |
| | | rejected: '审查驳回', |
| | | abandoned: '已放弃' |
| | | } |
| | | return statusMap[status] || '未知状态' |
| | | } |
| | | |
| | | const selectStatus = (status) => { |
| | | currentStatus.value = status |
| | | } |
| | | currentStatus.value = status; |
| | | resetAndLoad(); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索关键词:', searchKeyword.value) |
| | | } |
| | | resetAndLoad(); |
| | | }; |
| | | |
| | | const resetFilters = () => { |
| | | currentStatus.value = 'all' |
| | | searchKeyword.value = '' |
| | | currentStatus.value = "all"; |
| | | searchKeyword.value = ""; |
| | | resetAndLoad(); |
| | | }; |
| | | |
| | | // 构建查询参数 |
| | | const buildQueryParams = () => { |
| | | const params = { |
| | | pageNum: pageNum.value, |
| | | pageSize: pageSize.value, |
| | | // receiveStatus: "1,2,3,4,5", |
| | | }; |
| | | |
| | | // 添加搜索关键词 |
| | | if (searchKeyword.value) { |
| | | params.name = searchKeyword.value; |
| | | } |
| | | |
| | | // 添加状态筛选 |
| | | if (currentStatus.value !== "all") { |
| | | switch (currentStatus.value) { |
| | | case "pending": |
| | | // 待审查:expertconclusion为空 |
| | | params.expertconclusion = null; |
| | | break; |
| | | case "approved": |
| | | params.expertconclusion = 1; // 同意 |
| | | break; |
| | | case "rejected": |
| | | params.expertconclusion = 2; // 不同意 |
| | | break; |
| | | case "abandoned": |
| | | params.expertconclusion = 3; // 放弃 |
| | | break; |
| | | } |
| | | } |
| | | |
| | | return params; |
| | | }; |
| | | |
| | | // 重置并加载 |
| | | const resetAndLoad = () => { |
| | | pageNum.value = 1; |
| | | hasMore.value = true; |
| | | loadCaseData(); |
| | | }; |
| | | |
| | | // 下拉刷新 |
| | | const onRefresh = async () => { |
| | | refreshing.value = true |
| | | setTimeout(() => { |
| | | refreshing.value = false |
| | | loadInitialData() |
| | | }, 1000) |
| | | } |
| | | refreshing.value = true; |
| | | await resetAndLoad(); |
| | | refreshing.value = false; |
| | | }; |
| | | |
| | | // 上拉加载更多 |
| | | const onLoadMore = async () => { |
| | | if (!hasMore.value || loading.value) return |
| | | loading.value = true |
| | | setTimeout(() => { |
| | | loading.value = false |
| | | }, 500) |
| | | } |
| | | if (!hasMore.value || loading.value) return; |
| | | pageNum.value++; |
| | | await loadCaseData(true); |
| | | }; |
| | | |
| | | // 统计 |
| | | const stateTotal = async () => { |
| | | const resTotal = await uni.$uapi.get( |
| | | `/project/ethicalreviewopinions/stateTotal`, |
| | | ); |
| | | if (resTotal.code == 200) |
| | | // 更新统计数据 |
| | | stats.value = resTotal.data[0]; |
| | | }; |
| | | // 加载案例数据 |
| | | const loadCaseData = async (isLoadMore = false) => { |
| | | if (loading.value) return; |
| | | |
| | | loading.value = true; |
| | | |
| | | try { |
| | | const params = buildQueryParams(); |
| | | |
| | | const res = await uni.$uapi.get( |
| | | `/project/ethicalreviewopinions/listnew`, |
| | | params, |
| | | ); |
| | | |
| | | console.log(res, "11"); |
| | | |
| | | if (res.code == 200) { |
| | | const list = res.rows || []; |
| | | const totalCount = res.total || 0; |
| | | |
| | | if (isLoadMore) { |
| | | reviewList.value = [...reviewList.value, ...list]; |
| | | } else { |
| | | reviewList.value = list; |
| | | } |
| | | console.log(reviewList.value, "reviewList.value"); |
| | | |
| | | total.value = totalCount; |
| | | hasMore.value = reviewList.value.length < totalCount; |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || "加载失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("加载案例数据失败:", error); |
| | | uni.showToast({ |
| | | title: "数据加载失败,请重试", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | loading.value = false; |
| | | uni.stopPullDownRefresh(); |
| | | } |
| | | }; |
| | | |
| | | // 查看详情 |
| | | const viewDetail = (review) => { |
| | | uni.navigateTo({ |
| | | url: `/pages/ethicalReview/ethicalInfo?id=${review.id}&status=${review.status}` |
| | | }) |
| | | } |
| | | url: `/pages/ethicalReview/ethicalInfo?fcid=${ |
| | | review.fcid |
| | | }&type=review&status=${review.expertconclusion || "pending"}&id=${ |
| | | review.id |
| | | }`, |
| | | }); |
| | | }; |
| | | |
| | | const downloadReport = (review) => { |
| | | // 下载报告 |
| | | const downloadReport = async (review) => { |
| | | if (!review.conclusionannex) { |
| | | uni.showToast({ |
| | | title: '开始下载审查报告', |
| | | icon: 'success' |
| | | }) |
| | | title: "暂无报告可下载", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | const annexes = review.conclusionannex |
| | | .split(";") |
| | | .filter((item) => item.trim()); |
| | | |
| | | if (annexes.length == 0) { |
| | | uni.showToast({ |
| | | title: "暂无报告可下载", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.showLoading({ |
| | | title: "下载中...", |
| | | mask: true, |
| | | }); |
| | | |
| | | // 下载第一个附件 |
| | | const fileUrl = annexes[0]; |
| | | const downloadTask = uni.downloadFile({ |
| | | url: fileUrl, |
| | | success: (downloadRes) => { |
| | | if (downloadRes.statusCode == 200) { |
| | | const tempFilePath = downloadRes.tempFilePath; |
| | | |
| | | // 保存到本地 |
| | | uni.saveFile({ |
| | | tempFilePath: tempFilePath, |
| | | success: (saveRes) => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "下载成功", |
| | | icon: "success", |
| | | duration: 2000, |
| | | }); |
| | | |
| | | // 在微信小程序中可以打开文件 |
| | | if (uni.getSystemInfoSync().platform == "weixin") { |
| | | uni.openDocument({ |
| | | filePath: saveRes.savedFilePath, |
| | | showMenu: true, |
| | | success: () => { |
| | | console.log("打开文档成功"); |
| | | }, |
| | | fail: (err) => { |
| | | console.error("打开文档失败", err); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | fail: (saveErr) => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "保存文件失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | }, |
| | | }); |
| | | } else { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "下载失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "下载失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | console.error("下载文件失败:", err); |
| | | }, |
| | | }); |
| | | |
| | | // 监听下载进度 |
| | | downloadTask.onProgressUpdate((res) => { |
| | | console.log("下载进度:", res.progress); |
| | | }); |
| | | } catch (error) { |
| | | uni.hideLoading(); |
| | | console.error("下载报告失败:", error); |
| | | uni.showToast({ |
| | | title: "下载失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 提起申诉 |
| | | const submitAppeal = (review) => { |
| | | uni.navigateTo({ |
| | | url: `/pages/ethics/appeal?id=${review.id}` |
| | | }) |
| | | } |
| | | url: `/pages/ethics/appeal?id=${review.fcid || review.id}&caseNo=${ |
| | | review.caseNo || "" |
| | | }&name=${review.name || ""}`, |
| | | }); |
| | | }; |
| | | |
| | | const restartReview = (review) => { |
| | | // 重新开始 |
| | | const restartReview = async (review) => { |
| | | uni.showModal({ |
| | | title: '重新开始审查', |
| | | content: '确定要重新开始这个审查流程吗?', |
| | | success: (res) => { |
| | | title: "重新开始审查", |
| | | content: "确定要重新开始这个审查流程吗?", |
| | | success: async (res) => { |
| | | if (res.confirm) { |
| | | try { |
| | | uni.showLoading({ |
| | | title: "处理中...", |
| | | mask: true, |
| | | }); |
| | | |
| | | const response = await uni.$uapi.put( |
| | | `/project/ethicalreviewopinions/reset/${review.fcid || review.id}`, |
| | | ); |
| | | |
| | | uni.hideLoading(); |
| | | |
| | | if (response.code == 200) { |
| | | uni.showToast({ |
| | | title: '审查已重新开始', |
| | | icon: 'success' |
| | | }) |
| | | title: "审查已重新开始", |
| | | icon: "success", |
| | | duration: 2000, |
| | | }); |
| | | |
| | | // 重新加载数据 |
| | | resetAndLoad(); |
| | | } else { |
| | | uni.showToast({ |
| | | title: response.msg || "操作失败", |
| | | icon: "none", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | uni.hideLoading(); |
| | | console.error("重新开始审查失败:", error); |
| | | uni.showToast({ |
| | | title: "操作失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 导出审查数据 |
| | | const exportReviews = async () => { |
| | | try { |
| | | uni.showLoading({ |
| | | title: "导出中...", |
| | | mask: true, |
| | | }); |
| | | |
| | | const params = buildQueryParams(); |
| | | delete params.pageNum; |
| | | delete params.pageSize; |
| | | |
| | | const res = await uni.$uapi.get( |
| | | `/project/ethicalreviewopinions/export`, |
| | | params, |
| | | ); |
| | | |
| | | uni.hideLoading(); |
| | | |
| | | if (res.code == 200) { |
| | | const fileUrl = res.data || res.url; |
| | | if (fileUrl) { |
| | | uni.showToast({ |
| | | title: "导出成功", |
| | | icon: "success", |
| | | duration: 2000, |
| | | }); |
| | | |
| | | // 在新窗口中打开下载链接 |
| | | window.open(fileUrl, "_blank"); |
| | | } else { |
| | | uni.showToast({ |
| | | title: "导出文件获取失败", |
| | | icon: "none", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || "导出失败", |
| | | icon: "none", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | uni.hideLoading(); |
| | | console.error("导出数据失败:", error); |
| | | uni.showToast({ |
| | | title: "导出失败", |
| | | icon: "error", |
| | | duration: 2000, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 生命周期 |
| | | onLoad(() => { |
| | | loadInitialData() |
| | | }) |
| | | onLoad(async () => { |
| | | // 获取字典数据 |
| | | dict.value = await useDict( |
| | | "sys_IDType", |
| | | "sys_user_sex", |
| | | "sys_Nation", |
| | | "sys_BloodType", |
| | | "sys_Infectious", |
| | | "sys_AgeUnit", |
| | | "ReviewForm_status", |
| | | "sys_ethical", |
| | | "expert_Conclusion", |
| | | ); |
| | | |
| | | const loadInitialData = () => { |
| | | // 计算统计数据 |
| | | stats.value = { |
| | | totalReviews: reviews.value.length, |
| | | approvedReviews: reviews.value.filter(r => r.status === 'approved').length, |
| | | rejectedReviews: reviews.value.filter(r => r.status === 'rejected').length, |
| | | abandonedReviews: reviews.value.filter(r => r.status === 'abandoned').length |
| | | } |
| | | } |
| | | // 加载数据 |
| | | await loadCaseData(); |
| | | await stateTotal(); |
| | | }); |
| | | |
| | | onShow(() => { |
| | | // 页面显示时刷新数据 |
| | | resetAndLoad(); |
| | | }); |
| | | |
| | | // 监听下拉刷新 |
| | | onPullDownRefresh(() => { |
| | | onRefresh(); |
| | | }); |
| | | |
| | | // 监听上拉触底 |
| | | onReachBottom(() => { |
| | | onLoadMore(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | |
| | | padding: 20rpx; |
| | | |
| | | .stats-card { |
| | | background: linear-gradient(135deg, #747CF9, #9B7CF9); |
| | | background: linear-gradient(135deg, #747cf9, #9b7cf9); |
| | | border-radius: 16rpx; |
| | | padding: 40rpx 20rpx; |
| | | display: flex; |
| | |
| | | .status-filter { |
| | | display: flex; |
| | | margin-bottom: 20rpx; |
| | | flex-wrap: wrap; |
| | | |
| | | text { |
| | | flex: 1; |
| | | // min-width: 120rpx; |
| | | text-align: center; |
| | | font-size: 26rpx; |
| | | color: #606266; |
| | | padding: 16rpx 0; |
| | | position: relative; |
| | | margin: 0 8rpx; |
| | | |
| | | &.active { |
| | | color: #747CF9; |
| | | color: #747cf9; |
| | | font-weight: 500; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 0; |
| | | transform: translateX(-50%); |
| | | width: 40rpx; |
| | | height: 4rpx; |
| | | background: #747CF9; |
| | | background: #747cf9; |
| | | border-radius: 2rpx; |
| | | } |
| | | } |
| | |
| | | align-items: center; |
| | | |
| | | .hospital-badge { |
| | | background: linear-gradient(135deg, #747CF9, #9B7CF9); |
| | | background: linear-gradient(135deg, #747cf9, #9b7cf9); |
| | | width: 64rpx; |
| | | height: 64rpx; |
| | | border-radius: 12rpx; |
| | |
| | | font-weight: 600; |
| | | display: block; |
| | | margin-bottom: 4rpx; |
| | | max-width: 300rpx; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .hospital-no { |
| | | font-size: 26rpx; |
| | | color: #909399; |
| | | margin-right: 16rpx; |
| | | background: #f5f5f5; |
| | | padding: 4rpx 12rpx; |
| | | border-radius: 8rpx; |
| | | } |
| | | |
| | | .expert-type { |
| | | font-size: 22rpx; |
| | | color: #747CF9; |
| | | color: #747cf9; |
| | | background: #f0f2ff; |
| | | padding: 4rpx 12rpx; |
| | | border-radius: 12rpx; |
| | | margin-top: 4rpx; |
| | | display: inline-block; |
| | | } |
| | | } |
| | | } |
| | |
| | | border-radius: 20rpx; |
| | | font-size: 24rpx; |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | |
| | | &.approved { |
| | | background: #f6ffed; |
| | |
| | | &.abandoned { |
| | | background: #f5f5f5; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | &.pending { |
| | | background: #e6f7ff; |
| | | color: #1890ff; |
| | | } |
| | | |
| | | &.unknown { |
| | | background: #f5f5f5; |
| | | color: #bfbfbf; |
| | | } |
| | | } |
| | | } |
| | |
| | | font-size: 26rpx; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | display: block; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | } |
| | | } |
| | |
| | | .detail-text { |
| | | font-size: 24rpx; |
| | | color: #606266; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | } |
| | | } |
| | |
| | | font-size: 24rpx; |
| | | color: #52c41a; |
| | | font-weight: 500; |
| | | margin-right: 8rpx; |
| | | } |
| | | |
| | | .conclusion-content { |
| | |
| | | } |
| | | } |
| | | |
| | | .abandon-reason { |
| | | background: #f5f5f5; |
| | | .opinion-section { |
| | | background: #e6f7ff; |
| | | border-radius: 8rpx; |
| | | padding: 20rpx; |
| | | margin-bottom: 20rpx; |
| | | |
| | | .reason-label { |
| | | .opinion-label { |
| | | font-size: 24rpx; |
| | | color: #8c8c8c; |
| | | color: #1890ff; |
| | | font-weight: 500; |
| | | margin-right: 8rpx; |
| | | } |
| | | |
| | | .reason-content { |
| | | .opinion-content { |
| | | font-size: 24rpx; |
| | | color: #303133; |
| | | } |
| | |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 16rpx; |
| | | flex-wrap: wrap; |
| | | |
| | | .action-btn { |
| | | flex: 1; |
| | | min-width: 200rpx; |
| | | height: 64rpx; |
| | | border: none; |
| | | border-radius: 32rpx; |
| | |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6rpx; |
| | | margin-bottom: 8rpx; |
| | | |
| | | &.detail-btn { |
| | | background: #f5f5f5; |
| | | color: #747CF9; |
| | | color: #747cf9; |
| | | } |
| | | |
| | | &.download-btn { |
| | |
| | | |
| | | &.restart-btn { |
| | | background: #f0f2ff; |
| | | color: #747CF9; |
| | | color: #747cf9; |
| | | border: 1rpx solid #adc6ff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .load-more { |
| | | text-align: center; |
| | | padding: 32rpx; |
| | | color: #909399; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .empty-state { |
| | |
| | | } |
| | | |
| | | .empty-action { |
| | | background: linear-gradient(135deg, #747CF9, #9B7CF9); |
| | | background: linear-gradient(135deg, #747cf9, #9b7cf9); |
| | | color: #fff; |
| | | border: none; |
| | | border-radius: 32rpx; |
| | | padding: 16rpx 32rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | } |
| | | |
| | | .load-complete { |
| | | text-align: center; |
| | | padding: 32rpx; |
| | | color: #909399; |
| | | font-size: 24rpx; |
| | | } |
| | | } |
| | | } |
| | |
| | | .ethics-review-list { |
| | | padding: 20rpx; |
| | | |
| | | .stats-card { |
| | | padding: 30rpx 15rpx; |
| | | |
| | | .stat-item { |
| | | .count { |
| | | font-size: 30rpx; |
| | | } |
| | | |
| | | .label { |
| | | font-size: 22rpx; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .review-item .basic-info .info-row { |
| | | grid-template-columns: 1fr; |
| | | gap: 16rpx; |
| | | } |
| | | |
| | | .review-header { |
| | | flex-direction: column; |
| | | align-items: flex-start !important; |
| | | |
| | | .status-tag { |
| | | margin-top: 16rpx; |
| | | } |
| | | } |
| | | |
| | | .action-buttons { |
| | | flex-direction: column; |
| | | |
| | | .action-btn { |
| | | min-width: 100% !important; |
| | | } |
| | | } |
| | | } |
| | | } |