This commit is contained in:
sjk
2026-01-23 16:27:47 +08:00
parent 213229953b
commit e8e6d913df
26 changed files with 4294 additions and 431 deletions

View File

@@ -35,16 +35,16 @@ const API_CONFIG: Record<EnvType, EnvConfig> = {
// 测试环境 - 服务器测试
test: {
baseURL: 'https://lehang.tech', // 测试服务器Go服务
pythonURL: 'https://lehang.tech', // 测试服务器Python服务
websocketURL: 'wss://lehang.tech', // 测试服务器WebSocket服务
pythonURL: 'https://api.lehang.tech', // 测试服务器Python服务
websocketURL: 'wss://api.lehang.tech', // 测试服务器WebSocket服务
timeout: 90000
},
// 生产环境
prod: {
baseURL: 'https://lehang.tech', // 生产环境Go服务
pythonURL: 'https://lehang.tech', // 生产环境Python服务
websocketURL: 'wss://lehang.tech', // 生产环境WebSocket服务
pythonURL: 'https://api.lehang.tech', // 生产环境Python服务
websocketURL: 'wss://api.lehang.tech', // 生产环境WebSocket服务
timeout: 90000
}
};

View File

@@ -41,7 +41,10 @@ Page({
qrcodeError: '', // 二维码加载错误提示
qrcodeLoading: false, // 二维码是否正在加载
isDevelopment: isDevelopment(), // 是否开发环境
isGettingCode: false // 是否正在获取验证码(防抖
isScanning: false, // 是否正在扫码过程中(用户保存二维码后切到小红书
// 消息确认机制
processedMessageIds: [] as string[], // 已处理的消息ID集合避免重复处理
},
onLoad() {
@@ -73,6 +76,9 @@ Page({
console.log('[页面生命周期] 当前登录方式:', this.data.loginType);
console.log('[页面生命周期] 是否正在等待登录结果:', (this.data as any).waitingLoginResult);
console.log('[页面生命周期] 是否需要验证码:', this.data.needCaptcha);
console.log('[页面生命周期] 是否正在扫码:', this.data.isScanning);
console.log('[页面生命周期] sessionId:', this.data.sessionId);
console.log('[页面生命周期] WebSocket连接状态:', this.data.socketConnected);
// 临时标记为隐藏状态,阻止重连
this.setData({ pageHidden: true } as any);
@@ -95,6 +101,18 @@ Page({
return;
}
// 如果用户正在扫码过程中保存二维码后切到小红书不关闭WebSocket
if (this.data.isScanning) {
console.log('[页面生命周期] 用户正在扫码保持WebSocket连接');
return;
}
// 如果sessionId存在且WebSocket连接活跃保持连接用户可能正在等待验证码或准备登录
if (this.data.sessionId && this.data.socketConnected) {
console.log('[页面生命周期] 有活跃的WebSocket连接保持连接用户可能正在登录流程中');
return;
}
console.log('[页面生命周期] 关闭WebSocket连接');
// 页面隐藏时也关闭WebSocket连接
this.closeWebSocket();
@@ -114,6 +132,24 @@ Page({
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (readyState === 0 || readyState === 1) {
console.log('[页面生命周期] WebSocket连接已存在且正常无需重建');
// 连接正常,主动拉取未确认消息
if (readyState === 1) {
console.log('[WebSocket] 主动拉取未确认消息');
try {
socketTask.send({
data: JSON.stringify({ type: 'pull_unconfirmed' }),
success: () => {
console.log('[WebSocket] 拉取未确认消息请求已发送');
},
fail: (err: any) => {
console.error('[WebSocket] 拉取未确认消息请求失败:', err);
}
});
} catch (e) {
console.error('[WebSocket] 拉取未确认消息异常:', e);
}
}
return;
}
console.log(`[页面生命周期] WebSocket连接状态异常: ${readyState},准备重建`);
@@ -163,12 +199,6 @@ Page({
// 获取验证码
async getVerifyCode() {
// 防抖:如果正在获取中,直接返回
if (this.data.isGettingCode) {
console.log('[发送验证码] 正在处理中,忽略重复点击');
return;
}
if (this.data.countdown > 0) {
return;
}
@@ -183,9 +213,6 @@ Page({
return;
}
// 设置防抖标记
this.setData({ isGettingCode: true });
// 立即显示加载动画
wx.showToast({
title: '正在连接...',
@@ -284,8 +311,6 @@ Page({
},
fail: (err: any) => {
console.error('[发送验证码] WebSocket消息发送失败:', err);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.showToast({
title: '发送失败,请重试',
icon: 'none',
@@ -296,8 +321,6 @@ Page({
} catch (error: any) {
console.error('[发送验证码] 异常:', error);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.showToast({
title: error.message || '发送失败,请重试',
icon: 'none',
@@ -538,17 +561,35 @@ Page({
console.log('需要验证码验证:', status.captcha_type);
wx.showToast({
title: status.message || '需要验证码验证',
icon: 'none',
duration: 3000
});
this.setData({
needCaptcha: true,
captchaType: status.captcha_type || 'unknown',
qrcodeImage: status.qrcode_image || ''
});
// 判断是否成功获取到二维码
if (status.qrcode_image) {
wx.showToast({
title: status.message || '需要验证码验证',
icon: 'none',
duration: 3000
});
this.setData({
needCaptcha: true,
captchaType: status.captcha_type || 'unknown',
qrcodeImage: status.qrcode_image
});
} else {
// 未能获取到二维码显示Toast提示
console.warn('验证码发送成功,但未能获取二维码');
wx.showToast({
title: '页面加载异常,请重试',
icon: 'none',
duration: 3000
});
this.setData({
needCaptcha: false, // 不显示弹窗
captchaType: '',
qrcodeImage: ''
});
}
} else if (status.status === 'processing') {
// 仍在处理中,继续轮询
@@ -815,8 +856,82 @@ Page({
// 刷新二维码
async refreshQRCode() {
console.log('[刷新二维码] 用户点击刷新按钮');
console.log('[刷新二维码] needCaptcha:', this.data.needCaptcha);
console.log('[刷新二维码] qrcodeStatus:', this.data.qrcodeStatus);
console.log('[刷新二维码] socketTask存在:', !!this.data.socketTask);
console.log('[刷新二维码] qrcodeSessionId:', this.data.qrcodeSessionId);
// 判断是风控二维码还是扫码登录二维码
// 风控二维码needCaptcha=true 且 qrcodeStatus=5使用WebSocket刷新
// 扫码登录二维码qrcodeSessionId存在使用HTTP API刷新
if (this.data.needCaptcha && this.data.qrcodeStatus === 5) {
// 风控二维码刷新(验证码登录时)
console.log('[刷新二维码] 确认是风控二维码使用WebSocket刷新');
// 检查WebSocket连接
if (!this.data.socketTask) {
console.error('[刷新二维码] WebSocket未连接');
wx.showToast({
title: 'WebSocket未连接请重新发送验证码',
icon: 'none',
duration: 3000
});
return;
}
// 显示加载提示
wx.showLoading({
title: '正在刷新...',
mask: true
});
// 发送刷新请求
try {
this.data.socketTask.send({
data: JSON.stringify({
type: 'refresh_qrcode'
}),
success: () => {
console.log('[刷新二维码] 刷新请求发送成功');
// 更新状态为加载中
this.setData({
qrcodeStatus: 0,
captchaTitle: '正在刷新二维码...'
});
},
fail: (err: any) => {
console.error('[刷新二维码] 发送失败:', err);
wx.hideLoading();
wx.showToast({
title: '刷新失败,请重试',
icon: 'none',
duration: 2000
});
}
});
} catch (error) {
console.error('[刷新二维码] 异常:', error);
wx.hideLoading();
wx.showToast({
title: '刷新失败,请重试',
icon: 'none',
duration: 2000
});
}
return;
}
// 扫码登录二维码刷新HTTP API
console.log('[刷新二维码] 使用HTTP API刷新扫码登录二维码');
const { qrcodeSessionId } = this.data;
if (!qrcodeSessionId) {
wx.showToast({
title: '请重新发送验证码',
icon: 'none',
duration: 2000
});
return;
}
@@ -1114,17 +1229,17 @@ Page({
// 建立WebSocket连接
connectWebSocket(sessionId: string) {
console.log('[WebSocket] ========== connectWebSocket被调用 ==========');
console.log('[WebSocket] 调用栈:', new Error().stack?.split('\n').slice(1, 4).join('\n'));
const stack = new Error().stack;
console.log('[WebSocket] 调用栈:', stack ? stack.split('\n').slice(1, 4).join('\n') : '无法获取');
console.log('[WebSocket] SessionID:', sessionId);
console.log('[WebSocket] 当前needCaptcha状态:', this.data.needCaptcha);
console.log('[WebSocket] ===========================================');
// 清除正常关闭标记,新连接默认为非正常关闭
// 重置重连计数,允许新连接重连
this.setData({
normalClose: false,
reconnectCount: 0
} as any);
// 如果已有连接且处于OPEN状态不重复连接
if (this.data.socketTask && this.data.socketReadyState === 1) {
console.log('[WebSocket] 连接已存在且活跃,不重复连接');
return;
}
// 关闭旧连接
if (this.data.socketTask) {
@@ -1139,14 +1254,25 @@ Page({
console.log('[WebSocket] 已调用close(),等待关闭...');
} catch (e) {
console.log('[WebSocket] 关闭旧连接失败(可能已关闭):', e);
} finally {
// 重置标记,允许新连接重连
this.setData({
normalClose: false,
reconnectCount: 0
} as any);
}
// 等待50ms让旧连接完全关闭
setTimeout(() => {
this._doConnect(sessionId);
}, 50);
} else {
// 没有旧连接,直接连接
this._doConnect(sessionId);
}
},
// 执行实际的WebSocket连接内部方法
_doConnect(sessionId: string) {
// 清除正常关闭标记,新连接默认为非正常关闭
// 重置重连计数,允许新连接重连
this.setData({
normalClose: false,
reconnectCount: 0
} as any);
// 获取WebSocket服务地址使用配置化地址
const wsURL = API.websocketURL;
@@ -1229,6 +1355,44 @@ Page({
try {
const data = JSON.parse(res.data as string);
// 提取message_id
const messageId = data.message_id;
// 检查是否已处理过该消息(去重)
if (messageId && this.data.processedMessageIds.includes(messageId)) {
console.log('[WebSocket] 消息已处理,忽略重复消息:', messageId);
return;
}
// 添加到已处理集合
if (messageId) {
const processedIds = this.data.processedMessageIds;
processedIds.push(messageId);
// 保持集合大小最多100条
if (processedIds.length > 100) {
processedIds.shift();
}
this.setData({ processedMessageIds: processedIds });
// 立即发送ACK确认
try {
socketTask.send({
data: JSON.stringify({
type: 'ack',
message_id: messageId
}),
success: () => {
console.log('[WebSocket] ACK确认已发送:', messageId);
},
fail: (err: any) => {
console.error('[WebSocket] ACK确认发送失败:', messageId, err);
}
});
} catch (e) {
console.error('[WebSocket] 发送ACK异常:', e);
}
}
// 处理二维码状态消息
if (data.type === 'qrcode_status') {
console.log('📡 二维码状态变化:', `status=${data.status}`, data.message);
@@ -1269,13 +1433,16 @@ Page({
}
// 处理扫码成功消息(发送验证码阶段的风控)
else if (data.type === 'qrcode_scan_success') {
console.log('扫码验证完成!', data.message);
console.log('扫码验证完成!', data.message);
// 关闭验证码弹窗
// 清除扫码标记
this.setData({
needCaptcha: false
needCaptcha: false,
isScanning: false
});
console.log('[扫码成功] 已清除isScanning标记');
// 显示提示:扫码成功,请重新发送验证码
wx.showToast({
title: data.message || '扫码成功,请重新发送验证码',
@@ -1290,24 +1457,51 @@ Page({
}
// 处理二维码失效消息
else if (data.type === 'qrcode_expired') {
console.log('⚠️ 二维码已失效!', data.message);
console.log('二维码已失效!', data.message);
// 关闭验证码弹窗
// 关闭弹窗,保持显示,等待用户点击刷新
// 更新二维码状态为5过期
this.setData({
needCaptcha: false
qrcodeStatus: 5,
captchaTitle: data.message || '二维码已过期,点击二维码区域刷新'
});
// 显示提示:二维码已失效
wx.showToast({
title: data.message || '二维码已失效,请重新发送验证码',
icon: 'none',
duration: 3000
});
console.log('[二维码过期] 已更新状态为5等待用户点击刷新');
console.log('[WebSocket] 保持连接,等待用户点击刷新按钮');
console.log('[WebSocket] 二维码已失效,关闭弹窗');
console.log('[WebSocket] 保持连接,等待用户重新操作');
// 不关闭WebSocket保持连接用于刷新二维码
}
// 处理二维码刷新结果
else if (data.type === 'qrcode_refreshed') {
console.log('二维码刷新结果:', data);
// 不关闭WebSocket保持连接用于重新发送验证码
wx.hideLoading(); // 隐藏loading
if (data.success) {
// 刷新成功,更新二维码图片
this.setData({
qrcodeImage: data.qrcode_image,
qrcodeStatus: 1, // 恢复为等待扫码状态
captchaTitle: '请使用小红书APP扫码'
});
console.log('[二维码刷新] 刷新成功,已更新二维码图片');
wx.showToast({
title: data.message || '二维码已刷新',
icon: 'success',
duration: 2000
});
} else {
// 刷新失败
console.error('[二维码刷新] 失败:', data.message);
wx.showToast({
title: data.message || '刷新失败,请重试',
icon: 'none',
duration: 3000
});
}
}
// 处理登录成功消息(点击登录按钮阶段的风控)
else if (data.type === 'login_success') {
@@ -1317,15 +1511,18 @@ Page({
// 判断是扫码验证成功还是真正的登录成功
if (data.storage_state) {
// 真正的登录成功,包含 storage_state
console.log('登录成功!', data);
console.log('登录成功!', data);
wx.hideToast();
// 关闭验证码弹窗
// 关闭验证码弹窗并清除扫码标记
this.setData({
needCaptcha: false
needCaptcha: false,
isScanning: false
});
console.log('[登录成功] 已清除isScanning标记');
// 关闭WebSocket
this.closeWebSocket();
@@ -1395,29 +1592,45 @@ Page({
else if (data.type === 'need_captcha') {
console.log('⚠️ 需要扫码验证:', data.captcha_type);
// 显示二维码弹窗
this.setData({
needCaptcha: true,
captchaType: data.captcha_type || 'unknown',
qrcodeImage: data.qrcode_image || ''
});
wx.hideToast();
wx.showToast({
title: data.message || '需要扫码验证',
icon: 'none',
duration: 3000
});
console.log('[WebSocket] 已显示风控二维码');
// 判断是否成功获取到二维码
if (data.qrcode_image) {
// 显示二维码弹窗
this.setData({
needCaptcha: true,
captchaType: data.captcha_type || 'unknown',
qrcodeImage: data.qrcode_image
});
wx.hideToast();
wx.showToast({
title: data.message || '需要扫码验证',
icon: 'none',
duration: 3000
});
console.log('[WebSocket] 已显示风控二维码');
} else {
// 未能获取到二维码显示Toast提示
console.warn('[WebSocket] 验证码发送成功,但未能获取二维码');
this.setData({
needCaptcha: false, // 不显示弹窗
captchaType: '',
qrcodeImage: ''
});
wx.hideToast();
wx.showToast({
title: '页面加载异常,请重试',
icon: 'none',
duration: 3000
});
}
}
// 处理code_sent消息验证码发送结果
else if (data.type === 'code_sent') {
console.log('[WebSocket] 验证码发送结果:', data);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.hideToast();
if (data.success) {
@@ -1486,35 +1699,44 @@ Page({
console.log('[WebSocket] 关闭代码:', res.code || '未知');
console.log('[WebSocket] 是否正常关闭:', res.code === 1000 ? '是' : '否');
console.log('[WebSocket] 是否主动关闭:', (this.data as any).normalClose ? '是' : '否');
console.log('=========================================')
console.log('[WebSocket] 当前sessionId:', this.data.sessionId);
console.log('[WebSocket] 页面是否隐藏:', (this.data as any).pageHidden);
console.log('[WebSocket] 当前重连计数:', (this.data as any).reconnectCount || 0);
console.log('=========================================');
socketConnected = false;
// 更新页面状态
this.setData({
socketConnected: false,
socketReadyState: 3 // CLOSED
});
// 清理ping定时器
if ((this.data as any).pingTimer) {
clearInterval((this.data as any).pingTimer);
}
// 判断是否是正常关闭
const isNormalClose = (this.data as any).normalClose || res.code === 1000;
// 清除正常关闭标记
this.setData({ normalClose: false } as any);
// 检查是否需要重连(只要页面还在且未隐藏就重连)
// 增加重连计数
const reconnectCount = (this.data as any).reconnectCount || 0;
// 如果是主动关闭reconnectCount >= 999不重连
if (reconnectCount >= 999) {
console.log('[WebSocket] 主动关闭,不重连');
return;
}
// 最多重连5次
if (reconnectCount < 5) {
const delay = Math.min(1000 * Math.pow(2, reconnectCount), 10000); // 指数退避: 1s, 2s, 4s, 8s, 10s
console.log(`[WebSocket] 将在${delay}ms后进行第${reconnectCount + 1}次重连`);
setTimeout(() => {
// 检查页面是否还在且未隐藏通过sessionId存在和pageHidden来判断
if (this.data.sessionId && !(this.data as any).pageHidden) {
@@ -1527,7 +1749,7 @@ Page({
}, delay);
} else {
console.log('[WebSocket] 已达到最大重连次数,停止重连');
// 只有非正常关闭才提示用户
if (!isNormalClose) {
wx.showToast({
@@ -1624,6 +1846,10 @@ Page({
console.log('[保存二维码] 开始保存');
// 标记用户正在扫码过程中
this.setData({ isScanning: true });
console.log('[保存二维码] 已设置isScanning=true保持WebSocket连接');
// base64转为临时文件
const base64Data = this.data.qrcodeImage.replace(/^data:image\/\w+;base64,/, '');
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`;
@@ -1643,9 +1869,9 @@ Page({
success: () => {
console.log('[保存二维码] 保存成功');
wx.showToast({
title: '二维码已保存到相册',
title: '二维码已保存,请在小红书中扫码',
icon: 'success',
duration: 2000
duration: 3000
});
},
fail: (err) => {
@@ -1693,13 +1919,18 @@ Page({
// 关闭验证码弹窗
closeCaptcha() {
console.log('[关闭弹窗] 用户手动关闭二维码弹窗');
// 关闭弹窗时清除扫码标记(用户放弃扫码)
this.setData({
needCaptcha: false,
qrcodeImage: '',
captchaTitle: '',
qrcodeStatus: 0,
qrcodeStatusText: ''
qrcodeStatusText: '',
isScanning: false
});
console.log('[关闭弹窗] 已清除isScanning标记');
},
// 测试WebSocket连接

View File

@@ -24,29 +24,26 @@
<text class="page-title">请绑定小红书账号</text>
<text class="page-subtitle">手机号未注册小红书会导致绑定失败</text>
<!-- 登录方式切换 -->
<view class="login-type-tabs">
<!-- 登录方式切换 - 暂时隐藏扫码登录 -->
<!-- <view class="login-type-tabs">
<view class="tab {{loginType === 'phone' ? 'active' : ''}}" bindtap="switchToPhone">
<text>手机号登录</text>
</view>
<view class="tab {{loginType === 'qrcode' ? 'active' : ''}}" bindtap="switchToQRCode">
<text>扫码登录</text>
</view>
</view>
</view> -->
<!-- 二维码扫码登录区域 -->
<view class="qrcode-login-section" wx:if="{{loginType === 'qrcode'}}">
<!-- 二维码扫码登录区域 - 暂时隐藏 -->
<!-- <view class="qrcode-login-section" wx:if="{{loginType === 'qrcode'}}">
<view class="qrcode-container">
<!-- 加载中 -->
<view class="qrcode-loading" wx:if="{{qrcodeLoading}}">
<view class="loading-spinner"></view>
<text class="loading-text">{{loadingText}}</text>
</view>
<!-- 二维码图片 -->
<image class="qrcode-img" src="{{qrcodeImage}}" mode="aspectFit" wx:if="{{qrcodeImage && !qrcodeLoading}}"></image>
<!-- 二维码状态覆盖层(只在过期或出错时显示) -->
<view class="qrcode-status" wx:if="{{qrcodeExpired && !qrcodeLoading}}">
<view class="status-content">
<text class="status-text">{{statusText || '二维码已过期'}}</text>
@@ -54,7 +51,6 @@
</view>
</view>
<!-- 错误提示 -->
<view class="qrcode-error" wx:if="{{qrcodeError && !qrcodeLoading}}">
<view class="error-content">
<text class="error-icon">⚠️</text>
@@ -64,7 +60,6 @@
</view>
</view>
<!-- 登录链接显示区域 -->
<view class="qr-url-section" wx:if="{{qrUrl}}">
<view class="url-label">登录链接可复制到浏览器或小红书APP</view>
<view class="url-content">
@@ -77,7 +72,7 @@
<text class="tip-text">请使用小红书APP扫描二维码</text>
<text class="tip-desc">扫码后即可完成绑定</text>
</view>
</view>
</view> -->
<!-- 手机号登录区域 -->
<view class="phone-login-section" wx:if="{{loginType === 'phone'}}">
@@ -106,9 +101,17 @@
</view>
<!-- 已过期透明层 -->
<view class="scan-overlay error" wx:if="{{qrcodeStatus === 5}}">
<view class="scan-icon">
<text class="icon-cross">×</text>
<view class="scan-overlay expired" wx:if="{{qrcodeStatus === 5}}">
<view class="expired-container">
<view class="expired-icon-box">
<text class="expired-icon">⟳</text>
</view>
<view class="expired-content">
<text class="expired-title">二维码已过期</text>
</view>
<view class="refresh-button" bindtap="refreshQRCode">
<text class="refresh-text">刷新</text>
</view>
</view>
</view>
</view>
@@ -116,9 +119,8 @@
<!-- 提示文本 -->
<view class="qr-tips">
<text class="tip-main">{{captchaTitle || '请使用小红书APP扫码'}}</text>
<text class="tip-sub" wx:if="{{qrcodeStatus === 2}}">在APP中确认完成验证</text>
<text class="tip-sub" wx:if="{{qrcodeStatus === 2}}">在APP中确认</text>
<text class="tip-sub" wx:if="{{qrcodeStatus !== 2 && qrcodeStatus !== 5}}">长按二维码可保存</text>
<text class="tip-sub error" wx:if="{{qrcodeStatus === 5}}">请关闭后重新获取</text>
</view>
</view>
@@ -128,7 +130,7 @@
</view>
</view>
<view class="bind-form" wx:if="{{!needCaptcha}}">
<view class="bind-form">
<view class="input-row">
<text class="label">手机号</text>
<picker mode="selector" range="{{countryCodes}}" value="{{countryCodeIndex}}" bindchange="onCountryCodeChange">

View File

@@ -348,6 +348,271 @@ page {
box-shadow: 0 8rpx 24rpx rgba(255, 77, 79, 0.4);
}
/* 过期状态样式 */
.scan-overlay.expired {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
}
.expired-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 40rpx;
animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes slideUp {
from {
transform: translateY(40rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* 过期图标容器 */
.expired-icon-box {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 32rpx rgba(255, 107, 107, 0.3);
margin-bottom: 20rpx;
animation: rotateIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes rotateIn {
from {
transform: rotate(-180deg) scale(0);
opacity: 0;
}
to {
transform: rotate(0deg) scale(1);
opacity: 1;
}
}
/* 过期图标 */
.expired-icon {
font-size: 60rpx;
color: #fff;
font-weight: 300;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 过期内容 */
.expired-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
margin-bottom: 24rpx;
}
.expired-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
/* 刷新按钮 */
.refresh-button {
width: 200rpx;
height: 72rpx;
border-radius: 36rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 107, 0.4);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.refresh-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s;
}
.refresh-button:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
}
.refresh-button:active::before {
left: 100%;
}
.refresh-text {
font-size: 28rpx;
font-weight: 500;
color: #fff;
letter-spacing: 2rpx;
}
/* 错误提示样式 */
.qr-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 40rpx;
min-height: 500rpx;
}
.error-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
animation: bounceIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes bounceIn {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.error-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.error-desc {
font-size: 28rpx;
color: #999;
text-align: center;
line-height: 1.6;
margin-bottom: 32rpx;
}
.retry-button {
width: 280rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
color: #fff;
font-size: 28rpx;
font-weight: 500;
border: none;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 107, 0.4);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.retry-button::after {
border: none;
}
.retry-button:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
}
/* 内联错误提示样式 */
.qr-error-inline {
display: flex;
align-items: center;
gap: 24rpx;
background: linear-gradient(135deg, #FFF5F5 0%, #FFE8E8 100%);
padding: 24rpx 32rpx;
margin: 0 32rpx 32rpx;
border-radius: 16rpx;
border: 2rpx solid #FFCDD2;
animation: slideDown 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes slideDown {
from {
transform: translateY(-20rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.qr-error-inline .error-icon {
font-size: 48rpx;
margin: 0;
animation: none;
flex-shrink: 0;
}
.qr-error-inline .error-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.qr-error-inline .error-text {
font-size: 28rpx;
font-weight: 500;
color: #D32F2F;
margin: 0;
}
.qr-error-inline .error-desc {
font-size: 24rpx;
color: #F44336;
margin: 0;
text-align: left;
}
.retry-button-inline {
padding: 12rpx 32rpx;
height: auto;
line-height: 1.4;
border-radius: 20rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
color: #fff;
font-size: 26rpx;
font-weight: 500;
border: none;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
flex-shrink: 0;
}
.retry-button-inline::after {
border: none;
}
.retry-button-inline:active {
transform: scale(0.95);
box-shadow: 0 2rpx 6rpx rgba(255, 107, 107, 0.25);
}
@keyframes scaleIn {
from {
transform: scale(0);