2025-07-23 22:32:01 +08:00
|
|
|
|
<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">
|
2025-07-27 21:56:11 +08:00
|
|
|
|
<text class="tip-item">1. 二维码有效期为{{ qrExpireTime }}秒,请及时扫描</text>
|
2025-07-23 22:32:01 +08:00
|
|
|
|
<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('');
|
2025-07-27 21:56:11 +08:00
|
|
|
|
const qrExpireTime = ref(60); // 默认60秒
|
2025-07-23 22:32:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 会议信息
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-07-27 21:56:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 启动默认倒计时,等待二维码数据解析后更新
|
|
|
|
|
|
startTimer();
|
|
|
|
|
|
|
2025-07-23 22:32:01 +08:00
|
|
|
|
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 || '';
|
2025-07-27 21:56:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 从二维码数据中解析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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 22:32:01 +08:00
|
|
|
|
generateQRCodeImage(qrData);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('生成二维码失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 21:56:11 +08:00
|
|
|
|
// 倒计时已在onLoad中启动,这里不需要重复启动
|
2025-07-23 22:32:01 +08:00
|
|
|
|
} 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 = () => {
|
2025-07-27 21:56:11 +08:00
|
|
|
|
qrTimer.value = qrExpireTime.value;
|
2025-07-23 22:32:01 +08:00
|
|
|
|
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();
|
2025-07-27 21:56:11 +08:00
|
|
|
|
// 重新启动倒计时
|
|
|
|
|
|
startTimer();
|
2025-07-23 22:32:01 +08:00
|
|
|
|
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>
|