| | |
| | | <view class="review-overview card"> |
| | | <view class="overview-header"> |
| | | <text class="title">伦理审查任务</text> |
| | | <view class="status-badge" :class="reviewStatus"> |
| | | {{ statusText }} |
| | | <view |
| | | class="status-badge" |
| | | :class="getStatusClass(caseInfo.receiveStatus)" |
| | | > |
| | | {{ getStatusText(caseInfo.receiveStatus) }} |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | <view class="compact-info-item"> |
| | | <up-icon name="file-text" size="14" color="#909399" /> |
| | | <text class="compact-label">住院号</text> |
| | | <text class="compact-value">{{ caseInfo.hospitalNo }}</text> |
| | | <text class="compact-value">{{ caseInfo.inpatientno || "--" }}</text> |
| | | </view> |
| | | <view class="compact-info-item"> |
| | | <up-icon name="account" size="14" color="#909399" /> |
| | | <text class="compact-label">捐献者</text> |
| | | <text class="compact-value">{{ caseInfo.donorName }}</text> |
| | | <text class="compact-value">{{ caseInfo.name || "--" }}</text> |
| | | </view> |
| | | <view class="compact-info-item"> |
| | | <up-icon name="man" size="14" color="#909399" /> |
| | | <text class="compact-label">性别/年龄</text> |
| | | <text class="compact-value">{{ caseInfo.gender }}/{{ caseInfo.age }}岁</text> |
| | | <text class="compact-value" |
| | | >{{ caseInfo.sex || "--" }}/{{ caseInfo.age || "--" |
| | | }}{{ caseInfo.ageunit || "岁" }}</text |
| | | > |
| | | </view> |
| | | <view class="compact-info-item"> |
| | | <up-icon name="heart" size="14" color="#909399" /> |
| | | <text class="compact-label">疾病诊断</text> |
| | | <text class="compact-value">{{ caseInfo.diagnosis }}</text> |
| | | <text class="compact-value">{{ |
| | | caseInfo.diagnosisname || "--" |
| | | }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | <view class="material-left"> |
| | | <up-icon :name="material.icon" :color="material.color" size="18" /> |
| | | <text class="file-name">{{ material.name }}</text> |
| | | <view v-if="material.type" class="file-meta"> |
| | | <text class="file-type">{{ material.type || "文件" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="material-right"> |
| | | <text class="file-size">{{ material.size }}</text> |
| | |
| | | <view class="review-form card"> |
| | | <view class="section-header"> |
| | | <text class="section-title">审查意见</text> |
| | | <view v-if="isTimeout" class="timeout-badge"> |
| | | <up-icon name="clock" size="16" /> |
| | | <text>已超时</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="form-content"> |
| | |
| | | <view class="form-group"> |
| | | <text class="form-label">审查结论</text> |
| | | <u-radio-group |
| | | v-model="form.conclusion" |
| | | v-model="form.expertconclusion" |
| | | placement="column" |
| | | activeColor="#007aff" |
| | | @change="onConclusionChange" |
| | | :disabled="isReadonly" |
| | | > |
| | | <u-radio |
| | | v-for="option in conclusionOptions" |
| | |
| | | <view class="form-group"> |
| | | <text class="form-label">详细意见</text> |
| | | <u--textarea |
| | | v-model="form.opinion" |
| | | v-model="form.expertopinion" |
| | | placeholder="请输入详细的审查意见和改进建议..." |
| | | maxlength="1000" |
| | | count |
| | | :height="120" |
| | | border="surround" |
| | | :disabled="isReadonly" |
| | | ></u--textarea> |
| | | </view> |
| | | |
| | | <!-- 风险评估 --> |
| | | <view class="form-group"> |
| | | <text class="form-label">风险评估</text> |
| | | <view class="risk-assessment"> |
| | | <view class="risk-item"> |
| | | <text class="risk-label">受试者风险等级</text> |
| | | <view class="risk-slider-compact"> |
| | | <view class="risk-levels"> |
| | | <text class="level-label" :class="{ active: form.riskLevel >= 1 }">低</text> |
| | | <text class="level-label" :class="{ active: form.riskLevel >= 2 }">中低</text> |
| | | <text class="level-label" :class="{ active: form.riskLevel >= 3 }">中</text> |
| | | <text class="level-label" :class="{ active: form.riskLevel >= 4 }">中高</text> |
| | | <text class="level-label" :class="{ active: form.riskLevel >= 5 }">高</text> |
| | | </view> |
| | | <slider |
| | | v-model="form.riskLevel" |
| | | min="1" |
| | | max="5" |
| | | step="1" |
| | | activeColor="#f56c6c" |
| | | backgroundColor="#e4e7ed" |
| | | block-color="#f56c6c" |
| | | block-size="20" |
| | | /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 签名确认 --> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="action-bar-compact"> |
| | | <button |
| | | class="action-btn save-btn" |
| | | @tap="saveDraft" |
| | | <!-- 签名确认区域 --> |
| | | <view class="signature-section card"> |
| | | <view class="section-header"> |
| | | <text class="section-title">专家签名确认</text> |
| | | <view v-if="isTimeout" class="timeout-badge"> |
| | | <up-icon name="clock" size="16" /> |
| | | <text>已超时,不可操作</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="signature-content"> |
| | | <!-- 显示已签名图片 --> |
| | | <view v-if="signatureData.signatureUrl" class="signed-preview"> |
| | | <image |
| | | :src="signatureData.signatureUrl" |
| | | mode="aspectFit" |
| | | class="signature-image" |
| | | @tap="previewSignature" |
| | | /> |
| | | <view class="signature-info"> |
| | | <text class="signature-name">签名人:{{ expertInfo.name }}</text> |
| | | <text class="signature-time">{{ |
| | | signatureData.signatureTime |
| | | }}</text> |
| | | <view class="signature-actions" v-if="!isReadonly"> |
| | | <button class="re-sign-btn" @tap="removeSignature"> |
| | | <u-icon name="photo" size="16" /> |
| | | <text>重新签名</text> |
| | | </button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 添加签名按钮 --> |
| | | <view |
| | | v-else |
| | | class="signature-upload" |
| | | @tap="openSignaturePanel" |
| | | v-if="!isReadonly" |
| | | > |
| | | <view class="signature-upload-area"> |
| | | <up-icon name="edit-pen" size="48" color="#c0c4cc" /> |
| | | <text class="upload-hint">点击进行手写签名</text> |
| | | <text class="upload-tip">签名将作为重要凭证</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 超时状态下显示提示 --> |
| | | <view |
| | | v-if="isReadonly && !signatureData.signatureUrl" |
| | | class="signature-disabled" |
| | | > |
| | | <view class="signature-disabled-area"> |
| | | <up-icon name="close-circle" size="48" color="#dcdfe6" /> |
| | | <text class="disabled-hint">当前任务已超时</text> |
| | | <text class="disabled-tip">无法进行签名操作</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="action-bar-compact" v-if="!isReadonly"> |
| | | <button class="action-btn save-btn" @tap="saveDraft" v-if="showSaveBtn"> |
| | | <up-icon name="file-text" size="16" color="#606266" /> |
| | | <text>保存草稿</text> |
| | | </button> |
| | | <button |
| | | class="action-btn submit-btn" |
| | | @tap="submitReview" |
| | | :disabled="!canSubmit" |
| | | v-if="showSubmitBtn" |
| | | > |
| | | <up-icon name="checkmark" size="16" color="#fff" /> |
| | | <text>提交审查</text> |
| | | <text>{{ submitBtnText }}</text> |
| | | </button> |
| | | </view> |
| | | |
| | | <!-- 只读状态提示 --> |
| | | <view v-if="isReadonly" class="readonly-tip card"> |
| | | <up-icon name="info-circle" size="20" color="#fa8c16" /> |
| | | <text>当前任务已超时,仅可查看,不可操作</text> |
| | | </view> |
| | | |
| | | <!-- 优化的签名弹窗组件 --> |
| | | <u-popup |
| | | :show="showSignaturePanel" |
| | | @close="closeSignaturePanel" |
| | | mode="bottom" |
| | | :round="20" |
| | | :closeable="true" |
| | | closeIcon="close" |
| | | > |
| | | <view class="signature-modal"> |
| | | <view class="modal-header"> |
| | | <text class="modal-title">手写签名</text> |
| | | <text class="modal-subtitle">请在下方区域进行签名</text> |
| | | </view> |
| | | |
| | | <view class="signature-canvas-container"> |
| | | <!-- 手写签名画布 --> |
| | | <canvas |
| | | canvas-id="signatureCanvas" |
| | | class="signature-canvas" |
| | | :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" |
| | | @touchstart="onTouchStart" |
| | | @touchmove="onTouchMove" |
| | | @touchend="onTouchEnd" |
| | | disable-scroll |
| | | ></canvas> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="canvas-actions"> |
| | | <button class="action-btn clear-btn" @tap="clearCanvas"> |
| | | <up-icon name="trash" size="20" /> |
| | | <text>清空</text> |
| | | </button> |
| | | <button |
| | | class="action-btn redo-btn" |
| | | @tap="undoLastStroke" |
| | | :disabled="!canUndo" |
| | | > |
| | | <up-icon name="play-left" size="20" /> |
| | | <text>撤销</text> |
| | | </button> |
| | | <button |
| | | class="action-btn confirm-btn" |
| | | @tap="confirmSignature" |
| | | :disabled="!hasSignature" |
| | | > |
| | | <up-icon name="checkmark" size="20" /> |
| | | <text>确认签名</text> |
| | | </button> |
| | | </view> |
| | | |
| | | <!-- 签名预览 --> |
| | | <view v-if="tempSignatureData" class="signature-preview"> |
| | | <text class="preview-title">签名预览</text> |
| | | <image |
| | | :src="tempSignatureData" |
| | | mode="aspectFit" |
| | | class="preview-image" |
| | | /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-popup> |
| | | |
| | | <!-- 提交确认弹窗 --> |
| | | <u-modal |
| | | :show="showSubmitModal" |
| | | title="确认提交" |
| | | content="确定要提交审查意见吗?提交后将无法修改。" |
| | | :title="modalTitle" |
| | | :content="modalContent" |
| | | showCancelButton |
| | | @confirm="confirmSubmit" |
| | | @cancel="showSubmitModal = false" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { ref, computed, reactive, onMounted, nextTick } from "vue"; |
| | | import { onLoad, onShow } from "@dcloudio/uni-app"; |
| | | import dayjs from "dayjs"; |
| | | import { useUserStore } from "@/stores/user"; |
| | | const userStore = useUserStore(); |
| | | |
| | | const dict = ref({}); |
| | | // 响应式数据 |
| | | const caseInfo = ref({ |
| | | hospitalNo: "D230415", |
| | | donorName: "张某某", |
| | | gender: "男", |
| | | age: "45", |
| | | diagnosis: "终末期肝病" |
| | | hospitalNo: "", |
| | | donorName: "", |
| | | gender: "", |
| | | age: "", |
| | | diagnosis: "", |
| | | receiveStatus: "0", |
| | | endtime: "", |
| | | }); |
| | | |
| | | const materials = ref([ |
| | | { id: 1, name: "捐献者知情同意书.pdf", icon: "file-text", color: "#f56c6c", size: "2.3MB" }, |
| | | { id: 2, name: "医学评估报告.docx", icon: "file-text", color: "#1890ff", size: "1.1MB" }, |
| | | { id: 3, name: "实验室检查结果.xlsx", icon: "file-text", color: "#52c41a", size: "0.8MB" }, |
| | | { id: 4, name: "影像学资料.jpg", icon: "photo", color: "#fa8c16", size: "3.2MB" } |
| | | ]); |
| | | |
| | | const id = ref(null); |
| | | const fcid = ref(null); |
| | | const materials = ref([]); |
| | | const form = ref({ |
| | | conclusion: "", |
| | | opinion: "", |
| | | riskLevel: 3 |
| | | expertconclusion: "", // 专家结论 |
| | | expertopinion: "", // 专家意见 |
| | | }); |
| | | |
| | | const expertInfo = ref({ |
| | | name: "孔心涓", |
| | | title: "主委专家" |
| | | name: "", |
| | | title: "", |
| | | }); |
| | | |
| | | const reviewStatus = ref("pending"); |
| | | const showSubmitModal = ref(false); |
| | | const modalTitle = ref("确认提交"); |
| | | const modalContent = ref("确定要提交审查意见吗?提交后将无法修改。"); |
| | | |
| | | // 计算属性 |
| | | const statusText = computed(() => { |
| | | const statusMap = { |
| | | pending: "待审查", |
| | | drafted: "草稿", |
| | | submitted: "已提交" |
| | | }; |
| | | return statusMap[reviewStatus.value]; |
| | | }); |
| | | |
| | | const canSubmit = computed(() => { |
| | | return form.value.conclusion !== "" && form.value.opinion.trim().length > 10; |
| | | }); |
| | | |
| | | const currentTime = computed(() => { |
| | | return new Date().toLocaleString('zh-CN', { |
| | | year: 'numeric', |
| | | month: '2-digit', |
| | | day: '2-digit', |
| | | hour: '2-digit', |
| | | minute: '2-digit' |
| | | return new Date().toLocaleString("zh-CN", { |
| | | year: "numeric", |
| | | month: "2-digit", |
| | | day: "2-digit", |
| | | hour: "2-digit", |
| | | minute: "2-digit", |
| | | }); |
| | | }); |
| | | |
| | | const conclusionOptions = ref([ |
| | | { label: "同意", value: "approved" }, |
| | | { label: "修改后同意", value: "approved_with_modifications" }, |
| | | { label: "修改后重审", value: "re-review" }, |
| | | { label: "不同意", value: "disapproved" } |
| | | { label: "同意", value: "1" }, // 对应receiveStatus 5-完成 |
| | | { label: "驳回", value: "2" }, // 对应receiveStatus 6-驳回 |
| | | ]); |
| | | |
| | | // 方法 |
| | | const loadReviewData = (reviewId) => { |
| | | console.log("加载审查数据:", reviewId); |
| | | // 签名相关数据 |
| | | const showSignaturePanel = ref(false); |
| | | const signatureData = reactive({ |
| | | signatureUrl: "", |
| | | signatureTime: "", |
| | | fileName: "", |
| | | serverData: null, |
| | | }); |
| | | |
| | | // 手写签名相关 |
| | | const canvasWidth = 650; |
| | | const canvasHeight = 300; |
| | | let ctx = null; |
| | | let isDrawing = false; |
| | | let lastX = 0; |
| | | let lastY = 0; |
| | | let strokeHistory = []; |
| | | const tempSignatureData = ref(""); |
| | | const canUndo = computed(() => strokeHistory.length > 0); |
| | | const hasSignature = computed(() => tempSignatureData.value !== ""); |
| | | |
| | | // 上传配置 |
| | | const uploadConfig = reactive({ |
| | | uploadUrl: "/api/common/upload", |
| | | extraParams: { |
| | | caseNo: "", |
| | | expertId: "", |
| | | expertName: "", |
| | | type: "ethics_review_signature", |
| | | bizType: "expert_review", |
| | | }, |
| | | }); |
| | | |
| | | // 按钮显示控制 |
| | | const showSaveBtn = ref(true); |
| | | const showSubmitBtn = ref(true); |
| | | const submitBtnText = ref("提交审查"); |
| | | |
| | | // 状态转换 |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | 0: "待接收", |
| | | 1: "未接收", |
| | | 2: "已接收", |
| | | 3: "超时", |
| | | 4: "中止", |
| | | 5: "完成", |
| | | 6: "驳回", |
| | | }; |
| | | return statusMap[status] || "未知状态"; |
| | | }; |
| | | |
| | | const getStatusClass = (status) => { |
| | | const classMap = { |
| | | 0: "pending", |
| | | 1: "pending", |
| | | 2: "pending", |
| | | 3: "submitted", |
| | | 4: "submitted", |
| | | 5: "success", |
| | | 6: "error", |
| | | }; |
| | | return classMap[status] || "pending"; |
| | | }; |
| | | |
| | | // 是否超时 |
| | | const isTimeout = computed(() => { |
| | | return caseInfo.value.receiveStatus === "3"; |
| | | }); |
| | | |
| | | // 是否只读模式 |
| | | const isReadonly = computed(() => { |
| | | return isTimeout.value; |
| | | }); |
| | | |
| | | // 是否可以提交 |
| | | const canSubmit = computed(() => { |
| | | if (isReadonly.value) return false; |
| | | return ( |
| | | form.value.expertconclusion !== "" && |
| | | form.value.expertopinion.trim().length > 0 && |
| | | signatureData.signatureUrl !== "" && |
| | | signatureData.signatureUrl.startsWith("http") // 确保是完整的URL路径 |
| | | ); |
| | | }); |
| | | |
| | | // 生命周期 |
| | | onLoad(async (options) => { |
| | | id.value = options.id; |
| | | fcid.value = options.fcid; |
| | | if (fcid.value) { |
| | | await loadCaseData(fcid.value); |
| | | } |
| | | }); |
| | | |
| | | onShow(() => { |
| | | // 页面显示时检查用户登录状态 |
| | | const userInfo = uni.getStorageSync("userInfo"); |
| | | if (userInfo) { |
| | | expertInfo.value.name = userInfo.nickName || userInfo.userName || ""; |
| | | } |
| | | }); |
| | | |
| | | // 加载案件数据 |
| | | const loadCaseData = async (id) => { |
| | | try { |
| | | uni.showLoading({ title: "加载中..." }); |
| | | |
| | | // 构建查询参数 |
| | | const params = { |
| | | fcid: id, |
| | | pageNum: 1, |
| | | pageSize: 1, |
| | | }; |
| | | |
| | | const res = await uni.$uapi.get( |
| | | `/project/ethicalreviewopinions/listnew`, |
| | | params, |
| | | ); |
| | | |
| | | if (res.code === 200 && res.rows && res.rows.length > 0) { |
| | | const data = res.rows[0]; |
| | | console.log("加载的审查数据:", data); |
| | | |
| | | // 设置案件信息 |
| | | caseInfo.value = { |
| | | ...caseInfo.value, |
| | | inpatientno: data.inpatientno, |
| | | name: data.name, |
| | | sex: data.sex, |
| | | age: data.age, |
| | | ageunit: data.ageunit, |
| | | expertType: data.expertType, |
| | | diagnosisname: data.diagnosisname, |
| | | receiveStatus: data.receiveStatus || "0", |
| | | endtime: data.endtime || "", |
| | | caseNo: data.caseNo || "", |
| | | }; |
| | | |
| | | // 设置表单数据 |
| | | if (data.expertconclusion) { |
| | | form.value.expertconclusion = data.expertconclusion.toString(); |
| | | } |
| | | if (data.expertopinion) { |
| | | form.value.expertopinion = data.expertopinion; |
| | | } |
| | | |
| | | // 设置专家信息 |
| | | if (data.expertname) { |
| | | expertInfo.value.name = data.expertname; |
| | | } else { |
| | | // 从用户信息获取 |
| | | const userInfo = uni.getStorageSync("userInfo"); |
| | | if (userInfo) { |
| | | expertInfo.value.name = userInfo.nickName || userInfo.userName || ""; |
| | | } |
| | | } |
| | | |
| | | // 设置签名 |
| | | if (data.sigin) { |
| | | // 检查签名URL是否是完整路径 |
| | | if (data.sigin.startsWith("http")) { |
| | | signatureData.signatureUrl = data.sigin; |
| | | } else { |
| | | // 如果不是完整路径,可能需要拼接基础URL |
| | | signatureData.signatureUrl = `/api${ |
| | | data.sigin.startsWith("/") ? "" : "/" |
| | | }${data.sigin}`; |
| | | } |
| | | signatureData.signatureTime = data.conclusiontime || ""; |
| | | } |
| | | |
| | | // 解析附件 |
| | | if (data.filePatch) { |
| | | parseAnnexFiles(data.filePatch); |
| | | } |
| | | |
| | | // 检查是否超时 |
| | | checkTimeoutStatus(data); |
| | | |
| | | // 根据状态控制按钮显示 |
| | | updateButtonStatus(data.receiveStatus); |
| | | |
| | | // 更新上传参数 |
| | | uploadConfig.extraParams.caseNo = data.caseNo || ""; |
| | | uploadConfig.extraParams.expertName = expertInfo.value.name; |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || "未找到审查数据", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | if (error.message === "未登录") { |
| | | // ✅ 什么都不做,拦截器已经处理 |
| | | return; |
| | | } |
| | | uni.showToast({ title: "加载失败", icon: "none" }); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | }; |
| | | |
| | | // 检查是否超时 |
| | | const checkTimeoutStatus = (data) => { |
| | | if (data.receiveStatus === "3") { |
| | | // 状态已经是超时 |
| | | return; |
| | | } |
| | | |
| | | // 如果有截止时间,检查是否超过当前时间 |
| | | if (data.endtime) { |
| | | const endTime = new Date(data.endtime); |
| | | const now = new Date(); |
| | | if (now > endTime) { |
| | | // 标记为超时状态 |
| | | caseInfo.value.receiveStatus = "3"; |
| | | updateButtonStatus("3"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 解析附件文件 |
| | | const parseAnnexFiles = (filePatch) => { |
| | | if (!filePatch) return; |
| | | |
| | | try { |
| | | // 解析JSON字符串 |
| | | let fileList = []; |
| | | |
| | | // 检查是否是JSON字符串格式 |
| | | if (filePatch.startsWith("[") && filePatch.endsWith("]")) { |
| | | // 处理转义字符 |
| | | const cleanJson = filePatch.replace(/\\"/g, '"'); |
| | | fileList = JSON.parse(cleanJson); |
| | | } else if (filePatch.includes("fileName")) { |
| | | // 尝试直接解析 |
| | | fileList = JSON.parse(filePatch); |
| | | } else { |
| | | // 旧格式处理 |
| | | const oldFileList = filePatch.split(";").filter((item) => item.trim()); |
| | | fileList = oldFileList.map((file) => ({ |
| | | fileName: file.split("/").pop() || `附件`, |
| | | path: file, |
| | | fileUrl: file, |
| | | })); |
| | | } |
| | | |
| | | // 转换为需要的格式 |
| | | materials.value = fileList.map((file, index) => { |
| | | const fileName = file.fileName || `附件${index + 1}`; |
| | | const fileUrl = file.fileUrl || file.path || file.url || ""; |
| | | const ext = fileName.split(".").pop().toLowerCase(); |
| | | let icon = "file-text"; |
| | | let color = "#909399"; |
| | | |
| | | if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext)) { |
| | | icon = "photo"; |
| | | color = "#fa8c16"; |
| | | } else if (["doc", "docx"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#1890ff"; |
| | | } else if (["xls", "xlsx", "csv"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#52c41a"; |
| | | } else if (["pdf"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#f56c6c"; |
| | | } else if (["txt", "text"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#909399"; |
| | | } else if (["zip", "rar", "7z"].includes(ext)) { |
| | | icon = "folder"; |
| | | color = "#722ed1"; |
| | | } |
| | | |
| | | return { |
| | | id: file.infoid || index + 1, |
| | | name: fileName, |
| | | icon: icon, |
| | | color: color, |
| | | size: "--", |
| | | url: fileUrl, |
| | | type: ext, |
| | | createTime: file.createTime || "", |
| | | }; |
| | | }); |
| | | } catch (error) { |
| | | console.error("解析附件失败:", error, filePatch); |
| | | |
| | | // 回退到旧格式处理 |
| | | const fileList = filePatch.split(";").filter((item) => item.trim()); |
| | | materials.value = fileList.map((file, index) => { |
| | | const fileName = file.split("/").pop() || `附件${index + 1}`; |
| | | const ext = fileName.split(".").pop().toLowerCase(); |
| | | let icon = "file-text"; |
| | | let color = "#909399"; |
| | | |
| | | if (["jpg", "jpeg", "png", "gif", "bmp"].includes(ext)) { |
| | | icon = "photo"; |
| | | color = "#fa8c16"; |
| | | } else if (["doc", "docx"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#1890ff"; |
| | | } else if (["xls", "xlsx"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#52c41a"; |
| | | } else if (["pdf"].includes(ext)) { |
| | | icon = "file-text"; |
| | | color = "#f56c6c"; |
| | | } |
| | | |
| | | return { |
| | | id: index + 1, |
| | | name: fileName, |
| | | icon: icon, |
| | | color: color, |
| | | size: "--", |
| | | url: file, |
| | | }; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 更新按钮状态 |
| | | const updateButtonStatus = (status) => { |
| | | const statusNum = status; |
| | | switch (statusNum) { |
| | | case "3": // 超时 |
| | | case "4": // 中止 |
| | | case "5": // 已完成 |
| | | case "6": // 已驳回 |
| | | showSaveBtn.value = false; |
| | | showSubmitBtn.value = false; |
| | | break; |
| | | default: |
| | | showSaveBtn.value = true; |
| | | showSubmitBtn.value = true; |
| | | submitBtnText.value = "提交审查"; |
| | | } |
| | | }; |
| | | |
| | | // 打开签名面板 |
| | | const openSignaturePanel = () => { |
| | | if (isReadonly.value) { |
| | | uni.showToast({ |
| | | title: "当前任务已超时,不可操作", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | showSignaturePanel.value = true; |
| | | nextTick(() => { |
| | | initCanvas(); |
| | | }); |
| | | }; |
| | | |
| | | // 关闭签名面板 |
| | | const closeSignaturePanel = () => { |
| | | showSignaturePanel.value = false; |
| | | clearCanvas(); |
| | | }; |
| | | |
| | | // 初始化画布 |
| | | const initCanvas = () => { |
| | | ctx = uni.createCanvasContext("signatureCanvas"); |
| | | clearCanvas(); |
| | | }; |
| | | |
| | | // 清空画布 |
| | | const clearCanvas = () => { |
| | | if (!ctx) return; |
| | | |
| | | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| | | ctx.setStrokeStyle("#000000"); |
| | | ctx.setLineWidth(3); |
| | | ctx.setLineCap("round"); |
| | | ctx.setLineJoin("round"); |
| | | ctx.draw(); |
| | | strokeHistory = []; |
| | | tempSignatureData.value = ""; |
| | | }; |
| | | |
| | | // 撤销上一步 |
| | | const undoLastStroke = () => { |
| | | if (strokeHistory.length === 0) return; |
| | | |
| | | // 移除最后一步 |
| | | strokeHistory.pop(); |
| | | |
| | | // 重新绘制 |
| | | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| | | ctx.setStrokeStyle("#000000"); |
| | | ctx.setLineWidth(3); |
| | | ctx.setLineCap("round"); |
| | | ctx.setLineJoin("round"); |
| | | |
| | | // 绘制历史笔迹 |
| | | strokeHistory.forEach((stroke) => { |
| | | ctx.beginPath(); |
| | | ctx.moveTo(stroke.startX, stroke.startY); |
| | | ctx.lineTo(stroke.endX, stroke.endY); |
| | | ctx.stroke(); |
| | | }); |
| | | |
| | | ctx.draw(); |
| | | |
| | | // 更新预览 |
| | | if (strokeHistory.length === 0) { |
| | | tempSignatureData.value = ""; |
| | | } else { |
| | | getCanvasImage(); |
| | | } |
| | | }; |
| | | |
| | | // 触摸开始 |
| | | const onTouchStart = (e) => { |
| | | if (isReadonly.value) return; |
| | | |
| | | isDrawing = true; |
| | | const touch = e.touches[0]; |
| | | lastX = touch.x; |
| | | lastY = touch.y; |
| | | ctx.beginPath(); |
| | | ctx.moveTo(lastX, lastY); |
| | | }; |
| | | |
| | | // 触摸移动 |
| | | const onTouchMove = (e) => { |
| | | if (!isDrawing || isReadonly.value) return; |
| | | |
| | | const touch = e.touches[0]; |
| | | const currentX = touch.x; |
| | | const currentY = touch.y; |
| | | |
| | | ctx.lineTo(currentX, currentY); |
| | | ctx.stroke(); |
| | | ctx.draw(true); |
| | | |
| | | // 保存笔迹到历史记录 |
| | | strokeHistory.push({ |
| | | startX: lastX, |
| | | startY: lastY, |
| | | endX: currentX, |
| | | endY: currentY, |
| | | }); |
| | | |
| | | lastX = currentX; |
| | | lastY = currentY; |
| | | }; |
| | | |
| | | // 触摸结束 |
| | | const onTouchEnd = () => { |
| | | if (!isDrawing) return; |
| | | |
| | | isDrawing = false; |
| | | ctx.closePath(); |
| | | |
| | | // 更新预览 |
| | | getCanvasImage(); |
| | | }; |
| | | |
| | | // 获取画布图片 |
| | | const getCanvasImage = () => { |
| | | uni.canvasToTempFilePath({ |
| | | canvasId: "signatureCanvas", |
| | | success: (res) => { |
| | | tempSignatureData.value = res.tempFilePath; |
| | | }, |
| | | fail: (err) => { |
| | | console.error("获取画布图片失败:", err); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 确认签名 |
| | | const confirmSignature = async () => { |
| | | if (!tempSignatureData.value) { |
| | | uni.showToast({ |
| | | title: "请先签名", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | uni.showLoading({ title: "保存签名中..." }); |
| | | |
| | | // 上传签名文件 |
| | | const uploadResult = await uploadSignature(tempSignatureData.value); |
| | | |
| | | if (uploadResult) { |
| | | // 更新签名数据 |
| | | signatureData.signatureUrl = uploadResult.url; |
| | | signatureData.signatureTime = new Date().toLocaleString("zh-CN"); |
| | | signatureData.fileName = |
| | | uploadResult.originalFilename || `signature_${Date.now()}.png`; |
| | | signatureData.serverData = uploadResult; |
| | | |
| | | // 保存到本地 |
| | | saveSignatureToLocal(uploadResult); |
| | | |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "签名保存成功", |
| | | icon: "success", |
| | | }); |
| | | |
| | | showSignaturePanel.value = false; |
| | | clearCanvas(); |
| | | } else { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "签名上传失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("签名上传失败:", error); |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "签名上传失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 上传签名文件方法 |
| | | const uploadSignature = (filePath) => { |
| | | return new Promise((resolve, reject) => { |
| | | const token = uni.getStorageSync("token"); |
| | | |
| | | // 获取用户信息 |
| | | const userInfo = uni.getStorageSync("userInfo"); |
| | | const expertName = |
| | | userInfo?.nickName || userInfo?.userName || expertInfo.value.name; |
| | | |
| | | uni.uploadFile({ |
| | | url: "/api/common/upload", |
| | | filePath: filePath, |
| | | name: "file", |
| | | header: { |
| | | Authorization: `Bearer ${token}`, |
| | | }, |
| | | formData: { |
| | | // 添加额外参数,参考附件组件 |
| | | bizType: "expert_review_signature", |
| | | caseNo: caseInfo.value.caseNo || "", |
| | | expertName: expertName, |
| | | uploadType: "signature", |
| | | }, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | const data = JSON.parse(res.data); |
| | | console.log("签名上传成功:", data); |
| | | |
| | | if (data.code === 200) { |
| | | resolve({ |
| | | url: data.url, |
| | | fileName: data.fileName, |
| | | newFileName: data.newFileName, |
| | | originalFilename: data.originalFilename, |
| | | filePath: data.filePath || data.fileName, |
| | | size: data.size, |
| | | }); |
| | | } else { |
| | | reject(new Error(data.msg || "上传失败")); |
| | | } |
| | | } else { |
| | | reject(new Error(`上传失败,状态码: ${res.statusCode}`)); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | reject(err); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // 保存到本地 |
| | | const saveSignatureToLocal = (uploadData) => { |
| | | try { |
| | | const signatureInfo = { |
| | | signatureUrl: signatureData.signatureUrl, |
| | | signatureTime: signatureData.signatureTime, |
| | | fileName: signatureData.fileName, |
| | | uploadData: uploadData, // 保存上传返回的数据 |
| | | caseNo: caseInfo.value.caseNo, |
| | | timestamp: Date.now(), |
| | | }; |
| | | |
| | | uni.setStorageSync("expert_review_signature", signatureInfo); |
| | | } catch (error) { |
| | | console.error("保存签名到本地失败:", error); |
| | | } |
| | | }; |
| | | |
| | | // 删除签名 |
| | | const removeSignature = () => { |
| | | if (isReadonly.value) { |
| | | uni.showToast({ |
| | | title: "当前任务已超时,不可操作", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.showModal({ |
| | | title: "提示", |
| | | content: "确定要删除签名吗?", |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | signatureData.signatureUrl = ""; |
| | | signatureData.signatureTime = ""; |
| | | signatureData.fileName = ""; |
| | | signatureData.serverData = null; |
| | | uni.removeStorageSync("expert_review_signature"); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 预览签名 |
| | | // 预览签名 |
| | | const previewSignature = () => { |
| | | if (signatureData.signatureUrl) { |
| | | // 检查URL是否需要拼接基础路径 |
| | | let previewUrl = signatureData.signatureUrl; |
| | | if (!previewUrl.startsWith("http")) { |
| | | // 可能需要拼接服务器基础URL |
| | | previewUrl = `${userStore?.baseUrlHt || ""}${previewUrl}`; |
| | | } |
| | | |
| | | uni.previewImage({ |
| | | urls: [previewUrl], |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 预览材料 |
| | | // 预览材料 |
| | | const previewMaterial = (material) => { |
| | | if (material.url) { |
| | | uni.showLoading({ title: "加载中..." }); |
| | | |
| | | const fileExt = |
| | | material.type || material.url.split(".").pop().toLowerCase(); |
| | | |
| | | // 判断文件类型 |
| | | if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(fileExt)) { |
| | | // 图片直接预览 |
| | | uni.previewImage({ |
| | | urls: [material.url], |
| | | current: 0, |
| | | success: () => { |
| | | console.log("图片预览成功"); |
| | | }, |
| | | fail: (err) => { |
| | | console.error("图片预览失败:", err); |
| | | uni.showToast({ |
| | | title: "图片加载失败", |
| | | icon: "none", |
| | | }); |
| | | }, |
| | | complete: () => { |
| | | uni.hideLoading(); |
| | | }, |
| | | }); |
| | | } else if ( |
| | | ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"].includes( |
| | | fileExt, |
| | | ) |
| | | ) { |
| | | // 文档文件下载后打开 |
| | | uni.downloadFile({ |
| | | url: material.url, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | const filePath = res.tempFilePath; |
| | | uni.openDocument({ |
| | | filePath: filePath, |
| | | showMenu: true, |
| | | fileType: fileExt === "pdf" ? "pdf" : "", |
| | | success: () => { |
| | | console.log("打开文档成功"); |
| | | }, |
| | | fail: (err) => { |
| | | console.error("打开文档失败:", err); |
| | | uni.showToast({ |
| | | title: "无法打开该文件", |
| | | icon: "none", |
| | | }); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error("下载文件失败:", err); |
| | | uni.showToast({ |
| | | title: "文件下载失败", |
| | | icon: "none", |
| | | }); |
| | | }, |
| | | complete: () => { |
| | | uni.hideLoading(); |
| | | }, |
| | | }); |
| | | } else { |
| | | // 其他文件类型 |
| | | uni.showToast({ |
| | | title: `暂不支持预览${fileExt}格式文件`, |
| | | icon: "none", |
| | | }); |
| | | uni.hideLoading(); |
| | | } |
| | | } else { |
| | | uni.showToast({ |
| | | title: `预览: ${material.name}`, |
| | | icon: "none" |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const onConclusionChange = (value) => { |
| | | console.log("选中结论:", value); |
| | | }; |
| | | |
| | | const saveDraft = () => { |
| | | // 保存草稿 |
| | | const saveDraft = async () => { |
| | | if (isReadonly.value) { |
| | | uni.showToast({ |
| | | title: "草稿保存成功", |
| | | icon: "success" |
| | | title: "当前任务已超时,不可操作", |
| | | icon: "none", |
| | | }); |
| | | reviewStatus.value = "drafted"; |
| | | return; |
| | | } |
| | | |
| | | if (!validateForm(true)) return; |
| | | |
| | | try { |
| | | uni.showLoading({ title: "保存中..." }); |
| | | |
| | | const submitData = { |
| | | fcid: fcid.value, |
| | | expertconclusion: form.value.expertconclusion, |
| | | expertopinion: form.value.expertopinion, |
| | | sigin: signatureData.signatureUrl, // 使用上传后的完整路径 |
| | | receiveStatus: "2", // 已接收状态 |
| | | conclusiontime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | |
| | | const res = await uni.$uapi.post( |
| | | "/project/ethicalreviewopinions/edit", |
| | | submitData, |
| | | ); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ |
| | | title: "保存成功", |
| | | icon: "success", |
| | | }); |
| | | // 更新本地状态 |
| | | caseInfo.value.receiveStatus = "2"; |
| | | updateButtonStatus("2"); |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || "保存失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("保存草稿失败:", error); |
| | | uni.showToast({ |
| | | title: "保存失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | }; |
| | | |
| | | // 提交审查 |
| | | const submitReview = () => { |
| | | console.log(1); |
| | | |
| | | if (isReadonly.value) { |
| | | uni.showToast({ |
| | | title: "当前任务已超时,不可操作", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | console.log(2); |
| | | |
| | | if (!validateForm()) return; |
| | | |
| | | modalTitle.value = "确认提交"; |
| | | modalContent.value = "确定要提交审查意见吗?提交后将无法修改。"; |
| | | showSubmitModal.value = true; |
| | | }; |
| | | |
| | | const confirmSubmit = () => { |
| | | // 确认提交 |
| | | const confirmSubmit = async () => { |
| | | try { |
| | | uni.showLoading({ title: "提交中..." }); |
| | | console.log(caseInfo.value, "form.value"); |
| | | |
| | | setTimeout(() => { |
| | | uni.hideLoading(); |
| | | const submitData = { |
| | | id: fcid.value, |
| | | expertconclusion: form.value.expertconclusion, |
| | | expertopinion: form.value.expertopinion, |
| | | sigin: signatureData.signatureUrl, |
| | | expertType: caseInfo.value.expertType, |
| | | // 根据结论设置状态 |
| | | receiveStatus: form.value.expertconclusion == "1" ? "5" : "6", // 5-完成, 6-驳回 |
| | | conclusiontime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | |
| | | const res = await uni.$uapi.post( |
| | | "/project/ethicalreviewopinions/edit", |
| | | submitData, |
| | | ); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ |
| | | title: "审查意见提交成功", |
| | | icon: "success" |
| | | title: "提交成功", |
| | | icon: "success", |
| | | duration: 2000, |
| | | }); |
| | | reviewStatus.value = "submitted"; |
| | | |
| | | // 更新本地状态 |
| | | caseInfo.value.receiveStatus = submitData.receiveStatus; |
| | | updateButtonStatus(submitData.receiveStatus); |
| | | |
| | | showSubmitModal.value = false; |
| | | |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 1500); |
| | | }, 2000); |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || "提交失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("提交失败:", error); |
| | | uni.showToast({ |
| | | title: "提交失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | }; |
| | | |
| | | // 表单验证 |
| | | const validateForm = (isDraft = false) => { |
| | | if (!form.value.expertconclusion && !isDraft) { |
| | | uni.showToast({ |
| | | title: "请选择审查结论", |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | console.log(3); |
| | | |
| | | if (!form.value.expertopinion.trim() && !isDraft) { |
| | | uni.showToast({ |
| | | title: "请输入审查意见", |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | console.log(signatureData, "signatureData"); |
| | | |
| | | if (!signatureData.signatureUrl && !isDraft) { |
| | | uni.showToast({ |
| | | title: "请进行手写签名", |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | </script> |
| | | |
| | |
| | | background: #fff2e8; |
| | | color: #fa8c16; |
| | | } |
| | | &.drafted { |
| | | &.submitted { |
| | | background: #e6f7ff; |
| | | color: #1890ff; |
| | | } |
| | | &.submitted { |
| | | &.success { |
| | | background: #f6ffed; |
| | | color: #52c41a; |
| | | } |
| | | &.error { |
| | | background: #fff1f0; |
| | | color: #ff4d4f; |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | } |
| | | .material-left { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 1; |
| | | |
| | | .file-info { |
| | | margin-left: 16rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 26rpx; |
| | | color: #303133; |
| | | max-width: 400rpx; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .file-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | margin-top: 4rpx; |
| | | |
| | | .file-type { |
| | | font-size: 20rpx; |
| | | color: #909399; |
| | | background: #f0f2f5; |
| | | padding: 2rpx 8rpx; |
| | | border-radius: 4rpx; |
| | | } |
| | | |
| | | .file-time { |
| | | font-size: 20rpx; |
| | | color: #909399; |
| | | } |
| | | } |
| | | } |
| | | .materials-section { |
| | | padding: 24rpx; |
| | | |
| | |
| | | .review-form { |
| | | padding: 24rpx; |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | |
| | | .section-title { |
| | | font-size: 28rpx; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .timeout-badge { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4rpx; |
| | | padding: 4rpx 12rpx; |
| | | background: #fff2e8; |
| | | border-radius: 12rpx; |
| | | font-size: 22rpx; |
| | | color: #fa8c16; |
| | | } |
| | | } |
| | | |
| | | .form-group { |
| | | margin-bottom: 32rpx; |
| | | |
| | |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | } |
| | | |
| | | .risk-assessment { |
| | | .risk-item { |
| | | .risk-label { |
| | | display: block; |
| | | font-size: 26rpx; |
| | | color: #606266; |
| | | margin-bottom: 16rpx; |
| | | } |
| | | |
| | | .risk-slider-compact { |
| | | .risk-levels { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 12rpx; |
| | | |
| | | .level-label { |
| | | font-size: 22rpx; |
| | | color: #c0c4cc; |
| | | transition: color 0.3s; |
| | | |
| | | &.active { |
| | | color: #f56c6c; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | .action-bar-compact { |
| | | position: fixed; |
| | | bottom: 96rpx; |
| | | bottom: 0rpx; |
| | | left: 0; |
| | | right: 0; |
| | | display: flex; |
| | |
| | | } |
| | | |
| | | &.submit-btn { |
| | | background: linear-gradient(135deg, #0f95b0, #89C4C1) !important; |
| | | background: linear-gradient(135deg, #0f95b0, #89c4c1) !important; |
| | | color: #fff; |
| | | |
| | | &:disabled { |
| | |
| | | |
| | | &:active:not(:disabled) { |
| | | transform: scale(0.98); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .readonly-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | padding: 20rpx 24rpx; |
| | | font-size: 24rpx; |
| | | color: #fa8c16; |
| | | } |
| | | } |
| | | |
| | | .signature-section { |
| | | margin-top: 20rpx; |
| | | padding: 24rpx; |
| | | background: #fff; |
| | | border-radius: 16rpx; |
| | | box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | |
| | | .section-title { |
| | | font-size: 28rpx; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .timeout-badge { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4rpx; |
| | | padding: 4rpx 12rpx; |
| | | background: #fff2e8; |
| | | border-radius: 12rpx; |
| | | font-size: 22rpx; |
| | | color: #fa8c16; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-content { |
| | | .signed-preview { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20rpx; |
| | | background: #f8f9fa; |
| | | border-radius: 12rpx; |
| | | gap: 20rpx; |
| | | |
| | | .signature-image { |
| | | width: 200rpx; |
| | | height: 100rpx; |
| | | border: 1rpx solid #dcdfe6; |
| | | border-radius: 8rpx; |
| | | background: #fff; |
| | | } |
| | | |
| | | .signature-info { |
| | | flex: 1; |
| | | |
| | | .signature-name { |
| | | display: block; |
| | | font-size: 26rpx; |
| | | color: #333; |
| | | margin-bottom: 8rpx; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .signature-time { |
| | | display: block; |
| | | font-size: 24rpx; |
| | | color: #666; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .signature-actions { |
| | | margin-top: 12rpx; |
| | | |
| | | .re-sign-btn { |
| | | padding: 8rpx 16rpx; |
| | | border: 1rpx solid #dcdfe6; |
| | | background: #fff; |
| | | border-radius: 8rpx; |
| | | font-size: 24rpx; |
| | | color: #f56c6c; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6rpx; |
| | | |
| | | &:active { |
| | | background: #f5f7fa; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-upload { |
| | | .signature-upload-area { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 200rpx; |
| | | border: 2rpx dashed #c0c4cc; |
| | | border-radius: 12rpx; |
| | | background: #fafafa; |
| | | transition: all 0.3s; |
| | | |
| | | &:active { |
| | | background: #f0f2f5; |
| | | border-color: #007aff; |
| | | } |
| | | |
| | | .upload-hint { |
| | | font-size: 28rpx; |
| | | color: #606266; |
| | | margin-top: 16rpx; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .upload-tip { |
| | | font-size: 24rpx; |
| | | color: #909399; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-disabled { |
| | | .signature-disabled-area { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 200rpx; |
| | | border: 2rpx solid #e4e7ed; |
| | | border-radius: 12rpx; |
| | | background: #f8f9fa; |
| | | |
| | | .disabled-hint { |
| | | font-size: 28rpx; |
| | | color: #909399; |
| | | margin-top: 16rpx; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .disabled-tip { |
| | | font-size: 24rpx; |
| | | color: #c0c4cc; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 签名模态框样式 |
| | | .signature-modal { |
| | | padding: 30rpx; |
| | | background: #fff; |
| | | border-radius: 20rpx 20rpx 0 0; |
| | | max-height: 80vh; |
| | | overflow: hidden; |
| | | |
| | | .modal-header { |
| | | text-align: center; |
| | | margin-bottom: 30rpx; |
| | | |
| | | .modal-title { |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | display: block; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .modal-subtitle { |
| | | font-size: 24rpx; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .signature-canvas-container { |
| | | .signature-canvas { |
| | | display: block; |
| | | margin: 0 auto; |
| | | border: 2rpx solid #e4e7ed; |
| | | border-radius: 8rpx; |
| | | background: #fff; |
| | | touch-action: none; |
| | | } |
| | | |
| | | .canvas-actions { |
| | | display: flex; |
| | | gap: 20rpx; |
| | | margin: 20rpx 0; |
| | | padding: 0 20rpx; |
| | | |
| | | .action-btn { |
| | | flex: 1; |
| | | height: 60rpx; |
| | | border-radius: 30rpx; |
| | | border: 1rpx solid #dcdfe6; |
| | | background: #fff; |
| | | font-size: 24rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8rpx; |
| | | |
| | | &.clear-btn { |
| | | color: #f56c6c; |
| | | border-color: #f56c6c; |
| | | } |
| | | |
| | | &.redo-btn { |
| | | color: #1890ff; |
| | | border-color: #1890ff; |
| | | |
| | | &:disabled { |
| | | color: #c0c4cc; |
| | | border-color: #e4e7ed; |
| | | } |
| | | } |
| | | |
| | | &.confirm-btn { |
| | | background: linear-gradient(135deg, #0f95b0, #89c4c1); |
| | | color: #fff; |
| | | border: none; |
| | | |
| | | &:disabled { |
| | | background: #c0c4cc; |
| | | opacity: 0.6; |
| | | } |
| | | } |
| | | |
| | | &:active:not(:disabled) { |
| | | transform: scale(0.98); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-preview { |
| | | margin-top: 20rpx; |
| | | padding: 20rpx; |
| | | background: #f8f9fa; |
| | | border-radius: 8rpx; |
| | | |
| | | .preview-title { |
| | | display: block; |
| | | font-size: 24rpx; |
| | | color: #606266; |
| | | margin-bottom: 12rpx; |
| | | } |
| | | |
| | | .preview-image { |
| | | width: 200rpx; |
| | | height: 80rpx; |
| | | border: 1rpx solid #dcdfe6; |
| | | border-radius: 4rpx; |
| | | background: #fff; |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-modal { |
| | | padding: 20rpx; |
| | | |
| | | .signature-canvas-container { |
| | | .signature-canvas { |
| | | width: 100% !important; |
| | | height: 250rpx !important; |
| | | } |
| | | |
| | | .canvas-actions { |
| | | // flex-direction: column; |
| | | gap: 12rpx; |
| | | padding: 0; |
| | | |
| | | .action-btn { |
| | | height: 72rpx; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |