longz
2026-04-27 25df88d0ab9d7444ed1d162b9efa1259ac2f6f66
yuyueMiniProgram/src/pages/index/index.vue
@@ -1,150 +1,369 @@
<script setup lang="ts">
import { ref } from 'vue'
import QRCode from 'qrcode'
import { fetchMyApprovalTodos, fetchMyVisitorCodes, fetchVisitRecords } from '@/api/appointment'
import { useMiniAuthStore } from '@/store/auth'
import type { MiniMyVisitorCodeOrderItem, MiniMyVisitorCodeTicketItem } from '@/types/appointment'
const { safeAreaInsets } = uni.getSystemInfoSync()
interface VisitorCodeDisplayTicket extends MiniMyVisitorCodeTicketItem {
  qrCode: string
}
// 弹窗显示状态
const showLoginPopup = ref(false)
interface VisitorCodeDisplayOrder extends Omit<MiniMyVisitorCodeOrderItem, 'qrCodes'> {
  timeText: string
  qrCodes: VisitorCodeDisplayTicket[]
}
// 登录状态
const isLoggedIn = ref(true)
// 用户角色: 'employee' 员工 | 'visitor' 普通访客
const userRole = ref<'employee' | 'visitor'>('visitor')
// 访客码数据
const visitorList = ref([
  {
    id: 1,
    name: '王敏',
    department: '管理部',
    area: 'AAAAA',
    time: '2023-10-10 11:30至2023-10-15 18:30',
    qrCode: '/static/home/成都喜望科技有限公司.png',
    visitorName: '王琦',
  },
  {
    id: 2,
    name: '李华',
    department: '技术部',
    area: 'BBBBB',
    time: '2023-11-01 09:00至2023-11-01 18:00',
    qrCode: '/static/home/成都喜望科技有限公司.png',
    visitorName: '张三',
  },
])
const currentVisitorIndex = ref(0)
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)
// 切换访客码
function handlePrevVisitor() {
  if (currentVisitorIndex.value > 0) {
    currentVisitorIndex.value--
const profile = computed(() => authStore.profile)
const isEmployee = computed(() => profile.value?.userType === 'EMPLOYEE')
const userRoleText = computed(() => {
  if (!profile.value) {
    return ''
  }
  if (profile.value.userType === 'EMPLOYEE') {
    return profile.value.deptLeaderFlag ? '部门领导' : '员工'
  }
  return '普通访客'
})
const secondaryText = computed(() => {
  if (!profile.value) {
    return ''
  }
  if (profile.value.userType === 'EMPLOYEE') {
    return profile.value.deptName || '未分配部门'
  }
  return profile.value.companyName || ''
})
const currentOrder = computed(() => visitorOrders.value[currentOrderIndex.value] || null)
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 handleNextVisitor() {
  if (currentVisitorIndex.value < visitorList.value.length - 1) {
    currentVisitorIndex.value++
function handleNextOrder() {
  if (currentOrderIndex.value < visitorOrders.value.length - 1) {
    currentOrderIndex.value++
  }
}
function onSwiperChange(e: any) {
  currentVisitorIndex.value = e.detail.current
function onSwiperChange(e: { detail?: { current?: number } }) {
  currentOrderIndex.value = e.detail?.current || 0
}
// 下载访客码
function handleDownloadVisitorCode(qrCodeUrl: string) {
  // 小程序环境中获取图片信息并保存到相册
  uni.showLoading({ title: '保存中...' })
function currentTicketIndex(orderId: number) {
  return ticketIndexMap.value[orderId] ?? 0
}
  // 这里如果是本地路径如 /static/xxx,可以直接用。如果后续换成网络图,需要先 uni.downloadFile
  // 为了演示,统一走 uni.getImageInfo 以校验文件存在并保存
  uni.getImageInfo({
    src: qrCodeUrl,
    success: (image) => {
      uni.saveImageToPhotosAlbum({
        filePath: image.path,
        success: () => {
          uni.hideLoading()
          uni.showToast({
            title: '保存成功',
            icon: 'success',
          })
        },
        fail: (err) => {
          uni.hideLoading()
          // 拒绝授权后会走 fail
          if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
            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' })
                      }
                    },
                  })
                }
              },
            })
          }
          else {
            uni.showToast({
              title: '保存失败',
              icon: 'none',
            })
          }
        },
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 formatDateTime(value?: string | null) {
  if (!value) {
    return ''
  }
  return value.replace('T', ' ').slice(0, 16)
}
function buildVisitorOrders(orders: MiniMyVisitorCodeOrderItem[]): VisitorCodeDisplayOrder[] {
  return orders.map(order => ({
    ...order,
    timeText: formatVisitTime(order),
    qrCodes: order.qrCodes.map((ticket: MiniMyVisitorCodeTicketItem) => ({
      ...ticket,
      qrCode: '',
    })),
  }))
}
async function loadVisitorCodes() {
  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 = await Promise.all(displayOrders.map(async order => ({
      ...order,
      qrCodes: await Promise.all(order.qrCodes.map(async ticket => ({
        ...ticket,
        qrCode: await createQrImage(ticket.payload),
      }))),
    })))
    if (qrRenderToken.value === currentToken) {
      visitorOrders.value = hydratedOrders
    }
  }
  catch (error) {
    visitorOrders.value = []
    ticketIndexMap.value = {}
    uni.showToast({
      title: error instanceof Error ? error.message : '加载访客码失败',
      icon: 'none',
    })
  }
  finally {
    if (qrRenderToken.value === currentToken) {
      loadingVisitorCodes.value = false
    }
  }
}
async function loadEmployeeDashboardCounts() {
  if (!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 = 300
  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, () => {
      uni.canvasToTempFilePath({
        canvasId,
        width: size,
        height: size,
        destWidth: size * 2,
        destHeight: size * 2,
        success: res => resolve(res.tempFilePath),
        fail: reject,
      })
    },
    fail: () => {
    })
  })
}
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: '获取图片失败',
        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 handleShowLoginPopup() {
  showLoginPopup.value = true
function handleOpenParkFlow() {
  uni.navigateTo({
    url: `/pages/aboutUs/index?title=${encodeURIComponent('入园流程')}&code=CONTENT_PARK_FLOW_HTML`,
  })
}
// 取消弹窗
function handleCancelLogin() {
  showLoginPopup.value = false
function handleOpenApprovalTodo() {
  uni.navigateTo({ url: '/pages/reviewList/index' })
}
// 确认登录(获取手机号回调)
function handleConfirmLogin(e: any) {
  showLoginPopup.value = false
  if (e.detail.errMsg === 'getPhoneNumber:ok') {
    // 用户同意授权,这里可以拿到 e.detail.code,发送给后端换取手机号
    console.log('获取手机号成功:', e.detail)
    isLoggedIn.value = true // 模拟登录成功状态
    uni.showToast({
      title: '登录成功',
      icon: 'success',
    })
  }
  else {
    // 用户拒绝授权
    console.log('用户拒绝授权手机号:', e.detail.errMsg)
    uni.showToast({
      title: '已取消登录',
      icon: 'none',
    })
  }
function handleOpenVisitRecords() {
  uni.navigateTo({
    url: '/pages/reception/index',
  })
}
function buildVisitorCodeSharePath(orderUuid?: string | null) {
  const uuid = orderUuid || currentOrder.value?.orderUuid
  return uuid ? `/pages/visitorCode/index?uuid=${encodeURIComponent(uuid)}` : '/pages/index/index'
}
onShareAppMessage((res) => {
  const orderUuid = res?.target?.dataset?.orderUuid as string | undefined
  const visitedUserDisplay = res?.target?.dataset?.visitedUserDisplay as string | undefined
  return {
    title: visitedUserDisplay ? `${visitedUserDisplay}的访客码` : '访客码分享',
    path: buildVisitorCodeSharePath(orderUuid),
  }
})
onShow(() => {
  authStore.restoreSession()
  void loadVisitorCodes()
  void loadEmployeeDashboardCounts()
})
definePage({
  name: 'home',
@@ -157,48 +376,39 @@
<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="relative z-4" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }">
      <!-- 导航栏标题 -->
      <view class="mb-[64rpx] h-[88rpx] flex items-center px-[32rpx]">
        <text class="text-[36rpx] text-white font-medium">
          预约小程序
        </text>
      </view>
      <!-- 欢迎使用区 -->
      <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]" @click="!isLoggedIn && handleShowLoginPopup()">
      <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]">
        <image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" />
        <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]">
          <template v-if="!isLoggedIn">
            <text>欢迎使用,</text>
            <view class="login-text relative">
              请登录
              <view class="login-text__underline absolute bottom-0 left-0 h-[4rpx] w-full bg-white" />
            </view>
          </template>
          <template v-else>
            <text>王丽</text>
          </template>
        <view class="flex flex-col">
          <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]">
            <text>{{ profile?.displayName || '用户' }}</text>
          </view>
          <text class="mt-[8rpx] text-[24rpx] text-white/90">
            {{ userRoleText }}<template v-if="secondaryText"> · {{ secondaryText }}</template>
          </text>
        </view>
      </view>
      <!-- 待办/待接待区 -->
      <view v-if="userRole === 'employee'" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
      <view v-if="isEmployee" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
        <view
          class="rounded-[16rpx] p-[24rpx] pb-[26rpx]"
          style="background: linear-gradient(180deg, #CCE4FE 0%, #FAFCFD 40%, #FDFDFD 100%); box-shadow: 0 4rpx 8rpx 0 rgba(0, 104, 255, 0.2);border: 1rpx solid #fff;"
        >
          <view class="flex gap-[30rpx]">
            <!-- 待办 -->
            <view
              class="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;
              box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)"
              @click="handleOpenApprovalTodo"
            >
              <image src="/static/home/toDo.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
              <view class="mt-[8rpx] flex flex-col">
