457 lines
11 KiB
Vue
457 lines
11 KiB
Vue
<template>
|
||
<view class="qr-code-page">
|
||
<view class="qr-container">
|
||
<!-- 页面头部 -->
|
||
<view class="page-header">
|
||
<text class="page-title">签到二维码</text>
|
||
<text class="page-subtitle">请使用微信扫描二维码进行签到</text>
|
||
</view>
|
||
|
||
<!-- 二维码显示区域 -->
|
||
<view class="qr-display">
|
||
<view class="qr-card">
|
||
<view class="qr-header">
|
||
<text class="qr-title">{{ meetingInfo?.qdmc || '签到二维码' }}</text>
|
||
<view class="qr-timer">
|
||
<text class="timer-text">{{ qrTimer }}s</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="qr-content">
|
||
<canvas
|
||
canvas-id="qrcode"
|
||
class="qr-canvas"
|
||
:style="{ width: qrSize + 'px', height: qrSize + 'px' }"
|
||
></canvas>
|
||
</view>
|
||
|
||
<view class="qr-info">
|
||
<view class="info-item">
|
||
<text class="info-label">会议时间:</text>
|
||
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="info-label">会议地点:</text>
|
||
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="action-buttons">
|
||
<button @click="refreshQRCode" class="refresh-btn">
|
||
<u-icon name="reload" size="16" color="#409EFF" />
|
||
<text class="btn-text">刷新二维码</text>
|
||
</button>
|
||
<button @click="goBack" class="back-btn">
|
||
<u-icon name="arrow-left" size="16" color="#666" />
|
||
<text class="btn-text">返回</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 使用说明 -->
|
||
<view class="usage-tips">
|
||
<view class="tips-header">
|
||
<u-icon name="info-circle" size="16" color="#409EFF" />
|
||
<text class="tips-title">使用说明</text>
|
||
</view>
|
||
<view class="tips-content">
|
||
<text class="tip-item">1. 二维码有效期为{{ qrExpireTime }}秒,请及时扫描</text>
|
||
<text class="tip-item">2. 使用微信扫一扫功能扫描二维码</text>
|
||
<text class="tip-item">3. 扫描后将跳转到签到确认页面</text>
|
||
<text class="tip-item">4. 请确保网络连接正常</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { qdFindByIdApi, generateQRCodeApi } from '@/api/base/server';
|
||
|
||
// 页面参数
|
||
const qdId = ref('');
|
||
const qrExpireTime = ref(60); // 默认60秒
|
||
|
||
// 会议信息
|
||
const meetingInfo = ref<any>(null);
|
||
|
||
// 二维码相关
|
||
const qrTimer = ref(60);
|
||
const qrSize = ref(200);
|
||
let qrTimerInterval: any = null;
|
||
let qrCanvas: any = null;
|
||
|
||
onLoad((options) => {
|
||
if (options?.qdId) {
|
||
qdId.value = options.qdId;
|
||
}
|
||
|
||
// 启动默认倒计时,等待二维码数据解析后更新
|
||
startTimer();
|
||
|
||
loadMeetingInfo();
|
||
});
|
||
|
||
onMounted(() => {
|
||
initQRCode();
|
||
});
|
||
|
||
// 加载会议信息
|
||
const loadMeetingInfo = async () => {
|
||
try {
|
||
const result = await qdFindByIdApi({ id: qdId.value });
|
||
if (result && result.resultCode === 1) {
|
||
meetingInfo.value = result.result;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载会议信息失败:', error);
|
||
}
|
||
};
|
||
|
||
// 初始化二维码
|
||
const initQRCode = async () => {
|
||
try {
|
||
// 生成二维码数据,只传递qdId
|
||
const result = await generateQRCodeApi({
|
||
qdId: qdId.value
|
||
});
|
||
|
||
if (result && result.resultCode === 1) {
|
||
// 确保二维码数据是字符串格式
|
||
const qrData = result.result || '';
|
||
|
||
// 从二维码数据中解析rqgqtime参数
|
||
const urlParams = new URLSearchParams(qrData.split('?')[1] || '');
|
||
const rqgqtime = urlParams.get('rqgqtime');
|
||
if (rqgqtime) {
|
||
const expireTime = parseInt(rqgqtime) || 60;
|
||
qrExpireTime.value = expireTime;
|
||
qrTimer.value = expireTime;
|
||
// 重新启动倒计时
|
||
startTimer();
|
||
}
|
||
|
||
generateQRCodeImage(qrData);
|
||
} else {
|
||
throw new Error('生成二维码失败');
|
||
}
|
||
|
||
// 倒计时已在onLoad中启动,这里不需要重复启动
|
||
} catch (error) {
|
||
console.error('生成二维码失败:', error);
|
||
uni.showToast({ title: '生成二维码失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 生成二维码图片
|
||
const generateQRCodeImage = (qrData: string) => {
|
||
try {
|
||
// 使用qrcode库生成真正的二维码
|
||
import('qrcode').then((QRCode) => {
|
||
// 使用回调函数方式避免类型错误
|
||
(QRCode as any).toDataURL(qrData, {
|
||
width: qrSize.value,
|
||
height: qrSize.value,
|
||
margin: 2,
|
||
color: {
|
||
dark: '#000000',
|
||
light: '#FFFFFF'
|
||
},
|
||
errorCorrectionLevel: 'M'
|
||
}, (err: any, url: string) => {
|
||
if (err) {
|
||
console.error('生成二维码失败:', err);
|
||
// 如果生成失败,显示错误信息
|
||
qrCanvas = uni.createCanvasContext('qrcode');
|
||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#ffffff');
|
||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#ff0000');
|
||
qrCanvas.setFontSize(12);
|
||
qrCanvas.fillText('二维码生成失败', qrSize.value / 2 - 40, qrSize.value / 2);
|
||
qrCanvas.draw();
|
||
return;
|
||
}
|
||
|
||
// 使用uni-app的方式处理图片
|
||
// 将二维码数据直接绘制到canvas上
|
||
qrCanvas = uni.createCanvasContext('qrcode');
|
||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||
|
||
// 使用uni-app的drawImage方法
|
||
qrCanvas.drawImage(url, 0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.draw();
|
||
|
||
});
|
||
}).catch((error) => {
|
||
console.error('加载qrcode库失败:', error);
|
||
// 如果无法加载qrcode库,显示提示信息
|
||
qrCanvas = uni.createCanvasContext('qrcode');
|
||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#ffffff');
|
||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#666666');
|
||
qrCanvas.setFontSize(12);
|
||
qrCanvas.fillText('请安装qrcode库', qrSize.value / 2 - 30, qrSize.value / 2);
|
||
qrCanvas.draw();
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('生成二维码异常:', error);
|
||
// 异常处理,显示错误信息
|
||
qrCanvas = uni.createCanvasContext('qrcode');
|
||
qrCanvas.clearRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#ffffff');
|
||
qrCanvas.fillRect(0, 0, qrSize.value, qrSize.value);
|
||
qrCanvas.setFillStyle('#ff0000');
|
||
qrCanvas.setFontSize(12);
|
||
qrCanvas.fillText('二维码生成异常', qrSize.value / 2 - 40, qrSize.value / 2);
|
||
qrCanvas.draw();
|
||
}
|
||
};
|
||
|
||
// 启动倒计时
|
||
const startTimer = () => {
|
||
qrTimer.value = qrExpireTime.value;
|
||
if (qrTimerInterval) {
|
||
clearInterval(qrTimerInterval);
|
||
}
|
||
|
||
qrTimerInterval = setInterval(() => {
|
||
qrTimer.value--;
|
||
if (qrTimer.value <= 0) {
|
||
clearInterval(qrTimerInterval);
|
||
uni.showToast({ title: '二维码已过期', icon: 'none' });
|
||
// 可以在这里自动刷新二维码
|
||
refreshQRCode();
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
// 刷新二维码
|
||
const refreshQRCode = async () => {
|
||
try {
|
||
await initQRCode();
|
||
// 重新启动倒计时
|
||
startTimer();
|
||
uni.showToast({ title: '二维码已刷新', icon: 'success' });
|
||
} catch (error) {
|
||
uni.showToast({ title: '刷新失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string) => {
|
||
if (!time) return '未设置';
|
||
const date = new Date(time);
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
// 组件销毁时清理定时器
|
||
onUnmounted(() => {
|
||
if (qrTimerInterval) {
|
||
clearInterval(qrTimerInterval);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.qr-code-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.qr-container {
|
||
max-width: 400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
// 页面头部
|
||
.page-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
|
||
.page-title {
|
||
display: block;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: white;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 16px;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
}
|
||
|
||
// 二维码显示区域
|
||
.qr-display {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.qr-card {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.qr-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
|
||
.qr-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
|
||
.qr-timer {
|
||
background: linear-gradient(135deg, #ff4757 0%, #ff3742 100%);
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
|
||
.timer-text {
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.qr-content {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
|
||
.qr-canvas {
|
||
border: 2px solid #f0f0f0;
|
||
border-radius: 8px;
|
||
background: white;
|
||
}
|
||
}
|
||
|
||
.qr-info {
|
||
.info-item {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
|
||
.info-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 14px;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 操作按钮
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 30px;
|
||
|
||
.refresh-btn, .back-btn {
|
||
flex: 1;
|
||
height: 44px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.refresh-btn {
|
||
background: #409EFF;
|
||
color: white;
|
||
}
|
||
|
||
.back-btn {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
// 使用说明
|
||
.usage-tips {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
backdrop-filter: blur(10px);
|
||
|
||
.tips-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.tips-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: white;
|
||
}
|
||
}
|
||
|
||
.tips-content {
|
||
.tip-item {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
margin-bottom: 6px;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式优化
|
||
@media (max-width: 375px) {
|
||
.qr-code-page {
|
||
padding: 15px;
|
||
}
|
||
|
||
.qr-card {
|
||
padding: 20px;
|
||
}
|
||
|
||
.qr-size {
|
||
width: 180px;
|
||
height: 180px;
|
||
}
|
||
|
||
.page-header .page-title {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
</style> |