verificationResult.vue 8.87 KB
<template>
  <div class="verify-page">
    <div class="status-card">
      <div class="status-icon" :class="verify_status">
        <van-icon :name="status_icon" size="24" :color="status_icon_color" />
      </div>
      <div class="status-text">
        <div class="title">{{ status_title }}</div>
        <div class="desc" v-if="verify_status === 'idle'">扫描预约码二维码进行核销</div>
        <div class="desc" v-else>{{ msg }}</div>
      </div>
    </div>

    <div class="info-card">
      <div class="card-title">核销记录信息</div>

      <template v-if="verify_info && Object.keys(verify_info).length > 0">
        <div class="row">
          <div class="label">姓名</div>
          <div class="value">{{ verify_info.person_name || '-' }}</div>
        </div>
        <div class="row">
          <div class="label">证件号码</div>
          <div class="value">{{ format_id_number(verify_info.id_number) || '-' }}</div>
        </div>
        <div class="row">
          <div class="label">状态</div>
          <div class="value highlight">{{ verify_info.status || '-' }}</div>
        </div>
        <div class="row">
          <div class="label">预约开始</div>
          <div class="value">{{ verify_info.begin_time || '-' }}</div>
        </div>
        <div class="row last">
          <div class="label">预约结束</div>
          <div class="value">{{ verify_info.end_time || '-' }}</div>
        </div>
      </template>

      <template v-else-if="verify_status === 'fail'">
        <div class="fail-reason">
          <div class="label">失败原因</div>
          <div class="reason">{{ msg }}</div>
        </div>
      </template>

      <template v-else>
        <div class="empty">暂无核销信息</div>
      </template>
    </div>

    <div class="verify-footer">
      <!-- <van-field v-model="manual_code" placeholder="可粘贴/输入预约码(非微信环境备用)" clearable /> -->
      <div class="btn-wrap">
        <van-button
          block
          color="#A67939"
          :loading="verify_status === 'verifying'"
          :disabled="verify_status === 'verifying'"
          @click="start_scan_and_verify"
        >
          核销
        </van-button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import wx from 'weixin-js-sdk'
import { showToast } from 'vant'
import { mainStore } from '@/store'
import { checkRedeemPermissionAPI, verifyTicketAPI } from '@/api/redeem'
import { wxInfo } from '@/utils/tools'

const store = mainStore()
const $route = useRoute()
const $router = useRouter()

const manual_code = ref('')
const verify_code = ref('')
const verify_info = ref({})
const verify_status = ref('idle')
const msg = ref('请点击下方按钮进行核销')

const status_title = computed(() => {
  if (verify_status.value === 'verifying') return '核销中'
  if (verify_status.value === 'success') return '核销成功'
  if (verify_status.value === 'fail') return '核销失败'
  return '核销'
})

const status_icon = computed(() => {
  if (verify_status.value === 'verifying') return 'clock-o'
  if (verify_status.value === 'success') return 'passed'
  if (verify_status.value === 'fail') return 'close'
  return 'info-o'
})

const status_icon_color = computed(() => {
  if (verify_status.value === 'fail') return '#E24A4A'
  return '#A67939'
})

const format_id_number = (id) => {
  if (!id || typeof id !== 'string' || id.length < 10) return id
  return id.replace(/^(.{6})(?:\d+)(.{4})$/, '$1********$2')
}

const normalize_scan_result = (raw) => {
  if (!raw) return ''
  const text = String(raw)
  const barcode_split = text.split(',')
  const candidate = barcode_split.length > 1 ? barcode_split[barcode_split.length - 1] : text
  if (candidate.includes('qr_code=')) {
    try {
      const url = new URL(candidate)
      return url.searchParams.get('qr_code') || candidate
    } catch (e) {
      const match = candidate.match(/(?:\?|&)qr_code=([^&]+)/)
      if (match && match[1]) return decodeURIComponent(match[1])
    }
  }
  return candidate
}

const ensure_permission = async () => {
  const permission_res = await checkRedeemPermissionAPI()
  if (!permission_res || permission_res?.code !== 1) {
    $router.replace({ path: '/volunteerLogin' })
    return false
  }
  if (permission_res?.data) store.changeUserInfo(permission_res.data)
  if (permission_res?.data?.can_redeem !== true) {
    $router.replace({ path: '/volunteerLogin' })
    return false
  }
  return true
}