@@ -206,16 +416,16 @@
                  待办
                </text>
                <text class="text-[32rpx] text-[#056DFF] font-bold">
                  -
                  {{ approvalTodoCount }}
                </text>
              </view>
            </view>
            <!-- 待接待 -->
            <view
              class="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;
              box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)"
              @click="handleOpenVisitRecords"
            >
              <image src="/static/home/wait.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
              <view class="mt-[8rpx] flex flex-col">
@@ -223,7 +433,7 @@
                  待接待
                </text>
                <text class="text-[32rpx] text-[#F86911] font-bold">
                  -
                  {{ visitRecordCount }}
                </text>
              </view>
            </view>
@@ -231,9 +441,7 @@
        </view>
      </view>
      <!-- 功能区 -->
      <view class="flex gap-[16rpx] pl-[24rpx] pr-[22rpx]">
        <!-- 预约 -->
        <view
          class="h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]"
          style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
@@ -244,10 +452,9 @@
            预约
          </text>
        </view>
        <!-- 预约记录 & 常用来访人 -->
        <view class="flex flex-1 flex-col gap-[16rpx]">
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            @click="() => { router.push({ name: 'record' }) }"
          >
@@ -259,6 +466,7 @@
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            @click="uni.navigateTo({ url: '/pages/choosePerson/index?mode=manage' })"
          >
            <image src="/static/home/visitor.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
            <text class="text-[28rpx] text-[#000000]">
