195 lines
5.2 KiB
JavaScript
195 lines
5.2 KiB
JavaScript
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 - 有效期(毫秒),默认300000(5分钟)
|
||
* @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
|
||
}
|
||
}
|