<script setup lang="ts">
|
import QRCode from 'qrcode'
|
import { fetchMyApprovalTodos, fetchMyVisitorCodes, fetchVisitRecords } from '@/api/appointment'
|
import { useMiniAuthStore } from '@/store/auth'
|
import type { MiniMyVisitorCodeOrderItem, MiniMyVisitorCodeTicketItem } from '@/types/appointment'
|
import { getApiErrorCode } from '@/utils/mini-login'
|
|
interface VisitorCodeDisplayTicket extends MiniMyVisitorCodeTicketItem {
|
qrCode: string
|
}
|
|
interface VisitorCodeDisplayOrder extends Omit<MiniMyVisitorCodeOrderItem, 'qrCodes'> {
|
timeText: string
|
qrCodes: VisitorCodeDisplayTicket[]
|
}
|
|
function wait(ms: number) {
|
return new Promise(resolve => setTimeout(resolve, ms))
|
}
|
|
const systemInfo = uni.getSystemInfoSync()
|
const { safeAreaInsets } = systemInfo
|
const parkFlowBottomPx = Math.max(24, Math.round(systemInfo.windowHeight / 8))
|
const router = useRouter()
|
const authStore = useMiniAuthStore()
|
const visitorOrders = ref<VisitorCodeDisplayOrder[]>([])
|
const currentOrderIndex = ref(0)
|
const loadingVisitorCodes = ref(false)
|
const qrRenderToken = ref(0)
|
const ticketIndexMap = ref<Record<number, number>>({})
|
const approvalTodoCount = ref(0)
|
const visitRecordCount = ref(0)
|
|
const profile = computed(() => authStore.profile)
|
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
const isEmployee = computed(() => profile.value?.userType === 'EMPLOYEE')
|
const showEmployeeDashboard = computed(() => isLoggedIn.value && isEmployee.value)
|
const welcomeName = computed(() => {
|
if (!isLoggedIn.value) {
|
return '欢迎使用,请登录'
|
}
|
const displayName = profile.value?.displayName?.trim()
|
if (displayName) {
|
return displayName
|
}
|
const mobile = profile.value?.mobile?.trim() || ''
|
if (mobile.length >= 4) {
|
return mobile.slice(-4)
|
}
|
return '用户'
|
})
|
const userRoleText = computed(() => {
|
if (!profile.value || !isLoggedIn.value) {
|
return ''
|
}
|
if (profile.value.userType === 'EMPLOYEE') {
|
return '员工'
|
}
|
return ''
|
})
|
const secondaryText = computed(() => {
|
if (!profile.value || !isLoggedIn.value) {
|
return ''
|
}
|
if (profile.value.userType === 'EMPLOYEE') {
|
return profile.value.deptName || '未分配部门'
|
}
|
return profile.value.companyName || ''
|
})
|
const approvalTodoDisplay = computed(() => isLoggedIn.value ? String(approvalTodoCount.value) : '-')
|
const visitRecordDisplay = computed(() => isLoggedIn.value ? String(visitRecordCount.value) : '-')
|
const currentOrder = computed(() => visitorOrders.value[currentOrderIndex.value] || null)
|
const currentOrderHasMultipleTickets = computed(() => (currentOrder.value?.qrCodes?.length || 0) > 1)
|
const hasLongAccessAreaText = computed(() =>
|
visitorOrders.value.some(order => (order.accessAreaText || '').trim().length > 24),
|
)
|
const visitorCodeCardMinHeightClass = computed(() => {
|
if (hasLongAccessAreaText.value) {
|
return currentOrderHasMultipleTickets.value ? 'min-h-[940rpx]' : 'min-h-[900rpx]'
|
}
|
return currentOrderHasMultipleTickets.value ? 'min-h-[844rpx]' : 'min-h-[800rpx]'
|
})
|
const currentTicket = computed(() => {
|
if (!currentOrder.value) {
|
return null
|
}
|
const index = ticketIndexMap.value[currentOrder.value.orderId] ?? 0
|
return currentOrder.value.qrCodes[index] || currentOrder.value.qrCodes[0] || null
|
})
|
|
function handlePrevOrder() {
|
if (currentOrderIndex.value > 0) {
|
currentOrderIndex.value--
|
}
|
}
|
|
function handleNextOrder() {
|
if (currentOrderIndex.value < visitorOrders.value.length - 1) {
|
currentOrderIndex.value++
|
}
|
}
|
|
function onSwiperChange(e: { detail?: { current?: number } }) {
|
currentOrderIndex.value = e.detail?.current || 0
|
}
|
|
function currentTicketIndex(orderId: number) {
|
return ticketIndexMap.value[orderId] ?? 0
|
}
|
|
function handlePrevTicket(orderId: number) {
|
const currentIndex = currentTicketIndex(orderId)
|
if (currentIndex <= 0) {
|
return
|
}
|
ticketIndexMap.value = {
|
...ticketIndexMap.value,
|
[orderId]: currentIndex - 1,
|
}
|
}
|
|
function handleNextTicket(orderId: number) {
|
const order = visitorOrders.value.find(item => item.orderId === orderId)
|
const currentIndex = currentTicketIndex(orderId)
|
if (!order || currentIndex >= order.qrCodes.length - 1) {
|
return
|
}
|
ticketIndexMap.value = {
|
...ticketIndexMap.value,
|
[orderId]: currentIndex + 1,
|
}
|
}
|
|
function formatVisitTime(order: MiniMyVisitorCodeOrderItem) {
|
return order.visitTimeText || '暂无有效期'
|
}
|
|
function isUnauthorizedError(error: unknown) {
|
return getApiErrorCode(error) === 401 || getApiErrorCode(error) === 403
|
}
|
|
function formatDateTime(value?: string | null) {
|
if (!value) {
|
return ''
|
}
|
return value.replace('T', ' ').slice(0, 16)
|
}
|
|
function buildVisitorOrders(orders: MiniMyVisitorCodeOrderItem[]): VisitorCodeDisplayOrder[] {
|
const mapped = orders.map(order => ({
|
...order,
|
timeText: formatVisitTime(order),
|
qrCodes: order.qrCodes.map((ticket: MiniMyVisitorCodeTicketItem) => ({
|
...ticket,
|
qrCode: '',
|
})),
|
}))
|
return mapped.sort((a, b) => resolveOrderSortTime(a) - resolveOrderSortTime(b))
|
}
|
|
function resolveOrderSortTime(order: MiniMyVisitorCodeOrderItem) {
|
const extractDate = (text?: string | null) => {
|
const raw = (text || '').trim()
|
const match = raw.match(/(\d{4}-\d{2}-\d{2})/)
|
if (!match) {
|
return Number.MAX_SAFE_INTEGER
|
}
|
const ts = new Date(`${match[1]}T00:00:00`).getTime()
|
return Number.isNaN(ts) ? Number.MAX_SAFE_INTEGER : ts
|
}
|
const fromVisitText = extractDate(order.visitTimeText)
|
if (fromVisitText !== Number.MAX_SAFE_INTEGER) {
|
return fromVisitText
|
}
|
return extractDate(order.validStartTime || order.validEndTime || '')
|
}
|
|
async function loadVisitorCodes() {
|
if (!isLoggedIn.value) {
|
visitorOrders.value = []
|
ticketIndexMap.value = {}
|
currentOrderIndex.value = 0
|
loadingVisitorCodes.value = false
|
return
|
}
|
loadingVisitorCodes.value = true
|
const currentToken = qrRenderToken.value + 1
|
qrRenderToken.value = currentToken
|
try {
|
const orders = await fetchMyVisitorCodes()
|
const displayOrders = buildVisitorOrders(orders)
|
visitorOrders.value = displayOrders
|
currentOrderIndex.value = 0
|
ticketIndexMap.value = Object.fromEntries(displayOrders.map(item => [item.orderId, 0]))
|
await nextTick()
|
const hydratedOrders: VisitorCodeDisplayOrder[] = []
|
for (const order of displayOrders) {
|
const hydratedTickets: VisitorCodeDisplayTicket[] = []
|
for (const ticket of order.qrCodes) {
|
hydratedTickets.push({
|
...ticket,
|
qrCode: await createQrImage(ticket.payload),
|
})
|
}
|
hydratedOrders.push({
|
...order,
|
qrCodes: hydratedTickets,
|
})
|
}
|
if (qrRenderToken.value === currentToken) {
|
visitorOrders.value = hydratedOrders
|
}
|
}
|
catch (error) {
|
visitorOrders.value = []
|
ticketIndexMap.value = {}
|
if (isUnauthorizedError(error)) {
|
return
|
}
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载访客码失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
if (qrRenderToken.value === currentToken) {
|
loadingVisitorCodes.value = false
|
}
|
}
|
}
|
|
async function loadEmployeeDashboardCounts() {
|
if (!isLoggedIn.value || !isEmployee.value) {
|
approvalTodoCount.value = 0
|
visitRecordCount.value = 0
|
return
|
}
|
try {
|
const [todoResult, visitRecordResult] = await Promise.all([
|
fetchMyApprovalTodos({ page: 1, size: 1 }),
|
fetchVisitRecords({ page: 1, size: 1, futureOnly: true }),
|
])
|
approvalTodoCount.value = todoResult.total
|
visitRecordCount.value = visitRecordResult.total
|
}
|
catch (error) {
|
approvalTodoCount.value = 0
|
visitRecordCount.value = 0
|
console.warn('load employee dashboard counts failed', error)
|
}
|
}
|
|
async function createQrImage(payload: string) {
|
// #ifdef H5
|
return QRCode.toDataURL(payload, {
|
errorCorrectionLevel: 'M',
|
margin: 1,
|
width: 300,
|
})
|
// #endif
|
|
const qr = QRCode.create(payload, {
|
errorCorrectionLevel: 'M',
|
margin: 1,
|
})
|
const size = 400
|
const moduleCount = qr.modules.size
|
const tileSize = size / moduleCount
|
const canvasId = 'visitorQrCanvas'
|
|
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, () => {
|
void wait(500).then(() => {
|
uni.canvasToTempFilePath({
|
canvasId,
|
width: size,
|
height: size,
|
destWidth: size * 2,
|
destHeight: size * 2,
|
success: res => resolve(res.tempFilePath),
|
fail: reject,
|
})
|
})
|
})
|
})
|
}
|
|
function handleDownloadVisitorCode(qrCodeUrl: string) {
|
if (!qrCodeUrl) {
|
uni.showToast({
|
title: '访客码生成中',
|
icon: 'none',
|
})
|
return
|
}
|
|
uni.showLoading({ title: '保存中...' })
|
void resolveSaveFilePath(qrCodeUrl)
|
.then(filePath => saveImageToAlbum(filePath))
|
.catch((error) => {
|
uni.hideLoading()
|
uni.showToast({
|
title: error instanceof Error ? error.message : '保存失败',
|
icon: 'none',
|
})
|
})
|
}
|
|
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
|
}
|
|
if (src.startsWith('wxfile://') || src.startsWith('file://') || src.startsWith('/')) {
|
resolve(src)
|
return
|
}
|
|
uni.getImageInfo({
|
src,
|
success: image => resolve(image.path),
|
fail: () => reject(new Error('获取图片失败')),
|
})
|
})
|
}
|
|
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({
|
success(settingdata) {
|
if (settingdata.authSetting['scope.writePhotosAlbum']) {
|
uni.showToast({ title: '授权成功,请重新点击下载', icon: 'none' })
|
}
|
else {
|
uni.showToast({ title: '授权失败,无法保存', icon: 'none' })
|
}
|
},
|
})
|
}
|
},
|
})
|
return
|
}
|
uni.showToast({
|
title: '保存失败',
|
icon: 'none',
|
})
|
console.error('save visitor code failed', err)
|
},
|
})
|
}
|
|
function handleOpenParkFlow() {
|
uni.navigateTo({
|
url: `/pages/aboutUs/index?title=${encodeURIComponent('入园流程')}&code=CONTENT_PARK_FLOW_HTML`,
|
})
|
}
|
|
function showLoginPrompt(message?: string) {
|
authStore.showLoginPrompt(message)
|
}
|
|
function hideLoginPrompt() {
|
authStore.hideLoginPrompt()
|
}
|
|
function ensureLoggedIn(action?: () => void, message?: string) {
|
if (isLoggedIn.value) {
|
action?.()
|
return true
|
}
|
showLoginPrompt(message)
|
return false
|
}
|
|
async function reloadHomeData() {
|
if (!isLoggedIn.value) {
|
visitorOrders.value = []
|
ticketIndexMap.value = {}
|
currentOrderIndex.value = 0
|
approvalTodoCount.value = 0
|
visitRecordCount.value = 0
|
return
|
}
|
await Promise.all([
|
loadVisitorCodes(),
|
loadEmployeeDashboardCounts(),
|
])
|
}
|
|
function handleOpenApprovalTodo() {
|
ensureLoggedIn(() => {
|
uni.navigateTo({ url: '/pages/reviewList/index' })
|
}, '您还未登录,请先登录')
|
}
|
|
function handleOpenVisitRecords() {
|
ensureLoggedIn(() => {
|
uni.navigateTo({
|
url: '/pages/reception/index',
|
})
|
}, '您还未登录,请先登录')
|
}
|
|
function resolveShareTicketUuid(order?: VisitorCodeDisplayOrder | null) {
|
const targetOrder = order || currentOrder.value
|
if (!targetOrder) {
|
return ''
|
}
|
const index = ticketIndexMap.value[targetOrder.orderId] ?? 0
|
return targetOrder.qrCodes[index]?.ticketUuid || targetOrder.qrCodes[0]?.ticketUuid || ''
|
}
|
|
function buildVisitorCodeSharePath(ticketUuid?: string | null) {
|
const uuid = ticketUuid || resolveShareTicketUuid()
|
return uuid ? `/pages/visitorCodeShare/index?ticketUuid=${encodeURIComponent(uuid)}` : '/pages/index/index'
|
}
|
|
onShareAppMessage((res) => {
|
const ticketUuid = res?.target?.dataset?.ticketUuid as string | undefined
|
if (!ticketUuid) {
|
return {
|
title: '红宇集团访客预约',
|
path: '/pages/index/index',
|
}
|
}
|
return {
|
title: '访客码的分享',
|
path: buildVisitorCodeSharePath(ticketUuid),
|
}
|
})
|
|
onShow(() => {
|
authStore.restoreSession()
|
void reloadHomeData()
|
})
|
|
onMounted(() => {
|
uni.$on('mini-login-success', reloadHomeData)
|
})
|
|
onUnmounted(() => {
|
uni.$off('mini-login-success', reloadHomeData)
|
})
|
|
definePage({
|
name: 'home',
|
layout: 'tabbar',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
</script>
|
|
<template>
|
<view class="relative">
|
<image src="/static/home/bg.png" class="absolute left-0 top-0 z-0 h-[1624rpx] w-full" mode="scaleToFill" />
|
<view class="home-header-backdrop" />
|
<view class="home-content-backdrop" />
|
|
<view class="relative z-4 pb-[160rpx]" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }">
|
<view class="mb-[24rpx] h-[88rpx] flex items-center px-[32rpx]">
|
<text class="text-[36rpx] text-white font-medium">
|
红宇集团访客预约
|
</text>
|
</view>
|
|
<view class="mb-[28rpx] flex items-center pl-[26rpx] pr-[22rpx]">
|
<image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" />
|
<view class="flex flex-col">
|
<view class="welcome-title flex items-center text-[56rpx] text-white font-[600]">
|
<text>{{ welcomeName }}</text>
|
</view>
|
</view>
|
</view>
|
|
<view v-if="showEmployeeDashboard" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
|
<view
|
class="dashboard-panel rounded-[16rpx] p-[24rpx] pb-[26rpx]"
|
>
|
<view class="flex gap-[30rpx]">
|
<view
|
class="home-feature-card h-[126rpx] w-[312rpx] flex rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
|
style="background: url('/static/home/toDo_bg.png') no-repeat center center;
|
background-size: 100%;box-sizing: border-box;"
|
@click="handleOpenApprovalTodo"
|
>
|
<image src="/static/home/toDo.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
|
<view class="mt-[8rpx] flex flex-col">
|
<text class="mb-[4rpx] text-[24rpx] text-[#056DFF] font-[600]">
|
待办
|
</text>
|
<text class="text-[32rpx] text-[#056DFF] font-bold">
|
{{ approvalTodoDisplay }}
|
</text>
|
</view>
|
</view>
|
<view
|
class="home-feature-card h-[126rpx] w-[312rpx] flex items-center rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
|
style="background: url('/static/home/wait_bg.png') no-repeat center center;
|
background-size: 100%;box-sizing: border-box;"
|
@click="handleOpenVisitRecords"
|
>
|
<image src="/static/home/wait.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
|
<view class="mt-[8rpx] flex flex-col">
|
<text class="mb-[4rpx] text-[24rpx] text-[#F86911] font-[600]">
|
待接待
|
</text>
|
<text class="text-[32rpx] text-[#F86911] font-bold">
|
{{ visitRecordDisplay }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="flex gap-[16rpx] pl-[24rpx] pr-[22rpx]">
|
<view
|
class="home-action-card h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]"
|
style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);"
|
@click="ensureLoggedIn(() => { router.push({ name: 'appointment' }) })"
|
>
|
<image src="/static/home/appointment.png" class="mb-[12rpx] h-[96rpx] w-[96rpx]" />
|
<text class="text-[28rpx] text-[#1a1a1a]">
|
预约
|
</text>
|
</view>
|
<view class="flex flex-1 flex-col gap-[16rpx]">
|
<view
|
class="home-action-card flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
|
style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);"
|
@click="ensureLoggedIn(() => { router.push({ name: 'record' }) })"
|
>
|
<image src="/static/home/record.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
|
<text class="text-[28rpx] text-[#000000]">
|
预约记录
|
</text>
|
</view>
|
<view
|
class="home-action-card flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
|
style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);"
|
@click="ensureLoggedIn(() => { uni.navigateTo({ url: '/pages/commonVisitors/index' }) })"
|
>
|
<image src="/static/home/visitor.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
|
<text class="text-[28rpx] text-[#000000]">
|
常用来访人
|
</text>
|
</view>
|
</view>
|
</view>
|
|
<view v-if="isLoggedIn" class="mt-[32rpx] px-[24rpx]">
|
<view class="mb-[20rpx] flex items-center justify-between">
|
<text class="text-[34rpx] text-[#1a1a1a]">
|
我的访客码
|
</text>
|
<view v-if="visitorOrders.length > 0" class="flex items-center">
|
<image
|
src="/static/home/chevron_left.png"
|
class="h-[40rpx] w-[40rpx] pr-[24rpx]"
|
@click="handlePrevOrder"
|
/>
|
<text class="text-[28rpx] text-[#999999] line-height-[40rpx]">
|
{{ currentOrderIndex + 1 }}/{{ visitorOrders.length }}
|
</text>
|
<image
|
src="/static/home/chevron_right.png"
|
class="h-[40rpx] w-[40rpx] pl-[24rpx]"
|
@click="handleNextOrder"
|
/>
|
</view>
|
</view>
|
|
<view
|
v-if="loadingVisitorCodes"
|
class="h-[420rpx] flex items-center justify-center rounded-[12rpx] bg-white text-[28rpx] text-[#999999]"
|
style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);"
|
>
|
加载访客码中...
|
</view>
|
|
<view
|
v-else-if="visitorOrders.length === 0"
|
class="h-[420rpx] flex items-center justify-center rounded-[12rpx] bg-white text-[28rpx] text-[#999999]"
|
style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);"
|
>
|
暂无可展示的访客码
|
</view>
|
|
<swiper
|
v-else
|
class="w-full"
|
:class="visitorCodeCardMinHeightClass"
|
:current="currentOrderIndex"
|
style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);"
|
@change="onSwiperChange"
|
>
|
<swiper-item v-for="order in visitorOrders" :key="order.orderId">
|
<view
|
class="h-full w-full flex flex-col rounded-[12rpx] bg-white py-[24rpx]"
|
style="box-sizing: border-box;overflow: hidden;"
|
>
|
<view class="flex flex-col items-center">
|
<text class="mb-[16rpx] px-[32rpx] text-center text-[28rpx] text-[#1a1a1a]">
|
{{ order.visitedUserDisplay }}
|
</text>
|
<view
|
class="mb-[28rpx] mt-[16rpx] h-[48rpx] w-full flex items-center justify-center px-[20rpx]"
|
style="background: linear-gradient(90deg, rgba(0,104,255,0) 0%, rgba(0,104,255) 50%, rgba(0,104,255,0) 100%);"
|
>
|
<text class="truncate text-[24rpx] text-white">
|
{{ order.timeText }}
|
</text>
|
</view>
|
</view>
|
|
<view class="relative flex items-center justify-center px-[72rpx]">
|
<image
|
v-if="order.qrCodes.length > 1 && currentTicketIndex(order.orderId) > 0"
|
src="/static/home/left.png"
|
class="absolute left-[32rpx] h-[48rpx] w-[24rpx]"
|
@click="handlePrevTicket(order.orderId)"
|
/>
|
<image
|
:src="currentOrder?.orderId === order.orderId ? currentTicket?.qrCode : order.qrCodes[0]?.qrCode"
|
class="h-[400rpx] w-[400rpx]"
|
mode="aspectFit"
|
/>
|
<image
|
v-if="order.qrCodes.length > 1 && currentTicketIndex(order.orderId) < order.qrCodes.length - 1"
|
src="/static/home/right.png"
|
class="absolute right-[32rpx] h-[48rpx] w-[24rpx]"
|
@click="handleNextTicket(order.orderId)"
|
/>
|
</view>
|
|
<view class="mt-[12rpx] flex flex-col items-center justify-center gap-[8rpx] pb-[24rpx]">
|
<text class="text-[24rpx] text-[rgba(26,26,26,0.5)]">
|
{{ (currentOrder?.orderId === order.orderId ? currentTicket?.visitorName : order.qrCodes[0]?.visitorName) }}的访客码
|
</text>
|
<text
|
v-if="order.accessAreaText"
|
class="px-[40rpx] text-center text-[22rpx] leading-[32rpx] text-[rgba(26,26,26,0.45)]"
|
>
|
通行区域:{{ order.accessAreaText }}
|
</text>
|
<text v-if="order.qrCodes.length > 1" class="text-[22rpx] text-[rgba(26,26,26,0.45)]">
|
{{ currentTicketIndex(order.orderId) + 1 }}/{{ order.qrCodes.length }}
|
</text>
|
</view>
|
|
<view class="mt-auto h-[104rpx] flex items-center justify-center gap-[32rpx] pt-[20rpx]" style="border-top: 2rpx dashed #D8D8D8;border-radius: 12rpx;">
|
<view
|
class="w-[144rpx] flex flex-col items-center justify-center"
|
@click="handleDownloadVisitorCode(currentOrder?.orderId === order.orderId ? currentTicket?.qrCode || '' : order.qrCodes[0]?.qrCode || '')"
|
>
|
<image src="/static/home/download.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" />
|
<text class="text-[24rpx] text-[#1a1a1a]">
|
下载访客码
|
</text>
|
</view>
|
<button
|
class="m-0 w-[144rpx] flex flex-col items-center justify-center border-none bg-transparent p-0 line-height-normal after:border-none"
|
open-type="share"
|
:data-ticket-uuid="resolveShareTicketUuid(order)"
|
:data-visited-user-display="order.visitedUserDisplay"
|
>
|
<image src="/static/home/share.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" />
|
<text class="text-[24rpx] text-[#1a1a1a]">
|
分享好友
|
</text>
|
</button>
|
</view>
|
<view class="h-[20rpx] w-full shrink-0" />
|
</view>
|
</swiper-item>
|
</swiper>
|
</view>
|
|
<view class="mt-[48rpx] flex justify-center pb-[32rpx]">
|
<text class="text-[24rpx] text-[#999999]">
|
备案号:湘ICP备2026025199号-1X
|
</text>
|
</view>
|
</view>
|
|
<image
|
v-if="isLoggedIn"
|
src="/static/home/process.png"
|
class="fixed right-[-42rpx] z-5 h-[128rpx] w-[128rpx]"
|
:style="{ bottom: `${parkFlowBottomPx}px` }"
|
@click="handleOpenParkFlow"
|
/>
|
<canvas canvas-id="visitorQrCanvas" class="qr-canvas" />
|
</view>
|
</template>
|
|
<style scoped>
|
.home-content-backdrop {
|
position: absolute;
|
left: 0;
|
top: 420rpx;
|
z-index: 0;
|
width: 100%;
|
height: 924rpx;
|
background:
|
linear-gradient(180deg, rgba(196, 232, 249, 0) 0%, rgba(196, 232, 249, 0.72) 12%, rgba(226, 244, 250, 0.9) 38%, rgba(239, 248, 252, 0.96) 100%);
|
pointer-events: none;
|
}
|
|
.home-header-backdrop {
|
position: absolute;
|
left: 0;
|
top: 0;
|
z-index: 1;
|
width: 100%;
|
height: 520rpx;
|
background: linear-gradient(180deg, rgba(23, 145, 255, 0.96) 0%, rgba(39, 161, 255, 0.82) 62%, rgba(39, 161, 255, 0) 100%);
|
pointer-events: none;
|
}
|
|
.dashboard-panel {
|
border: 1rpx solid rgba(255, 255, 255, 0.92);
|
background: linear-gradient(180deg, rgba(190, 222, 255, 0.98) 0%, rgba(245, 251, 255, 0.96) 48%, rgba(255, 255, 255, 0.98) 100%);
|
box-shadow: 0 14rpx 30rpx rgba(31, 111, 206, 0.22), 0 4rpx 10rpx rgba(255, 255, 255, 0.7) inset;
|
}
|
|
.home-feature-card,
|
.home-action-card {
|
border: 1rpx solid rgba(255, 255, 255, 0.92);
|
box-shadow: 0 14rpx 28rpx rgba(35, 111, 205, 0.2), 0 4rpx 10rpx rgba(255, 255, 255, 0.65) inset;
|
}
|
|
.home-action-card {
|
box-sizing: border-box;
|
}
|
|
.welcome-title {
|
font-family: 'YouSheBiaoTiHei', sans-serif;
|
}
|
|
.welcome-title text {
|
font-family: 'YouSheBiaoTiHei', sans-serif;
|
}
|
|
.qr-canvas {
|
position: fixed;
|
left: -9999px;
|
top: -9999px;
|
width: 400px;
|
height: 400px;
|
pointer-events: none;
|
}
|
</style>
|