@@ -267,74 +475,111 @@
          </view>
        </view>
      </view>
      <!-- 登录后:我的访客码区域 -->
      <view v-if="isLoggedIn" class="mt-[32rpx] px-[24rpx]">
        <!-- 头部标题与切换 -->
      <view class="mt-[32rpx] px-[24rpx]">
        <view class="mb-[20rpx] flex items-center justify-between">
          <text class="text-[34rpx] text-[#1a1a1a]">
            我的访客码
          </text>
          <view class="flex items-center">
          <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="handlePrevVisitor"
              @click="handlePrevOrder"
            />
            <text class="text-[28rpx] text-[#999999] line-height-[40rpx]">
              {{ currentVisitorIndex + 1 }}/{{ visitorList.length }}
              {{ currentOrderIndex + 1 }}/{{ visitorOrders.length }}
            </text>
            <image
              src="/static/home/chevron_right.png"
              class="h-[40rpx] w-[40rpx] pl-[24rpx]"
              @click="handleNextVisitor"
              @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
          class="min-h-[840rpx] w-full"
          :current="currentVisitorIndex"
          v-else
          class="min-h-[760rpx] w-full"
          :current="currentOrderIndex"
          style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);"
          @change="onSwiperChange"
        >
          <swiper-item v-for="(item) in visitorList" :key="item.id">
          <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] text-[28rpx] text-[#1a1a1a]">
                  {{ item.name }}({{ item.department }})
                <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"
                  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="text-[24rpx] text-white">
                    {{ item.time }}
                  <text class="truncate text-[24rpx] text-white">
                    {{ order.timeText }}
                  </text>
                </view>
              </view>
              <!-- 二维码区 -->
              <view class="relative flex items-center justify-center px-[64rpx]">
                <image src="/static/home/left.png" class="absolute left-[32rpx] h-[48rpx] w-[24rpx]" @click="handlePrevVisitor" />
                <image :src="item.qrCode" class="h-[448rpx] w-[448rpx]" mode="aspectFit" />
                <image src="/static/home/right.png" class="absolute right-[32rpx] h-[48rpx] w-[24rpx]" @click="handleNextVisitor" />
              <view class="relative flex items-center justify-center px-[72rpx]">
                <image
                  v-if="order.qrCodes.length > 1"
                  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-[360rpx] w-[360rpx]"
                  mode="aspectFit"
                />
                <image
                  v-if="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-[20rpx] flex justify-center pb-[36rpx]">
              <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)]">
                  {{ item.visitorName }}的访客码
                  {{ (currentOrder?.orderId === order.orderId ? currentTicket?.visitorName : order.qrCodes[0]?.visitorName) }}的访客码
                </text>
                <text
                  v-if="order.accessAreaText"
                  class="px-[40rpx] text-center text-[22rpx] 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="h-[112rpx] flex items-center justify-center gap-[32rpx] pt-[28rpx]" style="border-top: 2rpx dashed #D8D8D8;border-radius: 12rpx;">
                <view class="w-[144rpx] flex flex-col items-center justify-center" @click="handleDownloadVisitorCode(item.qrCode)">
              <view class="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]">
                    下载访客码
