| | |
| | | <up-icon name="man" size="14" color="#909399" /> |
| | | <text class="compact-label">性别/年龄</text> |
| | | <text class="compact-value" |
| | | >{{ caseInfo.sex || "--" }}/{{ caseInfo.age || "--" |
| | | >{{ caseInfo.sex == 1 ? '男' : caseInfo.sex == 2 ? '女' : '未知' }}/{{ caseInfo.age || "--" |
| | | }}{{ caseInfo.ageunit || "岁" }}</text |
| | | > |
| | | </view> |
| | |
| | | <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> |
| | | <!-- 全屏签名弹框(Canvas 2D 同步绘制,解决断点与 iOS 兼容问题) --> |
| | | <view v-if="showSignaturePanel" class="fullscreen-signature"> |
| | | <!-- 顶部栏 --> |
| | | <view class="fs-header"> |
| | | <text class="fs-header-btn fs-cancel" @tap="closeSignaturePanel">取消</text> |
| | | <text class="fs-title">手写签名</text> |
| | | <text class="fs-header-btn fs-clear" @tap="clearCanvas">清空</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="fs-body"> |
| | | <!-- H5 端:canvas 由 JS 原生创建 + 原生事件绑定(绕开组件编译层的坐标差异,根治笔迹错位) --> |
| | | <!-- #ifdef H5 --> |
| | | <view id="h5CanvasWrap" ref="h5CanvasWrap" class="fs-canvas-wrap"></view> |
| | | <!-- #endif --> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <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> |
| | | <!-- 小程序/App 端:canvas 2d 组件 --> |
| | | <!-- #ifndef H5 --> |
| | | <canvas |
| | | id="signatureCanvas" |
| | | canvas-id="signatureCanvas" |
| | | type="2d" |
| | | class="fs-canvas" |
| | | @touchstart="onTouchStart" |
| | | @touchmove="onTouchMove" |
| | | @touchend="onTouchEnd" |
| | | @touchcancel="onTouchEnd" |
| | | disable-scroll |
| | | ></canvas> |
| | | <!-- #endif --> |
| | | |
| | | <!-- 签名预览 --> |
| | | <view v-if="tempSignatureData" class="signature-preview"> |
| | | <text class="preview-title">签名预览</text> |
| | | <image |
| | | :src="tempSignatureData" |
| | | mode="aspectFit" |
| | | class="preview-image" |
| | | /> |
| | | </view> |
| | | <view v-if="!hasSignature" class="fs-hint" @tap.stop> |
| | | <up-icon name="edit-pen" size="44" color="#c0c4cc" /> |
| | | <text>请在此区域签名</text> |
| | | </view> |
| | | </view> |
| | | </u-popup> |
| | | |
| | | <!-- 笔触粗细选择 --> |
| | | <view class="fs-toolbar"> |
| | | <view |
| | | class="toolbar-item" |
| | | v-for="s in strokeSizes" |
| | | :key="s.value" |
| | | @tap="strokeWidth = s.value" |
| | | > |
| | | <view |
| | | class="size-dot" |
| | | :style="{ |
| | | width: s.value * 2 + 'px', |
| | | height: s.value * 2 + 'px', |
| | | background: strokeWidth == s.value ? '#0f95b0' : '#c0c4cc', |
| | | }" |
| | | ></view> |
| | | <text class="size-label" :class="{ active: strokeWidth == s.value }">{{ |
| | | s.label |
| | | }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 底部操作栏 --> |
| | | <view class="fs-footer"> |
| | | <button class="fs-btn fs-undo" :disabled="!canUndo" @tap="undoLastStroke"> |
| | | 撤销 |
| | | </button> |
| | | <button class="fs-btn fs-confirm" :disabled="!hasSignature" @tap="confirmSignature"> |
| | | 确认签名 |
| | | </button> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="showHomeButton" class="home-fab" @click="goHome"> |
| | | <uni-icons type="home" size="24" color="#fff" /> |
| | |
| | | @confirm="confirmSubmit" |
| | | @cancel="showSubmitModal = false" |
| | | ></u-modal> |
| | | |
| | | <!-- H5 内嵌 PDF 预览组件 --> |
| | | <!-- #ifdef H5 --> |
| | | <pdf-preview |
| | | :show="pdfPreview.show" |
| | | :url="pdfPreview.url" |
| | | :title="pdfPreview.title" |
| | | @close="pdfPreview.show = false" |
| | | /> |
| | | <!-- #endif --> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, reactive, onMounted, nextTick } from "vue"; |
| | | import { ref, computed, reactive, onMounted, nextTick, getCurrentInstance } from "vue"; |
| | | import { onLoad, onShow } from "@dcloudio/uni-app"; |
| | | import dayjs from "dayjs"; |
| | | import { useUserStore } from "@/stores/user"; |
| | | import { useHomeButton } from "@/stores/useHomeButton"; |
| | | // #ifdef H5 |
| | | import PdfPreview from "@/components/pdf-preview/index.vue"; |
| | | // #endif |
| | | const { showHomeButton, checkAutoLogin, goHome } = useHomeButton(); |
| | | const userStore = useUserStore(); |
| | | |
| | |
| | | const fcid = ref(null); |
| | | const materials = ref([]); |
| | | |
| | | // ==================== 新增:根据环境获取 baseUrlHt ==================== |
| | | // PDF 预览(H5 端内嵌 pdf.js 组件预览) |
| | | const pdfPreview = reactive({ show: false, url: "", title: "" }); |
| | | |
| | | // ============== 新增:根据环境获取 baseUrlHt ============== |
| | | const getBaseUrlHt = () => { |
| | | // #ifdef H5 |
| | | // H5 环境:根据浏览器地址动态拼接 |
| | | // H5 环境:文件统一走「同源」地址(window.location.origin),由部署该页面的 nginx 反向代理 |
| | | // /profile/ 到后端。这样文件与页面同源,浏览器不会触发 CORS(彻底规避后端重复 |
| | | // Access-Control-Allow-Origin 头导致的跨域报错,也规避了页面在 :8091、文件在 :443 的跨域)。 |
| | | try { |
| | | const href = window.location.href; |
| | | |
| | | // 规则 1:192.168.120.27:8091 → 120.27 + :9095 |
| | | if (href.startsWith('http://192.168.120.27:8091')) { |
| | | return 'http://192.168.120.27:9095'; |
| | | } |
| | | |
| | | // 规则 2:192.168.76.25:8091 → 76.25 + :9095 |
| | | if (href.startsWith('http://192.168.76.25:8091')) { |
| | | return 'http://www.qdopo.com:9095'; |
| | | } |
| | | |
| | | // 规则 3:其余全部走生产 |
| | | return 'https://opo.qduh.cn'; |
| | | return ( |
| | | window.location.origin || |
| | | `${window.location.protocol}//${window.location.host}` |
| | | ); |
| | | } catch { |
| | | return 'https://opo.qduh.cn'; |
| | | return "https://opo.qduh.cn"; |
| | | } |
| | | // #endif |
| | | |
| | |
| | | }); |
| | | |
| | | const conclusionOptions = ref([ |
| | | { label: "同意", value: "1" }, // 对应receiveStatus 5-完成 |
| | | { label: "驳回", value: "2" }, // 对应receiveStatus 6-驳回 |
| | | { label: "不同意", value: "0" }, |
| | | { label: "同意", value: "1" }, |
| | | { label: "弃权", value: "3" }, |
| | | { label: "修改后同意", value: "4" }, |
| | | ]); |
| | | |
| | | // 签名相关数据 |
| | |
| | | serverData: null, |
| | | }); |
| | | |
| | | // 手写签名相关 |
| | | const canvasWidth = 650; |
| | | const canvasHeight = 300; |
| | | let ctx = null; |
| | | // 手写签名相关(Canvas 2D 全屏版) |
| | | let ctx = null; // Canvas 2D 渲染上下文(同步绘制) |
| | | let canvasNode = null; // canvas 节点(用于导出图片) |
| | | let canvasCssWidth = 0; // 画布 CSS 宽度 |
| | | let canvasCssHeight = 0; // 画布 CSS 高度 |
| | | let canvasRect = { left: 0, top: 0 }; // 画布相对视口的位置(逻辑像素),用于触摸坐标换算 |
| | | let strokes = ref([]); // 所有笔画(每笔为 { points: [...], width: n },用于撤销) |
| | | let currentStroke = []; // 当前正在绘制的点数组 |
| | | let currentStrokeWidth = 4; // 当前笔触粗细(px) |
| | | 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 strokeSizes = [ |
| | | { label: "细", value: 2 }, |
| | | { label: "中", value: 4 }, |
| | | { label: "粗", value: 6 }, |
| | | ]; |
| | | const strokeWidth = ref(4); // 当前选中的粗细(对新笔画生效) |
| | | const hasSignature = ref(false); |
| | | const canUndo = computed(() => strokes.value.length > 0); |
| | | |
| | | // 上传配置 |
| | | const uploadConfig = reactive({ |
| | |
| | | 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路径 |
| | | ); |
| | |
| | | console.log(caseInfo.value.receiveStatus, "caseInfo.value"); |
| | | |
| | | // 设置表单数据 |
| | | if (data.expertconclusion) { |
| | | if ( |
| | | data.expertconclusion !== null && |
| | | data.expertconclusion !== undefined && |
| | | data.expertconclusion !== "" |
| | | ) { |
| | | form.value.expertconclusion = data.expertconclusion.toString(); |
| | | } |
| | | if (data.expertopinion) { |
| | |
| | | } |
| | | }; |
| | | |
| | | // ==================== 新增:获取完整URL(平台安全版) ==================== |
| | | // ============== 新增:获取完整URL(平台安全版) ============== |
| | | const getFullUrl = (path) => { |
| | | if (!path) return ''; |
| | | |
| | |
| | | return `${baseUrlHt}${path.startsWith('/') ? '' : '/'}${path}`; |
| | | }; |
| | | |
| | | // ==================== 修改:replaceUrlDomain 使用 getFullUrl ==================== |
| | | // ============== 修改:replaceUrlDomain 使用 getFullUrl ============== |
| | | const replaceUrlDomain = (url) => { |
| | | return getFullUrl(url); |
| | | }; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 打开签名面板 |
| | | // 打开签名面板(全屏弹框) |
| | | const openSignaturePanel = () => { |
| | | if (isReadonly.value) { |
| | | uni.showToast({ |
| | |
| | | } |
| | | |
| | | showSignaturePanel.value = true; |
| | | // 每次打开都重置签名状态,重新初始化画布 |
| | | strokes.value = []; |
| | | currentStroke = []; |
| | | hasSignature.value = false; |
| | | isDrawing = false; |
| | | ctx = null; |
| | | canvasNode = null; |
| | | // 等待全屏弹框 DOM 渲染完成后再初始化画布 |
| | | nextTick(() => { |
| | | initCanvas(); |
| | | setTimeout(() => { |
| | | initCanvas(); |
| | | }, 100); |
| | | }); |
| | | }; |
| | | |
| | | // 关闭签名面板 |
| | | const closeSignaturePanel = () => { |
| | | showSignaturePanel.value = false; |
| | | clearCanvas(); |
| | | // #ifdef H5 |
| | | // 移除原生事件监听,避免重复绑定 |
| | | if (canvasNode) { |
| | | canvasNode.removeEventListener("touchstart", onH5TouchStart); |
| | | canvasNode.removeEventListener("touchmove", onH5TouchMove); |
| | | canvasNode.removeEventListener("touchend", onH5TouchEnd); |
| | | canvasNode.removeEventListener("touchcancel", onH5TouchEnd); |
| | | canvasNode.removeEventListener("mousedown", onH5MouseDown); |
| | | canvasNode.removeEventListener("mousemove", onH5MouseMove); |
| | | canvasNode.removeEventListener("mouseup", onH5MouseUp); |
| | | } |
| | | // #endif |
| | | ctx = null; |
| | | canvasNode = null; |
| | | }; |
| | | |
| | | // 初始化画布 |
| | | // 初始化画布(Canvas 2D 同步 API) |
| | | const initCanvas = () => { |
| | | ctx = uni.createCanvasContext("signatureCanvas"); |
| | | clearCanvas(); |
| | | // #ifdef H5 |
| | | initH5Canvas(); |
| | | // #endif |
| | | |
| | | // #ifndef H5 |
| | | initMPCanvas(); |
| | | // #endif |
| | | }; |
| | | |
| | | // 清空画布 |
| | | const clearCanvas = () => { |
| | | if (!ctx) return; |
| | | // #ifdef H5 |
| | | // ============== H5 端:原生 canvas 方案 ============== |
| | | // 不用 uni-app 的 canvas 组件(其组件层对 type="2d" 的节点/坐标处理各版本不一致), |
| | | // 而是用 JS 动态创建原生 canvas + 原生事件绑定,坐标换算完全受控,从根上消除笔迹偏移。 |
| | | const h5CanvasWrap = ref(null); |
| | | |
| | | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| | | ctx.setStrokeStyle("#000000"); |
| | | ctx.setLineWidth(3); |
| | | ctx.setLineCap("round"); |
| | | ctx.setLineJoin("round"); |
| | | ctx.draw(); |
| | | strokeHistory = []; |
| | | tempSignatureData.value = ""; |
| | | }; |
| | | const initH5Canvas = () => { |
| | | // 拿容器真实 DOM:uni-app H5 里 <view> 编译成 uni-view 组件,ref 拿到的是组件实例, |
| | | // 必须用 getElementById 或 .$el 才能拿到真实 DOM,否则 appendChild 会报错、canvas 创建不出来。 |
| | | let wrap = document.getElementById("h5CanvasWrap"); |
| | | if (!wrap && h5CanvasWrap.value) { |
| | | wrap = h5CanvasWrap.value.$el || h5CanvasWrap.value; |
| | | } |
| | | if (!wrap) { |
| | | console.error("[签名] H5 画布容器获取失败(id=h5CanvasWrap 未找到)"); |
| | | return; |
| | | } |
| | | // 清空容器,避免重复初始化产生多个 canvas |
| | | wrap.innerHTML = ""; |
| | | const canvas = document.createElement("canvas"); |
| | | canvas.style.display = "block"; |
| | | canvas.style.background = "#ffffff"; |
| | | canvas.style.touchAction = "none"; |
| | | wrap.appendChild(canvas); |
| | | |
| | | // 撤销上一步 |
| | | const undoLastStroke = () => { |
| | | if (strokeHistory.length == 0) return; |
| | | // 读取容器实际渲染尺寸并让 canvas 撑满(不依赖百分比,避免 flex 容器中解析异常导致尺寸为 0) |
| | | const fit = () => { |
| | | const wr = wrap.getBoundingClientRect(); |
| | | if (!wr.width || !wr.height) return false; |
| | | canvas.style.width = wr.width + "px"; |
| | | canvas.style.height = wr.height + "px"; |
| | | return true; |
| | | }; |
| | | |
| | | // 移除最后一步 |
| | | strokeHistory.pop(); |
| | | const bind = () => { |
| | | try { |
| | | const rect = canvas.getBoundingClientRect(); |
| | | canvasNode = canvas; |
| | | canvasCssWidth = rect.width; |
| | | canvasCssHeight = rect.height; |
| | | canvasRect = { left: rect.left, top: rect.top }; |
| | | const dpr = window.devicePixelRatio || 2; |
| | | canvas.width = rect.width * dpr; |
| | | canvas.height = rect.height * dpr; |
| | | ctx = canvas.getContext("2d"); |
| | | ctx.scale(dpr, dpr); |
| | | console.log("[签名] H5 画布初始化", { |
| | | cssW: canvasCssWidth, |
| | | cssH: canvasCssHeight, |
| | | dpr, |
| | | rect: canvasRect, |
| | | }); |
| | | redraw(); |
| | | |
| | | // 重新绘制 |
| | | ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| | | ctx.setStrokeStyle("#000000"); |
| | | ctx.setLineWidth(3); |
| | | ctx.setLineCap("round"); |
| | | ctx.setLineJoin("round"); |
| | | // 触摸事件(手机/平板) |
| | | canvas.addEventListener("touchstart", onH5TouchStart, { passive: false }); |
| | | canvas.addEventListener("touchmove", onH5TouchMove, { passive: false }); |
| | | canvas.addEventListener("touchend", onH5TouchEnd); |
| | | canvas.addEventListener("touchcancel", onH5TouchEnd); |
| | | // 鼠标事件(PC 浏览器调试) |
| | | canvas.addEventListener("mousedown", onH5MouseDown); |
| | | canvas.addEventListener("mousemove", onH5MouseMove); |
| | | canvas.addEventListener("mouseup", onH5MouseUp); |
| | | } catch (err) { |
| | | console.error("[签名] H5 画布初始化失败:", err); |
| | | } |
| | | }; |
| | | |
| | | // 绘制历史笔迹 |
| | | 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 = ""; |
| | | // 布局可能尚未稳定(弹框刚显示),尺寸为 0 时重试几次 |
| | | if (fit()) { |
| | | bind(); |
| | | } else { |
| | | getCanvasImage(); |
| | | let tries = 0; |
| | | const timer = setInterval(() => { |
| | | tries++; |
| | | if (fit() || tries >= 10) { |
| | | clearInterval(timer); |
| | | if (fit()) { |
| | | bind(); |
| | | } else { |
| | | console.error("[签名] H5 画布容器尺寸始终为 0,初始化失败"); |
| | | } |
| | | } |
| | | }, 50); |
| | | } |
| | | }; |
| | | |
| | | // 触摸开始 |
| | | 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); |
| | | // H5 触摸坐标:实时取 getBoundingClientRect,与触摸视口坐标同源换算 |
| | | let lastH5TouchEndTime = 0; // 记录触摸结束时间,用于屏蔽触摸后浏览器派发的兼容 mouse 事件 |
| | | const onH5TouchStart = (e) => { |
| | | if (!ctx || isReadonly.value) return; |
| | | const t = e.touches[0]; |
| | | if (!t) return; |
| | | const rect = canvasNode.getBoundingClientRect(); |
| | | beginStroke({ x: t.clientX - rect.left, y: t.clientY - rect.top }); |
| | | }; |
| | | |
| | | // 触摸移动 |
| | | const onTouchMove = (e) => { |
| | | if (!isDrawing || isReadonly.value) return; |
| | | const onH5TouchMove = (e) => { |
| | | e.preventDefault(); |
| | | if (!isDrawing || !ctx) return; |
| | | const t = e.touches[0]; |
| | | if (!t) return; |
| | | const rect = canvasNode.getBoundingClientRect(); |
| | | continueStroke({ x: t.clientX - rect.left, y: t.clientY - rect.top }); |
| | | }; |
| | | |
| | | const touch = e.touches[0]; |
| | | const currentX = touch.x; |
| | | const currentY = touch.y; |
| | | const onH5TouchEnd = () => { |
| | | lastH5TouchEndTime = Date.now(); |
| | | endStroke(); |
| | | }; |
| | | |
| | | ctx.lineTo(currentX, currentY); |
| | | ctx.stroke(); |
| | | ctx.draw(true); |
| | | // H5 鼠标事件(PC 调试用) |
| | | let h5MouseDown = false; |
| | | const onH5MouseDown = (e) => { |
| | | // 触摸结束后 500ms 内的兼容 mouse 事件直接忽略,避免平板/手机上画多余的点 |
| | | if (Date.now() - lastH5TouchEndTime < 500) return; |
| | | if (!ctx || isReadonly.value) return; |
| | | h5MouseDown = true; |
| | | const rect = canvasNode.getBoundingClientRect(); |
| | | beginStroke({ x: e.clientX - rect.left, y: e.clientY - rect.top }); |
| | | }; |
| | | |
| | | // 保存笔迹到历史记录 |
| | | strokeHistory.push({ |
| | | startX: lastX, |
| | | startY: lastY, |
| | | endX: currentX, |
| | | endY: currentY, |
| | | const onH5MouseMove = (e) => { |
| | | if (!h5MouseDown || !isDrawing || !ctx) return; |
| | | const rect = canvasNode.getBoundingClientRect(); |
| | | continueStroke({ x: e.clientX - rect.left, y: e.clientY - rect.top }); |
| | | }; |
| | | |
| | | const onH5MouseUp = () => { |
| | | h5MouseDown = false; |
| | | endStroke(); |
| | | }; |
| | | // #endif |
| | | |
| | | // #ifndef H5 |
| | | // ============== 小程序/App 端:canvas 2d ============== |
| | | // 一次 fields({ node, rect }) 查询同时拿到 node 和「逻辑像素」位置/尺寸, |
| | | // 避免 node 与 boundingClientRect 两次查询之间布局变化导致基准错位。 |
| | | const initMPCanvas = () => { |
| | | const instance = getCurrentInstance().proxy; |
| | | const query = uni.createSelectorQuery().in(instance); |
| | | query |
| | | .select("#signatureCanvas") |
| | | .fields({ node: true, rect: true }) |
| | | .exec((res) => { |
| | | const node = res && res[0] && res[0].node; |
| | | const rect = res && res[0] && res[0].rect; |
| | | if (!node) { |
| | | console.error("[签名] 画布节点获取失败:", res); |
| | | uni.showToast({ title: "签名初始化失败,请重试", icon: "none" }); |
| | | return; |
| | | } |
| | | if (rect && rect.width > 0) { |
| | | // 一次查询同时拿到节点和位置(逻辑像素),避免两次查询间布局变化导致基准错位 |
| | | canvasRect = { left: rect.left, top: rect.top }; |
| | | setupCanvas(node, rect.width, rect.height); |
| | | return; |
| | | } |
| | | // 部分基础库 fields 拿不到 rect,兜底单独查一次 boundingClientRect |
| | | uni |
| | | .createSelectorQuery() |
| | | .in(instance) |
| | | .select("#signatureCanvas") |
| | | .boundingClientRect((r) => { |
| | | if (r && r.width > 0) { |
| | | canvasRect = { left: r.left, top: r.top }; |
| | | setupCanvas(node, r.width, r.height); |
| | | } else { |
| | | console.error("[签名] 画布尺寸获取失败:", r); |
| | | uni.showToast({ title: "签名初始化失败,请重试", icon: "none" }); |
| | | } |
| | | }) |
| | | .exec(); |
| | | }); |
| | | }; |
| | | |
| | | // 每次落笔前刷新画布基准位置,防御页面滚动/布局变化导致的偏移 |
| | | const refreshCanvasRect = () => { |
| | | uni |
| | | .createSelectorQuery() |
| | | .in(getCurrentInstance().proxy) |
| | | .select("#signatureCanvas") |
| | | .boundingClientRect((rect) => { |
| | | if (rect && rect.left !== undefined) { |
| | | canvasRect = { left: rect.left, top: rect.top }; |
| | | } |
| | | }) |
| | | .exec(); |
| | | }; |
| | | // #endif |
| | | |
| | | // 配置画布(设置物理像素,防止模糊) |
| | | const setupCanvas = (node, cssW, cssH) => { |
| | | canvasNode = node; |
| | | canvasCssWidth = cssW; |
| | | canvasCssHeight = cssH; |
| | | const dpr = uni.getSystemInfoSync().pixelRatio || 2; |
| | | node.width = cssW * dpr; |
| | | node.height = cssH * dpr; |
| | | ctx = node.getContext("2d"); |
| | | ctx.scale(dpr, dpr); |
| | | // 调试用:打印画布尺寸,排查签名偏移(同时可据此判断浏览器跑的是不是最新包) |
| | | console.log("[签名] 画布初始化", { |
| | | cssW, |
| | | cssH, |
| | | dpr, |
| | | bufferW: node.width, |
| | | bufferH: node.height, |
| | | }); |
| | | redraw(); |
| | | }; |
| | | |
| | | lastX = currentX; |
| | | lastY = currentY; |
| | | // 重绘所有笔画(Canvas 2D 同步 API,无丢帧问题) |
| | | const redraw = () => { |
| | | if (!ctx) return; |
| | | |
| | | // 白色背景(导出图片时即为白底黑字);先 clearRect 彻底清空再铺白底,确保撤销/清空生效 |
| | | ctx.clearRect(0, 0, canvasCssWidth, canvasCssHeight); |
| | | ctx.fillStyle = "#ffffff"; |
| | | ctx.fillRect(0, 0, canvasCssWidth, canvasCssHeight); |
| | | |
| | | // 线条通用样式 |
| | | ctx.strokeStyle = "#000000"; |
| | | ctx.lineCap = "round"; |
| | | ctx.lineJoin = "round"; |
| | | |
| | | // 重绘每一笔(每笔记录自己的粗细,切换粗细不影响已画笔画) |
| | | strokes.value.forEach((stroke) => { |
| | | const points = stroke.points; |
| | | ctx.lineWidth = stroke.width; |
| | | if (points.length == 1) { |
| | | drawDot(points[0], stroke.width); |
| | | } else if (points.length > 1) { |
| | | ctx.beginPath(); |
| | | ctx.moveTo(points[0].x, points[0].y); |
| | | for (let i = 1; i < points.length; i++) { |
| | | ctx.lineTo(points[i].x, points[i].y); |
| | | } |
| | | ctx.stroke(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 画一个圆点(处理单击不出墨的情况,半径随笔触粗细) |
| | | const drawDot = (p, w) => { |
| | | const r = Math.max(1, (w || 3) / 2); |
| | | ctx.beginPath(); |
| | | ctx.arc(p.x, p.y, r, 0, Math.PI * 2); |
| | | ctx.fillStyle = "#000000"; |
| | | ctx.fill(); |
| | | }; |
| | | |
| | | // 获取触摸点相对画布的坐标(小程序/App 端:视口坐标 - 画布位置) |
| | | // #ifndef H5 |
| | | const getTouchPos = (e) => { |
| | | const t = e.touches[0] || e.changedTouches[0]; |
| | | if (!t) return { x: 0, y: 0 }; |
| | | const cx = typeof t.clientX == "number" ? t.clientX : t.pageX; |
| | | const cy = typeof t.clientY == "number" ? t.clientY : t.pageY; |
| | | return { x: cx - canvasRect.left, y: cy - canvasRect.top }; |
| | | }; |
| | | // #endif |
| | | |
| | | // ============== 共用绘制逻辑(H5 原生事件与小程序组件事件共用) ============== |
| | | // 落笔 |
| | | const beginStroke = (pos) => { |
| | | if (!ctx || isReadonly.value) return; |
| | | isDrawing = true; |
| | | currentStroke = [pos]; |
| | | currentStrokeWidth = strokeWidth.value; // 记录本笔的粗细 |
| | | strokes.value.push({ points: currentStroke, width: currentStrokeWidth }); |
| | | hasSignature.value = true; |
| | | ctx.lineWidth = currentStrokeWidth; |
| | | drawDot(pos, currentStrokeWidth); |
| | | }; |
| | | |
| | | // 续笔 |
| | | const continueStroke = (pos) => { |
| | | if (!isDrawing || !ctx) return; |
| | | const prev = currentStroke[currentStroke.length - 1]; |
| | | if (prev) { |
| | | ctx.beginPath(); |
| | | ctx.lineWidth = currentStrokeWidth; |
| | | ctx.moveTo(prev.x, prev.y); |
| | | ctx.lineTo(pos.x, pos.y); |
| | | ctx.stroke(); |
| | | } |
| | | currentStroke.push(pos); |
| | | }; |
| | | |
| | | // 收笔 |
| | | const endStroke = () => { |
| | | if (!isDrawing) return; |
| | | isDrawing = false; |
| | | currentStroke = []; |
| | | }; |
| | | |
| | | // 触摸开始(小程序/App 端组件事件) |
| | | // #ifndef H5 |
| | | const onTouchStart = (e) => { |
| | | if (!ctx || isReadonly.value) return; |
| | | // 落笔前异步刷新画布基准位置,防御页面滚动/布局变化导致的偏移 |
| | | refreshCanvasRect(); |
| | | const pos = getTouchPos(e); |
| | | beginStroke(pos); |
| | | }; |
| | | |
| | | // 触摸移动(Canvas 2D 同步绘制,线条实时连续) |
| | | const onTouchMove = (e) => { |
| | | if (!isDrawing || !ctx) return; |
| | | const pos = getTouchPos(e); |
| | | continueStroke(pos); |
| | | }; |
| | | |
| | | // 触摸结束 |
| | | const onTouchEnd = () => { |
| | | if (!isDrawing) return; |
| | | endStroke(); |
| | | }; |
| | | // #endif |
| | | |
| | | isDrawing = false; |
| | | ctx.closePath(); |
| | | |
| | | // 更新预览 |
| | | getCanvasImage(); |
| | | // 清空画布 |
| | | const clearCanvas = () => { |
| | | strokes.value = []; |
| | | currentStroke = []; |
| | | hasSignature.value = false; |
| | | redraw(); |
| | | }; |
| | | |
| | | // 获取画布图片 |
| | | // 撤销上一笔 |
| | | const undoLastStroke = () => { |
| | | if (strokes.value.length == 0) return; |
| | | strokes.value.pop(); |
| | | hasSignature.value = strokes.value.length > 0; |
| | | redraw(); |
| | | }; |
| | | |
| | | // 生成签名图片(Promise 形式,返回临时文件路径) |
| | | const getCanvasImage = () => { |
| | | uni.canvasToTempFilePath({ |
| | | canvasId: "signatureCanvas", |
| | | success: (res) => { |
| | | tempSignatureData.value = res.tempFilePath; |
| | | }, |
| | | fail: (err) => { |
| | | console.error("获取画布图片失败:", err); |
| | | }, |
| | | return new Promise((resolve, reject) => { |
| | | if (!canvasNode) { |
| | | reject(new Error("画布未初始化")); |
| | | return; |
| | | } |
| | | // #ifdef MP-WEIXIN |
| | | uni.canvasToTempFilePath({ |
| | | canvas: canvasNode, |
| | | fileType: "png", |
| | | success: (res) => resolve(res.tempFilePath), |
| | | fail: (err) => { |
| | | console.error("获取画布图片失败:", err); |
| | | reject(err); |
| | | }, |
| | | }); |
| | | // #endif |
| | | // #ifndef MP-WEIXIN |
| | | try { |
| | | canvasNode.toBlob( |
| | | (blob) => { |
| | | if (blob) { |
| | | // H5 端:生成带 .png 扩展名的 File 对象,上传时才能保留图片格式 |
| | | if (typeof File !== "undefined") { |
| | | const file = new File([blob], `signature_${Date.now()}.png`, { |
| | | type: "image/png", |
| | | }); |
| | | resolve(file); |
| | | } else { |
| | | resolve(URL.createObjectURL(blob)); |
| | | } |
| | | } else { |
| | | reject(new Error("生成图片失败")); |
| | | } |
| | | }, |
| | | "image/png", |
| | | ); |
| | | } catch (err) { |
| | | reject(err); |
| | | } |
| | | // #endif |
| | | }); |
| | | }; |
| | | |
| | | // 确认签名 |
| | | const confirmSignature = async () => { |
| | | if (!tempSignatureData.value) { |
| | | if (!hasSignature.value) { |
| | | uni.showToast({ |
| | | title: "请先签名", |
| | | icon: "none", |
| | |
| | | try { |
| | | uni.showLoading({ title: "保存签名中..." }); |
| | | |
| | | // 生成签名图片 |
| | | const filePath = await getCanvasImage(); |
| | | |
| | | // 上传签名文件 |
| | | const uploadResult = await uploadSignature(tempSignatureData.value); |
| | | const uploadResult = await uploadSignature(filePath); |
| | | |
| | | if (uploadResult) { |
| | | // 使用 getFullUrl 处理签名URL |
| | |
| | | }); |
| | | |
| | | showSignaturePanel.value = false; |
| | | clearCanvas(); |
| | | ctx = null; |
| | | canvasNode = null; |
| | | } else { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | |
| | | const expertName = |
| | | userInfo?.nickName || userInfo?.userName || expertInfo.value.name; |
| | | |
| | | // 统一解析上传成功的响应 |
| | | const parseUploadData = (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 || "上传失败")); |
| | | } |
| | | }; |
| | | |
| | | // #ifdef H5 |
| | | // H5 端 filePath 为带 .png 扩展名的 File 对象,用 FormData 手动上传以保留图片格式 |
| | | if (typeof File !== "undefined" && filePath instanceof File) { |
| | | const formData = new FormData(); |
| | | formData.append("file", filePath, filePath.name); |
| | | formData.append("bizType", "expert_review_signature"); |
| | | formData.append("caseNo", caseInfo.value.caseNo || ""); |
| | | formData.append("expertName", expertName); |
| | | formData.append("uploadType", "signature"); |
| | | |
| | | const xhr = new XMLHttpRequest(); |
| | | xhr.open("POST", "/api/common/upload"); |
| | | xhr.setRequestHeader("Authorization", `Bearer ${token}`); |
| | | xhr.onload = () => { |
| | | if (xhr.status === 200) { |
| | | try { |
| | | parseUploadData(JSON.parse(xhr.responseText)); |
| | | } catch (e) { |
| | | reject(new Error("上传响应解析失败")); |
| | | } |
| | | } else { |
| | | reject(new Error(`上传失败,状态码: ${xhr.status}`)); |
| | | } |
| | | }; |
| | | xhr.onerror = () => reject(new Error("上传失败")); |
| | | xhr.send(formData); |
| | | return; |
| | | } |
| | | // #endif |
| | | |
| | | uni.uploadFile({ |
| | | url: "/api/common/upload", |
| | | filePath: filePath, |
| | |
| | | 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 || "上传失败")); |
| | | } |
| | | parseUploadData(data); |
| | | } else { |
| | | reject(new Error(`上传失败,状态码: ${res.statusCode}`)); |
| | | } |
| | |
| | | }); |
| | | }; |
| | | |
| | | // ==================== 修改:预览签名 ==================== |
| | | // ============== 修改:预览签名 ============== |
| | | const previewSignature = () => { |
| | | if (signatureData.signatureUrl) { |
| | | // 使用 getFullUrl 处理URL |
| | |
| | | } |
| | | }; |
| | | |
| | | // ==================== 修改:预览材料 ==================== |
| | | // ============== 修改:预览材料 ============== |
| | | const previewMaterial = (material) => { |
| | | if (material.url) { |
| | | uni.showLoading({ title: "加载中..." }); |
| | |
| | | material.type || material.url.split(".").pop().toLowerCase(); |
| | | |
| | | // 使用 getFullUrl 处理URL |
| | | const fileUrl = getFullUrl(material.url); |
| | | let fileUrl = getFullUrl(material.url); |
| | | |
| | | // URL 编码(处理中文文件名,避免双重编码) |
| | | try { |
| | | const urlObj = new URL(fileUrl); |
| | | // decodeURI 先解码已有的编码,encodeURI 重新编码,确保只编码一次 |
| | | fileUrl = urlObj.origin + encodeURI(decodeURI(urlObj.pathname)) + (urlObj.search || ""); |
| | | } catch { |
| | | // 降级 |
| | | try { |
| | | fileUrl = encodeURI(decodeURI(fileUrl)); |
| | | } catch { |
| | | fileUrl = encodeURI(fileUrl); |
| | | } |
| | | } |
| | | |
| | | // 判断文件类型 |
| | | if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(fileExt)) { |
| | |
| | | fileExt, |
| | | ) |
| | | ) { |
| | | // 文档文件下载后打开 |
| | | // 文档文件处理 |
| | | // #ifdef H5 |
| | | // H5 环境:直接用浏览器打开,支持 PDF 在线预览 |
| | | uni.hideLoading(); |
| | | if (fileExt == "pdf") { |
| | | // PDF 用 pdf.js 组件内嵌预览,避免手机浏览器直接触发下载 |
| | | pdfPreview.url = fileUrl; |
| | | pdfPreview.title = material.name || "PDF 预览"; |
| | | pdfPreview.show = true; |
| | | } else { |
| | | // 其他文档用 <a> 标签触发下载 |
| | | const link = document.createElement("a"); |
| | | link.href = fileUrl; |
| | | link.download = material.name || material.url.split("/").pop(); |
| | | link.target = "_blank"; |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | } |
| | | // #endif |
| | | |
| | | // #ifdef MP-WEIXIN |
| | | // 小程序环境:下载后用 openDocument 打开 |
| | | const token = uni.getStorageSync("token") || ""; |
| | | uni.downloadFile({ |
| | | url: fileUrl, |
| | | header: { |
| | | Authorization: token ? `Bearer ${token}` : "", |
| | | }, |
| | | success: (res) => { |
| | | if (res.statusCode == 200) { |
| | | const filePath = res.tempFilePath; |
| | |
| | | }); |
| | | }, |
| | | }); |
| | | } else { |
| | | console.error("下载状态码异常:", res.statusCode); |
| | | uni.showToast({ |
| | | title: `服务器返回异常(${res.statusCode})`, |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error("下载文件失败:", err); |
| | | console.error("下载URL:", fileUrl); |
| | | uni.showToast({ |
| | | title: "文件下载失败", |
| | | title: "文件下载失败,请检查网络或域名配置", |
| | | icon: "none", |
| | | duration: 3000, |
| | | }); |
| | | }, |
| | | complete: () => { |
| | | uni.hideLoading(); |
| | | }, |
| | | }); |
| | | // #endif |
| | | } else { |
| | | // 其他文件类型 |
| | | uni.showToast({ |
| | |
| | | expertopinion: form.value.expertopinion, |
| | | sigin: signatureData.signatureUrl, |
| | | expertType: caseInfo.value.expertType, |
| | | // 根据结论设置状态 |
| | | receiveStatus: form.value.expertconclusion == "1" ? "5" : "6", // 5-完成, 6-驳回 |
| | | // 根据结论设置状态:同意/修改后同意 → 完成(5),不同意/弃权 → 驳回(6) |
| | | receiveStatus: |
| | | form.value.expertconclusion == "1" || form.value.expertconclusion == "4" |
| | | ? "5" |
| | | : "6", |
| | | conclusiontime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | // 签名模态框样式 |
| | | .signature-modal { |
| | | padding: 30rpx; |
| | | background: #fff; |
| | | border-radius: 20rpx 20rpx 0 0; |
| | | max-height: 80vh; |
| | | overflow: hidden; |
| | | // 全屏签名弹框样式 |
| | | .fullscreen-signature { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 10080; |
| | | background: #f5f7fa; |
| | | display: flex; |
| | | flex-direction: column; |
| | | // 适配刘海屏/状态栏 |
| | | padding-top: constant(safe-area-inset-top); |
| | | padding-top: env(safe-area-inset-top); |
| | | |
| | | .modal-header { |
| | | text-align: center; |
| | | margin-bottom: 30rpx; |
| | | .fs-header { |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 0 30rpx; |
| | | background: #fff; |
| | | border-bottom: 1rpx solid #ebeef5; |
| | | |
| | | .modal-title { |
| | | .fs-title { |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | display: block; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .modal-subtitle { |
| | | font-size: 24rpx; |
| | | color: #909399; |
| | | .fs-header-btn { |
| | | font-size: 28rpx; |
| | | padding: 10rpx 20rpx; |
| | | |
| | | &.fs-cancel { |
| | | color: #606266; |
| | | } |
| | | |
| | | &.fs-clear { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .signature-canvas-container { |
| | | .signature-canvas { |
| | | .fs-body { |
| | | flex: 1; |
| | | position: relative; |
| | | margin: 20rpx; |
| | | border-radius: 12rpx; |
| | | overflow: hidden; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); |
| | | |
| | | .fs-canvas { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: block; |
| | | margin: 0 auto; |
| | | border: 2rpx solid #e4e7ed; |
| | | border-radius: 8rpx; |
| | | background: #fff; |
| | | // H5 端防止触摸时页面滚动 |
| | | 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); |
| | | } |
| | | } |
| | | // H5 端原生 canvas 容器(canvas 由 JS 创建并撑满) |
| | | .fs-canvas-wrap { |
| | | width: 100%; |
| | | height: 100%; |
| | | background: #fff; |
| | | } |
| | | |
| | | .signature-preview { |
| | | margin-top: 20rpx; |
| | | padding: 20rpx; |
| | | background: #f8f9fa; |
| | | border-radius: 8rpx; |
| | | .fs-hint { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 16rpx; |
| | | color: #c0c4cc; |
| | | font-size: 28rpx; |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .preview-title { |
| | | display: block; |
| | | font-size: 24rpx; |
| | | color: #606266; |
| | | margin-bottom: 12rpx; |
| | | // 笔触粗细选择栏 |
| | | .fs-toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 48rpx; |
| | | padding: 16rpx 30rpx; |
| | | background: #fff; |
| | | border-top: 1rpx solid #f0f2f5; |
| | | |
| | | .toolbar-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | padding: 8rpx 16rpx; |
| | | |
| | | .size-dot { |
| | | border-radius: 50%; |
| | | transition: background 0.2s; |
| | | } |
| | | |
| | | .preview-image { |
| | | width: 200rpx; |
| | | height: 80rpx; |
| | | border: 1rpx solid #dcdfe6; |
| | | border-radius: 4rpx; |
| | | .size-label { |
| | | font-size: 22rpx; |
| | | color: #909399; |
| | | |
| | | &.active { |
| | | color: #0f95b0; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | &:active { |
| | | opacity: 0.7; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .fs-footer { |
| | | display: flex; |
| | | gap: 24rpx; |
| | | padding: 20rpx 30rpx; |
| | | padding-bottom: calc(20rpx + constant(safe-area-inset-bottom)); |
| | | padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | border-top: 1rpx solid #ebeef5; |
| | | |
| | | .fs-btn { |
| | | flex: 1; |
| | | height: 88rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 30rpx; |
| | | border-radius: 44rpx; |
| | | margin: 0; |
| | | line-height: 1; |
| | | |
| | | &.fs-undo { |
| | | color: #1890ff; |
| | | background: #fff; |
| | | border: 1rpx solid #1890ff; |
| | | |
| | | &:disabled { |
| | | color: #c0c4cc; |
| | | border-color: #e4e7ed; |
| | | background: #f5f7fa; |
| | | } |
| | | } |
| | | |
| | | &.fs-confirm { |
| | | background: linear-gradient(135deg, #0f95b0, #89c4c1); |
| | | color: #fff; |
| | | border: none; |
| | | |
| | | &:disabled { |
| | | background: #c0c4cc; |
| | | opacity: 0.6; |
| | | } |
| | | } |
| | | |
| | | &::after { |
| | | border: none; |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | .signature-modal { |
| | | padding: 20rpx; |
| | | .fullscreen-signature { |
| | | .fs-body { |
| | | margin: 12rpx; |
| | | } |
| | | |
| | | .signature-canvas-container { |
| | | .signature-canvas { |
| | | width: 100% !important; |
| | | height: 250rpx !important; |
| | | } |
| | | |
| | | .canvas-actions { |
| | | // flex-direction: column; |
| | | gap: 12rpx; |
| | | padding: 0; |
| | | |
| | | .action-btn { |
| | | height: 72rpx; |
| | | } |
| | | } |
| | | .fs-footer { |
| | | gap: 16rpx; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | </style> |