offlineQrCode.vue 6.5 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;
    }
    .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>