|
|
@@ -0,0 +1,312 @@
|
|
|
+<template>
|
|
|
+ <view class="signature-page">
|
|
|
+ <!-- H5 降级提示:不支持 screen.orientation.lock 时提示横置设备(不报错) -->
|
|
|
+ <view v-if="h5DegradeHint" class="rotate-hint">
|
|
|
+ <text class="rotate-hint-text">🔄 请横置设备以获得最佳签名体验</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 顶部标题 -->
|
|
|
+ <view class="sig-header">
|
|
|
+ <text class="sig-title">✍️ 手写签名</text>
|
|
|
+ <text class="sig-subtitle">请在下方空白区域书写签名,完成后点击「确认」</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 画布区(横屏铺满,高度按视口短边计算) -->
|
|
|
+ <view class="sig-canvas-area">
|
|
|
+ <hand-signature
|
|
|
+ ref="signaturePad"
|
|
|
+ :value="prefill"
|
|
|
+ :require-agree="requireAgree"
|
|
|
+ :agree-text="agreeText"
|
|
|
+ :height="canvasHeight"
|
|
|
+ @confirm="onConfirm"
|
|
|
+ @clear="onClear"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部固定操作栏:重写 / 确认 / 返回 -->
|
|
|
+ <view class="sig-footer">
|
|
|
+ <button class="sig-btn sig-btn-back" @click="onBack">返回</button>
|
|
|
+ <button class="sig-btn sig-btn-clear" @click="onRewrite">重写</button>
|
|
|
+ <button class="sig-btn sig-btn-confirm" @click="onPageConfirm">确认</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+/*
|
|
|
+ * 独立横屏全屏签名页(WI-1/WI-2)
|
|
|
+ * - 复用通用 HandSignature 组件(只读复用,不改组件本身)
|
|
|
+ * - 入参:prefill 经 utils/signature-bus.js 模块级写入(base64 过长不进 URL);
|
|
|
+ * requireAgree / agreeText 走 URL 参数
|
|
|
+ * - 横屏:微信 wx.setPageOrientation({orientation:'landscape'});
|
|
|
+ * H5 screen.orientation.lock('landscape') 包裹 try/catch,不支持时显示「请横置设备」提示
|
|
|
+ * - 回传:onConfirm 经 getOpenerEventChannel().emit('signatureConfirm', dataUrl) 后 navigateBack;
|
|
|
+ * 返回按钮直接 navigateBack(不回传)
|
|
|
+ */
|
|
|
+import HandSignature from '@/components/hand-signature/hand-signature.vue'
|
|
|
+import { consumeSignaturePrefill } from '@/utils/signature-bus.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: { HandSignature },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 预填签名(base64 data URL,编辑/重新签名场景回显)
|
|
|
+ prefill: '',
|
|
|
+ // 是否要求先勾选同意声明
|
|
|
+ requireAgree: false,
|
|
|
+ // 同意声明文案
|
|
|
+ agreeText: '我已阅读并确认以上内容真实、准确',
|
|
|
+ // 画布高度(px,按横屏视口高度计算)
|
|
|
+ canvasHeight: 200,
|
|
|
+ // H5 无法锁横屏时降级提示
|
|
|
+ h5DegradeHint: false,
|
|
|
+ // 防止重复 navigateBack
|
|
|
+ _backed: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad: function(options) {
|
|
|
+ options = options || {}
|
|
|
+ // 读取签名页预填值(读后即清空,避免残留串扰)
|
|
|
+ this.prefill = consumeSignaturePrefill()
|
|
|
+ // URL 参数:requireAgree=1 / agreeText(中文已编码,运行时会自动解码,此处兜底解码一次)
|
|
|
+ this.requireAgree = options.requireAgree === '1' || options.requireAgree === 1
|
|
|
+ if (options.agreeText) {
|
|
|
+ var text = this.decodeURIComponentSafe(String(options.agreeText))
|
|
|
+ if (text) this.agreeText = text
|
|
|
+ }
|
|
|
+ this.canvasHeight = this.computeCanvasHeight()
|
|
|
+ this.enterLandscape()
|
|
|
+ },
|
|
|
+ onUnload: function() {
|
|
|
+ // 离开签名页恢复竖屏,不污染其它页面
|
|
|
+ this.restorePortrait()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 兼容运行时未解码/已解码两种情况(含特殊字符时兜底不抛错)
|
|
|
+ decodeURIComponentSafe: function(text) {
|
|
|
+ var out = text
|
|
|
+ try {
|
|
|
+ out = decodeURIComponent(text)
|
|
|
+ } catch (e) {
|
|
|
+ out = text
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ },
|
|
|
+ // 横屏视口高度(短边)减去顶部标题/底部操作栏/同意声明占位,得到画布可书写高度
|
|
|
+ computeCanvasHeight: function() {
|
|
|
+ var win = null
|
|
|
+ try {
|
|
|
+ if (typeof uni.getWindowInfo === 'function') {
|
|
|
+ win = uni.getWindowInfo()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ win = null
|
|
|
+ }
|
|
|
+ var vh = (win && win.windowHeight) ? win.windowHeight : 400
|
|
|
+ // 顶部标题 ~66px + 底部操作栏 92px + 同意声明 ~40px + 留白 16px
|
|
|
+ var reserved = this.requireAgree ? 214 : 174
|
|
|
+ var h = parseInt(vh, 10) - reserved
|
|
|
+ return h > 160 ? h : 160
|
|
|
+ },
|
|
|
+ // 进入签名页:切横屏(平台分流,条件编译)
|
|
|
+ enterLandscape: function() {
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ try {
|
|
|
+ if (typeof wx !== 'undefined' && typeof wx.setPageOrientation === 'function') {
|
|
|
+ wx.setPageOrientation({
|
|
|
+ orientation: 'landscape'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ // #endif
|
|
|
+ // #ifdef H5
|
|
|
+ try {
|
|
|
+ var so = (typeof window !== 'undefined' && window.screen) ? window.screen.orientation : null
|
|
|
+ if (so && typeof so.lock === 'function') {
|
|
|
+ var p = so.lock('landscape')
|
|
|
+ if (p && typeof p.catch === 'function') {
|
|
|
+ var self = this
|
|
|
+ p.catch(function() {
|
|
|
+ // 如非全屏/浏览器不支持 → 降级提示横置设备,不报错
|
|
|
+ self.h5DegradeHint = true
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.h5DegradeHint = true
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.h5DegradeHint = true
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+ // 离开签名页:恢复竖屏
|
|
|
+ restorePortrait: function() {
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ try {
|
|
|
+ if (typeof wx !== 'undefined' && typeof wx.setPageOrientation === 'function') {
|
|
|
+ wx.setPageOrientation({
|
|
|
+ orientation: 'portrait'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ // #endif
|
|
|
+ // #ifdef H5
|
|
|
+ try {
|
|
|
+ var so = (typeof window !== 'undefined' && window.screen) ? window.screen.orientation : null
|
|
|
+ if (so && typeof so.unlock === 'function') {
|
|
|
+ so.unlock()
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+ // HandSignature 确认导出成功(内部「确认签名」按钮已被隐藏,由页面「确认」驱动 handleConfirm 走到这里)
|
|
|
+ onConfirm: function(dataUrl) {
|
|
|
+ var channel = this.getOpenerEventChannel ? this.getOpenerEventChannel() : null
|
|
|
+ if (channel && typeof channel.emit === 'function') {
|
|
|
+ channel.emit('signatureConfirm', dataUrl || '')
|
|
|
+ }
|
|
|
+ this.doBack()
|
|
|
+ },
|
|
|
+ onClear: function() {
|
|
|
+ // 画布被清空(重写触发):无额外处理
|
|
|
+ },
|
|
|
+ // 页面「重写」:调用组件清空画布
|
|
|
+ onRewrite: function() {
|
|
|
+ var pad = this.$refs.signaturePad
|
|
|
+ if (pad && typeof pad.handleClear === 'function') {
|
|
|
+ pad.handleClear()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 页面「确认」:校验后再触发组件导出
|
|
|
+ onPageConfirm: function() {
|
|
|
+ var pad = this.$refs.signaturePad
|
|
|
+ if (!pad) return
|
|
|
+ // 明确判定组件内既无绘制也无预填签名时拦截,避免导出一张空白图;
|
|
|
+ // 读取不到内部状态时放行(组件自身校验不同意/无绘制场景)
|
|
|
+ if (pad.hasContent === false && !pad.currentDataUrl) {
|
|
|
+ uni.showToast({ title: '请先完成手写签名', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (typeof pad.handleConfirm === 'function') {
|
|
|
+ pad.handleConfirm()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 页面「返回」:不回传直接退出
|
|
|
+ onBack: function() {
|
|
|
+ this.doBack()
|
|
|
+ },
|
|
|
+ doBack: function() {
|
|
|
+ if (this._backed) return
|
|
|
+ this._backed = true
|
|
|
+ uni.navigateBack()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.signature-page {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ background: $app-bg;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* H5 降级提示(绝对定位,不占布局高度) */
|
|
|
+.rotate-hint {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ left: 12px;
|
|
|
+ right: 12px;
|
|
|
+ z-index: 20;
|
|
|
+ padding: 10px 16px;
|
|
|
+ background: $brand-primary-soft;
|
|
|
+ border: 2rpx solid $brand-primary;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.rotate-hint-text {
|
|
|
+ font-size: calc(26rpx * var(--font-scale, 1));
|
|
|
+ color: $brand-primary;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部标题 */
|
|
|
+.sig-header {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 16px 16px 6px;
|
|
|
+}
|
|
|
+.sig-title {
|
|
|
+ display: block;
|
|
|
+ font-size: calc(32rpx * var(--font-scale, 1));
|
|
|
+ font-weight: bold;
|
|
|
+ color: $facade-text-strong;
|
|
|
+}
|
|
|
+.sig-subtitle {
|
|
|
+ display: block;
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-muted;
|
|
|
+}
|
|
|
+
|
|
|
+/* 画布区:占满剩余空间 */
|
|
|
+.sig-canvas-area {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 0 16px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部固定操作栏 */
|
|
|
+.sig-footer {
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 92px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 12px;
|
|
|
+ background: $app-surface;
|
|
|
+ box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.sig-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 72px;
|
|
|
+ line-height: 72px;
|
|
|
+ margin: 0 6px;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ font-size: calc(30rpx * var(--font-scale, 1));
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.sig-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.sig-btn-back {
|
|
|
+ background: $app-surface;
|
|
|
+ color: $facade-text-body;
|
|
|
+ border: 2rpx solid $facade-fill-2;
|
|
|
+}
|
|
|
+.sig-btn-clear {
|
|
|
+ background: $facade-fill-2;
|
|
|
+ color: $facade-text-strong;
|
|
|
+}
|
|
|
+.sig-btn-confirm {
|
|
|
+ background: linear-gradient(135deg, $accent-indigo 0%, $accent-indigo-deep 100%);
|
|
|
+ color: $app-surface;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<!--
|
|
|
+ 非 scoped 样式:穿透 HandSignature 内部,隐藏其自带的「重写 / 确认签名」按钮行,
|
|
|
+ 改由签名页底部固定操作栏统一承载(满足「底部操作栏:重写/确认/返回」需求)。
|
|
|
+ 可行性依据:uni-app 编译的自定义组件默认 addGlobalClass=true(等价 styleIsolation:
|
|
|
+ apply-shared),页面样式可作用到组件内部;H5 无样式隔离,故以 .signature-page
|
|
|
+ 祖先限定作用域避免影响其它页面。
|
|
|
+-->
|
|
|
+<style lang="scss">
|
|
|
+.signature-page .hand-signature .signature-actions {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+</style>
|