加入加载东湖

This commit is contained in:
future
2026-06-12 15:43:01 +08:00
parent 0402a1e82d
commit b345ceeb42
2 changed files with 155 additions and 25 deletions
+94 -19
View File
@@ -35,7 +35,8 @@
<view class="QR"> <view class="QR">
<image :src="image" mode="" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' } "></image> <image :src="image" mode="" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' } "></image>
</view> </view>
<view v-if="!image || STQRC" class="loadingBox" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' }"> <!-- 加载动画区域 - 设置最小尺寸确保进入页面时可见 -->
<view v-if="!image || STQRC" class="loadingBox" :style="{width: (width > 0 ? Math.min(width, 500) : 500) + 'rpx',height: (height > 0 ? Math.min(height, 500) : 500) + 'rpx' }">
<view class="loading-spinner"> <view class="loading-spinner">
<view v-if="!isCheckIn" class="spinner-circle"></view> <view v-if="!isCheckIn" class="spinner-circle"></view>
<view v-else> <view v-else>
@@ -268,10 +269,13 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
} }
onLoad(() => { onLoad(() => {
uni.showLoading({ // 测试模式下不显示全局loading,让页面内的加载动画显示
title: '生成签到二维码...', if (!TEST_MODE) {
mask: true uni.showLoading({
}) title: '生成签到二维码...',
mask: true
})
}
// 测试模式:直接生成假二维码,内容为"欢迎来到活氧舱" // 测试模式:直接生成假二维码,内容为"欢迎来到活氧舱"
if (TEST_MODE) { if (TEST_MODE) {
@@ -315,37 +319,67 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
}) })
// 测试模式:生成假二维码(内容为"欢迎来到活氧舱") // 测试模式:生成假二维码(内容为"欢迎来到活氧舱")
const generateTestQRCode = () => { // 不发送请求,使用缓存机制避免重复请求,二维码内容保持不变
// 使用 canvas 生成简单的二维码图案(模拟 // @param {boolean} isRefresh - 是否是刷新操作(true=点击刷新按钮,false=首次进入页面
// 由于无法直接生成真正的二维码图片,使用一个静态图片或占位图 const generateTestQRCode = (isRefresh = false) => {
// 在实际测试环境中,可以使用第三方库生成二维码 // 检查是否有缓存的二维码图片
const cachedQRImage = getCacheData("TestQRImage")
const qrContent = "欢迎来到活氧舱"
// 模拟网络延迟 if (cachedQRImage) {
// 使用缓存的二维码图片,不发送请求
console.log("测试模式:使用缓存的二维码图片")
image.value = cachedQRImage.image
width.value = cachedQRImage.width
height.value = cachedQRImage.height
qrcode.value = qrContent
status.value = 'waiting'
QRStatus.value = '请出示二维码签到'
uni.hideLoading()
// 只有刷新操作才显示提示
if (isRefresh) {
uni.showToast({
title: '二维码已刷新',
icon: 'success',
duration: 1500
})
}
return
}
// 首次生成,发送一次请求并缓存
console.log("测试模式:首次生成二维码,发送请求并缓存")
setTimeout(() => { setTimeout(() => {
// 使用一个在线二维码生成服务生成内容为"欢迎来到活氧舱"的二维码
const qrContent = "欢迎来到活氧舱"
qrcode.value = qrContent qrcode.value = qrContent
// 使用在线API生成二维码图片
// 注意:实际使用时应该考虑离线方案
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(qrContent)}` const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(qrContent)}`
// 将在线图片下载为base64(在uniapp中)
uni.request({ uni.request({
url: qrUrl, url: qrUrl,
method: 'GET', method: 'GET',
responseType: 'arraybuffer', responseType: 'arraybuffer',
success: (res) => { success: (res) => {
const base64 = uni.arrayBufferToBase64(res.data) const base64 = uni.arrayBufferToBase64(res.data)
image.value = `data:image/png;base64,${base64}` const qrImage = `data:image/png;base64,${base64}`
image.value = qrImage
width.value = 500 width.value = 500
height.value = 500 height.value = 500
status.value = 'waiting' status.value = 'waiting'
QRStatus.value = '请出示二维码签到' QRStatus.value = '请出示二维码签到'
// 缓存二维码图片,后续刷新不再请求
setCacheData("TestQRImage", {
image: qrImage,
width: 500,
height: 500,
content: qrContent
})
uni.hideLoading() uni.hideLoading()
}, },
fail: () => { fail: () => {
// 如果无法获取在线二维码,使用默认占位图
image.value = '' image.value = ''
width.value = 500 width.value = 500
height.value = 500 height.value = 500
@@ -354,7 +388,7 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
uni.hideLoading() uni.hideLoading()
} }
}) })
}, 1000) }, 500)
} }
// 页面卸载时关闭WebSocket连接(不清除缓存,让缓存自然过期) // 页面卸载时关闭WebSocket连接(不清除缓存,让缓存自然过期)
@@ -401,8 +435,20 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
return return
} }
// 清空图片,显示加载状态
image.value = "" image.value = ""
QRStatus.value = "正在刷新二维码..." QRStatus.value = "正在刷新二维码..."
// 测试模式:重新生成二维码,但不发送请求,内容不变
if (TEST_MODE) {
// 延迟显示,让用户看到刷新效果
setTimeout(() => {
generateTestQRCode(true) // 传递 isRefresh=true
}, 300)
return
}
// 非测试模式:正常刷新逻辑
setTimeout(() => { setTimeout(() => {
getStorage(null) getStorage(null)
}, 500) }, 500)
@@ -439,7 +485,13 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
onlyFromCamera: false, onlyFromCamera: false,
scanType: ['qrCode'], scanType: ['qrCode'],
success: (res) => { success: (res) => {
console.log(res) console.log('扫码结果:', res)
// 测试模式下,如果扫描的是我们生成的二维码内容,直接模拟签到成功
if (TEST_MODE && res.result === '欢迎来到活氧舱') {
console.log('测试模式:模拟签到成功')
handleTestModeCheckIn()
return
}
checkIn(res.result) checkIn(res.result)
}, },
fail: (err) => { fail: (err) => {
@@ -452,6 +504,28 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
}) })
} }
// 测试模式:模拟签到成功(不请求后端)
const handleTestModeCheckIn = () => {
closeWebSocket()
const now = new Date()
const dateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
status.value = 'scanned'
errorText.value = ''
QRStatus.value = `${dateTime} 成功签到`
isCheckIn.value = true
STQRC.value = true
setCacheData("checkInTime", QRStatus.value)
setCacheData("isCheckIn", isCheckIn.value)
uni.showToast({
title: '签到成功!',
icon: 'success',
duration: 2000
})
}
// 手动签到接口 // 手动签到接口
const checkIn = (qrContent) => { const checkIn = (qrContent) => {
console.log(qrContent) console.log(qrContent)
@@ -776,6 +850,7 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
align-items: center; align-items: center;
margin: 0 auto 64rpx; margin: 0 auto 64rpx;
max-width: 100%; max-width: 100%;
min-height: 580rpx; /* 设置最小高度确保加载动画区域可见 */
.QR { .QR {
position: relative; position: relative;
@@ -138,7 +138,13 @@
class="course-card" class="course-card"
@tap="handleCourseClick(course)" @tap="handleCourseClick(course)"
> >
<!-- 图片容器 -->
<view class="card-image-wrapper"> <view class="card-image-wrapper">
<!-- 占位符灰色背景 -->
<view class="card-image-placeholder">
<view class="placeholder-spinner"></view>
</view>
<!-- 实际图片 -->
<image <image
class="card-image" class="card-image"
:src="course.image" :src="course.image"
@@ -223,12 +229,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh } from '@dcloudio/uni-app'
// 测试模式 // 测试模式
const TEST_MODE = true const TEST_MODE = true
// 搜索关键词 // 搜索关键词
const keyword = ref('') const keyword = ref('')
// 当前页 // 当前页
@@ -241,7 +249,7 @@ const hasMore = ref(true)
const loadingMore = ref(false) const loadingMore = ref(false)
// 活动筛选标签 // 活动筛选标签
const activeFilter = ref('all') const activeFilter = ref('all')
// 课程列表 // 课程列表(不包含图片URL,只包含基本信息)
const courses = ref([]) const courses = ref([])
// 骨架屏数量 // 骨架屏数量
const skeletonCount = ref(pageSize) const skeletonCount = ref(pageSize)
@@ -299,6 +307,7 @@ const sortOptions = [
// 处理滚动事件 // 处理滚动事件
const handleScroll = (e) => { const handleScroll = (e) => {
// 收起高级筛选
if (showAdvancedFilter.value && !isScrolling.value) { if (showAdvancedFilter.value && !isScrolling.value) {
isScrolling.value = true isScrolling.value = true
showAdvancedFilter.value = false showAdvancedFilter.value = false
@@ -633,18 +642,32 @@ const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size =
} }
if (result && result.content && result.content.length > 0) { if (result && result.content && result.content.length > 0) {
// 课程对象直接包含图片URL
const processedCourses = result.content.map(course => ({
id: course.id,
image: course.image,
tag: course.tag,
tagType: course.tagType,
courseType: course.courseType,
name: course.name,
duration: course.duration,
level: course.level,
participants: course.participants,
rawData: course.rawData
}))
if (append) { if (append) {
// 追加模式:逐个添加数据,同时移除骨架屏 // 追加模式:逐个添加数据,同时移除骨架屏
for (let i = 0; i < result.content.length; i++) { for (let i = 0; i < processedCourses.length; i++) {
await Promise.resolve() await Promise.resolve()
courses.value.push(result.content[i]) courses.value.push(processedCourses[i])
if (skeletonCount.value > 0) { if (skeletonCount.value > 0) {
skeletonCount.value-- skeletonCount.value--
} }
} }
} else { } else {
// 重置模式:直接替换 // 重置模式:直接替换
courses.value = result.content courses.value = processedCourses
skeletonCount.value = 0 skeletonCount.value = 0
} }
@@ -757,6 +780,14 @@ onMounted(() => {
currentFilterSignature = getFilterSignature() currentFilterSignature = getFilterSignature()
fetchCourses() fetchCourses()
}) })
// 组件卸载时清理资源
onUnmounted(() => {
// 清理滚动定时器
if (scrollTimer) {
clearTimeout(scrollTimer)
}
})
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -1058,12 +1089,36 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2; /* 确保图片在占位符之上 */
.course-card:active & { .course-card:active & {
transform: scale(1.03); transform: scale(1.03);
} }
} }
/* 图片占位符 */
.card-image-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.placeholder-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #e2e8f0;
border-top-color: #f97316;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.image-overlay { .image-overlay {
position: absolute; position: absolute;
left: 0; left: 0;