新增搜索课程和加载组件页面,签到页面添加遮罩防重复扫码,添加 request 便捷方法(get/post/put/delete)

This commit is contained in:
future
2026-06-05 21:26:26 +08:00
committed by liwentao
parent a7af34d22b
commit dc7da19aee
15 changed files with 2469 additions and 125 deletions
@@ -0,0 +1,74 @@
<!-- components/LoadingOverlay.vue -->
<template>
<view v-if="visible" class="loading-overlay">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">{{ text }}</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
text: { type: String, default: '加载中...' },
delay: { type: Number, default: 200 }
})
const visible = ref(false)
let timer = null
onMounted(() => {
timer = setTimeout(() => {
visible.value = true
}, props.delay)
})
onUnmounted(() => {
if (timer) clearTimeout(timer)
})
</script>
<style lang="scss" scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.loading-content {
background: rgba(0, 0, 0, 0.7);
border-radius: 16rpx;
padding: 32rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #fff;
font-size: 24rpx;
}
</style>
+63 -32
View File
@@ -104,7 +104,7 @@ import { onLoad, onUnload } from '@dcloudio/uni-app'
// 引入状态组件(路径与你保持一致)
import QrStatus from '@/components/QRCode/StatusCard.vue'
// 引入API封装
import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
import { getQRCode, checkIn as apiCheckIn } from '@/api/main.js'
let image = ref("")
let width = ref(0)
@@ -115,6 +115,7 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
const QRStatus = ref("生成中...")
const STQRC = ref(false)//是否扫码
const isCheckIn = ref(false)
const webSoketURL = "ws://localhost:8084/webSocket/checkIn"
const qrcode = ref("")
@@ -209,22 +210,31 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
}
/**
* 清除所有QR_开头的缓存(用于测试阶段)
* 清除所有与签到相关的缓存(用于测试阶段)
*/
const clearQRCache = () => {
try {
const keys = uni.getStorageInfoSync().keys || []
let clearedCount = 0
for (const key of keys) {
// 清除 QR_ 开头的缓存(页面内部缓存)
if (key.startsWith(CACHE_PREFIX)) {
uni.removeStorageSync(key)
clearedCount++
}
// 清除 API_CACHE_ 开头的缓存(通过 utils/cache.js 缓存的接口数据)
if (key.startsWith('API_CACHE_')) {
// 只清除与签到相关的 API 缓存
if (key.includes('checkIn') || key.includes('qrcode')) {
uni.removeStorageSync(key)
clearedCount++
}
}
}
console.log(`已清除 ${clearedCount}QR_开头的缓存`)
console.log(`已清除 ${clearedCount}签到相关缓存`)
return clearedCount
} catch (e) {
console.error('清除QR缓存失败:', e)
console.error('清除 QR 缓存失败:', e)
return 0
}
}
@@ -256,10 +266,8 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
duration: 2000
})
// 重请求二维码
setTimeout(() => {
getStorage(null)
}, 500)
// 重置页面状态后不再自动请求二维码
uni.hideLoading()
}
}
})
@@ -319,7 +327,7 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
errorText.value = '' // 重置错误文本
image.value = ""
getQRCode(true).then(res => {
getQRCode({ cache: true, cacheTime: 5 * 60 * 1000 }).then(res => {
console.log(res)
// 保存到本地缓存(用于签到状态判断)
setCacheData("QRInfo", res)
@@ -403,7 +411,7 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
// 手动签到接口
const checkIn = (qrContent) => {
console.log(qrContent)
apiCheckIn(qrContent).then(res => {
apiCheckIn({ qrContent }).then(res => {
closeWebSocket()
console.log(res)
status.value = 'scanned'
@@ -422,17 +430,20 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
console.error('签到请求失败:', err)
status.value = 'error'
errorText.value = err.message || '签到失败,请重试' // 对应错误文案
uni.showToast({
title: err.message,
icon: 'none'
})
})
}
// 建立WebSocket连接
const connectWebSocket = (qrContent) => {
const wsUrl = `ws://192.168.43.89:8084/webSocket/checkIn`
console.log('WebSocket 连接地址:', wsUrl)
console.log('WebSocket 连接地址:', webSoketURL)
socketTask = uni.connectSocket({
url: wsUrl,
url: webSoketURL,
success: () => {
console.log('WebSocket 连接中...')
},
@@ -477,17 +488,27 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
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 === '签到成功' || message.includes('签到成功')) {
// 签到成功,更新状态
status.value = 'scanned'
errorText.value = ''
isCheckIn.value = true
QRStatus.value = "签到成功"
// 缓存签到状态
setCacheData("isCheckIn", true)
setCacheData("checkInTime", "签到成功")
uni.showToast({
title: '签到成功!',
icon: 'success',
duration: 2000
})
// 关闭WebSocket连接
closeWebSocket()
} else if (message.startsWith('二维码无效')) {
uni.showToast({
title: message,
@@ -495,18 +516,32 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
duration: 2000
})
status.value = 'error'
errorText.value = '二维码无效,请刷新' // 对应错误文案
errorText.value = '二维码无效,请刷新'
// 隐藏遮罩,允许用户重新操作
STQRC.value = false
QRStatus.value = "生成中..."
setTimeout(() => {
closeWebSocket()
}, 3000)
} else if (message === '消息格式错误') {
uni.showToast({
title: '消息格式错误',
icon: 'none'
})
status.value = 'error'
errorText.value = '消息格式错误' // 对应错误文案
} else {
errorText.value = '消息格式错误'
// 隐藏遮罩,允许用户重新操作
STQRC.value = false
QRStatus.value = "生成中..."
} else if (message.includes('失败') || message.includes('错误')) {
// 其他失败情况
uni.showToast({
title: message,
icon: 'none',
duration: 2000
})
status.value = 'error'
errorText.value = message
// 隐藏遮罩,允许用户重新操作
STQRC.value = false
QRStatus.value = "生成中..."
} else {
console.log('未知消息:', message)
}
})
@@ -523,10 +558,6 @@ import { getQRCode, checkIn as apiCheckIn } from '@/request_api/main.js'
console.error('WebSocket 错误:', err)
status.value = 'error'
errorText.value = '连接失败,请重试' // 对应错误文案
uni.showToast({
title: '连接失败',
icon: 'none'
})
})
}
+133 -10
View File
@@ -1,3 +1,4 @@
<!-- pages/course/index.vue -->
<template>
<view class="tab-page">
<view class="tab-page__header">
@@ -5,27 +6,114 @@
<text class="tab-page__subtitle">精品团课 · 私教 · 线上课</text>
</view>
<RecommendCourses />
<view class="tab-page__actions">
<view class="tab-page__btn" hover-class="tab-page__btn--hover" @tap="goCourseList">
<text class="tab-page__btn-text">预约课程</text>
</view>
<view class="tab-page__btn tab-page__btn--ghost" hover-class="tab-page__btn--hover" @tap="goMyCourses">
<text class="tab-page__btn-text tab-page__btn-text--ghost">我的课程</text>
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton-container">
<view class="skeleton-item" v-for="i in 3" :key="i">
<view class="skeleton-img"></view>
<view class="skeleton-text"></view>
</view>
</view>
<!-- 真实内容 -->
<template v-else>
<RecommendCourses :data="courseData" />
<view class="tab-page__actions">
<view class="tab-page__btn" hover-class="tab-page__btn--hover" @tap="goCourseList">
<text class="tab-page__btn-text">预约课程</text>
</view>
<view class="tab-page__btn tab-page__btn--ghost" hover-class="tab-page__btn--hover" @tap="goMyCourses">
<text class="tab-page__btn-text tab-page__btn-text--ghost">我的课程</text>
</view>
</view>
</template>
<view class="bottom-placeholder"></view>
<TabBar :active="1" />
<TabBar @update:active="handleTabActive" />
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import RecommendCourses from '@/components/index/RecommendCourses.vue'
import TabBar from '@/components/TabBar.vue'
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
const loading = ref(true)
const courseData = ref(null)
// 从缓存加载数据
function loadFromCache() {
try {
const cached = uni.getStorageSync('course_cache')
if (cached && Date.now() - cached.time < 5 * 60 * 1000) {
courseData.value = cached.data
loading.value = false
return true
}
} catch (e) {
console.error('读取缓存失败', e)
}
return false
}
// 从网络加载数据
async function loadFromNetwork() {
loading.value = true
try {
// 模拟 API 请求
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({ code: 0, data: { list: [] } })
}, 500)
})
if (res.code === 0) {
courseData.value = res.data
// 更新缓存
uni.setStorageSync('course_cache', {
data: res.data,
time: Date.now()
})
}
} catch (err) {
console.error('加载失败', err)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 页面激活时刷新数据(可选)
function handleTabActive(index) {
// Tab 切换时后台刷新数据
if (index === 1 && !loading.value) {
loadFromNetwork()
}
}
onLoad(() => {
// 优先显示缓存
const hasCache = loadFromCache()
if (!hasCache) {
loadFromNetwork()
} else {
// 后台静默更新
setTimeout(() => {
loadFromNetwork()
}, 100)
}
})
onShow(() => {
// 每次显示时确保加载完成
if (loading.value && !courseData.value) {
loadFromNetwork()
}
})
function goCourseList() {
navigateToPage(PAGE.COURSE_LIST)
}
@@ -94,4 +182,39 @@ function goMyCourses() {
.bottom-placeholder {
height: 40rpx;
}
</style>
/* 骨架屏样式 */
.skeleton-container {
padding: 24rpx 32rpx;
}
.skeleton-item {
margin-bottom: 24rpx;
padding: 20rpx;
background: #fff;
border-radius: 20rpx;
}
.skeleton-img {
width: 100%;
height: 200rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 16rpx;
}
.skeleton-text {
height: 32rpx;
margin-top: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8rpx;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+4 -3
View File
@@ -15,8 +15,8 @@
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
<!-- 底部导航 -->
<TabBar :active="0" />
<!-- TabBar -->
<TabBar />
</view>
</template>
@@ -38,4 +38,5 @@ import TabBar from '@/components/TabBar.vue'
.bottom-placeholder {
height: 40rpx;
}
</style>
</style>
File diff suppressed because it is too large Load Diff