添加扫码签到功能和相关配置
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
const BASE_URL = 'http://localhost:8080/api'
|
// const BASE_URL = 'http://localhost:8080/api'
|
||||||
|
const BASE_URL = '/api'
|
||||||
|
|
||||||
export const request = (options) => {
|
export const request = (options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { request, setToken, clearToken, clearAllCache, clearCache } from '@/utils/request.js'
|
||||||
|
|
||||||
|
// ========== 登录相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序登录
|
||||||
|
* @param {object} data - 登录参数
|
||||||
|
* @param {string} data.code - 微信登录code
|
||||||
|
* @param {string} [data.encryptedData] - 加密数据
|
||||||
|
* @param {string} [data.iv] - 加密向量
|
||||||
|
* @returns {Promise} 登录结果
|
||||||
|
*/
|
||||||
|
export const login = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/member/auth/miniapp/login',
|
||||||
|
method: 'POST',
|
||||||
|
data: data,
|
||||||
|
needToken: false // 登录请求不需要token
|
||||||
|
}).then(res => {
|
||||||
|
// 登录成功,保存token
|
||||||
|
if (res.data && res.data.token) {
|
||||||
|
setToken(res.data.token)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* @returns {Promise} 退出结果
|
||||||
|
*/
|
||||||
|
export const logout = () => {
|
||||||
|
return request({
|
||||||
|
url: '/member/auth/logout',
|
||||||
|
method: 'POST'
|
||||||
|
}).then(res => {
|
||||||
|
// 退出成功,清除token和缓存
|
||||||
|
clearToken()
|
||||||
|
clearAllCache()
|
||||||
|
return res
|
||||||
|
}).catch(err => {
|
||||||
|
// 即使请求失败,也清除本地token
|
||||||
|
clearToken()
|
||||||
|
clearAllCache()
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 签到相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签到二维码
|
||||||
|
* @param {boolean} [cache=true] - 是否启用缓存
|
||||||
|
* @returns {Promise} 二维码数据
|
||||||
|
*/
|
||||||
|
export const getQRCode = (cache = true) => {
|
||||||
|
return request({
|
||||||
|
url: '/checkIn/qrcode',
|
||||||
|
method: 'GET',
|
||||||
|
cache: cache,
|
||||||
|
cacheTime: 5 * 60 * 1000 // 5分钟缓存
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码签到
|
||||||
|
* @param {string} qrContent - 二维码内容
|
||||||
|
* @returns {Promise} 签到结果
|
||||||
|
*/
|
||||||
|
export const checkIn = (qrContent) => {
|
||||||
|
return request({
|
||||||
|
url: '/checkIn/scan',
|
||||||
|
method: 'POST',
|
||||||
|
data: { qrContent }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 用户相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param {boolean} [cache=true] - 是否启用缓存
|
||||||
|
* @returns {Promise} 用户信息
|
||||||
|
*/
|
||||||
|
export const getUserInfo = (cache = true) => {
|
||||||
|
return request({
|
||||||
|
url: '/member/info',
|
||||||
|
method: 'GET',
|
||||||
|
cache: cache,
|
||||||
|
cacheTime: 30 * 60 * 1000 // 30分钟缓存
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
* @param {object} data - 用户信息
|
||||||
|
* @returns {Promise} 更新结果
|
||||||
|
*/
|
||||||
|
export const updateUserInfo = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/member/info',
|
||||||
|
method: 'PUT',
|
||||||
|
data: data
|
||||||
|
}).then(res => {
|
||||||
|
// 更新成功,清除用户信息缓存
|
||||||
|
const cacheKey = `GET_/member/info_{}`
|
||||||
|
clearCache(cacheKey)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 课程相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐课程列表
|
||||||
|
* @param {boolean} [cache=true] - 是否启用缓存
|
||||||
|
* @returns {Promise} 课程列表
|
||||||
|
*/
|
||||||
|
export const getRecommendCourses = (cache = true) => {
|
||||||
|
return request({
|
||||||
|
url: '/course/recommend',
|
||||||
|
method: 'GET',
|
||||||
|
cache: cache,
|
||||||
|
cacheTime: 10 * 60 * 1000 // 10分钟缓存
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课程详情
|
||||||
|
* @param {number} id - 课程ID
|
||||||
|
* @param {boolean} [cache=true] - 是否启用缓存
|
||||||
|
* @returns {Promise} 课程详情
|
||||||
|
*/
|
||||||
|
export const getCourseDetail = (id, cache = true) => {
|
||||||
|
return request({
|
||||||
|
url: `/course/${id}`,
|
||||||
|
method: 'GET',
|
||||||
|
cache: cache,
|
||||||
|
cacheTime: 15 * 60 * 1000 // 15分钟缓存
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
getQRCode,
|
||||||
|
checkIn,
|
||||||
|
getUserInfo,
|
||||||
|
updateUserInfo,
|
||||||
|
getRecommendCourses,
|
||||||
|
getCourseDetail
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<view class="qr-status">
|
||||||
|
<!-- 加载中状态 -->
|
||||||
|
<view v-if="status === 'loading'" class="status-loading">
|
||||||
|
<view class="status-icon">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
</view>
|
||||||
|
<text>生成中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 签到成功状态 -->
|
||||||
|
<view v-else-if="status === 'scanned'" class="status-success">
|
||||||
|
<view class="status-icon">
|
||||||
|
<uni-icons type="checkmarkcircle" size="40rpx" color="#2ECC71"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text>签到成功!</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错误状态(支持自定义文案) -->
|
||||||
|
<view v-else-if="status === 'error'" class="status-error">
|
||||||
|
<view class="status-icon">
|
||||||
|
<uni-icons type="closecircle" size="40rpx" color="#E74C3C"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text>{{ errorText || '签到失败,请重试' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
// 扩展Props,支持自定义错误文案
|
||||||
|
const props = defineProps({
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 自定义错误文本(可选)
|
||||||
|
errorText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 保留原样式,新增加载中样式 */
|
||||||
|
.qr-status {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-loading,
|
||||||
|
.status-waiting,
|
||||||
|
.status-success,
|
||||||
|
.status-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
font-size: 29rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载中样式 */
|
||||||
|
.status-loading {
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 4rpx solid #E9EDF2;
|
||||||
|
border-top-color: #FF6B35;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: #2ECC71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #E74C3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
v-for="(item, index) in entries"
|
v-for="(item, index) in entries"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="entry-item"
|
class="entry-item"
|
||||||
|
@tap="QEClick(item.path)"
|
||||||
>
|
>
|
||||||
<!-- 入口图标容器 -->
|
<!-- 入口图标容器 -->
|
||||||
<view :class="['entry-icon', { accent: item.accent }]">
|
<view :class="['entry-icon', { accent: item.accent }]">
|
||||||
@@ -21,37 +22,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
|
const QEClick = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url:"/pages/checkIn/checkIn"
|
||||||
|
})
|
||||||
|
}
|
||||||
// 快捷入口数据列表
|
// 快捷入口数据列表
|
||||||
const entries = [
|
const entries = [
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
||||||
title: '找课程',
|
title: '找课程',
|
||||||
desc: '精品课程',
|
desc: '精品课程',
|
||||||
accent: false ,
|
accent: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
||||||
title: '训练计划',
|
title: '训练计划',
|
||||||
desc: '个性定制',
|
desc: '个性定制',
|
||||||
accent: true ,
|
accent: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
||||||
title: '健身数据',
|
title: '健身数据',
|
||||||
desc: '记录分析',
|
desc: '记录分析',
|
||||||
accent: false ,
|
accent: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
||||||
title: '消息',
|
title: '消息',
|
||||||
desc: '通知消息',
|
desc: '通知消息',
|
||||||
accent: true,
|
accent: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
||||||
title: '签到',
|
title: '签到',
|
||||||
desc: '打卡签到',
|
desc: '打卡签到',
|
||||||
accent: false,
|
accent: false,
|
||||||
|
path: "/pages/checkIn/checkIn"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"quickapp" : {},
|
"quickapp" : {},
|
||||||
/* 小程序特有相关 */
|
/* 小程序特有相关 */
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid" : "wx8278fdbc9f158915",
|
"appid" : "wx8f0d644d1d8985f6",
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false
|
"urlCheck" : false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "健身房"
|
"navigationBarTitleText": "健身房"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/checkIn/checkIn",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "会员签到"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -0,0 +1,979 @@
|
|||||||
|
<template>
|
||||||
|
<view class="checkin-page">
|
||||||
|
<!-- 顶部导航栏(固定顶部) -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="header-title">会员签到</view>
|
||||||
|
<view class="header-subtitle">扫码入场,开启今日训练</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 会员信息卡片 -->
|
||||||
|
<view class="member-card card-default">
|
||||||
|
<view class="member-avatar">
|
||||||
|
<image src="/static/default-avatar.png" mode="aspectFill"></image>
|
||||||
|
</view>
|
||||||
|
<view class="member-info">
|
||||||
|
<view class="member-name">尊敬的会员</view>
|
||||||
|
<view class="member-level">
|
||||||
|
<text class="level-badge">黄金会员</text>
|
||||||
|
<text class="valid-date">有效期至 2026-12-31</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="member-points">
|
||||||
|
<view class="points-value">1280</view>
|
||||||
|
<view class="points-label">积分</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 二维码核心区域 -->
|
||||||
|
<view class="qr-container card-default">
|
||||||
|
<view class="qr-header">
|
||||||
|
<view class="qr-title">出示二维码签到</view>
|
||||||
|
<view class="qr-tip">将二维码对准前台扫码设备</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="QRBox">
|
||||||
|
<view class="QR">
|
||||||
|
<image :src="image" mode="" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' } "></image>
|
||||||
|
</view>
|
||||||
|
<view v-if="!image || STQRC" class="loadingBox" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' }">
|
||||||
|
<view class="loading-spinner">
|
||||||
|
<view v-if="!isCheckIn" class="spinner-circle"></view>
|
||||||
|
<view v-else>
|
||||||
|
<uni-icons type="checkmarkempty" size="30"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="loading-text">{{ QRStatus }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 二维码装饰边框 -->
|
||||||
|
<view v-else class="qr-border" :style="{width: Math.min(width, 500) + 80 + 'rpx',height: Math.min(height, 500) + 80 + 'rpx' }">
|
||||||
|
<view class="corner top-left"></view>
|
||||||
|
<view class="corner top-right"></view>
|
||||||
|
<view class="corner bottom-left"></view>
|
||||||
|
<view class="corner bottom-right"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态组件(传递状态和自定义错误文案)- 已签到时不显示 -->
|
||||||
|
<QrStatus
|
||||||
|
v-if="!isCheckIn"
|
||||||
|
:status="status"
|
||||||
|
:errorText="errorText"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作提示 -->
|
||||||
|
<view class="tips-section">
|
||||||
|
<view class="tips-title">温馨提示</view>
|
||||||
|
<view class="tips-list">
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text>二维码每5分钟自动刷新,请勿截图使用</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text>签到成功后可进入场馆开始训练</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text>如有问题请联系前台工作人员</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部按钮(z-index永久置顶) -->
|
||||||
|
<view class="bottom-actions">
|
||||||
|
<button @tap="handleLongPress">签到</button>
|
||||||
|
<button class="btn-refresh" @tap="refreshQR">
|
||||||
|
<uni-icons type="refresh" size="36rpx" color="#5E6F8D"></uni-icons>
|
||||||
|
<text>刷新二维码</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 测试用:手动清除缓存按钮 -->
|
||||||
|
<button class="btn-clear-cache" @tap="handleClearCache">
|
||||||
|
<uni-icons type="trash" size="36rpx" color="#ef4444"></uni-icons>
|
||||||
|
<text>清除缓存</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
|
// 引入状态组件(路径与你保持一致)
|
||||||
|
import QrStatus from '@/components/QRCode/StatusCard.vue'
|
||||||
|
// 引入API封装
|
||||||
|
import { getQRCode, checkIn as apiCheckIn } from '@/api/main.js'
|
||||||
|
|
||||||
|
let image = ref("")
|
||||||
|
let width = ref(0)
|
||||||
|
let height = ref(0)
|
||||||
|
let status = ref('loading')
|
||||||
|
let socketTask = null
|
||||||
|
const scanStatus = ref(false)
|
||||||
|
const QRStatus = ref("生成中...")
|
||||||
|
const STQRC = ref(false)//是否扫码
|
||||||
|
const isCheckIn = ref(false)
|
||||||
|
|
||||||
|
const qrcode = ref("")
|
||||||
|
|
||||||
|
// 新增:自定义错误文本变量
|
||||||
|
const errorText = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存键名前缀
|
||||||
|
*/
|
||||||
|
const CACHE_PREFIX = 'QR_'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取带前缀的缓存键名
|
||||||
|
* @param {string} key - 原始键名
|
||||||
|
* @returns {string} 带前缀的键名
|
||||||
|
*/
|
||||||
|
const getCacheKey = (key) => {
|
||||||
|
return CACHE_PREFIX + key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今日23:59:59的时间戳(当日过期时间)
|
||||||
|
* @returns {number} 今日23:59:59的时间戳
|
||||||
|
*/
|
||||||
|
const getTodayExpireTime = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const expire = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
|
||||||
|
return expire.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否过期
|
||||||
|
* @param {string} key - 缓存键名(不带前缀)
|
||||||
|
* @returns {boolean} 是否有效(未过期)
|
||||||
|
*/
|
||||||
|
const isCacheValid = (key) => {
|
||||||
|
try {
|
||||||
|
const cacheKey = getCacheKey(key)
|
||||||
|
const cacheData = uni.getStorageSync(cacheKey)
|
||||||
|
if (!cacheData) return false
|
||||||
|
|
||||||
|
if (typeof cacheData === 'object' && cacheData.expireTime) {
|
||||||
|
const now = Date.now()
|
||||||
|
return now <= cacheData.expireTime
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
console.error('检查缓存有效性失败:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存数据(自动校验过期时间)
|
||||||
|
* @param {string} key - 缓存键名(不带前缀)
|
||||||
|
* @returns {any} 缓存数据(过期返回null)
|
||||||
|
*/
|
||||||
|
const getCacheData = (key) => {
|
||||||
|
try {
|
||||||
|
if (!isCacheValid(key)) {
|
||||||
|
// 缓存已过期,清除缓存
|
||||||
|
uni.removeStorageSync(getCacheKey(key))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheData = uni.getStorageSync(getCacheKey(key))
|
||||||
|
if (typeof cacheData === 'object' && cacheData.data !== undefined) {
|
||||||
|
return cacheData.data
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取缓存数据失败:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存数据(自动设置当日23:59:59过期)
|
||||||
|
* @param {string} key - 缓存键名(不带前缀)
|
||||||
|
* @param {any} data - 要缓存的数据
|
||||||
|
*/
|
||||||
|
const setCacheData = (key, data) => {
|
||||||
|
try {
|
||||||
|
const cacheData = {
|
||||||
|
data: data,
|
||||||
|
expireTime: getTodayExpireTime()
|
||||||
|
}
|
||||||
|
uni.setStorageSync(getCacheKey(key), cacheData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('设置缓存数据失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有QR_开头的缓存(用于测试阶段)
|
||||||
|
*/
|
||||||
|
const clearQRCache = () => {
|
||||||
|
try {
|
||||||
|
const keys = uni.getStorageInfoSync().keys || []
|
||||||
|
let clearedCount = 0
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key.startsWith(CACHE_PREFIX)) {
|
||||||
|
uni.removeStorageSync(key)
|
||||||
|
clearedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`已清除 ${clearedCount} 个QR_开头的缓存`)
|
||||||
|
return clearedCount
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除QR缓存失败:', e)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用:手动清除缓存按钮点击事件
|
||||||
|
*/
|
||||||
|
const handleClearCache = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '清除缓存',
|
||||||
|
content: '确定要清除所有签到相关的缓存吗?(测试用)',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
const clearedCount = clearQRCache()
|
||||||
|
// 重置页面状态
|
||||||
|
image.value = ""
|
||||||
|
width.value = 0
|
||||||
|
height.value = 0
|
||||||
|
status.value = 'loading'
|
||||||
|
QRStatus.value = "生成中..."
|
||||||
|
STQRC.value = false
|
||||||
|
isCheckIn.value = false
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: `已清除 ${clearedCount} 个缓存`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新请求二维码
|
||||||
|
setTimeout(() => {
|
||||||
|
getStorage(null)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '生成签到二维码...',
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 读取签到状态缓存(自动检查过期)
|
||||||
|
// isCheckIn 代表签到状态,从缓存读取
|
||||||
|
const cachedIsCheckIn = getCacheData("isCheckIn")
|
||||||
|
// checkInTime 代表具体签到时间,从缓存读取(显示在 loading-text 中)
|
||||||
|
const cachedCheckInTime = getCacheData("checkInTime")
|
||||||
|
|
||||||
|
if(cachedIsCheckIn != null) {
|
||||||
|
console.log("进入缓存 - 签到状态")
|
||||||
|
isCheckIn.value = cachedIsCheckIn
|
||||||
|
STQRC.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经签到成功,直接显示成功状态,不需要请求后端
|
||||||
|
if(isCheckIn.value) {
|
||||||
|
console.log("已签到且有缓存,无需请求后端")
|
||||||
|
// 读取二维码图片缓存用于显示
|
||||||
|
const cachedQRInfo = getCacheData("QRInfo")
|
||||||
|
if(cachedQRInfo) {
|
||||||
|
image.value = cachedQRInfo.qrCodeBase64
|
||||||
|
width.value = cachedQRInfo.width * 2
|
||||||
|
height.value = cachedQRInfo.height * 2
|
||||||
|
}
|
||||||
|
// QRStatus 显示具体签到时间(从缓存读取,显示在 loading-text 中)
|
||||||
|
QRStatus.value = cachedCheckInTime || "已完成签到"
|
||||||
|
uni.hideLoading()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未签到或缓存失效,需要请求后端获取二维码
|
||||||
|
// QRStatus 重置为默认的请求状态
|
||||||
|
QRStatus.value = "生成中..."
|
||||||
|
getStorage(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时关闭WebSocket连接(不清除缓存,让缓存自然过期)
|
||||||
|
onUnload(() => {
|
||||||
|
closeWebSocket()
|
||||||
|
// 缓存会在当日23:59:59自动过期,页面卸载时不主动清除
|
||||||
|
// 如需测试,使用页面上的"清除缓存"按钮手动清除
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取二维码接口
|
||||||
|
const fetchQRCode = () => {
|
||||||
|
console.log(1111)
|
||||||
|
status.value = ''
|
||||||
|
errorText.value = '' // 重置错误文本
|
||||||
|
image.value = ""
|
||||||
|
|
||||||
|
getQRCode(true).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
// 保存到本地缓存(用于签到状态判断)
|
||||||
|
setCacheData("QRInfo", res)
|
||||||
|
getStorage(res)
|
||||||
|
qrcode.value = res.qrContent
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('获取二维码失败:', err)
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = err.message || '获取二维码失败'
|
||||||
|
uni.showToast({
|
||||||
|
title: errorText.value,
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshQR = () => {
|
||||||
|
// 如果已签到,不允许刷新二维码
|
||||||
|
if (isCheckIn.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '您已签到',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image.value = ""
|
||||||
|
QRStatus.value = "正在刷新二维码..."
|
||||||
|
setTimeout(() => {
|
||||||
|
getStorage(null)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStorage = res => {
|
||||||
|
let data = res
|
||||||
|
if(data == null) {
|
||||||
|
// 使用带过期检查的缓存读取
|
||||||
|
data = getCacheData("QRInfo")
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
if(!data) fetchQRCode()
|
||||||
|
|
||||||
|
if(data) {
|
||||||
|
image.value = data.qrCodeBase64
|
||||||
|
// 使用带过期时间的缓存(当日23:59:59过期)
|
||||||
|
setCacheData("QRInfo", data)
|
||||||
|
width.value = data.width * 2
|
||||||
|
height.value = data.height * 2
|
||||||
|
|
||||||
|
// 只有在未签到的情况下才连接WebSocket等待扫码
|
||||||
|
if (data.qrContent && !isCheckIn.value) {
|
||||||
|
console.log("未签到,连接WebSocket等待扫码")
|
||||||
|
connectWebSocket(data.qrContent)
|
||||||
|
} else if (isCheckIn.value) {
|
||||||
|
console.log("已签到,无需连接WebSocket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPress = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: false,
|
||||||
|
scanType: ['qrCode'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log(res)
|
||||||
|
checkIn(res.result)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('扫码失败:', err)
|
||||||
|
uni.showToast({
|
||||||
|
title: '扫码失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动签到接口
|
||||||
|
const checkIn = (qrContent) => {
|
||||||
|
console.log(qrContent)
|
||||||
|
apiCheckIn(qrContent).then(res => {
|
||||||
|
closeWebSocket()
|
||||||
|
console.log(res)
|
||||||
|
status.value = 'scanned'
|
||||||
|
errorText.value = '' // 成功重置错误文本
|
||||||
|
QRStatus.value = res.dateTime + " 成功签到"
|
||||||
|
isCheckIn.value = true
|
||||||
|
// 使用带过期时间的缓存(当日23:59:59过期)
|
||||||
|
setCacheData("checkInTime", QRStatus.value)
|
||||||
|
setCacheData("isCheckIn", isCheckIn.value)
|
||||||
|
uni.showToast({
|
||||||
|
title: '签到成功!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('签到请求失败:', err)
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = err.message || '签到失败,请重试' // 对应错误文案
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立WebSocket连接
|
||||||
|
const connectWebSocket = (qrContent) => {
|
||||||
|
const wsUrl = `ws://192.168.43.89:8084/webSocket/checkIn`
|
||||||
|
|
||||||
|
console.log('WebSocket 连接地址:', wsUrl)
|
||||||
|
|
||||||
|
socketTask = uni.connectSocket({
|
||||||
|
url: wsUrl,
|
||||||
|
success: () => {
|
||||||
|
console.log('WebSocket 连接中...')
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('WebSocket 连接失败:', err)
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = '连接失败,请重试' // 对应错误文案
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接打开成功
|
||||||
|
socketTask.onOpen(() => {
|
||||||
|
console.log('WebSocket 连接成功')
|
||||||
|
status.value = 'waiting'
|
||||||
|
errorText.value = '' // 成功重置错误文本
|
||||||
|
sendQRCodeInfo(qrContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送二维码信息到后端
|
||||||
|
const sendQRCodeInfo = (qrContent) => {
|
||||||
|
const qrCodeDto = {
|
||||||
|
qrContent: qrContent,
|
||||||
|
used: false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('发送二维码信息:', qrCodeDto)
|
||||||
|
|
||||||
|
socketTask.send({
|
||||||
|
data: JSON.stringify(qrCodeDto),
|
||||||
|
success: () => {
|
||||||
|
console.log('二维码信息发送成功')
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('发送失败:', err)
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = '连接失败,请重试' // 对应错误文案
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收后端消息
|
||||||
|
socketTask.onMessage((res) => {
|
||||||
|
console.log('收到 WebSocket 消息:', res.data)
|
||||||
|
const message = res.data
|
||||||
|
|
||||||
|
if (message === '正在进行签到') {
|
||||||
|
QRStatus.value = "正在进行签到..."
|
||||||
|
STQRC.value = true
|
||||||
|
// status.value = 'scanned'
|
||||||
|
// errorText.value = '' // 成功重置错误文本
|
||||||
|
// uni.showToast({
|
||||||
|
// title: '签到成功!',
|
||||||
|
// icon: 'success',
|
||||||
|
// duration: 2000
|
||||||
|
// })
|
||||||
|
} else if (message.startsWith('二维码无效')) {
|
||||||
|
uni.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = '二维码无效,请刷新' // 对应错误文案
|
||||||
|
setTimeout(() => {
|
||||||
|
closeWebSocket()
|
||||||
|
}, 3000)
|
||||||
|
} else if (message === '消息格式错误') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '消息格式错误',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = '消息格式错误' // 对应错误文案
|
||||||
|
} else {
|
||||||
|
console.log('未知消息:', message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
socketTask.onClose(() => {
|
||||||
|
console.log('WebSocket 连接关闭')
|
||||||
|
status.value = 'closed'
|
||||||
|
errorText.value = '' // 重置错误文本
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接错误
|
||||||
|
socketTask.onError((err) => {
|
||||||
|
console.error('WebSocket 错误:', err)
|
||||||
|
status.value = 'error'
|
||||||
|
errorText.value = '连接失败,请重试' // 对应错误文案
|
||||||
|
uni.showToast({
|
||||||
|
title: '连接失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭WebSocket连接
|
||||||
|
const closeWebSocket = () => {
|
||||||
|
if (socketTask) {
|
||||||
|
socketTask.close({
|
||||||
|
success: () => {
|
||||||
|
console.log('主动关闭 WebSocket')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socketTask = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.checkin-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #F9FAFE;
|
||||||
|
padding-top: 200rpx; /* 为固定顶部导航预留空间 */
|
||||||
|
padding-bottom: 160rpx; /* 为底部按钮预留空间 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航(固定顶部) */
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99998; /* 低于底部按钮,确保底部按钮永远最上层 */
|
||||||
|
background: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%);
|
||||||
|
padding: 64rpx 48rpx 48rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border-bottom-left-radius: 56rpx;
|
||||||
|
border-bottom-right-radius: 56rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 45rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
font-size: 26rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用卡片样式 */
|
||||||
|
.card-default {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.03), 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1rpx solid #E9EDF2;
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会员信息卡片 */
|
||||||
|
.member-card {
|
||||||
|
margin: 0 32rpx 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 4rpx solid #FF6B35;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 32rpx;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1E2A3A;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-level {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid-date {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-points {
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
|
||||||
|
.points-value {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二维码容器 */
|
||||||
|
.qr-container {
|
||||||
|
margin: 0 32rpx 48rpx;
|
||||||
|
padding: 48rpx 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.qr-header {
|
||||||
|
margin-bottom: 64rpx;
|
||||||
|
|
||||||
|
.qr-title {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E2A3A;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-tip {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二维码核心区域 */
|
||||||
|
.QRBox {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto 64rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.QR {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: white;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingBox {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 40rpx;
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.spinner-circle {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 6rpx solid #E9EDF2;
|
||||||
|
border-top-color: #FF6B35;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
width: 250rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二维码装饰边框 */
|
||||||
|
.qr-border {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 6rpx solid #FF6B35;
|
||||||
|
|
||||||
|
&.top-left {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top-right {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-right-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom-left {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-left-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom-right {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-right-radius: 40rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 温馨提示 */
|
||||||
|
.tips-section {
|
||||||
|
margin: 0 32rpx 48rpx;
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1E2A3A;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-list {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
border: 1rpx solid #E9EDF2;
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
background: #FF6B35;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮(z-index永久置顶+安全区域适配) */
|
||||||
|
.bottom-actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99999; /* 极高z-index确保永远在最上层 */
|
||||||
|
padding: 32rpx 48rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-top: 1rpx solid #E9EDF2;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.btn-refresh {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: #F2F5F9;
|
||||||
|
color: #5E6F8D;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 29rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #E9EDF2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试用:清除缓存按钮 */
|
||||||
|
.btn-clear-cache {
|
||||||
|
width: calc(100% - 96rpx);
|
||||||
|
height: 88rpx;
|
||||||
|
background: #FEF2F2;
|
||||||
|
color: #EF4444;
|
||||||
|
border: 1rpx solid #FECACA;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 29rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #FEE2E2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.checkin-page {
|
||||||
|
background-color: #121826;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #123A5E 0%, #1A4A6F 100%);
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-default {
|
||||||
|
background: #1E2636;
|
||||||
|
border-color: #2A3346;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card .member-info .member-name {
|
||||||
|
color: #EDF2F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card .member-info .member-level .valid-date,
|
||||||
|
.member-card .member-points .points-label {
|
||||||
|
color: #9AA9C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container .qr-header .qr-title {
|
||||||
|
color: #EDF2F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container .qr-header .qr-tip {
|
||||||
|
color: #9AA9C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.QRBox .loadingBox {
|
||||||
|
background: rgba(30, 38, 54, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.QRBox .loadingBox .loading-spinner .loading-text {
|
||||||
|
color: #9AA9C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-section .tips-title {
|
||||||
|
color: #EDF2F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-section .tips-list {
|
||||||
|
background: #1E2636;
|
||||||
|
border-color: #2A3346;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-section .tips-list .tip-item text {
|
||||||
|
color: #9AA9C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
background: #1E2636;
|
||||||
|
border-color: #2A3346;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions .btn-refresh {
|
||||||
|
background: #0F141F;
|
||||||
|
color: #9AA9C1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #2A3346;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-cache {
|
||||||
|
background: #3D1919;
|
||||||
|
color: #FCA5A5;
|
||||||
|
border-color: #5C2B2B;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #4A2525;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
const BASE_URL = 'http://192.168.43.89:8084/api'
|
||||||
|
|
||||||
|
// 缓存相关常量
|
||||||
|
const CACHE_PREFIX = 'API_CACHE_'
|
||||||
|
const CACHE_EXPIRE_TIME = 5 * 60 * 1000 // 默认缓存时间5分钟
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存数据
|
||||||
|
* @param {string} key - 缓存键名
|
||||||
|
* @returns {any} 缓存数据(过期返回null)
|
||||||
|
*/
|
||||||
|
export const getCache = (key) => {
|
||||||
|
try {
|
||||||
|
const cacheData = uni.getStorageSync(CACHE_PREFIX + key)
|
||||||
|
if (cacheData && cacheData.expireTime && Date.now() < cacheData.expireTime) {
|
||||||
|
return cacheData.data
|
||||||
|
}
|
||||||
|
// 缓存过期,清除
|
||||||
|
uni.removeStorageSync(CACHE_PREFIX + key)
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取缓存失败:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存数据
|
||||||
|
* @param {string} key - 缓存键名
|
||||||
|
* @param {any} data - 要缓存的数据
|
||||||
|
* @param {number} expireTime - 过期时间(毫秒),默认5分钟
|
||||||
|
*/
|
||||||
|
export const setCache = (key, data, expireTime = CACHE_EXPIRE_TIME) => {
|
||||||
|
try {
|
||||||
|
const cacheData = {
|
||||||
|
data: data,
|
||||||
|
expireTime: Date.now() + expireTime
|
||||||
|
}
|
||||||
|
uni.setStorageSync(CACHE_PREFIX + key, cacheData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('设置缓存失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指定缓存
|
||||||
|
* @param {string} key - 缓存键名
|
||||||
|
*/
|
||||||
|
export const clearCache = (key) => {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(CACHE_PREFIX + key)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除缓存失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有API缓存
|
||||||
|
*/
|
||||||
|
export const clearAllCache = () => {
|
||||||
|
try {
|
||||||
|
const keys = uni.getStorageInfoSync().keys || []
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key.startsWith(CACHE_PREFIX)) {
|
||||||
|
uni.removeStorageSync(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除所有缓存失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取token
|
||||||
|
* @returns {string|null} token
|
||||||
|
*/
|
||||||
|
export const getToken = () => {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync('token') || null
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取token失败:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置token
|
||||||
|
* @param {string} token - token值
|
||||||
|
*/
|
||||||
|
export const setToken = (token) => {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync('token', token)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('设置token失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除token
|
||||||
|
*/
|
||||||
|
export const clearToken = () => {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除token失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求缓存键名
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} data - 请求参数
|
||||||
|
* @param {string} method - 请求方法
|
||||||
|
* @returns {string} 缓存键名
|
||||||
|
*/
|
||||||
|
const generateCacheKey = (url, data, method) => {
|
||||||
|
const params = JSON.stringify(data || {})
|
||||||
|
return `${method}_${url}_${params}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用请求函数
|
||||||
|
* @param {object} options - 请求配置
|
||||||
|
* @param {string} options.url - 请求URL
|
||||||
|
* @param {string} [options.method='GET'] - 请求方法
|
||||||
|
* @param {object} [options.data={}] - 请求参数
|
||||||
|
* @param {object} [options.header={}] - 请求头
|
||||||
|
* @param {boolean} [options.cache=false] - 是否启用缓存
|
||||||
|
* @param {number} [options.cacheTime] - 缓存时间(毫秒)
|
||||||
|
* @param {boolean} [options.needToken=true] - 是否需要token
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
export const request = (options) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
data = {},
|
||||||
|
header = {},
|
||||||
|
cache = false,
|
||||||
|
cacheTime,
|
||||||
|
needToken = true
|
||||||
|
} = options
|
||||||
|
|
||||||
|
// 生成缓存键名
|
||||||
|
const cacheKey = cache ? generateCacheKey(url, data, method) : null
|
||||||
|
|
||||||
|
// 如果启用缓存且存在有效缓存,直接返回缓存数据
|
||||||
|
if (cache && cacheKey) {
|
||||||
|
const cachedData = getCache(cacheKey)
|
||||||
|
if (cachedData !== null) {
|
||||||
|
console.log(`[API] 命中缓存: ${url}`)
|
||||||
|
resolve(cachedData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
const requestHeader = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...header
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要token,自动添加到请求头
|
||||||
|
if (needToken) {
|
||||||
|
const token = getToken()
|
||||||
|
if (token) {
|
||||||
|
requestHeader['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.request({
|
||||||
|
url: BASE_URL + url,
|
||||||
|
method: method,
|
||||||
|
data: data,
|
||||||
|
header: requestHeader,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
// 如果启用缓存,保存响应数据
|
||||||
|
if (cache && cacheKey && res.data) {
|
||||||
|
setCache(cacheKey, res.data, cacheTime)
|
||||||
|
}
|
||||||
|
resolve(res.data)
|
||||||
|
} else if (res.statusCode === 401) {
|
||||||
|
// token过期,清除token并提示重新登录
|
||||||
|
clearToken()
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录已过期,请重新登录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
reject({ code: 401, message: '登录已过期' })
|
||||||
|
} else {
|
||||||
|
reject({ code: res.statusCode, message: res.data?.message || '请求失败' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error(`[API] 请求失败: ${url}`, err)
|
||||||
|
reject({ code: -1, message: '网络请求失败', error: err })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数导出
|
||||||
|
export const requestUtils = {
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
clearToken,
|
||||||
|
getCache,
|
||||||
|
setCache,
|
||||||
|
clearCache,
|
||||||
|
clearAllCache
|
||||||
|
}
|
||||||
|
|
||||||
|
export default request
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// 用来匹配地址,解决跨域问题
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [uni()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// 匹配所有 /api 开头的请求
|
||||||
|
'/api': {
|
||||||
|
target: 'http://192.168.43.89:8084', // 你的后端SpringBoot地址
|
||||||
|
changeOrigin: true, // 开启跨域伪装
|
||||||
|
// rewrite: (path) => path.replace(/^\/areyouok/, '')
|
||||||
|
// 举例:前端请求 /api/login → 代理成 http://localhost:8088/login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user