hookehuyr

feat(海报打卡): 添加加载状态和错误处理,集成API获取海报数据

实现海报打卡页面的加载状态和错误处理逻辑
集成API获取海报详情数据并更新页面状态
添加重新加载功能,优化用户体验
处理海报背景图片上传和保存逻辑
<template>
<view class="poster-checkin-page bg-gray-50 h-screen flex flex-col">
<!-- 活动信息区域 -->
<view v-if="pageState === 'normal' || pageState === 'no-checkin'" class="bg-white mx-4 mt-4 mb-2 rounded-lg shadow-sm p-4">
<!-- 活动主题 -->
<view class="text-lg font-bold text-gray-800 mb-2">
{{ activityInfo.title }}
<!-- 加载状态 -->
<view v-if="isLoading" class="flex-1 flex items-center justify-center">
<view class="text-center">
<view class="text-gray-500 mb-2">加载中...</view>
</view>
</view>
<!-- 打卡进度 -->
<view class="flex items-center mb-2">
<view class="flex items-center">
<view
v-for="(point, index) in activityInfo.checkPoints"
:key="index"
class="w-2 h-2 rounded-full mr-2 flex items-center justify-center"
:class="point.completed ? 'bg-orange-400' : 'bg-gray-300'"
>
</view>
<!-- 错误状态 -->
<view v-else-if="apiError" class="flex-1 flex items-center justify-center">
<view class="text-center">
<view class="text-red-500 mb-2">{{ apiError }}</view>
<view @tap="fetchPosterDetail" class="bg-blue-500 text-white px-4 py-2 rounded">
重新加载
</view>
<text class="text-xs text-gray-600">{{ activityInfo.completedCount }}/{{ activityInfo.totalCount }}</text>
</view>
<!-- 活动截止日期 -->
<view class="text-sm text-gray-500">
活动截止日期:{{ activityInfo.endDate }}
</view>
</view>
<!-- 海报预览区域 - 正常状态 -->
<view v-if="pageState === 'normal'" class="flex-1 mx-4 relative" style="overflow: visible; padding-bottom: 100rpx;">
<view class="h-full relative flex items-center justify-center">
<view v-if="currentPoster.path" class="w-full h-full relative">
<image
:src="currentPoster.path"
mode="widthFix"
class="w-full h-full"
/>
<!-- 打卡点标题 -->
<!-- <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
{{ posterList[currentPosterIndex]?.title || '海报生成中' }}
</view> -->
<!-- 点击预览提示 -->
<!-- <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
点击预览
</view> -->
<!-- 正常内容 -->
<template v-else>
<!-- 活动信息区域 -->
<view v-if="pageState === 'normal' || pageState === 'no-checkin'" class="bg-white mx-4 mt-4 mb-2 rounded-lg shadow-sm p-4">
<!-- 活动主题 -->
<view class="text-lg font-bold text-gray-800 mb-2">
{{ activityInfo.title }}
</view>
</view>
<!-- 左箭头按钮 -->
<view
class="absolute top-1/2 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200 z-10"
:class="currentPosterIndex > 0 ? 'bg-orange-400 text-white shadow-lg' : 'bg-gray-300 text-gray-500'"
style="left: -20rpx;"
@tap="previousPoster"
>
<Left size="16"></Left>
</view>
<!-- 打卡进度 -->
<view class="flex items-center mb-2">
<view class="flex items-center">
<view
v-for="(point, index) in activityInfo.checkPoints"
:key="index"
class="w-2 h-2 rounded-full mr-2 flex items-center justify-center"
:class="point.completed ? 'bg-orange-400' : 'bg-gray-300'"
>
</view>
</view>
<text class="text-xs text-gray-600">{{ activityInfo.completedCount }}/{{ activityInfo.totalCount }}</text>
</view>
<!-- 右箭头按钮 -->
<view
class="absolute top-1/2 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200 z-10"
:class="currentPosterIndex < posterList.length - 1 ? 'bg-orange-400 text-white shadow-lg' : 'bg-gray-300 text-gray-500'"
style="right: -20rpx;"
@tap="nextPoster"
>
<Right size="16"></Right>
<!-- 活动截止日期 -->
<view class="text-sm text-gray-500">
活动截止日期:{{ activityInfo.endDate }}
</view>
</view>
</view>
<!-- 没有打卡信息的空状态 -->
<view v-if="pageState === 'no-checkin'" class="flex-1 mx-4 mb-2 flex justify-center">
<view class="bg-white rounded-lg shadow-sm p-8 text-center max-w-sm">
<view class="text-6xl mb-4">📸</view>
<view class="text-lg font-bold text-gray-800 mb-2">打卡信息为空</view>
<view class="text-sm text-gray-500 mb-4">您还没有打卡记录,请先参加活动打卡后再来生成海报</view>
<view class="text-xs text-orange-500">完成打卡后即可生成专属海报</view>
</view>
</view>
<!-- 海报预览区域 - 正常状态 -->
<view v-if="pageState === 'normal'" class="flex-1 mx-4 relative" style="overflow: visible; padding-bottom: 100rpx;">
<view class="h-full relative flex items-center justify-center">
<view v-if="currentPoster.path" class="w-full h-full relative">
<image
:src="currentPoster.path"
mode="widthFix"
class="w-full h-full"
/>
<!-- 打卡点标题 -->
<!-- <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
{{ posterList[currentPosterIndex]?.title || '海报生成中' }}
</view> -->
<!-- 点击预览提示 -->
<!-- <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
点击预览
</view> -->
</view>
</view>
<!-- 底部操作按钮 - 仅在正常状态显示 -->
<view v-if="pageState === 'normal'" class="bg-white border-t border-gray-200 p-4 safe-area-bottom" style="position: fixed; bottom: 0; left: 0; right: 0;">
<view class="flex gap-4">
<!-- 左箭头按钮 -->
<view
class="flex-1 bg-gradient-to-r from-orange-400 to-orange-500 text-white py-3 px-6 rounded-lg font-medium shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
@click="chooseBackgroundImage"
class="absolute top-1/2 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200 z-10"
:class="currentPosterIndex > 0 ? 'bg-orange-400 text-white shadow-lg' : 'bg-gray-300 text-gray-500'"
style="left: -20rpx;"
@tap="previousPoster"
>
<text>{{ currentPosterHasCustomBackground ? '更改照片' : '上传照片' }}</text>
<Left size="16"></Left>
</view>
<!-- 右箭头按钮 -->
<view
class="flex-1 text-white py-3 px-6 rounded-lg font-medium shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
:class="posterPath ? 'bg-gradient-to-r from-green-400 to-green-500' : (posterGenerateFailed ? 'bg-gradient-to-r from-orange-400 to-orange-500' : 'bg-gray-400')"
@click="handlePosterAction"
:disabled="!posterPath && !posterGenerateFailed"
class="absolute top-1/2 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200 z-10"
:class="currentPosterIndex < posterList.length - 1 ? 'bg-orange-400 text-white shadow-lg' : 'bg-gray-300 text-gray-500'"
style="right: -20rpx;"
@tap="nextPoster"
>
<text>{{ posterPath ? '保存海报' : (posterGenerateFailed ? '重新生成' : '生成中...') }}</text>
<Right size="16"></Right>
</view>
</view>
</view>
<!-- 没有打卡信息的空状态 -->
<view v-if="pageState === 'no-checkin'" class="flex-1 mx-4 mb-2 flex justify-center">
<view class="bg-white rounded-lg shadow-sm p-8 text-center max-w-sm">
<view class="text-6xl mb-4">📸</view>
<view class="text-lg font-bold text-gray-800 mb-2">打卡信息为空</view>
<view class="text-sm text-gray-500 mb-4">您还没有打卡记录,请先参加活动打卡后再来生成海报</view>
<view class="text-xs text-orange-500">完成打卡后即可生成专属海报</view>
</view>
</view>
<!-- 底部操作按钮 - 仅在正常状态显示 -->
<view v-if="pageState === 'normal'" class="bg-white border-t border-gray-200 p-4 safe-area-bottom" style="position: fixed; bottom: 0; left: 0; right: 0;">
<view class="flex gap-4">
<view
class="flex-1 bg-gradient-to-r from-orange-400 to-orange-500 text-white py-3 px-6 rounded-lg font-medium shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
@click="chooseBackgroundImage"
>
<text>{{ currentPosterHasCustomBackground ? '更改照片' : '上传照片' }}</text>
</view>
<view
class="flex-1 text-white py-3 px-6 rounded-lg font-medium shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
:class="posterPath ? 'bg-gradient-to-r from-green-400 to-green-500' : (posterGenerateFailed ? 'bg-gradient-to-r from-orange-400 to-orange-500' : 'bg-gray-400')"
@click="handlePosterAction"
:disabled="!posterPath && !posterGenerateFailed"
>
<text>{{ posterPath ? '保存海报' : (posterGenerateFailed ? '重新生成' : '生成中...') }}</text>
</view>
</view>
</view>
</template>
<!-- 海报生成组件 - 仅在正常状态显示 -->
<PosterBuilder
......@@ -126,7 +146,7 @@ import { Left, Right } from '@nutui/icons-vue-taro'
import PosterBuilder from '@/components/PosterBuilder/index.vue'
import BASE_URL from '@/utils/config'
// 导入获取海报详情的API
import { getPosterDetailAPI } from '@/api/map'
import { getPosterDetailAPI, savePosterBackgroundAPI } from '@/api/map'
// 默认背景图
const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png'
......@@ -145,86 +165,158 @@ const pageParams = ref({
id: '',
})
// API数据状态
const apiData = ref(null)
const isLoading = ref(false)
const apiError = ref(null)
// TODO: 有真实数据的时候, 通过ID获取具体地点的海报信息, 赋值currentPosterIndex获取特定的海报
// 图片预览相关
const previewVisible = ref(false)
const previewImages = ref([])
// 活动信息数据 - 模拟不同状态,实际使用时从API获取
const activityInfo = ref({
title: '南京路乐龄时尚消费主题路线',
checkPoints: [
{ id: 1, name: '起点签到', completed: true },
{ id: 2, name: '商圈探索', completed: true },
{ id: 3, name: '文化体验', completed: true },
{ id: 4, name: '美食品鉴', completed: false },
{ id: 5, name: '终点打卡', completed: false }
],
completedCount: 3,
totalCount: 5,
endDate: '2025年9月7日'
})
/**
* 获取海报详情数据
*/
const fetchPosterDetail = async () => {
try {
isLoading.value = true
apiError.value = null
// activityInfo.value = {};
const response = await getPosterDetailAPI({})
// 海报数据列表 - 合并海报基础信息和内容数据,实际使用时从API获取
const posterList = ref([
{
id: 1,
title: '起点签到',
path: '',
checkPointId: 1,
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png',
// 海报内容数据
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '张大爷'
},
family: {
name: '幸福之家',
description: '一家人整整齐齐最重要'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
name: '卡点: 上海市第一百货商店'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '长按识别,来,我们一起打卡!'
},
{
id: 2,
title: '商圈探索',
if (response.code === 1) {
apiData.value = response.data
console.log('获取海报详情成功:', response.data)
// 根据API数据更新活动信息
updateActivityInfo()
// 根据API数据更新海报列表
updatePosterList()
// 根据pageParams.id设置当前海报索引
setCurrentPosterIndex()
} else {
apiError.value = response.msg || '获取海报详情失败'
console.error('获取海报详情失败:', response.msg)
}
} catch (error) {
apiError.value = '网络请求失败'
console.error('获取海报详情异常:', error)
} finally {
isLoading.value = false
}
}
/**
* 根据API数据更新活动信息
*/
const updateActivityInfo = () => {
if (!apiData.value) return
const { title, end_date, details, show_detail_index } = apiData.value
// 转换details为checkPoints格式
const checkPoints = details.map((detail, index) => ({
id: detail.id,
name: detail.name,
completed: detail.is_checked === true
}))
// 计算已完成数量
const completedCount = checkPoints.filter(point => point.completed).length
activityInfo.value = {
title: title || '海报打卡活动',
checkPoints,
completedCount,
totalCount: checkPoints.length,
endDate: end_date || '',
showDetailIndex: show_detail_index || 0
}
}
/**
* 根据API数据更新海报列表
*/
const updatePosterList = () => {
if (!apiData.value) return
const { details, family, qrcode_url } = apiData.value
// 只显示is_checked为真的关卡
const checkedDetails = details.filter(detail =>
detail.is_checked === true
)
posterList.value = checkedDetails.map((detail, index) => ({
id: detail.id,
title: detail.name,
path: '',
checkPointId: 2,
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%B3%E5%8D%A12-%E5%9B%BE%E5%B1%82%201.png',
checkPointId: detail.id,
backgroundImage: detail.background_url || defaultBackground,
// 海报内容数据
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '李奶奶'
avatar: family?.avatar_url || 'https://cdn.ipadbiz.cn/icon/tou@2x.png', // 默认头像,后续可从用户信息获取
nickname: '用户昵称' // 默认昵称,后续可从用户信息获取
},
family: {
name: '温馨小家',
description: '健康快乐每一天'
name: family?.name || '我的家庭',
description: ''
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
name: '南京路时尚citywalk'
logo: detail.main_slogan || 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%B3%E5%8D%A12-%E4%BB%8A%E6%9C%9D%E8%B7%9F%E6%97%81%E5%8F%8B%20%E4%B8%80%E9%81%93%E6%9D%A5%E4%B9%B0%E4%B9%B0%E4%B9%B0_1.png',
name: '第二关卡'
logo: detail.sub_slogan || 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
name: detail.name || '海报打卡活动',
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcode: qrcode_url,
qrcodeDesc: '长按识别,来,我们一起打卡!'
},
])
}))
}
/**
* 根据pageParams.id设置当前海报索引
*/
const setCurrentPosterIndex = () => {
if (!posterList.value.length) {
currentPosterIndex.value = 0
return
}
// 如果有指定的ID,查找对应的海报索引
if (pageParams.value.id) {
const targetIndex = posterList.value.findIndex(poster =>
poster.checkPointId.toString() === pageParams.value.id.toString()
)
if (targetIndex !== -1) {
currentPosterIndex.value = targetIndex
return
}
}
// posterList.value = []
// 如果没有ID或没找到对应的关卡ID,默认显示第一个已打卡的项(索引0)
// 因为posterList已经过滤了is_checked=true的项,所以第一个就是我们要的
currentPosterIndex.value = 0
}
// 活动信息数据 - 将由API数据填充
const activityInfo = ref({
title: '',
checkPoints: [],
completedCount: 0,
totalCount: 0,
endDate: '',
showDetailIndex: 0
})
// 海报数据列表 - 将由API数据填充
const posterList = ref([])
// 数据状态检查
const hasActivityInfo = computed(() => {
......@@ -416,7 +508,7 @@ const posterConfig = computed(() => {
/**
* 页面加载时初始化
*/
onMounted(() => {
onMounted(async () => {
Taro.setNavigationBarTitle({ title: '海报打卡' })
// 获取页面参数
......@@ -429,6 +521,16 @@ onMounted(() => {
console.log('海报打卡页面接收到的参数:', pageParams.value)
// 获取海报详情数据
await fetchPosterDetail()
// 数据获取完成后检查页面状态
if (pageState.value === 'no-activity') {
// 没有活动信息,显示确认对话框
showNoActivityConfirm()
return
}
// 页面加载时检查是否需要生成当前海报
generateCurrentPosterIfNeeded()
})
......@@ -576,9 +678,10 @@ const chooseBackgroundImage = () => {
uploadBackgroundImage(tempFile.path)
},
fail: () => {
fail: (error) => {
console.error('选择图片失败:', error)
// Taro.showToast({
// title: '选择图片失败',
// title: '选择图片失败,请重试',
// icon: 'none'
// })
}
......@@ -595,15 +698,33 @@ const uploadBackgroundImage = (filePath) => {
url: BASE_URL + '/admin/?m=srv&a=upload',
filePath,
name: 'file',
success: (uploadRes) => {
Taro.hideLoading()
success: async (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 0 && data.data) {
const currentIndex = currentPosterIndex.value
const currentPosterData = posterList.value[currentIndex]
// 为当前海报设置背景图
backgroundImages.value[currentIndex] = data.data.src
// 调用保存海报背景接口
try {
const saveResult = await savePosterBackgroundAPI({
detail_id: currentPosterData.checkPointId,
poster_background_url: data.data.src
})
if (saveResult.code === 1) {
console.log('海报背景保存成功:', saveResult.data)
} else {
console.warn('海报背景保存失败:', saveResult.msg)
// 即使保存失败,也继续生成海报,不影响用户体验
}
} catch (error) {
console.error('保存海报背景异常:', error)
// 即使保存失败,也继续生成海报,不影响用户体验
}
// 强制标记当前海报需要重新生成
posterGeneratedFlags.value[currentIndex] = false
delete posterConfigHashes.value[currentIndex]
......@@ -614,12 +735,15 @@ const uploadBackgroundImage = (filePath) => {
// 立即重新生成海报
generateCurrentPoster()
Taro.hideLoading()
Taro.showToast({ title: '上传成功', icon: 'success' })
} else {
Taro.hideLoading()
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
}
},
fail: () => {
fail: (error) => {
console.error('上传文件失败:', error)
Taro.hideLoading()
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
}
......@@ -755,22 +879,7 @@ const showNoActivityConfirm = () => {
})
}
/**
* 页面初始化
*/
onMounted(() => {
// 获取页面参数
const instance = Taro.getCurrentInstance()
if (instance.router && instance.router.params) {
pageParams.value = instance.router.params
}
// 检查页面状态
if (pageState.value === 'no-activity') {
// 没有活动信息,显示确认对话框
showNoActivityConfirm()
}
})
</script>
<style scoped>
......