SmartParks_visitore/pages/sys/qrpage/qrpage.vue
2025-08-21 11:23:54 +08:00

886 lines
19 KiB
Vue
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.

<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="navbar">
<text class="navbar-title">
<i class="fa fa-qrcode" style="margin-right: 8px;"></i>
动态二维码生成器
</text>
<view class="connection-status" :class="connectionState === 'connected' ? 'connected' : 'disconnected'">
<text class="status-text">{{ connectionState === 'connected' ? '已连接' : '未连接' }}</text>
</view>
</view>
<!-- 主内容区 -->
<view class="main-content">
<!-- 状态提示 -->
<view class="status-indicator" :class="qrCodeStatus">
<text class="status-text">{{ getStatusText() }}</text>
</view>
<!-- 二维码容器 -->
<view class="qr-container">
<view class="qr-card">
<view class="qr-wrapper" :class="{ 'fade-out': isClosing, 'fade-in': isGenerating }">
<uv-qrcode ref="qrcode" canvas-id="qrcode" :value="value" size="300rpx"></uv-qrcode>
</view>
<view class="qr-expiry" v-if="qrCodeStatus === 'active'">
<text>有效时长: {{ formatCountdown(remainingTime) }}</text>
</view>
</view>
</view>
<!-- 信息展示卡片 -->
<view class="info-card">
<view class="card-header">
<text class="card-title">二维码信息</text>
</view>
<view class="info-grid">
<view class="info-item">
<text class="info-label">设备码</text>
<text class="info-value">{{ info.deviceCode }}</text>
</view>
<view class="info-item">
<text class="info-label">用户账户</text>
<text class="info-value">{{ info.userAccount }}</text>
</view>
<view class="info-item">
<text class="info-label">生成时间</text>
<text class="info-value">{{ formatTime(qrCodeTimestamp) }}</text>
</view>
<view class="info-item">
<text class="info-label">二维码ID</text>
<text class="info-value">{{ info.qrCodeId }}</text>
</view>
</view>
</view>
</view>
<!-- 底部操作区 -->
<view class="bottom-bar">
<button class="action-btn primary" @click="manualRefresh" :disabled="isLoading">
<i class="fa fa-refresh" :class="{ 'fa-spin': isLoading }"></i>
<text class="btn-text">刷新二维码</text>
</button>
<!-- <button class="action-btn secondary" @click="resetConnection" :disabled="isLoading">
<i class="fa fa-sync-alt"></i>
<text class="btn-text">重置连接</text>
</button> -->
</view>
<!-- 提示弹窗 -->
<view v-if="showPopup" class="popup-overlay" @click="closePopup">
<view class="popup-dialog" @click.stop>
<view class="popup-icon" :class="popupType">
<i class="fa" :class="popupIcon"></i>
</view>
<text class="popup-text">{{ popupMessage }}</text>
<button class="popup-btn" @click="closePopup">确定</button>
</view>
</view>
</view>
</template>
<script>
import random from '../../../uview-ui/libs/function/random';
import uvQrcode from '@/uni_modules/uv-qrcode/components/uv-qrcode/uv-qrcode.vue';
export default {
components: {
uvQrcode
},
data() {
return {
// 核心数据
info: {
deviceCode: 'DEVICE-123456',
qrCodeId: random(100000, 999999),
userAccount: '' // 从用户信息获取
},
value: '12345', // 二维码内容
qrCodeTimestamp: 0, // 生成时间戳
expiryTime: 300000, // 5分钟有效期(ms)
remainingTime: 300000,
countdownInterval: null,
// 状态管理
qrCodeStatus: 'generating', // generating/active/closing
connectionState: 'connecting', // connecting/connected/disconnected
isLoading: false,
isClosing: false,
isGenerating: false,
sseEnable: true,
clientId: "dab457a1ea14411787c240db05bb0832",
// 弹窗相关
showPopup: false,
popupMessage: '',
popupType: 'info',
popupIcon: 'fa-info-circle',
popupTimer: null
};
},
onLoad() {
this.wsListener = (content) => {
let message=JSON.parse(content)
console.log(message.date)
if(this.qrCodeId==message.date){
this.generateQrCode();
}
};
// 从本地存储获取用户信息
const userInfo = uni.getStorageSync('userInfo') || {};
this.info.userAccount = userInfo.account || 'user@example.com';
// 生成第一个二维码
this.generateQrCode();
uni.$on('ws_qrcode', this.wsListener);
// uni.$emit(`ws_${message.type}`, message);
},
// onUnload() {
// // 清理资源
// this.clearCountdown();
// this.clearPopupTimer();
// this.closeEventSource();
// },
methods: {
/**
* 开始监听SSE消息
*/
// startListeningMessage() {
// if (!this.sseEnable) {
// this.connectionState = 'disconnected';
// this.showPopupMessage('SSE已禁用', 'warning', 'fa-exclamation-triangle');
// return;
// }
// const token = this.$store.state.vuex_token;
// if (!token) {
// this.connectionState = 'disconnected';
// this.showPopupMessage('未获取到令牌,请先登录', 'error', 'fa-exclamation-circle');
// return;
// }
// // 关闭现有连接
// this.closeEventSource();
// const apiURL = 'http://tc.cqsznc.com:7080/api';
// const sseAddr = `${apiURL}/resource/sse?clientid=${this.clientId}&Authorization=Bearer ${token}`;
// try {
// this.eventSource = new EventSource(sseAddr);
// this.eventSource.onopen = () => {
// console.log('SSE连接已打开');
// this.connectionState = 'connected';
// this.reconnectCount = 0;
// this.showPopupMessage('SSE连接成功', 'success', 'fa-check-circle');
// };
// this.eventSource.onmessage = (event) => {
// this.handleSSEMessage(event.data);
// };
// this.eventSource.onerror = () => {
// console.error('SSE连接异常');
// this.connectionState = 'disconnected';
// this.closeEventSource();
// // 重连逻辑
// if (this.reconnectCount < this.maxReconnect) {
// this.reconnectCount++;
// const delay = this.reconnectCount * 1000;
// this.showPopupMessage(
// `SSE连接断开${delay/1000}秒后尝试重连(${this.reconnectCount}/${this.maxReconnect}`,
// 'warning', 'fa-exclamation-triangle');
// setTimeout(() => this.startListeningMessage(), delay);
// } else {
// this.showPopupMessage('SSE重连失败请检查网络或手动重置', 'error', 'fa-exclamation-circle');
// }
// };
// } catch (e) {
// console.error('创建SSE连接失败:', e);
// this.connectionState = 'disconnected';
// this.showPopupMessage(`创建SSE连接失败: ${e.message}`, 'error', 'fa-exclamation-circle');
// }
// },
// /**
// * 处理SSE消息
// */
// handleSSEMessage(message) {
// if (!message) return;
// console.log(`接收到SSE消息: ${message}`);
// try {
// const parsedMessage = JSON.parse(message);
// if (parsedMessage.type === 'qrcode') {
// this.showPopupMessage('二维码已被扫描,正在生成新码', 'success', 'fa-check-circle');
// this.closeCurrentQrCode();
// return;
// }
// this.showPopupMessage(parsedMessage.content || message, 'info', 'fa-info-circle');
// } catch (e) {
// this.showPopupMessage(message, 'info', 'fa-info-circle');
// }
// },
// /**
// * 关闭EventSource连接
// */
// closeEventSource() {
// if (this.eventSource) {
// this.eventSource.close();
// this.eventSource = null;
// }
// },
/**
* 生成新的二维码
*/
generateQrCode() {
this.isLoading = true;
this.qrCodeStatus = 'generating';
this.isGenerating = true;
// 调用接口生成二维码
this.$u.api.getcode(this.info)
.then(res => {
console.log("二维码生成成功", res);
this.qrCodeId=res.msg;
this.value = "http://183.230.235.66:11010/visitore?code=" + res.msg; // 更新二维码内容
this.qrCodeTimestamp = Date.now(); // 记录生成时间
// this.remainingTime = this.expiryTime; // 重置倒计时
// 更新状态
this.qrCodeStatus = 'active';
// this.startCountdown(); // 启动倒计时
})
.catch(err => {
console.error("二维码生成失败", err);
this.showPopupMessage('二维码生成失败,请重试', 'error', 'fa-exclamation-circle');
this.qrCodeStatus = 'closing';
})
.finally(() => {
this.isLoading = false;
this.isGenerating = false;
});
},
/**
* 启动倒计时
*/
// startCountdown() {
// this.clearCountdown(); // 先清除现有定时器
// this.countdownInterval = setInterval(() => {
// this.remainingTime -= 1000;
// if (this.remainingTime <= 0) {
// this.showPopupMessage('二维码已过期,正在生成新码', 'warning', 'fa-exclamation-triangle');
// this.closeCurrentQrCode();
// }
// }, 1000);
// },
/**
* 清除倒计时
*/
// clearCountdown() {
// if (this.countdownInterval) {
// clearInterval(this.countdownInterval);
// this.countdownInterval = null;
// }
// },
/**
* 关闭当前二维码并生成新的
*/
closeCurrentQrCode() {
if (this.isClosing || this.qrCodeStatus !== 'active') return;
this.qrCodeStatus = 'closing';
this.isClosing = true;
// this.clearCountdown(); // 停止倒计时
console.log(`关闭二维码: ${this.info.qrCodeId}`);
// 延迟生成新码,等待动画完成
setTimeout(() => {
this.isClosing = false;
this.info.qrCodeId = random(100000, 999999); // 生成新ID
this.generateQrCode();
}, 1000);
},
/**
* 手动刷新二维码
*/
manualRefresh() {
this.closeCurrentQrCode();
},
/**
* 重置SSE连接
*/
// resetConnection() {
// this.showPopupMessage('正在重置SSE连接...', 'info', 'fa-spinner fa-spin');
// this.startListeningMessage();
// },
/**
* 显示弹窗提示
*/
showPopupMessage(message, type = 'info', icon = 'fa-info-circle') {
this.clearPopupTimer();
this.popupMessage = message;
this.popupType = type;
this.popupIcon = icon;
this.showPopup = true;
// 3秒后自动关闭
this.popupTimer = setTimeout(() => this.closePopup(), 3000);
},
/**
* 关闭弹窗
*/
closePopup() {
this.showPopup = false;
this.clearPopupTimer();
},
/**
* 清除弹窗定时器
*/
clearPopupTimer() {
if (this.popupTimer) {
clearTimeout(this.popupTimer);
this.popupTimer = null;
}
},
/**
* 获取状态文本
*/
getStatusText() {
switch (this.qrCodeStatus) {
case 'generating':
return '正在生成二维码...';
case 'active':
return '二维码有效,等待扫描';
case 'closing':
return '二维码已关闭,生成新码中...';
default:
return '未知状态';
}
},
/**
* 格式化时间(年月日时分秒)
*/
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return [
date.getFullYear(),
this.padZero(date.getMonth() + 1),
this.padZero(date.getDate())
].join('-') + ' ' + [
this.padZero(date.getHours()),
this.padZero(date.getMinutes()),
this.padZero(date.getSeconds())
].join(':');
},
/**
* 格式化倒计时(分:秒)
*/
formatCountdown(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = this.padZero(Math.floor(totalSeconds / 60));
const seconds = this.padZero(totalSeconds % 60);
return `${minutes}:${seconds}`;
},
/**
* 数字补零
*/
padZero(num) {
return num < 10 ? '0' + num : num;
}
}
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f0f2f5;
}
/* 导航栏样式 */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: #2563eb;
color: white;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
z-index: 10;
}
.navbar-title {
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
}
.connection-status {
padding: 5px 12px;
border-radius: 16px;
font-size: 14px;
display: flex;
align-items: center;
background-color: rgba(255, 255, 255, 0.15);
transition: all 0.3s ease;
}
.connection-status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
}
.connection-status.connected::before {
background-color: #10b981;
}
.connection-status.disconnected::before {
background-color: #ef4444;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* 主内容区样式 */
.main-content {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
}
/* 状态指示器 */
.status-indicator {
padding: 9px 18px;
border-radius: 22px;
margin-bottom: 24px;
font-size: 14px;
color: white;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
transition: all 0.3s ease;
}
.status-indicator::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-indicator.generating {
background-color: #2563eb;
}
.status-indicator.generating::before {
background-color: white;
animation: pulse 1.5s infinite;
}
.status-indicator.active {
background-color: #10b981;
}
.status-indicator.active::before {
background-color: white;
}
.status-indicator.closing {
background-color: #f59e0b;
}
.status-indicator.closing::before {
background-color: white;
}
/* 二维码容器 */
.qr-container {
margin-bottom: 28px;
width: 100%;
max-width: 360px;
}
.qr-card {
background-color: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.qr-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #2563eb, #60a5fa);
}
.qr-card:hover {
box-shadow: 0 10px 30px rgba(37, 99, 235, 0.15);
transform: translateY(-2px);
}
.qr-wrapper {
width: 100%;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
background-color: #fafafa;
border-radius: 12px;
margin-bottom: 16px;
}
.qr-expiry {
text-align: center;
color: #666;
font-size: 14px;
padding: 8px 0;
border-top: 1px dashed #eee;
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-out {
animation: fadeOut 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
/* 信息卡片 */
.info-card {
width: 100%;
background-color: white;
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: all 0.3s ease;
}
.info-card:hover {
box-shadow: 0 10px 30px rgba(37, 99, 235, 0.1);
}
.card-header {
padding: 16px 24px;
border-bottom: 1px solid #f5f5f5;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1f2329;
}
.info-grid {
padding: 16px 24px;
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.info-item {
display: flex;
flex-direction: column;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
color: #86909c;
font-size: 13px;
margin-bottom: 4px;
}
.info-value {
font-size: 15px;
color: #1f2329;
word-break: break-all;
line-height: 1.4;
}
/* 底部操作区 */
.bottom-bar {
display: flex;
padding: 16px 24px;
background-color: white;
border-top: 1px solid #f5f5f5;
gap: 16px;
}
.action-btn {
flex: 1;
padding: 14px 0;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border: none;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.action-btn.primary {
background-color: #2563eb;
color: white;
}
.action-btn.primary:hover {
background-color: #1d4ed8;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
transform: translateY(-1px);
}
.action-btn.primary:active {
transform: translateY(1px);
}
.action-btn.secondary {
background-color: #f5f7fa;
color: #4e5969;
}
.action-btn.secondary:hover {
background-color: #e8ebf0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
.action-btn.secondary:active {
transform: translateY(1px);
}
.btn-text {
margin-left: 8px;
}
/* 弹窗样式 */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
.popup-dialog {
background-color: white;
padding: 28px 24px;
border-radius: 16px;
width: 85%;
max-width: 320px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
animation: popupIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes popupIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.popup-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
font-size: 24px;
}
.popup-icon.info {
background-color: #dbeafe;
color: #2563eb;
}
.popup-icon.success {
background-color: #dcfce7;
color: #10b981;
}
.popup-icon.error {
background-color: #fee2e2;
color: #ef4444;
}
.popup-icon.warning {
background-color: #fef3c7;
color: #f59e0b;
}
.popup-text {
font-size: 16px;
margin-bottom: 24px;
text-align: center;
color: #1f2329;
line-height: 1.5;
}
.popup-btn {
background-color: #2563eb;
color: white;
border: none;
padding: 10px 28px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
transition: all 0.2s ease;
width: 100%;
}
.popup-btn:hover {
background-color: #1d4ed8;
}
/* 响应式调整 */
@media (min-width: 768px) {
.info-grid {
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.qr-container {
max-width: 400px;
}
}
@media (max-width: 375px) {
.navbar {
padding: 14px 18px;
}
.navbar-title {
font-size: 16px;
}
.main-content {
padding: 18px;
}
.qr-card {
padding: 18px;
}
.action-btn {
font-size: 14px;
padding: 12px 0;
}
.btn-text {
margin-left: 6px;
}
}
</style>