hookehuyr

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

实现海报打卡页面的加载状态和错误处理逻辑
集成API获取海报详情数据并更新页面状态
添加重新加载功能,优化用户体验
处理海报背景图片上传和保存逻辑
1 <template> 1 <template>
2 <view class="poster-checkin-page bg-gray-50 h-screen flex flex-col"> 2 <view class="poster-checkin-page bg-gray-50 h-screen flex flex-col">
3 + <!-- 加载状态 -->
4 + <view v-if="isLoading" class="flex-1 flex items-center justify-center">
5 + <view class="text-center">
6 + <view class="text-gray-500 mb-2">加载中...</view>
7 + </view>
8 + </view>
9 +
10 + <!-- 错误状态 -->
11 + <view v-else-if="apiError" class="flex-1 flex items-center justify-center">
12 + <view class="text-center">
13 + <view class="text-red-500 mb-2">{{ apiError }}</view>
14 + <view @tap="fetchPosterDetail" class="bg-blue-500 text-white px-4 py-2 rounded">
15 + 重新加载
16 + </view>
17 + </view>
18 + </view>
19 +
20 + <!-- 正常内容 -->
21 + <template v-else>
3 <!-- 活动信息区域 --> 22 <!-- 活动信息区域 -->
4 <view v-if="pageState === 'normal' || pageState === 'no-checkin'" class="bg-white mx-4 mt-4 mb-2 rounded-lg shadow-sm p-4"> 23 <view v-if="pageState === 'normal' || pageState === 'no-checkin'" class="bg-white mx-4 mt-4 mb-2 rounded-lg shadow-sm p-4">
5 <!-- 活动主题 --> 24 <!-- 活动主题 -->
...@@ -97,6 +116,7 @@ ...@@ -97,6 +116,7 @@
97 </view> 116 </view>
98 </view> 117 </view>
99 </view> 118 </view>
119 + </template>
100 120
101 <!-- 海报生成组件 - 仅在正常状态显示 --> 121 <!-- 海报生成组件 - 仅在正常状态显示 -->
102 <PosterBuilder 122 <PosterBuilder
...@@ -126,7 +146,7 @@ import { Left, Right } from '@nutui/icons-vue-taro' ...@@ -126,7 +146,7 @@ import { Left, Right } from '@nutui/icons-vue-taro'
126 import PosterBuilder from '@/components/PosterBuilder/index.vue' 146 import PosterBuilder from '@/components/PosterBuilder/index.vue'
127 import BASE_URL from '@/utils/config' 147 import BASE_URL from '@/utils/config'
128 // 导入获取海报详情的API 148 // 导入获取海报详情的API
129 -import { getPosterDetailAPI } from '@/api/map' 149 +import { getPosterDetailAPI, savePosterBackgroundAPI } from '@/api/map'
130 // 默认背景图 150 // 默认背景图
131 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' 151 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'
132 152
...@@ -145,86 +165,158 @@ const pageParams = ref({ ...@@ -145,86 +165,158 @@ const pageParams = ref({
145 id: '', 165 id: '',
146 }) 166 })
147 167
168 +// API数据状态
169 +const apiData = ref(null)
170 +const isLoading = ref(false)
171 +const apiError = ref(null)
172 +
148 // TODO: 有真实数据的时候, 通过ID获取具体地点的海报信息, 赋值currentPosterIndex获取特定的海报 173 // TODO: 有真实数据的时候, 通过ID获取具体地点的海报信息, 赋值currentPosterIndex获取特定的海报
149 174
150 // 图片预览相关 175 // 图片预览相关
151 const previewVisible = ref(false) 176 const previewVisible = ref(false)
152 const previewImages = ref([]) 177 const previewImages = ref([])
153 178
154 -// 活动信息数据 - 模拟不同状态,实际使用时从API获取 179 +/**
155 -const activityInfo = ref({ 180 + * 获取海报详情数据
156 - title: '南京路乐龄时尚消费主题路线', 181 + */
157 - checkPoints: [ 182 +const fetchPosterDetail = async () => {
158 - { id: 1, name: '起点签到', completed: true }, 183 + try {
159 - { id: 2, name: '商圈探索', completed: true }, 184 + isLoading.value = true
160 - { id: 3, name: '文化体验', completed: true }, 185 + apiError.value = null
161 - { id: 4, name: '美食品鉴', completed: false },
162 - { id: 5, name: '终点打卡', completed: false }
163 - ],
164 - completedCount: 3,
165 - totalCount: 5,
166 - endDate: '2025年9月7日'
167 -})
168 186
169 -// activityInfo.value = {}; 187 + const response = await getPosterDetailAPI({})
170 188
171 -// 海报数据列表 - 合并海报基础信息和内容数据,实际使用时从API获取 189 + if (response.code === 1) {
172 -const posterList = ref([ 190 + apiData.value = response.data
173 - { 191 + console.log('获取海报详情成功:', response.data)
174 - id: 1, 192 +
175 - title: '起点签到', 193 + // 根据API数据更新活动信息
176 - path: '', 194 + updateActivityInfo()
177 - checkPointId: 1, 195 +
178 - 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', 196 + // 根据API数据更新海报列表
179 - // 海报内容数据 197 + updatePosterList()
180 - user: { 198 +
181 - avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png', 199 + // 根据pageParams.id设置当前海报索引
182 - nickname: '张大爷' 200 + setCurrentPosterIndex()
183 - }, 201 +
184 - family: { 202 + } else {
185 - name: '幸福之家', 203 + apiError.value = response.msg || '获取海报详情失败'
186 - description: '一家人整整齐齐最重要' 204 + console.error('获取海报详情失败:', response.msg)
187 - }, 205 + }
188 - activity: { 206 + } catch (error) {
189 - logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png', 207 + apiError.value = '网络请求失败'
190 - name: '南京路时尚citywalk' 208 + console.error('获取海报详情异常:', error)
191 - }, 209 + } finally {
192 - level: { 210 + isLoading.value = false
193 - logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png', 211 + }
194 - name: '卡点: 上海市第一百货商店' 212 +}
195 - }, 213 +
196 - qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png', 214 +/**
197 - qrcodeDesc: '长按识别,来,我们一起打卡!' 215 + * 根据API数据更新活动信息
198 - }, 216 + */
199 - { 217 +const updateActivityInfo = () => {
200 - id: 2, 218 + if (!apiData.value) return
201 - title: '商圈探索', 219 +
220 + const { title, end_date, details, show_detail_index } = apiData.value
221 +
222 + // 转换details为checkPoints格式
223 + const checkPoints = details.map((detail, index) => ({
224 + id: detail.id,
225 + name: detail.name,
226 + completed: detail.is_checked === true
227 + }))
228 +
229 + // 计算已完成数量
230 + const completedCount = checkPoints.filter(point => point.completed).length
231 +
232 + activityInfo.value = {
233 + title: title || '海报打卡活动',
234 + checkPoints,
235 + completedCount,
236 + totalCount: checkPoints.length,
237 + endDate: end_date || '',
238 + showDetailIndex: show_detail_index || 0
239 + }
240 +}
241 +
242 +/**
243 + * 根据API数据更新海报列表
244 + */
245 +const updatePosterList = () => {
246 + if (!apiData.value) return
247 +
248 + const { details, family, qrcode_url } = apiData.value
249 +
250 + // 只显示is_checked为真的关卡
251 + const checkedDetails = details.filter(detail =>
252 + detail.is_checked === true
253 + )
254 +
255 + posterList.value = checkedDetails.map((detail, index) => ({
256 + id: detail.id,
257 + title: detail.name,
202 path: '', 258 path: '',
203 - checkPointId: 2, 259 + checkPointId: detail.id,
204 - backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%B3%E5%8D%A12-%E5%9B%BE%E5%B1%82%201.png', 260 + backgroundImage: detail.background_url || defaultBackground,
205 // 海报内容数据 261 // 海报内容数据
206 user: { 262 user: {
207 - avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png', 263 + avatar: family?.avatar_url || 'https://cdn.ipadbiz.cn/icon/tou@2x.png', // 默认头像,后续可从用户信息获取
208 - nickname: '李奶奶' 264 + nickname: '用户昵称' // 默认昵称,后续可从用户信息获取
209 }, 265 },
210 family: { 266 family: {
211 - name: '温馨小家', 267 + name: family?.name || '我的家庭',
212 - description: '健康快乐每一天' 268 + description: ''
213 }, 269 },
214 activity: { 270 activity: {
215 - logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png', 271 + 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',
216 - name: '南京路时尚citywalk'
217 }, 272 },
218 level: { 273 level: {
219 - 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', 274 + 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',
220 - name: '第二关卡' 275 + name: detail.name || '海报打卡活动',
221 }, 276 },
222 - qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png', 277 + qrcode: qrcode_url,
223 qrcodeDesc: '长按识别,来,我们一起打卡!' 278 qrcodeDesc: '长按识别,来,我们一起打卡!'
224 - }, 279 + }))
225 -]) 280 +}
281 +
282 +/**
283 + * 根据pageParams.id设置当前海报索引
284 + */
285 +const setCurrentPosterIndex = () => {
286 + if (!posterList.value.length) {
287 + currentPosterIndex.value = 0
288 + return
289 + }
290 +
291 + // 如果有指定的ID,查找对应的海报索引
292 + if (pageParams.value.id) {
293 + const targetIndex = posterList.value.findIndex(poster =>
294 + poster.checkPointId.toString() === pageParams.value.id.toString()
295 + )
296 +
297 + if (targetIndex !== -1) {
298 + currentPosterIndex.value = targetIndex
299 + return
300 + }
301 + }
302 +
303 + // 如果没有ID或没找到对应的关卡ID,默认显示第一个已打卡的项(索引0)
304 + // 因为posterList已经过滤了is_checked=true的项,所以第一个就是我们要的
305 + currentPosterIndex.value = 0
306 +}
226 307
227 -// posterList.value = [] 308 +// 活动信息数据 - 将由API数据填充
309 +const activityInfo = ref({
310 + title: '',
311 + checkPoints: [],
312 + completedCount: 0,
313 + totalCount: 0,
314 + endDate: '',
315 + showDetailIndex: 0
316 +})
317 +
318 +// 海报数据列表 - 将由API数据填充
319 +const posterList = ref([])
228 320
229 // 数据状态检查 321 // 数据状态检查
230 const hasActivityInfo = computed(() => { 322 const hasActivityInfo = computed(() => {
...@@ -416,7 +508,7 @@ const posterConfig = computed(() => { ...@@ -416,7 +508,7 @@ const posterConfig = computed(() => {
416 /** 508 /**
417 * 页面加载时初始化 509 * 页面加载时初始化
418 */ 510 */
419 -onMounted(() => { 511 +onMounted(async () => {
420 Taro.setNavigationBarTitle({ title: '海报打卡' }) 512 Taro.setNavigationBarTitle({ title: '海报打卡' })
421 513
422 // 获取页面参数 514 // 获取页面参数
...@@ -429,6 +521,16 @@ onMounted(() => { ...@@ -429,6 +521,16 @@ onMounted(() => {
429 521
430 console.log('海报打卡页面接收到的参数:', pageParams.value) 522 console.log('海报打卡页面接收到的参数:', pageParams.value)
431 523
524 + // 获取海报详情数据
525 + await fetchPosterDetail()
526 +
527 + // 数据获取完成后检查页面状态
528 + if (pageState.value === 'no-activity') {
529 + // 没有活动信息,显示确认对话框
530 + showNoActivityConfirm()
531 + return
532 + }
533 +
432 // 页面加载时检查是否需要生成当前海报 534 // 页面加载时检查是否需要生成当前海报
433 generateCurrentPosterIfNeeded() 535 generateCurrentPosterIfNeeded()
434 }) 536 })
...@@ -576,9 +678,10 @@ const chooseBackgroundImage = () => { ...@@ -576,9 +678,10 @@ const chooseBackgroundImage = () => {
576 678
577 uploadBackgroundImage(tempFile.path) 679 uploadBackgroundImage(tempFile.path)
578 }, 680 },
579 - fail: () => { 681 + fail: (error) => {
682 + console.error('选择图片失败:', error)
580 // Taro.showToast({ 683 // Taro.showToast({
581 - // title: '选择图片失败', 684 + // title: '选择图片失败,请重试',
582 // icon: 'none' 685 // icon: 'none'
583 // }) 686 // })
584 } 687 }
...@@ -595,15 +698,33 @@ const uploadBackgroundImage = (filePath) => { ...@@ -595,15 +698,33 @@ const uploadBackgroundImage = (filePath) => {
595 url: BASE_URL + '/admin/?m=srv&a=upload', 698 url: BASE_URL + '/admin/?m=srv&a=upload',
596 filePath, 699 filePath,
597 name: 'file', 700 name: 'file',
598 - success: (uploadRes) => { 701 + success: async (uploadRes) => {
599 - Taro.hideLoading()
600 const data = JSON.parse(uploadRes.data) 702 const data = JSON.parse(uploadRes.data)
601 if (data.code === 0 && data.data) { 703 if (data.code === 0 && data.data) {
602 const currentIndex = currentPosterIndex.value 704 const currentIndex = currentPosterIndex.value
705 + const currentPosterData = posterList.value[currentIndex]
603 706
604 // 为当前海报设置背景图 707 // 为当前海报设置背景图
605 backgroundImages.value[currentIndex] = data.data.src 708 backgroundImages.value[currentIndex] = data.data.src
606 709
710 + // 调用保存海报背景接口
711 + try {
712 + const saveResult = await savePosterBackgroundAPI({
713 + detail_id: currentPosterData.checkPointId,
714 + poster_background_url: data.data.src
715 + })
716 +
717 + if (saveResult.code === 1) {
718 + console.log('海报背景保存成功:', saveResult.data)
719 + } else {
720 + console.warn('海报背景保存失败:', saveResult.msg)
721 + // 即使保存失败,也继续生成海报,不影响用户体验
722 + }
723 + } catch (error) {
724 + console.error('保存海报背景异常:', error)
725 + // 即使保存失败,也继续生成海报,不影响用户体验
726 + }
727 +
607 // 强制标记当前海报需要重新生成 728 // 强制标记当前海报需要重新生成
608 posterGeneratedFlags.value[currentIndex] = false 729 posterGeneratedFlags.value[currentIndex] = false
609 delete posterConfigHashes.value[currentIndex] 730 delete posterConfigHashes.value[currentIndex]
...@@ -614,12 +735,15 @@ const uploadBackgroundImage = (filePath) => { ...@@ -614,12 +735,15 @@ const uploadBackgroundImage = (filePath) => {
614 // 立即重新生成海报 735 // 立即重新生成海报
615 generateCurrentPoster() 736 generateCurrentPoster()
616 737
738 + Taro.hideLoading()
617 Taro.showToast({ title: '上传成功', icon: 'success' }) 739 Taro.showToast({ title: '上传成功', icon: 'success' })
618 } else { 740 } else {
741 + Taro.hideLoading()
619 Taro.showToast({ title: data.msg || '上传失败', icon: 'none' }) 742 Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
620 } 743 }
621 }, 744 },
622 - fail: () => { 745 + fail: (error) => {
746 + console.error('上传文件失败:', error)
623 Taro.hideLoading() 747 Taro.hideLoading()
624 Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' }) 748 Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
625 } 749 }
...@@ -755,22 +879,7 @@ const showNoActivityConfirm = () => { ...@@ -755,22 +879,7 @@ const showNoActivityConfirm = () => {
755 }) 879 })
756 } 880 }
757 881
758 -/**
759 - * 页面初始化
760 - */
761 -onMounted(() => {
762 - // 获取页面参数
763 - const instance = Taro.getCurrentInstance()
764 - if (instance.router && instance.router.params) {
765 - pageParams.value = instance.router.params
766 - }
767 882
768 - // 检查页面状态
769 - if (pageState.value === 'no-activity') {
770 - // 没有活动信息,显示确认对话框
771 - showNoActivityConfirm()
772 - }
773 -})
774 </script> 883 </script>
775 884
776 <style scoped> 885 <style scoped>
......