const verify_ticket = async (code) => {
  const normalized = normalize_scan_result(code)
  if (!normalized) return
  if (verify_status.value === 'verifying') return

  verify_code.value = normalized
  verify_status.value = 'verifying'
  msg.value = '核销中...'

  const res = await verifyTicketAPI({ qr_code: normalized })
  if (res?.code === 1) {
    verify_status.value = 'success'
    msg.value = res?.msg || '核销成功'
    verify_info.value = res?.data || {}
    return
  }

  verify_status.value = 'fail'
  msg.value = res?.msg || '核销失败'
  verify_info.value = {}
}

const scan_in_wechat = async () => {
  const ok = await window.__wx_ready_promise
  if (!ok) return ''
  return new Promise((resolve) => {
    wx.scanQRCode({
      needResult: 1,
      scanType: ['qrCode', 'barCode'],
      success: (res) => resolve(res?.resultStr || ''),
      fail: () => resolve(''),
      cancel: () => resolve(''),
    })
  })
}

const start_scan_and_verify = async () => {
  const authed = await ensure_permission()
  if (!authed) return

  const in_wechat = wxInfo().isTable === true
  if (in_wechat) {
    const result = await scan_in_wechat()
    const code = normalize_scan_result(result)
    if (!code) {
      if (manual_code.value) await verify_ticket(manual_code.value)
      else showToast('未获取到二维码内容')
      return
    }
    await verify_ticket(code)
    return
  }

  if (manual_code.value) {
    await verify_ticket(manual_code.value)
    return
  }

  showToast('请在微信内扫码,或手动输入预约码')
}

onMounted(async () => {
  const authed = await ensure_permission()
  if (!authed) return
  const code = $route.query?.result || $route.query?.qr_code || ''
  const str_code = Array.isArray(code) ? code[0] : String(code || '')
  if (str_code) {
    manual_code.value = str_code
    await verify_ticket(str_code)
  }
})

watch(
  () => $route.query?.result,
  async (next) => {
    const code = Array.isArray(next) ? next[0] : String(next || '')
    if (!code) return
    if (verify_code.value === code) return
    const authed = await ensure_permission()
    if (!authed) return
    manual_code.value = code
    await verify_ticket(code)
  }
)
</script>

<style lang="less" scoped>
.verify-page {
  min-height: 100vh;
  background-color: #F6F6F6;
  padding: 16px;
  padding-bottom: 160px;
  box-sizing: border-box;

  .status-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    display: flex;
    align-items: center;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04);

    .status-icon {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background-color: #FFF7ED;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 12px;
    }

    .status-text {
      flex: 1;
      .title {
        font-size: 18px;
        font-weight: 600;
        color: #111827;
      }
      .desc {
        margin-top: 4px;
        font-size: 13px;
        color: #6B7280;
        word-break: break-all;
      }
    }
  }

  .info-card {
    margin-top: 12px;
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04);

    .card-title {
      font-size: 14px;
      color: #6B7280;
      margin-bottom: 12px;
    }

    .row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 10px 0;
      border-bottom: 1px solid #F3F4F6;

      .label {
        font-size: 14px;
        color: #6B7280;
      }
      .value {
        font-size: 16px;
        color: #111827;
        font-weight: 600;
        margin-left: 12px;
        word-break: break-all;
        text-align: right;
      }
      .highlight {
        color: #A67939;
      }
    }

    .row.last {
      border-bottom: 0;
    }

    .fail-reason {
      .label {
        font-size: 14px;
        color: #6B7280;
        margin-bottom: 8px;
      }
      .reason {
        font-size: 16px;
        color: #E24A4A;
        font-weight: 600;
        word-break: break-all;
      }
    }

    .empty {
      font-size: 14px;
      color: #9CA3AF;
      padding: 8px 0;
    }
  }

  .verify-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background-color: #fff;
    box-shadow: 0 -10px 18px rgba(0, 0, 0, 0.06);
    box-sizing: border-box;

    .btn-wrap {
      margin-top: 10px;
    }
  }
}
</style>