将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
986 lines
28 KiB
Vue
986 lines
28 KiB
Vue
<template>
|
|
<view v-if="show" class="contract-sign-dialog" @touchmove.stop.prevent="noop">
|
|
<view class="contract-sign-dialog__mask" @tap="handleRequestClose"></view>
|
|
<view
|
|
class="contract-sign-dialog__panel tn-safe-area-inset-bottom"
|
|
:class="{ 'is-landscape': isLandscape }"
|
|
@touchmove.stop.prevent="noop"
|
|
>
|
|
<view class="contract-sign-dialog__canvas-card">
|
|
<view class="contract-sign-dialog__canvas-tip">
|
|
<view class="contract-sign-dialog__canvas-tip-text">
|
|
<text class="ri-edit-2-line"></text>
|
|
<text>{{ writingHint }}</text>
|
|
</view>
|
|
<view class="contract-sign-dialog__inline-name">
|
|
<text class="ri-user-3-fill"></text>
|
|
<text>{{ displaySignerName }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="contract-sign-dialog__canvas-box" @touchmove.stop.prevent="noop">
|
|
<view class="contract-sign-dialog__canvas-guide" :class="{ 'is-chinese': isChineseName }">
|
|
<view v-if="isChineseName" class="contract-sign-dialog__canvas-guide-grid" :style="guideGridStyle">
|
|
<view
|
|
v-for="(char, index) in chineseGuideChars"
|
|
:key="`${char}-${index}`"
|
|
class="contract-sign-dialog__canvas-guide-cell"
|
|
:style="guideCellStyle"
|
|
>
|
|
<view class="contract-sign-dialog__guide-cross contract-sign-dialog__guide-cross--horizontal"></view>
|
|
<view class="contract-sign-dialog__guide-cross contract-sign-dialog__guide-cross--vertical"></view>
|
|
<text class="contract-sign-dialog__canvas-guide-char" :style="{ fontSize: guideFontSize, fontFamily: guideFontFamily }">
|
|
{{ char }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<view v-else class="contract-sign-dialog__canvas-guide-line" :style="guideAreaStyle">
|
|
<view class="contract-sign-dialog__guide-baseline"></view>
|
|
<text
|
|
class="contract-sign-dialog__canvas-guide-text is-en"
|
|
:style="{ fontSize: guideFontSize, fontFamily: guideFontFamily }"
|
|
>
|
|
{{ displaySignerName }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<canvas
|
|
class="contract-sign-dialog__canvas"
|
|
canvas-id="contract-sign-canvas"
|
|
disable-scroll="true"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
></canvas>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="contract-sign-dialog__toolbar">
|
|
<view class="tool-btn tool-btn--soft" :class="{ disabled: !canUndo }" @tap="handleUndo">
|
|
<text class="ri-play-reverse-line"></text>
|
|
<text>{{ $tr('撤销') }}</text>
|
|
</view>
|
|
<view class="tool-btn tool-btn--soft" :class="{ disabled: !hasSignature }" @tap="handleClear">
|
|
<text class="ri-delete-bin-line"></text>
|
|
<text>{{ $tr('重写') }}</text>
|
|
</view>
|
|
<view class="tool-btn tool-btn--ghost" @tap="handleRequestClose">
|
|
<text class="ri-close-line"></text>
|
|
<text>{{ $tr('关闭') }}</text>
|
|
</view>
|
|
<view class="tool-btn tool-btn--primary" :class="{ disabled: !hasSignature }" @tap="handleSave">
|
|
<text class="ri-check-line-circle"></text>
|
|
{{ $tr('确认签名') }}
|
|
</view>
|
|
</view>
|
|
<view v-if="!isLandscape" class="contract-sign-dialog__footer-tip">
|
|
<text class="ri-shield-check-line-fill"></text>
|
|
<text>{{ $tr('签名前请确认您已完整阅读合同内容') }}</text>
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-if="showCloseConfirm"
|
|
class="contract-sign-dialog__confirm-layer"
|
|
@touchmove.stop.prevent="noop"
|
|
>
|
|
<view class="contract-sign-dialog__confirm-mask"></view>
|
|
<view class="contract-sign-dialog__confirm-card">
|
|
<view class="contract-sign-dialog__confirm-title">{{ $tr('关闭提示') }}</view>
|
|
<view class="contract-sign-dialog__confirm-desc">
|
|
{{ $tr('您还未完成签名,确认要关闭吗?') }}
|
|
</view>
|
|
<view class="contract-sign-dialog__confirm-actions">
|
|
<view class="contract-sign-dialog__confirm-btn is-cancel" @tap="cancelRequestClose">{{ $tr('继续签名') }}</view>
|
|
<view class="contract-sign-dialog__confirm-btn is-confirm" @tap="confirmRequestClose">{{ $tr('确认关闭') }}</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="contract-sign-dialog__export-canvas-wrap">
|
|
<canvas
|
|
canvas-id="contract-sign-export-canvas"
|
|
:style="{ width: `${exportCanvasWidth}px`, height: `${exportCanvasHeight}px` }"
|
|
></canvas>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'ContractSignDialog',
|
|
props: {
|
|
show: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
signerName: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
canvasId: 'contract-sign-canvas',
|
|
ctx: null,
|
|
canvasWidth: 0,
|
|
canvasHeight: 0,
|
|
isLandscape: false,
|
|
ready: false,
|
|
drawing: false,
|
|
strokes: [],
|
|
currentStroke: [],
|
|
showCloseConfirm: false,
|
|
lineWidth: 5,
|
|
minPointDistance: 2,
|
|
strokeColor: '#0F172A'
|
|
}
|
|
},
|
|
computed: {
|
|
language() {
|
|
return this.$getLanguage ? this.$getLanguage() : 'zh'
|
|
},
|
|
displaySignerName() {
|
|
const name = String(this.signerName || '').trim()
|
|
return name || this.$tr('签名人缺失')
|
|
},
|
|
hasSignerName() {
|
|
return !!String(this.signerName || '').trim()
|
|
},
|
|
isChineseName() {
|
|
return /^[\u4E00-\u9FA5·]+$/.test(this.displaySignerName)
|
|
},
|
|
chineseGuideChars() {
|
|
return this.isChineseName
|
|
? this.displaySignerName.split('').filter(char => char && char.trim())
|
|
: []
|
|
},
|
|
hasSignature() {
|
|
return this.strokes.length > 0
|
|
},
|
|
canUndo() {
|
|
return this.strokes.length > 0
|
|
},
|
|
writingHint() {
|
|
return this.isChineseName ? this.$tr('请在此处手写中文正楷签名') : this.$tr('Please write your signature here')
|
|
},
|
|
guideFontSize() {
|
|
const length = this.displaySignerName.length
|
|
if (this.isChineseName) {
|
|
if (length <= 2) return this.isLandscape ? '176rpx' : '160rpx'
|
|
if (length === 3) return this.isLandscape ? '156rpx' : '142rpx'
|
|
if (length === 4) return this.isLandscape ? '138rpx' : '128rpx'
|
|
if (length <= 6) return this.isLandscape ? '122rpx' : '112rpx'
|
|
return this.isLandscape ? '108rpx' : '100rpx'
|
|
}
|
|
if (length >= 18) return this.isLandscape ? '88rpx' : '78rpx'
|
|
if (length >= 12) return this.isLandscape ? '100rpx' : '90rpx'
|
|
if (length >= 8) return this.isLandscape ? '116rpx' : '102rpx'
|
|
return this.isLandscape ? '132rpx' : '116rpx'
|
|
},
|
|
guideFontFamily() {
|
|
if (this.isChineseName) {
|
|
return '"STKaiti", "KaiTi", "Kaiti SC", "DFKai-SB", serif'
|
|
}
|
|
return '"Segoe Script", "Brush Script MT", "Lucida Handwriting", cursive'
|
|
},
|
|
guideCellStyle() {
|
|
const count = Math.max(this.chineseGuideChars.length, 1)
|
|
const gapRpx = this.isLandscape ? 12 : 8
|
|
const gapPx = uni.upx2px(gapRpx)
|
|
const totalGapPx = gapPx * Math.max(count - 1, 0)
|
|
const availableWidth = this.canvasWidth ? this.canvasWidth * 0.9 : 0
|
|
const availableHeight = this.canvasHeight ? this.canvasHeight * 0.9 : 0
|
|
if (!availableWidth || !availableHeight) {
|
|
return {}
|
|
}
|
|
const cellPx = Math.max(
|
|
Math.min((availableWidth - totalGapPx) / count, availableHeight),
|
|
0
|
|
)
|
|
return {
|
|
width: `${cellPx}px`,
|
|
height: `${cellPx}px`
|
|
}
|
|
},
|
|
guideAreaStyle() {
|
|
return {
|
|
width: '90%',
|
|
height: '90%'
|
|
}
|
|
},
|
|
guideGridStyle() {
|
|
return {
|
|
width: '90%',
|
|
height: '90%'
|
|
}
|
|
},
|
|
exportCanvasWidth() {
|
|
return this.isLandscape ? this.canvasHeight || 1 : this.canvasWidth || 1
|
|
},
|
|
exportCanvasHeight() {
|
|
return this.isLandscape ? this.canvasWidth || 1 : this.canvasHeight || 1
|
|
}
|
|
},
|
|
watch: {
|
|
show(value) {
|
|
if (value) {
|
|
this.isLandscape = true
|
|
this.applyLandscapeOrientation()
|
|
this.$nextTick(() => {
|
|
this.initCanvas()
|
|
})
|
|
} else {
|
|
this.restorePortraitOrientation()
|
|
}
|
|
},
|
|
isLandscape(value, oldValue) {
|
|
if (value === oldValue) return
|
|
this.clearSignatureContent()
|
|
}
|
|
},
|
|
beforeDestroy() {
|
|
this.restorePortraitOrientation()
|
|
this.ctx = null
|
|
},
|
|
methods: {
|
|
clearSignatureContent() {
|
|
this.strokes = []
|
|
this.currentStroke = []
|
|
this.drawing = false
|
|
if (this.ready) {
|
|
this.resetCanvas()
|
|
}
|
|
},
|
|
initCanvas() {
|
|
this.clearSignatureContent()
|
|
const query = uni.createSelectorQuery().in(this)
|
|
query.select('.contract-sign-dialog__canvas-box').boundingClientRect(rect => {
|
|
if (!rect) return
|
|
this.canvasWidth = rect.width
|
|
this.canvasHeight = rect.height
|
|
this.ctx = uni.createCanvasContext(this.canvasId, this)
|
|
this.ready = true
|
|
this.resetCanvas()
|
|
}).exec()
|
|
},
|
|
resetCanvas() {
|
|
if (!this.ctx || !this.ready) return
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
this.ctx.setLineCap('round')
|
|
this.ctx.setLineJoin('round')
|
|
this.ctx.draw()
|
|
},
|
|
redrawCanvas() {
|
|
if (!this.ctx || !this.ready) return
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
this.ctx.setLineCap('round')
|
|
this.ctx.setLineJoin('round')
|
|
this.strokes.forEach(stroke => {
|
|
this.drawStroke(stroke)
|
|
})
|
|
this.ctx.draw()
|
|
},
|
|
drawStroke(stroke) {
|
|
if (!Array.isArray(stroke) || stroke.length === 0) return
|
|
if (stroke.length === 1) {
|
|
const point = stroke[0]
|
|
this.ctx.beginPath()
|
|
this.ctx.arc(point.x, point.y, this.lineWidth / 2, 0, Math.PI * 2)
|
|
this.ctx.setFillStyle(this.strokeColor)
|
|
this.ctx.fill()
|
|
return
|
|
}
|
|
if (stroke.length === 2) {
|
|
this.ctx.beginPath()
|
|
this.ctx.setStrokeStyle(this.strokeColor)
|
|
this.ctx.setLineWidth(this.lineWidth)
|
|
this.ctx.moveTo(stroke[0].x, stroke[0].y)
|
|
this.ctx.lineTo(stroke[1].x, stroke[1].y)
|
|
this.ctx.stroke()
|
|
return
|
|
}
|
|
this.ctx.beginPath()
|
|
this.ctx.setStrokeStyle(this.strokeColor)
|
|
this.ctx.setLineWidth(this.lineWidth)
|
|
this.ctx.moveTo(stroke[0].x, stroke[0].y)
|
|
for (let index = 1; index < stroke.length - 1; index += 1) {
|
|
const currentPoint = stroke[index]
|
|
const nextPoint = stroke[index + 1]
|
|
const midPoint = this.getMidPoint(currentPoint, nextPoint)
|
|
this.ctx.quadraticCurveTo(currentPoint.x, currentPoint.y, midPoint.x, midPoint.y)
|
|
}
|
|
const secondLastPoint = stroke[stroke.length - 2]
|
|
const lastPoint = stroke[stroke.length - 1]
|
|
this.ctx.quadraticCurveTo(secondLastPoint.x, secondLastPoint.y, lastPoint.x, lastPoint.y)
|
|
this.ctx.stroke()
|
|
},
|
|
getMidPoint(pointA, pointB) {
|
|
return {
|
|
x: (pointA.x + pointB.x) / 2,
|
|
y: (pointA.y + pointB.y) / 2
|
|
}
|
|
},
|
|
getDistance(pointA, pointB) {
|
|
const dx = pointA.x - pointB.x
|
|
const dy = pointA.y - pointB.y
|
|
return Math.sqrt(dx * dx + dy * dy)
|
|
},
|
|
applyLandscapeOrientation() {
|
|
try {
|
|
if (typeof plus !== 'undefined' && plus.screen && plus.screen.lockOrientation) {
|
|
plus.screen.lockOrientation('landscape-primary')
|
|
return
|
|
}
|
|
} catch (error) {
|
|
console.error('切换系统横屏失败', error)
|
|
}
|
|
try {
|
|
if (
|
|
typeof window !== 'undefined' &&
|
|
window.screen &&
|
|
window.screen.orientation &&
|
|
window.screen.orientation.lock
|
|
) {
|
|
window.screen.orientation.lock('landscape').catch(() => {})
|
|
}
|
|
} catch (error) {
|
|
console.error('浏览器横屏锁定失败', error)
|
|
}
|
|
},
|
|
restorePortraitOrientation() {
|
|
try {
|
|
if (typeof plus !== 'undefined' && plus.screen && plus.screen.lockOrientation) {
|
|
plus.screen.lockOrientation('portrait-primary')
|
|
return
|
|
}
|
|
} catch (error) {
|
|
console.error('恢复竖屏失败', error)
|
|
}
|
|
try {
|
|
if (
|
|
typeof window !== 'undefined' &&
|
|
window.screen &&
|
|
window.screen.orientation &&
|
|
window.screen.orientation.lock
|
|
) {
|
|
window.screen.orientation.lock('portrait').catch(() => {})
|
|
}
|
|
} catch (error) {
|
|
console.error('浏览器竖屏锁定失败', error)
|
|
}
|
|
},
|
|
handleRequestClose() {
|
|
if (this.hasSignature) {
|
|
this.showCloseConfirm = true
|
|
return
|
|
}
|
|
this.handleClose()
|
|
},
|
|
cancelRequestClose() {
|
|
this.showCloseConfirm = false
|
|
},
|
|
confirmRequestClose() {
|
|
this.showCloseConfirm = false
|
|
this.handleClose()
|
|
},
|
|
noop() {},
|
|
buildPoint(event, key) {
|
|
const touches = event[key]
|
|
if (!touches || !touches.length) return null
|
|
return {
|
|
x: touches[0].x,
|
|
y: touches[0].y
|
|
}
|
|
},
|
|
handleTouchStart(event) {
|
|
if (!this.ready) return
|
|
const point = this.buildPoint(event, 'touches')
|
|
if (!point) return
|
|
this.drawing = true
|
|
this.currentStroke = [point]
|
|
this.strokes.push(this.currentStroke)
|
|
this.redrawCanvas()
|
|
},
|
|
handleTouchMove(event) {
|
|
if (!this.ready || !this.drawing) return
|
|
if (event.cancelable && !event.defaultPrevented) {
|
|
event.preventDefault()
|
|
}
|
|
const point = this.buildPoint(event, 'touches')
|
|
if (!point) return
|
|
const lastPoint = this.currentStroke[this.currentStroke.length - 1]
|
|
if (lastPoint && this.getDistance(lastPoint, point) < this.minPointDistance) {
|
|
return
|
|
}
|
|
this.currentStroke.push(point)
|
|
this.redrawCanvas()
|
|
},
|
|
handleTouchEnd(event) {
|
|
if (!this.ready || !this.drawing) return
|
|
const point = this.buildPoint(event, 'changedTouches')
|
|
if (point) {
|
|
this.currentStroke.push(point)
|
|
}
|
|
this.drawing = false
|
|
this.currentStroke = []
|
|
this.redrawCanvas()
|
|
},
|
|
handleUndo() {
|
|
if (!this.canUndo) return
|
|
this.strokes.pop()
|
|
this.redrawCanvas()
|
|
},
|
|
handleClear() {
|
|
this.clearSignatureContent()
|
|
},
|
|
handleClose() {
|
|
this.showCloseConfirm = false
|
|
this.isLandscape = false
|
|
this.restorePortraitOrientation()
|
|
this.$emit('close')
|
|
},
|
|
getSignatureBounds() {
|
|
if (!this.hasSignature) return null
|
|
let minX = Number.POSITIVE_INFINITY
|
|
let minY = Number.POSITIVE_INFINITY
|
|
let maxX = 0
|
|
let maxY = 0
|
|
this.strokes.forEach(stroke => {
|
|
;(stroke || []).forEach(point => {
|
|
if (!point) return
|
|
minX = Math.min(minX, point.x)
|
|
minY = Math.min(minY, point.y)
|
|
maxX = Math.max(maxX, point.x)
|
|
maxY = Math.max(maxY, point.y)
|
|
})
|
|
})
|
|
if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null
|
|
const padding = Math.max(this.lineWidth * 4, 12)
|
|
const x = Math.max(Math.floor(minX - padding), 0)
|
|
const y = Math.max(Math.floor(minY - padding), 0)
|
|
const width = Math.min(Math.ceil(maxX - minX + padding * 2), this.canvasWidth - x)
|
|
const height = Math.min(Math.ceil(maxY - minY + padding * 2), this.canvasHeight - y)
|
|
return {
|
|
x,
|
|
y,
|
|
width: Math.max(width, 1),
|
|
height: Math.max(height, 1)
|
|
}
|
|
},
|
|
getSignaturePointCount() {
|
|
return this.strokes.reduce((total, stroke) => total + ((stroke && stroke.length) || 0), 0)
|
|
},
|
|
isSignatureQualified(bounds) {
|
|
if (!bounds) return false
|
|
const pointCount = this.getSignaturePointCount()
|
|
const minWidth = Math.max(this.canvasWidth * 0.06, 24)
|
|
const minHeight = Math.max(this.canvasHeight * 0.05, 18)
|
|
const minArea = Math.max(this.canvasWidth * this.canvasHeight * 0.0035, 900)
|
|
const actualArea = bounds.width * bounds.height
|
|
return pointCount >= 4 && (bounds.width >= minWidth || bounds.height >= minHeight) && actualArea >= minArea
|
|
},
|
|
exportCurrentCanvas(cropRect) {
|
|
return new Promise((resolve, reject) => {
|
|
uni.canvasToTempFilePath({
|
|
canvasId: this.canvasId,
|
|
fileType: 'png',
|
|
quality: 1,
|
|
x: cropRect.x,
|
|
y: cropRect.y,
|
|
width: cropRect.width,
|
|
height: cropRect.height,
|
|
success: res => resolve(res.tempFilePath),
|
|
fail: reject
|
|
}, this)
|
|
})
|
|
},
|
|
normalizeLandscapeImage(tempFilePath, sourceWidth, sourceHeight) {
|
|
return new Promise((resolve, reject) => {
|
|
const tempCtx = uni.createCanvasContext('contract-sign-export-canvas', this)
|
|
tempCtx.restore()
|
|
tempCtx.save()
|
|
tempCtx.clearRect(0, 0, this.exportCanvasWidth, this.exportCanvasHeight)
|
|
tempCtx.translate(0, sourceWidth)
|
|
tempCtx.rotate(-90 * Math.PI / 180)
|
|
tempCtx.drawImage(tempFilePath, 0, 0, sourceWidth, sourceHeight)
|
|
tempCtx.draw(false, () => {
|
|
uni.canvasToTempFilePath({
|
|
canvasId: 'contract-sign-export-canvas',
|
|
fileType: 'png',
|
|
x: 0,
|
|
y: 0,
|
|
width: sourceHeight,
|
|
height: sourceWidth,
|
|
success: res => resolve(res.tempFilePath),
|
|
fail: reject
|
|
}, this)
|
|
})
|
|
})
|
|
},
|
|
handleSave() {
|
|
if (!this.hasSignerName) {
|
|
uni.showToast({
|
|
title: this.$tr('签名人信息缺失'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
if (!this.hasSignature) {
|
|
uni.showToast({
|
|
title: this.$tr('请先完成签名'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
const cropRect = this.getSignatureBounds()
|
|
if (!cropRect) {
|
|
uni.showToast({
|
|
title: this.$tr('获取签名范围失败'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
if (!this.isSignatureQualified(cropRect)) {
|
|
uni.showToast({
|
|
title: this.$tr('签名范围太小,请重新签名'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
this.exportCurrentCanvas(cropRect)
|
|
.then(tempFilePath => {
|
|
if (this.isLandscape) {
|
|
return this.normalizeLandscapeImage(tempFilePath, cropRect.width, cropRect.height)
|
|
}
|
|
return tempFilePath
|
|
})
|
|
.then(finalPath => {
|
|
this.$emit('save', finalPath)
|
|
})
|
|
.catch(error => {
|
|
console.error('导出签名图片失败', error)
|
|
uni.showToast({
|
|
title: this.$tr('导出签名失败,请重试'),
|
|
icon: 'none'
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.contract-sign-dialog {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.contract-sign-dialog__mask {
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(circle at top, rgba(73, 119, 196, 0.18), transparent 45%),
|
|
rgba(15, 23, 42, 0.58);
|
|
}
|
|
|
|
.contract-sign-dialog__panel {
|
|
position: absolute;
|
|
left: 20rpx;
|
|
right: 20rpx;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border-radius: 36rpx;
|
|
background: linear-gradient(180deg, #FFFFFF 0%, #F8FBFF 100%);
|
|
box-shadow: 0 28rpx 90rpx rgba(15, 23, 42, 0.24);
|
|
padding: 36rpx 32rpx 30rpx;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, 0.78);
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape {
|
|
left: 12rpx;
|
|
right: 12rpx;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
padding: 22rpx 22rpx 18rpx;
|
|
max-height: calc(100vh - 24rpx);
|
|
height: calc(100vh - 24rpx);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-card {
|
|
margin-top: 28rpx;
|
|
border-radius: 30rpx;
|
|
border: 1px solid rgba(42, 92, 170, 0.10);
|
|
background: linear-gradient(180deg, #FBFDFF 0%, #F2F7FD 100%);
|
|
padding: 26rpx;
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-tip {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-tip-text {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
font-size: 24rpx;
|
|
color: #607086;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.contract-sign-dialog__inline-name {
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8rpx;
|
|
min-width: 168rpx;
|
|
height: 64rpx;
|
|
padding: 0 20rpx;
|
|
border-radius: 999rpx;
|
|
background: linear-gradient(180deg, #EEF3FA 0%, #E5ECF6 100%);
|
|
color: #314966;
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
box-shadow: inset 0 0 0 1px rgba(85, 108, 141, 0.08);
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-box {
|
|
margin-top: 18rpx;
|
|
height: 460rpx;
|
|
border-radius: 24rpx;
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.95) 0%, rgba(247,250,255,0.98) 100%);
|
|
border: 1px solid rgba(42, 92, 170, 0.14);
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.9), 0 12rpx 30rpx rgba(34, 64, 110, 0.06);
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-card {
|
|
margin-top: 0;
|
|
padding: 18rpx;
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-box {
|
|
flex: 1;
|
|
min-height: 0;
|
|
height: auto;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-guide {
|
|
padding: 14rpx 18rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-guide-grid {
|
|
flex-wrap: nowrap;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__toolbar {
|
|
gap: 10rpx;
|
|
margin-top: 12rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .tool-btn {
|
|
height: 64rpx;
|
|
border-radius: 18rpx;
|
|
gap: 6rpx;
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .tool-btn text:first-child {
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-tip {
|
|
flex-shrink: 0;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__canvas-tip-text {
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__inline-name {
|
|
min-width: 132rpx;
|
|
height: 52rpx;
|
|
padding: 0 16rpx;
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__panel.is-landscape .contract-sign-dialog__footer-tip {
|
|
margin-top: 10rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-box::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(transparent 95%, rgba(42, 92, 170, 0.035) 100%);
|
|
background-size: 100% 72rpx;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 12rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-grid {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
align-content: center;
|
|
gap: 8rpx;
|
|
flex-wrap: wrap;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-cell {
|
|
border-radius: 20rpx;
|
|
background: rgba(42, 92, 170, 0.035);
|
|
border: 1px dashed rgba(42, 92, 170, 0.12);
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-line {
|
|
width: 90%;
|
|
height: 100%;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-line::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 12rpx;
|
|
right: 12rpx;
|
|
top: 50%;
|
|
border-top: 1px dashed rgba(42, 92, 170, 0.14);
|
|
}
|
|
|
|
.contract-sign-dialog__guide-cross {
|
|
position: absolute;
|
|
background: rgba(15, 23, 42, 0.08);
|
|
}
|
|
|
|
.contract-sign-dialog__guide-cross--horizontal {
|
|
left: 12rpx;
|
|
right: 12rpx;
|
|
top: 50%;
|
|
height: 1px;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.contract-sign-dialog__guide-cross--vertical {
|
|
top: 12rpx;
|
|
bottom: 12rpx;
|
|
left: 50%;
|
|
width: 1px;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.contract-sign-dialog__guide-baseline {
|
|
position: absolute;
|
|
left: 18rpx;
|
|
right: 18rpx;
|
|
top: 50%;
|
|
border-top: 1px dashed rgba(15, 23, 42, 0.08);
|
|
}
|
|
|
|
.contract-sign-dialog__canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-text {
|
|
position: relative;
|
|
z-index: 2;
|
|
max-width: calc(100% - 12rpx);
|
|
color: rgba(15, 23, 42, 0.14);
|
|
font-weight: 500;
|
|
letter-spacing: 2rpx;
|
|
text-align: center;
|
|
word-break: break-all;
|
|
transform: scaleY(1.08);
|
|
text-shadow: 0 0 1px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-text.is-en {
|
|
letter-spacing: 0;
|
|
font-style: italic;
|
|
transform: scaleY(1.02) rotate(-4deg);
|
|
}
|
|
|
|
.contract-sign-dialog__canvas-guide-char {
|
|
position: relative;
|
|
z-index: 1;
|
|
color: rgba(15, 23, 42, 0.14);
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
transform: scale(1.08) rotate(-3deg);
|
|
text-shadow: 0 0 1px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.contract-sign-dialog__toolbar {
|
|
margin-top: 30rpx;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.tool-btn {
|
|
height: 84rpx;
|
|
border-radius: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8rpx;
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
color: #35506F;
|
|
background: #F2F5F9;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.tool-btn:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.tool-btn--soft {
|
|
background: linear-gradient(180deg, #F7FAFE 0%, #EEF3F9 100%);
|
|
box-shadow: inset 0 0 0 1px rgba(85, 108, 141, 0.06);
|
|
}
|
|
|
|
.tool-btn--ghost {
|
|
color: #516173;
|
|
background: linear-gradient(180deg, #F4F7FB 0%, #E9EEF5 100%);
|
|
}
|
|
|
|
.tool-btn--primary {
|
|
color: #FFFFFF;
|
|
background: linear-gradient(135deg, #2A5CAA 0%, #3C74CB 100%);
|
|
box-shadow: 0 10rpx 24rpx rgba(42, 92, 170, 0.26);
|
|
}
|
|
|
|
.tool-btn.disabled {
|
|
opacity: 0.45;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.contract-sign-dialog__footer-tip {
|
|
margin-top: 18rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8rpx;
|
|
font-size: 22rpx;
|
|
color: #74859A;
|
|
}
|
|
|
|
.contract-sign-dialog__export-canvas-wrap {
|
|
position: fixed;
|
|
left: -9999px;
|
|
top: -9999px;
|
|
width: 0;
|
|
height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 10020;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 32rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-mask {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(15, 23, 42, 0.5);
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-card {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
max-width: 560rpx;
|
|
border-radius: 28rpx;
|
|
background: linear-gradient(180deg, #FFFFFF 0%, #F8FBFF 100%);
|
|
box-shadow: 0 24rpx 72rpx rgba(15, 23, 42, 0.22);
|
|
padding: 34rpx 28rpx 28rpx;
|
|
border: 1px solid rgba(255, 255, 255, 0.78);
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-title {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #10233F;
|
|
text-align: center;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-desc {
|
|
margin-top: 18rpx;
|
|
font-size: 24rpx;
|
|
line-height: 1.7;
|
|
color: #6B7A90;
|
|
text-align: center;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-actions {
|
|
margin-top: 28rpx;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-btn {
|
|
height: 78rpx;
|
|
border-radius: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-btn.is-cancel {
|
|
color: #516173;
|
|
background: linear-gradient(180deg, #F4F7FB 0%, #E9EEF5 100%);
|
|
}
|
|
|
|
.contract-sign-dialog__confirm-btn.is-confirm {
|
|
color: #FFFFFF;
|
|
background: linear-gradient(135deg, #2A5CAA 0%, #3C74CB 100%);
|
|
}
|
|
</style>
|