hrp-ui-base 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.cjs +5 -5
- package/dist/components.es.js +3326 -2756
- package/dist/style.css +1 -1
- package/package.json +3 -1
- package/src/components/layout/LayoutContainer.vue +5 -1
- package/src/components/layout/personal-sign/ImageCropper.vue +187 -0
- package/src/components/layout/personal-sign/PersonalSignDialog.vue +658 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
* @description: 个人签名弹窗
|
|
3
|
+
* @fileName: PersonalSignDialog.vue
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<el-dialog
|
|
7
|
+
v-model="dialogVisible"
|
|
8
|
+
title="个人签名"
|
|
9
|
+
width="600"
|
|
10
|
+
center
|
|
11
|
+
show-close
|
|
12
|
+
close-on-press-escape
|
|
13
|
+
>
|
|
14
|
+
<div class="personal-sign-content">
|
|
15
|
+
<div v-if="!toSignStatus" class="signbox">
|
|
16
|
+
<div v-if="isSign" class="haveSign">
|
|
17
|
+
<div class="signImgBox">
|
|
18
|
+
<el-image style="width: 133px; height: 100px" :src="nowBindUrlMsg" fit="contain" v-if="nowBind" />
|
|
19
|
+
<!-- 未签名提示 -->
|
|
20
|
+
<div v-else class="no-sign-tip">
|
|
21
|
+
<svg class="tip-icon" viewBox="0 0 1024 1024" width="32" height="32">
|
|
22
|
+
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#d9d9d9"/>
|
|
23
|
+
<path d="M464 336a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm32 120h64c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-64c-4.4 0-8-3.6-8-8V464c0-4.4 3.6-8 8-8z" fill="#d9d9d9"/>
|
|
24
|
+
</svg>
|
|
25
|
+
<span class="tip-text">当前还未进行签字</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<el-button class="sign-btn" type="primary" @click="handleReSignClick">{{ nowBind ? "重签" : "去签字" }}</el-button>
|
|
29
|
+
</div>
|
|
30
|
+
<div v-else class="unHaveSign">
|
|
31
|
+
<div class="unHaveSignUp">
|
|
32
|
+
<div class="qrCodeImgBox">
|
|
33
|
+
<QrcodeVue :size="143" :value="codeContent" class="order-print-big-qrcode" ref="QrCodeRef" />
|
|
34
|
+
<!-- 已扫描状态覆盖层 -->
|
|
35
|
+
<div v-if="ifScanned" class="qrcode-status-overlay scanned">
|
|
36
|
+
<svg class="status-icon" viewBox="0 0 1024 1024" width="24" height="24">
|
|
37
|
+
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" fill="#333333"/>
|
|
38
|
+
</svg>
|
|
39
|
+
<span class="status-text">已扫描</span>
|
|
40
|
+
</div>
|
|
41
|
+
<!-- 已过期状态覆盖层 -->
|
|
42
|
+
<div v-if="ifExpired" class="qrcode-status-overlay expired" @click="handleReSignClick">
|
|
43
|
+
<svg class="status-icon" viewBox="0 0 1024 1024" width="24" height="24">
|
|
44
|
+
<path d="M960 416V192l-73.056 73.056a447.712 447.712 0 0 0-373.6-201.088C265.92 63.968 65.312 264.544 65.312 512S265.92 960.032 513.344 960.032a448.064 448.064 0 0 0 415.232-279.488 38.368 38.368 0 1 0-70.272-29.568 371.36 371.36 0 0 1-344.96 232.064c-205.408 0-371.36-165.984-371.36-371.04 0-205.088 165.952-371.04 371.36-371.04 108.896 0 206.752 47.2 274.464 122.016L736 314.016h224a32 32 0 0 0 32-32z" fill="#b0b0b0"/>
|
|
45
|
+
</svg>
|
|
46
|
+
<span class="status-text">二维码已过期,点击刷新</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="qrCodeBtnBox">
|
|
50
|
+
<el-button type="primary" size="default" @click="handleReturnClick">取消</el-button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="uploadContent">使用钉钉扫码签名或点击上传</div>
|
|
54
|
+
<div class="uploadContent">为保证签名识别效果最佳,请选择纯白色背景、减少阴影的图片</div>
|
|
55
|
+
<div class="upload-link">
|
|
56
|
+
<a href="javascript:;" @click="triggerFileInput">
|
|
57
|
+
已有签名?点击上传
|
|
58
|
+
</a>
|
|
59
|
+
</div>
|
|
60
|
+
<!-- 隐藏的 file input -->
|
|
61
|
+
<input
|
|
62
|
+
type="file"
|
|
63
|
+
ref="fileInput"
|
|
64
|
+
style="display: none;"
|
|
65
|
+
accept=".jpg,.jpeg,.png,.bmp,.raw,.heif,.tiff,.gif"
|
|
66
|
+
@change="handleFileChange"
|
|
67
|
+
>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<template #footer>
|
|
72
|
+
<div class="dialog-footer">
|
|
73
|
+
<el-button @click="dialogVisible = false">取消</el-button>
|
|
74
|
+
<el-button type="primary" @click="handleSignSave">保存</el-button>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
<!-- 图片裁剪弹框 -->
|
|
78
|
+
<ImageCropper ref="imageCropperRef" @success="handleCropSuccess"></ImageCropper>
|
|
79
|
+
</el-dialog>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<script lang="ts" setup>
|
|
83
|
+
import { ref, watch } from 'vue'
|
|
84
|
+
import { ElMessage } from 'element-plus'
|
|
85
|
+
import QrcodeVue from 'qrcode.vue'
|
|
86
|
+
import ImageCropper from './ImageCropper.vue'
|
|
87
|
+
import FlowSignController from '../../../api/bms/flow/FlowSignController'
|
|
88
|
+
import FileUploadController from '../../../api/bms/file/FileUploadController'
|
|
89
|
+
|
|
90
|
+
const dialogVisible = ref<boolean>(false)
|
|
91
|
+
const nowBind = ref<string>('')
|
|
92
|
+
const nowBindUrlMsg = ref<string>('')
|
|
93
|
+
const toSignStatus = ref<boolean>(false)
|
|
94
|
+
const isSign = ref<boolean>(true)
|
|
95
|
+
const currentImgId = ref<string>('')
|
|
96
|
+
|
|
97
|
+
const codeContent = ref<string>('')
|
|
98
|
+
const qrCodeUUID = ref<string>('')
|
|
99
|
+
const ifScanned = ref<boolean>(false)
|
|
100
|
+
const ifExpired = ref<boolean>(false)
|
|
101
|
+
const qrCodeTimeRecord = ref<any>()
|
|
102
|
+
const queryQrCodeTimeRecord = ref<any>()
|
|
103
|
+
|
|
104
|
+
const fileInput = ref<HTMLInputElement>()
|
|
105
|
+
const imageCropperRef = ref<any>()
|
|
106
|
+
|
|
107
|
+
// 允许的图片格式
|
|
108
|
+
const allowedTypes = ['image/jpeg','image/png','image/bmp','image/raw','image/heif','image/tiff','image/gif']
|
|
109
|
+
const allowedExtensions = ['.jpg','.jpeg','.png','.bmp','.raw','.heif','.tiff','.gif']
|
|
110
|
+
const maxSize = 5 * 1024 * 1024
|
|
111
|
+
|
|
112
|
+
watch(nowBind, () => {
|
|
113
|
+
isSign.value = true
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
watch(dialogVisible, (newVal) => {
|
|
117
|
+
if (newVal === false) {
|
|
118
|
+
dialogClose()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const pageId = ref<string>('01-01-04-001')
|
|
123
|
+
|
|
124
|
+
const uploadOss = async (file: any) => {
|
|
125
|
+
FileUploadController.upload(file, pageId.value).then((data: any) => {
|
|
126
|
+
if (data.code === 200 && data.res) {
|
|
127
|
+
nowBind.value = data.res.url
|
|
128
|
+
nowBindUrlMsg.value = data.res.url
|
|
129
|
+
currentImgId.value = data.res.fileId
|
|
130
|
+
isSign.value = true
|
|
131
|
+
handleReturnClick()
|
|
132
|
+
} else {
|
|
133
|
+
ElMessage.error("数据发生错误,请重新上传!")
|
|
134
|
+
}
|
|
135
|
+
}).catch((err) => {
|
|
136
|
+
ElMessage.error("oss上传发生错误,请重试!")
|
|
137
|
+
console.error(err)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleReSignClick = async () => {
|
|
142
|
+
generateQrCode()
|
|
143
|
+
await updateQrCodeStatus("unscanned")
|
|
144
|
+
startQueryStatus()
|
|
145
|
+
isSign.value = false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const handleReturnClick = async () => {
|
|
149
|
+
await updateQrCodeStatus("expired")
|
|
150
|
+
clearInterval(qrCodeTimeRecord.value)
|
|
151
|
+
clearInterval(queryQrCodeTimeRecord.value)
|
|
152
|
+
isSign.value = true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 触发文件选择
|
|
156
|
+
const triggerFileInput = () => {
|
|
157
|
+
if (!fileInput.value) return
|
|
158
|
+
fileInput.value.click()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleFileChange = async (event: any) => {
|
|
162
|
+
const file = event.target.files[0]
|
|
163
|
+
if (!file) return
|
|
164
|
+
|
|
165
|
+
if (!allowedTypes.includes(file.type)) {
|
|
166
|
+
ElMessage.error(`仅支持 ${allowedExtensions.join(', ')} 格式`)
|
|
167
|
+
resetFileInput()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (file.size > maxSize) {
|
|
171
|
+
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(1)
|
|
172
|
+
ElMessage.error(`文件大小不能超过 ${maxSizeMB}MB`)
|
|
173
|
+
resetFileInput()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const processedBlob = await extractSignatureImageTransparent(file)
|
|
179
|
+
imageCropperRef.value?.open(processedBlob)
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.error(e)
|
|
182
|
+
ElMessage.error('签名图片处理失败')
|
|
183
|
+
} finally {
|
|
184
|
+
resetFileInput()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const resetFileInput = () => {
|
|
189
|
+
if (fileInput.value) {
|
|
190
|
+
fileInput.value.value = ''
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 裁剪成功回调
|
|
195
|
+
const handleCropSuccess = async (croppedBlob: Blob) => {
|
|
196
|
+
try {
|
|
197
|
+
const croppedFile = new File(
|
|
198
|
+
[croppedBlob],
|
|
199
|
+
'签名文件.png',
|
|
200
|
+
{ type: 'image/png' }
|
|
201
|
+
)
|
|
202
|
+
await uploadOss(croppedFile)
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error(e)
|
|
205
|
+
ElMessage.error('签名图片上传失败')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const generateUUID = () => {
|
|
210
|
+
const timestamp = performance.now().toString(36)
|
|
211
|
+
const random = Math.random().toString(36).slice(2)
|
|
212
|
+
const random2 = Math.random().toString(36).slice(2)
|
|
213
|
+
return `${timestamp}-${random}-${random2}`.replace(/\./g, '')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const generateQrCode = () => {
|
|
217
|
+
ifScanned.value = false
|
|
218
|
+
ifExpired.value = false
|
|
219
|
+
qrCodeUUID.value = generateUUID()
|
|
220
|
+
const urlStr = `${window.location.origin}/mobile/login/index.html?host=${localStorage.getItem('TenantHost')}&url=${'/full-screen-sign-process'}?qrCodeUUID=${qrCodeUUID.value}`
|
|
221
|
+
codeContent.value = urlStr
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const startPolling = () => {
|
|
225
|
+
qrCodeTimeRecord.value = setInterval(async () => {
|
|
226
|
+
try {
|
|
227
|
+
const data = await FlowSignController.startPolling(qrCodeUUID.value)
|
|
228
|
+
if (data.code === 200 && data.res) {
|
|
229
|
+
clearInterval(qrCodeTimeRecord.value)
|
|
230
|
+
nowBind.value = data.res.url
|
|
231
|
+
nowBindUrlMsg.value = data.res.url
|
|
232
|
+
currentImgId.value = data.res.fileId
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
}, 2000)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const handleSignSave = () => {
|
|
241
|
+
FlowSignController.personalSignSave(currentImgId.value).then(data => {
|
|
242
|
+
if (data.code === 200) {
|
|
243
|
+
ElMessage.success('个人签名上传成功')
|
|
244
|
+
}
|
|
245
|
+
}).catch(() => {
|
|
246
|
+
dialogVisible.value = false
|
|
247
|
+
}).finally(() => {
|
|
248
|
+
dialogVisible.value = false
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const getPersonalSign = async () => {
|
|
253
|
+
await FlowSignController.gerPersonalSign().then(data => {
|
|
254
|
+
if (data.code === 200 && data.res) {
|
|
255
|
+
nowBind.value = data.res.url
|
|
256
|
+
nowBindUrlMsg.value = data.res.url
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const startQueryStatus = () => {
|
|
262
|
+
queryQrCodeTimeRecord.value = setInterval(() => {
|
|
263
|
+
FlowSignController.queryQrCodeStatus(qrCodeUUID.value).then(data => {
|
|
264
|
+
if (data.code === 200 && data.res === "scanned") {
|
|
265
|
+
clearInterval(queryQrCodeTimeRecord.value)
|
|
266
|
+
ifScanned.value = true
|
|
267
|
+
startPolling()
|
|
268
|
+
} else if (data.code === 200 && (data.res === null || data.res === "expired")) {
|
|
269
|
+
clearInterval(queryQrCodeTimeRecord.value)
|
|
270
|
+
ifExpired.value = true
|
|
271
|
+
ElMessage.error("二维码已过期,请刷新")
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}, 2000)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const updateQrCodeStatus = (status: string) => {
|
|
278
|
+
if (status === 'scanned') {
|
|
279
|
+
ifScanned.value = true
|
|
280
|
+
} else if (status === 'expired') {
|
|
281
|
+
ifExpired.value = true
|
|
282
|
+
}
|
|
283
|
+
FlowSignController.updateQrCodeStatus(status, qrCodeUUID.value).then(data => {
|
|
284
|
+
if (data.code === 200 && data.res) {
|
|
285
|
+
console.log("二维码状态更新成功")
|
|
286
|
+
} else if (data.code === 200 && !data.res) {
|
|
287
|
+
ElMessage.error(data.msg || "二维码失效请刷新后重试")
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const dialogClose = () => {
|
|
293
|
+
clearInterval(qrCodeTimeRecord.value)
|
|
294
|
+
clearInterval(queryQrCodeTimeRecord.value)
|
|
295
|
+
ifScanned.value = false
|
|
296
|
+
ifExpired.value = false
|
|
297
|
+
isSign.value = true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 打开弹窗 */
|
|
301
|
+
const open = () => {
|
|
302
|
+
dialogVisible.value = true
|
|
303
|
+
getPersonalSign()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ====== 图片处理工具函数 ======
|
|
307
|
+
|
|
308
|
+
const extractSignatureImageTransparent = (file: File): Promise<Blob> => {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const img = new Image()
|
|
311
|
+
img.onload = () => {
|
|
312
|
+
const canvas = document.createElement('canvas')
|
|
313
|
+
const ctx = canvas.getContext('2d')!
|
|
314
|
+
|
|
315
|
+
canvas.width = img.width
|
|
316
|
+
canvas.height = img.height
|
|
317
|
+
ctx.drawImage(img, 0, 0)
|
|
318
|
+
|
|
319
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
320
|
+
const data = imageData.data
|
|
321
|
+
|
|
322
|
+
const grays: number[] = []
|
|
323
|
+
let min = 255, max = 0
|
|
324
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
325
|
+
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]
|
|
326
|
+
grays.push(gray)
|
|
327
|
+
min = Math.min(min, gray)
|
|
328
|
+
max = Math.max(max, gray)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const threshold = min + (max - min) * 0.35
|
|
332
|
+
|
|
333
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
334
|
+
const gray = grays[i / 4]
|
|
335
|
+
if (gray > threshold) {
|
|
336
|
+
data[i + 3] = 0
|
|
337
|
+
} else {
|
|
338
|
+
data[i] = data[i + 1] = data[i + 2] = 0
|
|
339
|
+
data[i + 3] = 255
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
enhanceContrast(data)
|
|
344
|
+
for (let i = 0; i < 2; i++) {
|
|
345
|
+
dilateAlpha(data, canvas.width, canvas.height, 1)
|
|
346
|
+
fillGapsAlpha(data, canvas.width, canvas.height)
|
|
347
|
+
}
|
|
348
|
+
removeHorizontalLines(data, canvas.width, canvas.height)
|
|
349
|
+
ctx.putImageData(imageData, 0, 0)
|
|
350
|
+
|
|
351
|
+
canvas.toBlob(
|
|
352
|
+
blob => blob ? resolve(blob) : reject(),
|
|
353
|
+
'image/png',
|
|
354
|
+
1
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
img.onerror = reject
|
|
359
|
+
img.src = URL.createObjectURL(file)
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const dilateAlpha = (data: Uint8ClampedArray, width: number, height: number, radius = 1) => {
|
|
364
|
+
const copy = new Uint8ClampedArray(data)
|
|
365
|
+
const idx = (x: number, y: number) => (y * width + x) * 4
|
|
366
|
+
|
|
367
|
+
for (let y = radius; y < height - radius; y++) {
|
|
368
|
+
for (let x = radius; x < width - radius; x++) {
|
|
369
|
+
const center = idx(x, y)
|
|
370
|
+
if (copy[center + 3] === 255) continue
|
|
371
|
+
|
|
372
|
+
let hasOpaqueNeighbor = false
|
|
373
|
+
for (let dy = -radius; dy <= radius && !hasOpaqueNeighbor; dy++) {
|
|
374
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
375
|
+
const i = idx(x + dx, y + dy)
|
|
376
|
+
if (copy[i + 3] === 255) {
|
|
377
|
+
hasOpaqueNeighbor = true
|
|
378
|
+
break
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (hasOpaqueNeighbor) {
|
|
384
|
+
data[center + 3] = 255
|
|
385
|
+
for (let c = 0; c < 3; c++) {
|
|
386
|
+
data[center + c] = copy[center + c]
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const fillGapsAlpha = (data: Uint8ClampedArray, width: number, height: number) => {
|
|
394
|
+
const copy = new Uint8ClampedArray(data)
|
|
395
|
+
const idx = (x: number, y: number) => (y * width + x) * 4
|
|
396
|
+
|
|
397
|
+
for (let y = 1; y < height - 1; y++) {
|
|
398
|
+
for (let x = 1; x < width - 1; x++) {
|
|
399
|
+
const i = idx(x, y)
|
|
400
|
+
if (copy[i + 3] === 255) continue
|
|
401
|
+
|
|
402
|
+
let opaqueCount = 0
|
|
403
|
+
let r = [0, 0, 0]
|
|
404
|
+
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
405
|
+
const neighbor = idx(x + dx, y + dy)
|
|
406
|
+
if (copy[neighbor + 3] === 255) {
|
|
407
|
+
opaqueCount++
|
|
408
|
+
for (let c = 0; c < 3; c++) r[c] += copy[neighbor + c]
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (opaqueCount >= 2) {
|
|
413
|
+
data[i + 3] = 255
|
|
414
|
+
for (let c = 0; c < 3; c++) data[i + c] = Math.round(r[c] / opaqueCount)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const enhanceContrast = (data: Uint8ClampedArray) => {
|
|
421
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
422
|
+
if (data[i + 3] === 255) {
|
|
423
|
+
for (let c = 0; c < 3; c++) {
|
|
424
|
+
const val = data[i + c] / 255
|
|
425
|
+
data[i + c] = Math.min(255, Math.pow(val, 0.4) * 255)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const removeHorizontalLines = (data: Uint8ClampedArray, width: number, height: number) => {
|
|
432
|
+
const threshold = width * 0.7
|
|
433
|
+
const idx = (x: number, y: number) => (y * width + x) * 4
|
|
434
|
+
|
|
435
|
+
for (let y = 0; y < height; y++) {
|
|
436
|
+
let count = 0
|
|
437
|
+
let startX = 0
|
|
438
|
+
for (let x = 0; x < width; x++) {
|
|
439
|
+
if (data[idx(x, y) + 3] === 255) {
|
|
440
|
+
if (count === 0) startX = x
|
|
441
|
+
count++
|
|
442
|
+
} else {
|
|
443
|
+
if (count > threshold) {
|
|
444
|
+
for (let i = startX; i < x; i++) {
|
|
445
|
+
data[idx(i, y) + 3] = 0
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
count = 0
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (count > threshold) {
|
|
452
|
+
for (let i = startX; i < width; i++) {
|
|
453
|
+
data[idx(i, y) + 3] = 0
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
defineExpose({ open })
|
|
460
|
+
</script>
|
|
461
|
+
|
|
462
|
+
<style scoped lang="scss">
|
|
463
|
+
.personal-sign-content {
|
|
464
|
+
width: 100%;
|
|
465
|
+
min-height: 220px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.signbox {
|
|
469
|
+
margin-top: 8px;
|
|
470
|
+
width: 100%;
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-direction: row;
|
|
473
|
+
justify-content: flex-start;
|
|
474
|
+
align-items: flex-start;
|
|
475
|
+
flex-wrap: wrap;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.haveSign {
|
|
479
|
+
display: flex;
|
|
480
|
+
justify-content: center;
|
|
481
|
+
align-items: flex-start;
|
|
482
|
+
width: 100%;
|
|
483
|
+
margin: 0 auto;
|
|
484
|
+
min-height: 197px;
|
|
485
|
+
text-align: center;
|
|
486
|
+
position: relative;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.sign-btn {
|
|
490
|
+
position: absolute;
|
|
491
|
+
right: calc(50% - 145px);
|
|
492
|
+
top: 72.5px;
|
|
493
|
+
margin-left: 16px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.unHaveSign {
|
|
497
|
+
width: 100%;
|
|
498
|
+
margin: 0 auto;
|
|
499
|
+
min-height: 197px;
|
|
500
|
+
text-align: center;
|
|
501
|
+
position: relative;
|
|
502
|
+
|
|
503
|
+
.uploadContent {
|
|
504
|
+
margin-bottom: 5px;
|
|
505
|
+
font-size: 11.5px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.upload-link {
|
|
509
|
+
font-weight: 500;
|
|
510
|
+
font-size: 10px;
|
|
511
|
+
color: #409eff;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.signImgBox, .qrCodeImgBox {
|
|
517
|
+
box-sizing: border-box;
|
|
518
|
+
width: 145px;
|
|
519
|
+
height: 145px;
|
|
520
|
+
overflow: hidden;
|
|
521
|
+
margin: 0 auto 15px;
|
|
522
|
+
position: relative;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.signImgBox {
|
|
526
|
+
width: 133px;
|
|
527
|
+
height: 100px;
|
|
528
|
+
margin-top: 50px;
|
|
529
|
+
display: flex;
|
|
530
|
+
align-items: center;
|
|
531
|
+
justify-content: center;
|
|
532
|
+
|
|
533
|
+
.no-sign-tip {
|
|
534
|
+
display: flex;
|
|
535
|
+
flex-direction: column;
|
|
536
|
+
align-items: center;
|
|
537
|
+
justify-content: center;
|
|
538
|
+
width: 100%;
|
|
539
|
+
height: 100%;
|
|
540
|
+
background: #fafafa;
|
|
541
|
+
border-radius: 4px;
|
|
542
|
+
gap: 8px;
|
|
543
|
+
|
|
544
|
+
.tip-icon {
|
|
545
|
+
flex-shrink: 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.tip-text {
|
|
549
|
+
font-size: 12px;
|
|
550
|
+
color: #999;
|
|
551
|
+
text-align: center;
|
|
552
|
+
line-height: 1.4;
|
|
553
|
+
padding: 0 8px;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.unHaveSignUp {
|
|
559
|
+
display: flex;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
position: relative;
|
|
562
|
+
|
|
563
|
+
.qrCodeBtnBox {
|
|
564
|
+
position: absolute;
|
|
565
|
+
right: calc(50% - 145px);
|
|
566
|
+
top: 72.5px;
|
|
567
|
+
margin-left: 16px;
|
|
568
|
+
display: flex;
|
|
569
|
+
flex-direction: column;
|
|
570
|
+
gap: 8px;
|
|
571
|
+
|
|
572
|
+
.el-button + .el-button {
|
|
573
|
+
margin-left: 0 !important;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.el-button {
|
|
577
|
+
min-width: 60px;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.uploadContent {
|
|
582
|
+
margin-top: 8px;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 二维码状态覆盖层样式
|
|
587
|
+
.qrcode-status-overlay {
|
|
588
|
+
position: absolute;
|
|
589
|
+
top: 0;
|
|
590
|
+
left: 0;
|
|
591
|
+
right: 0;
|
|
592
|
+
bottom: 0;
|
|
593
|
+
display: flex;
|
|
594
|
+
flex-direction: column;
|
|
595
|
+
align-items: center;
|
|
596
|
+
justify-content: center;
|
|
597
|
+
gap: 6px;
|
|
598
|
+
animation: fadeIn 0.25s ease-out;
|
|
599
|
+
z-index: 10;
|
|
600
|
+
|
|
601
|
+
&::before {
|
|
602
|
+
content: '';
|
|
603
|
+
position: absolute;
|
|
604
|
+
top: 0;
|
|
605
|
+
left: 0;
|
|
606
|
+
right: 0;
|
|
607
|
+
bottom: 0;
|
|
608
|
+
background-image:
|
|
609
|
+
repeating-linear-gradient(0deg, transparent, transparent 1px, currentColor 1px, currentColor 2px),
|
|
610
|
+
repeating-linear-gradient(90deg, transparent, transparent 1px, currentColor 1px, currentColor 2px);
|
|
611
|
+
opacity: 0.25;
|
|
612
|
+
z-index: 1;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.status-icon {
|
|
616
|
+
flex-shrink: 0;
|
|
617
|
+
position: relative;
|
|
618
|
+
z-index: 3;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.status-text {
|
|
622
|
+
font-size: 12px;
|
|
623
|
+
font-weight: 400;
|
|
624
|
+
letter-spacing: 0.5px;
|
|
625
|
+
line-height: 1;
|
|
626
|
+
position: relative;
|
|
627
|
+
z-index: 3;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
&.scanned {
|
|
631
|
+
background: rgba(255, 255, 255, 0.96);
|
|
632
|
+
color: rgba(100, 100, 100, 0.3);
|
|
633
|
+
|
|
634
|
+
.status-text {
|
|
635
|
+
color: #333333;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
&.expired {
|
|
640
|
+
background: rgba(40, 40, 40, 0.92);
|
|
641
|
+
color: rgba(180, 180, 180, 0.3);
|
|
642
|
+
cursor: pointer;
|
|
643
|
+
|
|
644
|
+
.status-text {
|
|
645
|
+
color: #e0e0e0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.status-icon path {
|
|
649
|
+
fill: #b0b0b0;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
@keyframes fadeIn {
|
|
655
|
+
from { opacity: 0; }
|
|
656
|
+
to { opacity: 1; }
|
|
657
|
+
}
|
|
658
|
+
</style>
|