vending-machine/mobile/utils/qrcode.js
2026-04-08 20:45:41 +08:00

195 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import UQRCode from 'uqrcodejs'
import { ref, onUnmounted } from 'vue'
// 默认二维码有效期5分钟毫秒
const DEFAULT_TTL = 300000
/**
* 生成会员二维码
* 使用uQRCode库将token编码为二维码的Base64图片
* @param {string} token - 用户token
* @returns {Promise<string>} base64编码的二维码图片
*/
export function generateQRCode(token) {
return new Promise((resolve, reject) => {
const qr = new UQRCode()
qr.data = token
qr.size = 256
qr.make()
// 使用Canvas绘制并导出Base64
// 在UniApp环境中使用Canvas上下文
const canvas = createCanvas(qr.moduleCount, 256)
const ctx = canvas.getContext('2d')
// 绘制白色背景
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, 256, 256)
// 绘制二维码模块
const tileSize = 256 / qr.moduleCount
ctx.fillStyle = '#000000'
for (let row = 0; row < qr.moduleCount; row++) {
for (let col = 0; col < qr.moduleCount; col++) {
if (qr.modules[row][col]) {
ctx.fillRect(col * tileSize, row * tileSize, tileSize, tileSize)
}
}
}
resolve(canvas.toDataURL('image/png'))
})
}
/**
* 创建Canvas元素兼容不同环境
* @param {number} moduleCount - 二维码模块数
* @param {number} size - 画布尺寸
* @returns {HTMLCanvasElement|object}
*/
function createCanvas(moduleCount, size) {
// 在H5/Node环境中使用DOM Canvas
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
return canvas
}
// 在UniApp原生环境中需要使用uni.createCanvasContext
// 此处返回一个简单的模拟对象用于非渲染场景
throw new Error('请在UniApp页面中使用组件方式生成二维码')
}
/**
* 纯数据二维码生成不依赖Canvas用于测试和数据传递
* 返回二维码的模块矩阵数据
* @param {string} token - 用户token
* @returns {{ modules: boolean[][], moduleCount: number }} 二维码模块数据
*/
export function generateQRCodeData(token) {
const qr = new UQRCode()
qr.data = token
qr.make()
// 将模块数据转换为简单的boolean二维数组
const modules = []
for (let row = 0; row < qr.moduleCount; row++) {
const rowData = []
for (let col = 0; col < qr.moduleCount; col++) {
rowData.push(!!qr.modules[row][col])
}
modules.push(rowData)
}
return { modules, moduleCount: qr.moduleCount }
}
/**
* 从二维码模块数据中解码token用于验证Round-Trip
* 注意uQRCode是编码库不支持解码。
* 此函数通过重新编码并比较模块数据来验证一致性。
* @param {string} originalToken - 原始token
* @param {{ modules: boolean[][], moduleCount: number }} qrData - 二维码模块数据
* @returns {boolean} 模块数据是否与原始token编码结果一致
*/
export function verifyQRCodeData(originalToken, qrData) {
const regenerated = generateQRCodeData(originalToken)
if (regenerated.moduleCount !== qrData.moduleCount) return false
for (let row = 0; row < regenerated.moduleCount; row++) {
for (let col = 0; col < regenerated.moduleCount; col++) {
if (regenerated.modules[row][col] !== qrData.modules[row][col]) return false
}
}
return true
}
/**
* 二维码组合式函数:管理二维码数据、倒计时、自动刷新
* @param {Function} getToken - 获取新token的异步函数
* @param {number} ttl - 有效期毫秒默认3000005分钟
* @returns {{ qrcodeData: Ref<string>, remainingTime: Ref<number>, refresh: Function, destroy: Function }}
*/
export function useQRCode(getToken, ttl = DEFAULT_TTL) {
// 二维码图片数据Base64或模块数据
const qrcodeData = ref('')
// 剩余时间(秒)
const remainingTime = ref(0)
// 是否正在加载
const loading = ref(false)
let countdownTimer = null
let refreshTimer = null
/**
* 刷新二维码获取新token并生成二维码
*/
async function refresh() {
loading.value = true
try {
const token = await getToken()
const data = generateQRCodeData(token)
// 存储为JSON字符串组件可解析使用
qrcodeData.value = JSON.stringify(data)
// 重置倒计时
remainingTime.value = Math.floor(ttl / 1000)
startCountdown()
} catch (e) {
console.error('二维码生成失败:', e)
} finally {
loading.value = false
}
}
/**
* 启动倒计时
*/
function startCountdown() {
stopCountdown()
countdownTimer = setInterval(() => {
remainingTime.value--
if (remainingTime.value <= 0) {
stopCountdown()
// 自动刷新
refresh()
}
}, 1000)
}
/**
* 停止倒计时
*/
function stopCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
/**
* 销毁:清除所有定时器
*/
function destroy() {
stopCountdown()
if (refreshTimer) {
clearTimeout(refreshTimer)
refreshTimer = null
}
}
// 组件卸载时自动销毁
try {
onUnmounted(destroy)
} catch (e) {
// 非组件上下文中忽略
}
return {
qrcodeData,
remainingTime,
loading,
refresh,
destroy
}
}