Files
zhgh_zjvtit/src/main/resources/static/components/plugins/sysSignature/h5Index.vue
T
2026-08-26 19:10:18 +08:00

292 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="h5-signature">
<slot v-if="$slots.default"></slot>
<div v-else class="signature-entry">
<!-- 未签名时展示统一空状态和入口组件放入 van-field 时仍保持完整宽度 -->
<div v-if="!signatureContent" class="signature-entry-panel">
<van-icon class="signature-placeholder-icon" name="edit"></van-icon>
<div class="signature-placeholder-text">请完成本人手写签名</div>
<van-button @click="openSignature" class="signature-open-button" block plain
native-type="button">进入签字板</van-button>
</div>
<!-- 已签名时直接预览结果并在下方提供统一的重新签名入口 -->
<div v-else class="signature-entry-panel signature-entry-panel--completed">
<van-image :src="signatureContent" class="signature-preview" fit="contain"></van-image>
<div class="signature-completed-text">
<van-icon name="passed"></van-icon>
<span>已完成本人手写签名</span>
</div>
</div>
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
type="button">重新签名</button>
</div>
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
<signature @save="save"></signature>
</van-popup>
</div>
</template>
<script>
module.exports = {
name: "h5-signature",
components: {
signature: httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime())
},
props: {
value: {
type: String,
default: ""
}
},
watch: {
value: {
handler(val) {
this.signatureContent = val
// 父页面异步初始化后仍未提供签名时,统一回显当前用户在“我的签名”中保存的签名。
if (!val) {
this.loadUserSignature()
}
},
immediate: true
}
},
data() {
return {
signatureContent: null,
userSignatureContent: "",
userSignatureLoaded: false,
userSignatureLoading: false,
showSignaturePanel: false,
historyLayerName: "",
historyLayerUnsubscribe: null
}
},
methods: {
/**
* 读取当前登录用户在“我的签名”中保存的签名。
* 无请求参数;接口返回用户签名记录,组件仅在当前 v-model 为空时回填签名 URL。
*/
loadUserSignature() {
if (this.value) return
if (this.userSignatureLoaded) {
this.applyUserSignature()
return
}
if (this.userSignatureLoading) return
this.userSignatureLoading = true
this.$axios.post("/platform/signature/get")
.then((res) => {
const signatureData = res.code === 0 ? (res.data || {}) : {}
this.userSignatureContent = signatureData.signature || ""
this.userSignatureLoaded = true
this.applyUserSignature()
})
.catch(() => {
// 默认签名读取失败时保留空状态,用户仍可进入签字板完成本次签名。
this.userSignatureLoaded = true
})
.finally(() => {
this.userSignatureLoading = false
})
},
// 仅为空值补充默认签名,避免覆盖历史业务签名或用户刚完成的现场签名。
applyUserSignature() {
if (this.value || !this.userSignatureContent) return
this.signatureContent = this.userSignatureContent
this.$emit("input", this.userSignatureContent)
},
/**
* 打开签字板。
* 无请求参数;优先向全局历史栈增加当前签字弹层,返回值为空。
*/
openSignature() {
const manager = window.h5HistoryLayerManager
if (manager && manager.open(this.historyLayerName)) return
this.showSignaturePanel = true
},
// 保存成功或业务主动关闭时同步回退签字弹层历史,管理器不可用时直接关闭 Popup。
closeSignaturePanel() {
const manager = window.h5HistoryLayerManager
if (manager && manager.close(this.historyLayerName)) return
this.showSignaturePanel = false
},
// Vant 组件自行关闭时仅处理仍位于栈顶的签字层,避免返回同步触发重复 history.back。
handleSignaturePopupClose() {
const manager = window.h5HistoryLayerManager
if (!manager) return
const stack = manager.stack
if (stack[stack.length - 1] === this.historyLayerName) {
manager.close(this.historyLayerName)
}
},
save(signature) {
const file = this.base64ToFile(signature, "signature.png")
const formData = new FormData()
formData.append("file", file)
axios
.post("/platform/signature/saveH5Signature", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
.then((res) => {
return res.data
})
.then((res) => {
if (res.code === 0) {
// 保存成功后使用项目统一的 Vant 反馈,避免浏览器原生弹框阻塞签字流程。
this.$toast.success({
message: "签名保存成功",
duration: 1500,
forbidClick: true
})
this.$emit("input", res.data)
this.closeSignaturePanel()
} else {
// 接口返回失败时保留签字板,方便用户重试。
this.$toast.fail({
message: res.msg || "签名保存失败,请重试",
duration: 2000
})
}
})
},
base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const parts = base64Data.split(";base64,")
const contentType = parts[0].split(":")[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], { type: contentType })
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, { type: contentType })
}
},
mounted() {
const manager = window.h5HistoryLayerManager
if (!manager || typeof manager.ensureRegistered !== "function" || typeof manager.subscribe !== "function") return
// 每个组件实例使用唯一弹层名称,同一页面存在多个签字组件时互不影响。
this.historyLayerName = "h5-signature-" + this._uid
manager.ensureRegistered("h5-signature-page-" + window.location.pathname + window.location.search)
this.historyLayerUnsubscribe = manager.subscribe((stack) => {
this.showSignaturePanel = stack.includes(this.historyLayerName)
})
},
beforeDestroy() {
if (typeof this.historyLayerUnsubscribe === "function") {
this.historyLayerUnsubscribe()
}
}
}
</script>
<style scoped>
.h5-signature {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.signature-entry {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
box-sizing: border-box;
}
.signature-entry-panel {
width: 100%;
min-height: 150px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px dashed #dfe6ef;
border-radius: 9px;
background: #fbfcfe;
}
.signature-placeholder-icon {
margin-bottom: 6px;
color: #c7cfda;
font-size: 38px;
}
.signature-placeholder-text {
margin-bottom: 12px;
color: #9aa4b2;
font-size: 13px;
line-height: 20px;
}
.signature-open-button {
width: 100%;
height: 40px;
color: var(--color-primary, #1989fa);
font-size: 15px;
font-weight: 500;
background: linear-gradient(90deg, #f0f7ff, #f5f9ff);
border-color: #d8e9ff;
border-radius: 7px;
}
.signature-entry-panel--completed {
min-height: 150px;
justify-content: space-between;
}
.signature-preview {
width: 100%;
height: 96px;
background: #fff;
border-radius: 6px;
overflow: hidden;
}
.signature-completed-text {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #5f6b7a;
font-size: 13px;
}
.signature-completed-text .van-icon {
margin-right: 5px;
color: #28a745;
font-size: 16px;
}
.signature-reset-button {
align-self: center;
margin-top: 10px;
padding: 3px 12px;
color: var(--color-primary, #1989fa);
font-size: 14px;
line-height: 22px;
background: transparent;
border: 0;
}
</style>