@@ -343,6 +588,8 @@
                <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-order-uuid="order.orderUuid"
                  :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]">
@@ -355,55 +602,20 @@
        </swiper>
      </view>
      <!-- 备案号 (登录后显示在底部) -->
      <view v-if="isLoggedIn" class="mt-[48rpx] flex justify-center pb-[32rpx]">
      <view class="mt-[48rpx] flex justify-center pb-[32rpx]">
        <text class="text-[24rpx] text-[#999999]">
          备案号:2435345342
        </text>
      </view>
    </view>
    <!-- 悬浮流程图标 -->
    <image src="/static/home/process.png" class="fixed bottom-[482rpx] right-[-42rpx] z-5 h-[128rpx] w-[128rpx]" />
    <!-- 未登录提示弹窗 -->
    <wd-popup v-model="showLoginPopup" custom-class="login-popup" position="center">
      <view
        class="box-border w-[590rpx] flex flex-col items-center px-[40rpx] pb-[32rpx] pt-[32rpx]"
        style="background: linear-gradient(180deg, #D1E6FE 0%, #FFFFFF 34%, #FFFFFF 100%);"
      >
        <!-- 标题 -->
        <text class="text-[36rpx] text-[#000000] font-bold leading-[50rpx]">
          提示
        </text>
        <!-- 图标 -->
        <image src="/static/home/tips.png" class="mt-[56rpx] h-[202rpx] w-[202rpx]" mode="aspectFit" />
        <!-- 提示文案 -->
        <text class="mt-[14rpx] text-[32rpx] text-[#000000] leading-[44rpx]">
          您还未登录,请先登录
        </text>
        <!-- 操作按钮 -->
        <view class="mt-[32rpx] w-full flex justify-between gap-[24rpx]">
          <view class="h-[80rpx] flex flex-1 items-center justify-center rounded-[40rpx] bg-[#F5F5F5]" @click="handleCancelLogin">
            <text class="text-[32rpx] text-[#989898] font-medium">
              再想想
            </text>
          </view>
          <button
            class="m-0 h-[80rpx] flex flex-1 items-center justify-center rounded-[40rpx] border-none bg-[#0C6FFF] p-0 after:border-none"
            open-type="getPhoneNumber"
            @getphonenumber="handleConfirmLogin"
          >
            <text class="text-[32rpx] text-[#FFFFFF] font-medium">
              去登录
            </text>
          </button>
        </view>
      </view>
    </wd-popup>
    <image
      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>
@@ -416,14 +628,12 @@
  font-family: 'YouSheBiaoTiHei', sans-serif;
}
.login-text {
  display: inline-block;
  position: relative;
  border-bottom: 4rpx solid #fff;
  padding-bottom: 2rpx;
}
:deep(.login-popup) {
  border-radius: 32rpx;
.qr-canvas {
  position: fixed;
  left: -9999px;
  top: -9999px;
  width: 300px;
  height: 300px;
  pointer-events: none;
}
</style>