添加扫码签到功能和相关配置
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) => {
|
||||
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"
|
||||
:key="index"
|
||||
class="entry-item"
|
||||
@tap="QEClick(item.path)"
|
||||
>
|
||||
<!-- 入口图标容器 -->
|
||||
<view :class="['entry-icon', { accent: item.accent }]">
|
||||
@@ -21,37 +22,44 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const QEClick = () => {
|
||||
uni.navigateTo({
|
||||
url:"/pages/checkIn/checkIn"
|
||||
})
|
||||
}
|
||||
// 快捷入口数据列表
|
||||
const entries = [
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
||||
title: '找课程',
|
||||
desc: '精品课程',
|
||||
accent: false ,
|
||||
accent: false
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
||||
title: '训练计划',
|
||||
desc: '个性定制',
|
||||
accent: true ,
|
||||
accent: true
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
||||
title: '健身数据',
|
||||
desc: '记录分析',
|
||||
accent: false ,
|
||||
accent: false
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
||||
title: '消息',
|
||||
desc: '通知消息',
|
||||
accent: true,
|
||||
accent: true
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
||||
title: '签到',
|
||||
desc: '打卡签到',
|
||||
accent: false,
|
||||
path: "/pages/checkIn/checkIn"
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx8278fdbc9f158915",
|
||||
"appid" : "wx8f0d644d1d8985f6",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "健身房"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/checkIn/checkIn",
|
||||
"style": {
|
||||
"navigationBarTitleText": "会员签到"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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