加入加载东湖
This commit is contained in:
@@ -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,7 +850,8 @@ 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;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user