<script setup lang="ts">
|
import QRCode from 'qrcode'
|
import { fetchSharedTicketVisitorCode } from '@/api/appointment'
|
import type { MiniMyVisitorCodeOrderItem, MiniMyVisitorCodeTicketItem } from '@/types/appointment'
|
|
interface VisitorCodeDisplayTicket extends MiniMyVisitorCodeTicketItem {
|
qrCode: string
|
}
|
|
interface VisitorCodeDisplayOrder extends Omit<MiniMyVisitorCodeOrderItem, 'qrCodes'> {
|
qrCodes: VisitorCodeDisplayTicket[]
|
}
|
|
definePage({
|
name: 'visitorCodeShare',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
const router = useRouter()
|
const sysInfo = uni.getSystemInfoSync()
|
const statusBarHeight = sysInfo.statusBarHeight || 0
|
const navBarHeight = 44
|
const ticketUuid = ref('')
|
const order = ref<VisitorCodeDisplayOrder | null>(null)
|
const loading = ref(false)
|
|
const currentTicket = computed(() => order.value?.qrCodes?.[0] || null)
|
|
function handleBack() {
|
router.back()
|
}
|
|
function ticketTimeText() {
|
return order.value?.visitTimeText || ''
|
}
|
|
async function createQrImage(payload: string) {
|
// #ifdef H5
|
return QRCode.toDataURL(payload, {
|
errorCorrectionLevel: 'M',
|
margin: 1,
|
width: 400,
|
})
|
// #endif
|
const qr = QRCode.create(payload, {
|
errorCorrectionLevel: 'M',
|
margin: 1,
|
})
|
const size = 400
|
const moduleCount = qr.modules.size
|
const tileSize = size / moduleCount
|
const canvasId = 'sharedSingleVisitorQrCanvas'
|
|
return new Promise<string>((resolve, reject) => {
|
const context = uni.createCanvasContext(canvasId)
|
context.setFillStyle('#FFFFFF')
|
context.fillRect(0, 0, size, size)
|
context.setFillStyle('#000000')
|
|
for (let row = 0; row < moduleCount; row += 1) {
|
for (let col = 0; col < moduleCount; col += 1) {
|
const index = row * moduleCount + col
|
if (!qr.modules.data[index]) {
|
continue
|
}
|
const x = Math.round(col * tileSize)
|
const y = Math.round(row * tileSize)
|
const width = Math.ceil((col + 1) * tileSize) - x
|
const height = Math.ceil((row + 1) * tileSize) - y
|
context.fillRect(x, y, width, height)
|
}
|
}
|
|
context.draw(false, () => {
|
uni.canvasToTempFilePath({
|
canvasId,
|
width: size,
|
height: size,
|
destWidth: size * 2,
|
destHeight: size * 2,
|
success: res => resolve(res.tempFilePath),
|
fail: reject,
|
})
|
})
|
})
|
}
|
|
async function loadData() {
|
if (!ticketUuid.value) {
|
uni.showToast({ title: '缺少分享参数', icon: 'none' })
|
return
|
}
|
loading.value = true
|
try {
|
const data = await fetchSharedTicketVisitorCode(ticketUuid.value)
|
const hydratedCodes: VisitorCodeDisplayTicket[] = []
|
for (const ticket of (data.qrCodes || [])) {
|
hydratedCodes.push({
|
...ticket,
|
qrCode: await createQrImage(ticket.payload),
|
})
|
}
|
order.value = {
|
...data,
|
qrCodes: hydratedCodes,
|
}
|
}
|
catch (error) {
|
order.value = null
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载访客码失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
function resolveSaveFilePath(src: string) {
|
return new Promise<string>((resolve, reject) => {
|
if (!src) {
|
reject(new Error('访客码生成中'))
|
return
|
}
|
if (src.startsWith('http://') || src.startsWith('https://')) {
|
uni.downloadFile({
|
url: src,
|
success: (res) => {
|
if (res.statusCode === 200 && res.tempFilePath) {
|
resolve(res.tempFilePath)
|
return
|
}
|
reject(new Error('下载图片失败'))
|
},
|
fail: () => reject(new Error('下载图片失败')),
|
})
|
return
|
}
|
resolve(src)
|
})
|
}
|
|
function saveImageToAlbum(filePath: string) {
|
uni.saveImageToPhotosAlbum({
|
filePath,
|
success: () => {
|
uni.hideLoading()
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
},
|
fail: (err) => {
|
uni.hideLoading()
|
const errMsg = err?.errMsg || ''
|
if (errMsg.includes('auth deny') || errMsg.includes('authorize no response')) {
|
uni.showModal({
|
title: '提示',
|
content: '需要您授权保存相册',
|
success(res) {
|
if (res.confirm) {
|
uni.openSetting()
|
}
|
},
|
})
|
return
|
}
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
},
|
})
|
}
|
|
function handleDownloadVisitorCode() {
|
if (!currentTicket.value?.qrCode) {
|
uni.showToast({ title: '访客码生成中', icon: 'none' })
|
return
|
}
|
uni.showLoading({ title: '保存中...' })
|
void resolveSaveFilePath(currentTicket.value.qrCode)
|
.then(filePath => saveImageToAlbum(filePath))
|
.catch((error) => {
|
uni.hideLoading()
|
uni.showToast({
|
title: error instanceof Error ? error.message : '保存失败',
|
icon: 'none',
|
})
|
})
|
}
|
|
function buildSharePath() {
|
return ticketUuid.value
|
? `/pages/visitorCodeShare/index?ticketUuid=${encodeURIComponent(ticketUuid.value)}`
|
: '/pages/index/index'
|
}
|
|
onShareAppMessage(() => ({
|
title: order.value?.visitedUserDisplay ? `${order.value.visitedUserDisplay}的访客码` : '访客码分享',
|
path: buildSharePath(),
|
}))
|
|
onLoad((options) => {
|
ticketUuid.value = options?.ticketUuid ? decodeURIComponent(options.ticketUuid) : ''
|
void loadData()
|
})
|
</script>
|
|
<template>
|
<view class="min-h-screen w-full relative box-border flex flex-col pb-[100rpx]">
|
<view class="fixed left-0 top-0 z-0 h-full w-full">
|
<image src="/static/viewCode/bg@2x.png" class="h-full w-full" mode="scaleToFill" />
|
</view>
|
|
<view class="relative z-50 w-full shrink-0" :style="{ height: `${statusBarHeight + navBarHeight}px` }">
|
<view class="fixed left-0 top-0 z-50 w-full" :style="{ height: `${statusBarHeight + navBarHeight}px` }">
|
<view class="absolute left-0 z-10 flex w-full items-center justify-between" :style="{ top: `${statusBarHeight}px`, height: `${navBarHeight}px` }">
|
<view class="relative z-10 flex h-full w-[88rpx] items-center justify-center" @click="handleBack">
|
<wd-icon name="arrow-left" size="40rpx" color="#ffffff" />
|
</view>
|
<view class="absolute left-0 z-0 w-full text-center text-[34rpx] text-white font-medium">
|
访客码
|
</view>
|
<view class="w-[88rpx]" />
|
</view>
|
</view>
|
</view>
|
|
<view class="relative z-10 flex flex-1 flex-col items-center px-[32rpx] pt-[24rpx]">
|
<view class="mb-[24rpx] flex items-center justify-center">
|
<wd-icon name="info-circle" size="32rpx" color="#FFC140" class="mr-[10rpx]" />
|
<text class="ml-[10rpx] text-[24rpx] text-[#FFF]">使用方式:前往访问单位,使用访客机刷此码进场</text>
|
</view>
|
|
<view v-if="loading" class="mt-[120rpx] text-[28rpx] text-white">
|
加载中...
|
</view>
|
|
<template v-else>
|
<view class="mb-[64rpx] w-full overflow-hidden rounded-[16rpx] bg-white shadow-[0_4rpx_16rpx_rgba(0,0,0,0.05)]">
|
<view class="h-[96rpx] flex items-center justify-center border-b-[2rpx] border-solid border-[#F5F5F5]">
|
<text class="text-[28rpx] text-[#1A1A1A] font-medium">
|
{{ currentTicket?.visitorName || '访客' }}的访客码
|
</text>
|
</view>
|
<view class="flex flex-col items-center py-[24rpx]">
|
<view class="mb-[16rpx] flex items-center">
|
<text class="text-[28rpx] text-[#1D2129] font-medium">被访人:</text>
|
<text class="text-[28rpx] text-[#B3B3B3]">{{ order?.visitedUserDisplay || '-' }}</text>
|
</view>
|
<view class="mb-[52rpx] flex h-[64rpx] w-full items-center justify-center" style="background: linear-gradient(90deg, rgba(0, 104, 255, 0) 0%, #0068FF 50%, rgba(0, 104, 255, 0) 100%);">
|
<text class="text-[28rpx] text-white">{{ ticketTimeText() || '-' }}</text>
|
</view>
|
|
<view class="mb-[20rpx] flex w-full items-center justify-center px-[40rpx] box-border">
|
<view class="h-[448rpx] w-[448rpx] flex items-center justify-center">
|
<image v-if="currentTicket?.qrCode" :src="currentTicket.qrCode" class="h-full w-full" mode="aspectFit" />
|
<view v-else class="text-[26rpx] text-[#999999]">暂无可用访客码</view>
|
</view>
|
</view>
|
|
<text v-if="order?.accessAreaText" class="mb-[10rpx] px-[40rpx] text-center text-[22rpx] text-[#999999]">
|
通行区域:{{ order.accessAreaText }}
|
</text>
|
<text class="mb-[16rpx] text-[24rpx] text-[#1A1A1A]">门岗访客机自主登记,刷身份证原件</text>
|
</view>
|
</view>
|
|
<view class="flex w-[480rpx] flex-col items-center gap-[32rpx]">
|
<view class="h-[96rpx] w-full flex items-center justify-center rounded-full bg-[#0068FF] shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)] transition-opacity active:opacity-70" @click="handleDownloadVisitorCode">
|
<image src="/static/viewCode/Download@2x.png" class="mr-[16rpx] h-[40rpx] w-[40rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-white font-medium">下载访客码</text>
|
</view>
|
<button class="m-0 h-[96rpx] w-full flex items-center justify-center rounded-full border-[2rpx] border-solid border-[#0068FF] bg-white p-0 transition-opacity active:opacity-70 after:border-none" open-type="share">
|
<image src="/static/home/share.png" class="mr-[16rpx] h-[40rpx] w-[40rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-[#0068FF] font-medium">分享好友</text>
|
</button>
|
</view>
|
</template>
|
</view>
|
</view>
|
<canvas canvas-id="sharedSingleVisitorQrCanvas" class="qr-canvas" />
|
</template>
|
|
<style scoped>
|
.qr-canvas {
|
position: fixed;
|
left: -9999px;
|
top: -9999px;
|
width: 400px;
|
height: 400px;
|
opacity: 0;
|
pointer-events: none;
|
}
|
</style>
|