offlineQrCode.vue 6.44 KB
<template>
  <view class="qr-code-page">
    <view v-if="userList.length" class="show-qrcode">
      <view class="qrcode-content">
        <view class="user-info">{{ userinfo.name }}&nbsp;{{ userinfo.id }}</view>
        <view class="user-qrcode">
          <view class="left" @tap="prevCode">
            <image :src="icon_1" />
          </view>
          <view class="center">
            <!-- 离线模式直接显示生成的 base64 图片 -->
            <image :src="currentQrCodeUrl" mode="aspectFit" />
            <!-- 离线模式不显示状态覆盖层,因为无法获取最新状态 -->
          </view>
          <view class="right" @tap="nextCode">
            <image :src="icon_2" />
          </view>
        </view>
        <view style="color: red; margin-top: 32rpx">{{ userinfo.datetime }}</view>
        <view style="color: #999; font-size: 24rpx; margin-top: 10rpx">(离线模式)</view>
      </view>
      <view class="user-list">
        <view
          @tap="selectUser(index)"
          v-for="(item, index) in userList"
          :key="index"
          :class="[
            'user-item',
            select_index === index ? 'checked' : '',
            userList.length > 1 && item.sort ? 'border' : ''
          ]"
        >
          {{ item.name }}
        </view>
      </view>
    </view>
    <view v-else class="no-qrcode">
      <image :src="icon_3" style="width: 320rpx; height: 320rpx" />
      <view class="no-qrcode-title">本地无缓存预约记录</view>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import QRCode from 'qrcode'

import icon_1 from '@/assets/images/左1@2x.png'
import icon_2 from '@/assets/images/右1@2x.png'
import icon_3 from '@/assets/images/暂无1@2x.png'

const props = defineProps({
  list: {
    type: Array,
    default: () => []
  }
})

const select_index = ref(0)
const userList = ref([])
const qrCodeImages = ref({}) // 存储生成的二维码图片 base64

const prevCode = () => {
  select_index.value = select_index.value - 1
  if (select_index.value < 0) {
    select_index.value = userList.value.length - 1
  }
}
const nextCode = () => {
  select_index.value = select_index.value + 1
  if (select_index.value > userList.value.length - 1) {
    select_index.value = 0
  }
}

function replaceMiddleCharacters(inputString) {
  if (!inputString || inputString.length < 15) {
    return inputString
  }
  const start = Math.floor((inputString.length - 8) / 2)
  const end = start + 8
  const replacement = '*'.repeat(8)
  return inputString.substring(0, start) + replacement + inputString.substring(end)
}

const formatId = id => replaceMiddleCharacters(id)

const userinfo = computed(() => {
  return {
    name: userList.value[select_index.value]?.name,
    id: formatId(userList.value[select_index.value]?.id_number),
    datetime: userList.value[select_index.value]?.datetime
  }
})

const currentQrCodeUrl = computed(() => {
  const key = userList.value[select_index.value]?.qr_code
  return qrCodeImages.value[key] || ''
})

const selectUser = index => {
  select_index.value = index
}

const generateQrCodes = () => {
  for (const item of userList.value) {
    if (item.qr_code && !qrCodeImages.value[item.qr_code]) {
      try {
        // 使用 create + SVG 手动生成,避免 Taro 中 Canvas 依赖问题
        const qr = QRCode.create(item.qr_code, { errorCorrectionLevel: 'M' })
        const size = qr.modules.size
        let d = ''
        for (let row = 0; row < size; row++) {
          for (let col = 0; col < size; col++) {
            if (qr.modules.get(col, row)) {
              d += `M${col},${row}h1v1h-1z`
            }
          }
        }
        const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><path d="${d}" fill="#000"/></svg>`

        // 转 Base64
        const buffer = new ArrayBuffer(svg.length)
        const view = new Uint8Array(buffer)
        for (let i = 0; i < svg.length; i++) {
          view[i] = svg.charCodeAt(i)
        }
        const b64 = Taro.arrayBufferToBase64(buffer)

        qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`
      } catch (err) {
        console.error('QR Gen Error', err)
      }
    }
  }
}

onMounted(() => {
  if (props.list && props.list.length > 0) {
    userList.value = props.list
    generateQrCodes()
  }
})

watch(
  () => props.list,
  newVal => {
    if (newVal && newVal.length > 0) {
      userList.value = newVal
      generateQrCodes()
    }
  },
  { deep: true }
)
</script>

<style lang="less">
.qr-code-page {
  .qrcode-content {
    padding: 32rpx 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #fff;
    border-radius: 16rpx;
    box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.27);

    .user-info {
      color: #a6a6a6;
      font-size: 37rpx;
      margin-top: 16rpx;
      margin-bottom: 16rpx;
      // 只显示一行
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      // 不能超过容器宽度
      max-width: 600rpx;
    }
    .user-qrcode {
      display: flex;
      align-items: center;
      .left {
        image {
          width: 56rpx;
          height: 56rpx;
          margin-right: 16rpx;
        }
      }
      .center {
        border: 2rpx solid #d1d1d1;
        border-radius: 40rpx;
        padding: 46rpx;
        position: relative;
        image {
          width: 400rpx;
          height: 400rpx;
        }
      }
      .right {
        image {
          width: 56rpx;
          height: 56rpx;
          margin-left: 16rpx;
        }
      }
    }
  }
  .user-list {
    display: flex;
    padding: 32rpx;
    align-items: center;
    flex-wrap: wrap;
    .user-item {
      position: relative;
      padding: 8rpx 16rpx;
      border: 2rpx solid #a67939;
      margin: 8rpx;
      border-radius: 10rpx;
      color: #a67939;
      &.checked {
        color: #fff;
        background-color: #a67939;
      }
      &.border {
        margin-right: 16rpx;
        &::after {
          position: absolute;
          right: -16rpx;
          top: calc(50% - 16rpx);
          content: '';
          height: 32rpx;
          border-right: 2rpx solid #a67939;
        }
      }
    }
  }

  .no-qrcode {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    margin-bottom: 32rpx;

    .no-qrcode-title {
      color: #a67939;
      font-size: 34rpx;
    }
  }
}
</style>