hookehuyr

feat(PosterCheckinDetail): 创建海报打卡详情页面

- 基于 PosterCheckin 创建新页面
- 完全使用 map_activity.js 新接口(getPosterDetailAPI)
- 支持从活动页面分享海报跳转

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -167,7 +167,7 @@ import { Left, Right } from '@nutui/icons-vue-taro' ...@@ -167,7 +167,7 @@ import { Left, Right } from '@nutui/icons-vue-taro'
167 import PosterBuilder from '@/components/PosterBuilder/index.vue' 167 import PosterBuilder from '@/components/PosterBuilder/index.vue'
168 import BASE_URL from '@/utils/config' 168 import BASE_URL from '@/utils/config'
169 // 导入获取海报详情的API 169 // 导入获取海报详情的API
170 -import { getPosterDetailAPI, savePosterBackgroundAPI } from '@/api/map_activity' 170 +import { getPosterDetailAPI, savePosterBackgroundAPI } from '@/api/map'
171 // 默认背景图 171 // 默认背景图
172 const defaultBackground = 172 const defaultBackground =
173 '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?imageMogr2/strip/quality/60' 173 '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?imageMogr2/strip/quality/60'
......
1 +/*
2 + * @Date: 2026-02-09
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-02-09
5 + * @FilePath: /lls_program/src/pages/PosterCheckinDetail/index.config.js
6 + * @Description: 海报打卡详情页面配置 - 完全使用 map_activity.js 新接口
7 + */
8 +export default {
9 + navigationBarTitleText: '海报打卡详情',
10 + usingComponents: {},
11 +}
1 +<template>
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>
22 + <!-- 活动信息区域 -->
23 + <view
24 + v-if="pageState === 'normal' || pageState === 'no-checkin'"
25 + class="bg-white mx-4 mt-4 mb-2 rounded-lg shadow-sm p-4"
26 + >
27 + <!-- 活动主题 -->
28 + <view class="text-lg font-bold text-gray-800 mb-2">
29 + {{ activityInfo.title }}
30 + </view>
31 +
32 + <!-- 打卡进度 -->
33 + <view class="flex items-center mb-2">
34 + <view class="flex items-center">
35 + <view
36 + v-for="(point, index) in activityInfo.checkPoints"
37 + :key="index"
38 + class="w-2 h-2 rounded-full mr-2 flex items-center justify-center"
39 + :class="point.completed ? 'bg-orange-400' : 'bg-gray-300'"
40 + >
41 + </view>
42 + </view>
43 + <text class="text-xs text-gray-600"
44 + >{{ activityInfo.completedCount }}/{{ activityInfo.totalCount }}</text
45 + >
46 + </view>
47 +
48 + <!-- 活动截止日期 -->
49 + <view class="text-sm text-gray-500"> 活动截止日期:{{ activityInfo.endDate }} </view>
50 + </view>
51 +
52 + <!-- 海报预览区域 - 正常状态 -->
53 + <view
54 + v-if="pageState === 'normal'"
55 + class="flex-1 mx-4 relative"
56 + style="overflow: visible; padding-bottom: 110rpx"
57 + >
58 + <view class="h-full relative flex items-center justify-center">
59 + <view v-if="currentPoster.path" class="w-full h-full relative">
60 + <image :src="currentPoster.path" mode="widthFix" class="w-full h-full" />
61 + <!-- 打卡点标题 -->
62 + <!-- <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
63 + {{ posterList[currentPosterIndex]?.title || '海报生成中' }}
64 + </view> -->
65 + <!-- 点击预览提示 -->
66 + <!-- <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
67 + 点击预览
68 + </view> -->
69 + </view>
70 + </view>
71 +
72 + <!-- 左箭头按钮 -->
73 + <view
74 + 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"
75 + :class="
76 + currentPosterIndex > 0
77 + ? 'bg-orange-400 text-white shadow-lg'
78 + : 'bg-gray-300 text-gray-500'
79 + "
80 + style="left: -20rpx"
81 + @tap="previousPoster"
82 + >
83 + <Left size="16"></Left>
84 + </view>
85 +
86 + <!-- 右箭头按钮 -->
87 + <view
88 + 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"
89 + :class="
90 + currentPosterIndex < posterList.length - 1
91 + ? 'bg-orange-400 text-white shadow-lg'
92 + : 'bg-gray-300 text-gray-500'
93 + "
94 + style="right: -20rpx"
95 + @tap="nextPoster"
96 + >
97 + <Right size="16"></Right>
98 + </view>
99 + </view>
100 +
101 + <!-- 没有打卡信息的空状态 -->
102 + <view v-if="pageState === 'no-checkin'" class="flex-1 mx-4 mb-2 flex justify-center">
103 + <view class="rounded-lg shadow-sm p-8 text-center max-w-sm">
104 + <view class="text-6xl mb-4">📸</view>
105 + <view class="text-lg font-bold text-gray-800 mb-2">您还没有打卡记录</view>
106 + <view class="text-sm text-orange-500 mb-4">完成打卡后即可生成专属海报</view>
107 + </view>
108 + </view>
109 +
110 + <!-- 底部操作按钮 - 仅在正常状态显示 -->
111 + <view
112 + v-if="pageState === 'normal'"
113 + class="bg-white border-t border-gray-200 p-4 safe-area-bottom z-50"
114 + style="position: fixed; bottom: 0; left: 0; right: 0"
115 + >
116 + <view class="flex gap-4">
117 + <view
118 + class="flex-1 bg-gradient-to-r from-orange-400 to-orange-500 text-white text-sm py-3 px-6 rounded-lg font-medium shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
119 + @click="chooseBackgroundImage"
120 + >
121 + <text>{{ currentPosterHasCustomBackground ? '更改照片' : '上传照片' }}</text>
122 + </view>
123 + <view
124 + class="flex-1 text-white py-3 px-6 rounded-lg font-medium text-sm shadow-lg active:scale-95 transition-transform duration-150 flex items-center justify-center gap-2"
125 + :class="
126 + posterPath
127 + ? 'bg-gradient-to-r from-green-400 to-green-500'
128 + : posterGenerateFailed
129 + ? 'bg-gradient-to-r from-orange-400 to-orange-500'
130 + : 'bg-gray-400'
131 + "
132 + @click="handlePosterAction"
133 + :disabled="!posterPath && !posterGenerateFailed"
134 + >
135 + <text>{{
136 + posterPath ? '保存海报' : posterGenerateFailed ? '重新生成' : '生成中...'
137 + }}</text>
138 + </view>
139 + </view>
140 + </view>
141 + </template>
142 +
143 + <!-- 海报生成组件 - 仅在正常状态显示 -->
144 + <PosterBuilder
145 + v-if="shouldGeneratePoster && pageState === 'normal'"
146 + :config="posterConfig"
147 + :show-loading="true"
148 + @success="onPosterSuccess"
149 + @fail="onPosterFail"
150 + />
151 +
152 + <!-- 图片预览 -->
153 + <nut-image-preview
154 + v-model:show="previewVisible"
155 + :images="previewImages"
156 + :init-no="0"
157 + :show-index="false"
158 + @close="closePreview"
159 + />
160 + </view>
161 +</template>
162 +
163 +<script setup>
164 +import { ref, onMounted, computed, watch } from 'vue'
165 +import Taro from '@tarojs/taro'
166 +import { Left, Right } from '@nutui/icons-vue-taro'
167 +import PosterBuilder from '@/components/PosterBuilder/index.vue'
168 +import BASE_URL from '@/utils/config'
169 +// 导入获取海报详情的API
170 +import { getPosterDetailAPI, savePosterBackgroundAPI } from '@/api/map_activity'
171 +// 默认背景图
172 +const defaultBackground =
173 + '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?imageMogr2/strip/quality/60'
174 +
175 +// 页面状态
176 +const posterPath = ref('') // 生成的海报路径
177 +const backgroundImages = ref({}) // 每个海报的用户上传背景图 {posterIndex: imageUrl}
178 +const shouldGeneratePoster = ref(false) // 是否应该生成海报
179 +const currentPosterIndex = ref(0) // 当前显示的海报索引
180 +const posterGenerateFailed = ref(false) // 海报生成是否失败
181 +const posterGeneratedFlags = ref({}) // 每个海报的生成状态 {posterIndex: boolean}
182 +const posterConfigHashes = ref({}) // 每个海报的配置哈希值,用于检测配置变化
183 +
184 +// 页面参数
185 +const pageParams = ref({
186 + from: '',
187 + id: '',
188 +})
189 +
190 +// API数据状态
191 +const apiData = ref(null)
192 +const isLoading = ref(false)
193 +const apiError = ref(null)
194 +
195 +// 图片预览相关
196 +const previewVisible = ref(false)
197 +const previewImages = ref([])
198 +
199 +/**
200 + * 获取海报详情数据
201 + */
202 +const fetchPosterDetail = async () => {
203 + try {
204 + isLoading.value = true
205 + apiError.value = null
206 +
207 + const accountInfo = wx.getAccountInfoSync()
208 + const envVersion = accountInfo.miniProgram.envVersion
209 + // 小程序版本。正式版为 "release",体验版为 "trial"。默认是正式版
210 + const env_version = envVersion === 'release' ? 'release' : 'trial'
211 +
212 + const response = await getPosterDetailAPI({ env_version })
213 +
214 + if (response.code === 1) {
215 + apiData.value = response.data
216 + console.log('获取海报详情成功:', response.data)
217 +
218 + // 根据API数据更新活动信息
219 + updateActivityInfo()
220 +
221 + // 根据API数据更新海报列表
222 + updatePosterList()
223 +
224 + // 根据pageParams.id设置当前海报索引
225 + setCurrentPosterIndex()
226 + } else {
227 + apiError.value = response.msg || '获取海报详情失败'
228 + console.error('获取海报详情失败:', response.msg)
229 + }
230 + } catch (error) {
231 + apiError.value = '网络请求失败'
232 + console.error('获取海报详情异常:', error)
233 + } finally {
234 + isLoading.value = false
235 + }
236 +}
237 +
238 +/**
239 + * 根据API数据更新活动信息
240 + */
241 +const updateActivityInfo = () => {
242 + if (!apiData.value) {
243 + return
244 + }
245 +
246 + const { title, end_date, details, show_detail_index } = apiData.value
247 +
248 + // 转换details为checkPoints格式
249 + const checkPoints = details.map((detail, index) => ({
250 + id: detail.id,
251 + name: detail.name,
252 + completed: detail.is_checked === true,
253 + }))
254 +
255 + // 计算已完成数量
256 + const completedCount = checkPoints.filter(point => point.completed).length
257 +
258 + activityInfo.value = {
259 + title: title || '海报打卡活动',
260 + checkPoints,
261 + completedCount,
262 + totalCount: checkPoints.length,
263 + endDate: end_date || '',
264 + showDetailIndex: show_detail_index || 0,
265 + }
266 +}
267 +
268 +/**
269 + * 根据API数据更新海报列表
270 + */
271 +const updatePosterList = () => {
272 + if (!apiData.value) {
273 + return
274 + }
275 +
276 + const { details, family, qrcode_url } = apiData.value
277 +
278 + // 只显示is_checked为真的关卡
279 + const checkedDetails = details.filter(detail => detail.is_checked === true)
280 +
281 + posterList.value = checkedDetails.map((detail, index) => ({
282 + id: detail.id,
283 + title: detail.name,
284 + path: '',
285 + checkPointId: detail.id,
286 + backgroundImage: detail.background_url || defaultBackground,
287 + // 海报内容数据
288 + user: {
289 + avatar:
290 + family?.avatar_url ||
291 + 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60',
292 + nickname: '用户昵称', // 默认昵称,后续可从用户信息获取
293 + },
294 + family: {
295 + name: family?.name || '我的家庭',
296 + description: '',
297 + },
298 + activity: {
299 + logo:
300 + detail.main_slogan ||
301 + 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png?imageMogr2/strip/quality/60',
302 + },
303 + level: {
304 + logo:
305 + detail.sub_slogan ||
306 + 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png?imageMogr2/strip/quality/60',
307 + name: detail.name || '海报打卡活动',
308 + },
309 + qrcode: qrcode_url,
310 + qrcodeDesc: '长按识别,来,我们一起打卡!',
311 + }))
312 +}
313 +
314 +/**
315 + * 根据pageParams.id设置当前海报索引
316 + */
317 +const setCurrentPosterIndex = () => {
318 + if (!posterList.value.length) {
319 + currentPosterIndex.value = 0
320 + return
321 + }
322 +
323 + // 如果有指定的ID,查找对应的海报索引
324 + if (pageParams.value.id) {
325 + const targetIndex = posterList.value.findIndex(
326 + poster => poster.checkPointId.toString() === pageParams.value.id.toString()
327 + )
328 +
329 + if (targetIndex !== -1) {
330 + currentPosterIndex.value = targetIndex
331 + return
332 + }
333 + }
334 +
335 + // 如果没有ID或没找到对应的关卡ID,默认显示第一个已打卡的项(索引0)
336 + // 因为posterList已经过滤了is_checked=true的项,所以第一个就是我们要的
337 + currentPosterIndex.value = 0
338 +}
339 +
340 +// 活动信息数据 - 将由API数据填充
341 +const activityInfo = ref({
342 + title: '',
343 + checkPoints: [],
344 + completedCount: 0,
345 + totalCount: 0,
346 + endDate: '',
347 + showDetailIndex: 0,
348 +})
349 +
350 +// 海报数据列表 - 将由API数据填充
351 +const posterList = ref([])
352 +
353 +// 数据状态检查
354 +const hasActivityInfo = computed(() => {
355 + return (
356 + activityInfo.value &&
357 + activityInfo.value.title &&
358 + activityInfo.value.checkPoints &&
359 + activityInfo.value.checkPoints.length > 0
360 + )
361 +})
362 +
363 +const hasCheckinInfo = computed(() => {
364 + return posterList.value && posterList.value.length > 0
365 +})
366 +
367 +// 页面显示状态
368 +const pageState = computed(() => {
369 + if (!hasActivityInfo.value) {
370 + return 'no-activity' // 没有活动信息
371 + }
372 + if (!hasCheckinInfo.value) {
373 + return 'no-checkin' // 有活动信息但没有打卡信息
374 + }
375 + return 'normal' // 正常状态
376 +})
377 +
378 +// 当前海报
379 +const currentPoster = computed(() => {
380 + return posterList.value[currentPosterIndex.value] || { path: '', title: '' }
381 +})
382 +
383 +// 当前海报的内容数据
384 +const currentMockData = computed(() => {
385 + const currentPoster = posterList.value[currentPosterIndex.value]
386 + if (!currentPoster) {
387 + return null
388 + }
389 +
390 + return {
391 + user: currentPoster.user,
392 + family: currentPoster.family,
393 + activity: currentPoster.activity,
394 + level: currentPoster.level,
395 + qrcode: currentPoster.qrcode,
396 + qrcodeDesc: currentPoster.qrcodeDesc,
397 + }
398 +})
399 +
400 +// 当前海报是否有用户上传的背景图
401 +const currentPosterHasCustomBackground = computed(() => {
402 + return !!backgroundImages.value[currentPosterIndex.value]
403 +})
404 +
405 +// 海报配置
406 +const posterConfig = computed(() => {
407 + const currentPosterData = posterList.value[currentPosterIndex.value]
408 + const bgImage =
409 + backgroundImages.value[currentPosterIndex.value] ||
410 + currentPosterData?.backgroundImage ||
411 + defaultBackground
412 +
413 + return {
414 + width: 600, // 从750减少到600,减小Canvas尺寸以控制文件大小
415 + height: 1067, // 按比例调整高度 (1334 * 600/750 = 1067)
416 + backgroundColor: '#f5f5f5',
417 + debug: false,
418 + borderRadius: 12, // 按比例调整圆角 (15 * 600/750 = 12)
419 + images: [
420 + // 背景图
421 + {
422 + x: 0,
423 + y: 0,
424 + width: 600, // 按比例调整 (750 * 0.8 = 600)
425 + height: 880, // 按比例调整 (1100 * 0.8 = 880)
426 + url: bgImage,
427 + zIndex: 0,
428 + },
429 + // 用户头像
430 + {
431 + x: 32, // 按比例调整 (40 * 0.8 = 32)
432 + y: 32, // 按比例调整 (40 * 0.8 = 32)
433 + width: 104, // 按比例调整 (130 * 0.8 = 104)
434 + height: 104, // 按比例调整 (130 * 0.8 = 104)
435 + url: currentMockData.value.user.avatar,
436 + borderRadius: 52, // 按比例调整 (65 * 0.8 = 52)
437 + zIndex: 2,
438 + },
439 + // 活动logo
440 + {
441 + x: 360, // 按比例调整 (450 * 0.8 = 360)
442 + y: 32, // 按比例调整 (40 * 0.8 = 32)
443 + width: 200, // 按比例调整 (250 * 0.8 = 200)
444 + height: 64, // 按比例调整 (80 * 0.8 = 64)
445 + url: currentMockData.value.activity.logo,
446 + zIndex: 2,
447 + },
448 + // 关卡徽章
449 + {
450 + x: 8, // 按比例调整 (10 * 0.8 = 8)
451 + y: 704, // 按比例调整 (880 * 0.8 = 704)
452 + width: 304, // 按比例调整 (380 * 0.8 = 304)
453 + height: 80, // 按比例调整 (100 * 0.8 = 80)
454 + url: currentMockData.value.level.logo,
455 + zIndex: 2,
456 + },
457 + // 小程序码
458 + {
459 + x: 24, // 按比例调整 (30 * 0.8 = 24)
460 + y: 880, // 按比例调整 (1100 * 0.8 = 880)
461 + width: 144, // 按比例调整 (180 * 0.8 = 144)
462 + height: 144, // 按比例调整 (180 * 0.8 = 144)
463 + url: currentMockData.value.qrcode,
464 + zIndex: 1,
465 + },
466 + ],
467 + texts: [
468 + // 家庭名称
469 + {
470 + x: 32, // 按比例调整 (40 * 0.8 = 32)
471 + y: 152, // 按比例调整 (190 * 0.8 = 152)
472 + text: currentMockData.value.family.name,
473 + fontSize: 29, // 按比例调整 (36 * 0.8 = 29)
474 + color: '#ffffff',
475 + fontWeight: 'bold',
476 + textAlign: 'left',
477 + shadowColor: 'rgba(0, 0, 0, 0.6)',
478 + shadowOffsetX: 2,
479 + shadowOffsetY: 2,
480 + shadowBlur: 4,
481 + zIndex: 2,
482 + },
483 + // 家庭描述
484 + // {
485 + // x: 32, // 按比例调整 (40 * 0.8 = 32)
486 + // y: 200, // 按比例调整 (250 * 0.8 = 200)
487 + // text: currentMockData.value.family.description,
488 + // fontSize: 22, // 按比例调整 (28 * 0.8 = 22)
489 + // color: '#ffffff',
490 + // textAlign: 'left',
491 + // shadowColor: 'rgba(0, 0, 0, 0.6)',
492 + // shadowOffsetX: 2,
493 + // shadowOffsetY: 2,
494 + // shadowBlur: 4,
495 + // zIndex: 2
496 + // },
497 + // 小程序码描述
498 + {
499 + x: 208, // 按比例调整 (260 * 0.8 = 208)
500 + y: 900, // 按比例调整 (1125 * 0.8 = 900)
501 + text: currentMockData.value.qrcodeDesc,
502 + fontSize: 24, // 按比例调整 (30 * 0.8 = 24)
503 + color: '#333333',
504 + lineHeight: 32, // 按比例调整 (40 * 0.8 = 32)
505 + lineNum: 2,
506 + width: 352, // 按比例调整 (440 * 0.8 = 352)
507 + textAlign: 'left',
508 + zIndex: 1,
509 + },
510 + // 关卡描述
511 + {
512 + x: 208, // 按比例调整 (260 * 0.8 = 208)
513 + y: 944, // 按比例调整 (1180 * 0.8 = 944)
514 + text: '打卡点: ' + currentMockData.value.level.name,
515 + fontSize: 26, // 按比例调整 (32 * 0.8 = 26)
516 + color: '#333333',
517 + lineHeight: 40, // 按比例调整 (50 * 0.8 = 40)
518 + lineNum: 2,
519 + width: 352, // 按比例调整 (440 * 0.8 = 352)
520 + textAlign: 'left',
521 + zIndex: 1,
522 + },
523 + ],
524 + blocks: [
525 + // 下半部分白色背景
526 + {
527 + x: 0,
528 + y: 840, // 按比例调整 (1050 * 0.8 = 840)
529 + width: 600, // 按比例调整 (750 * 0.8 = 600)
530 + height: 427, // 按比例调整 (534 * 0.8 = 427)
531 + backgroundColor: '#ffffff',
532 + zIndex: 0,
533 + },
534 + // 用户信息背景遮罩
535 + // {
536 + // x: 24, // 按比例调整 (30 * 0.8 = 24)
537 + // y: 144, // 按比例调整 (180 * 0.8 = 144)
538 + // width: 360, // 按比例调整 (450 * 0.8 = 360)
539 + // height: 112, // 按比例调整 (140 * 0.8 = 112)
540 + // backgroundColor: 'rgba(0,0,0,0.3)',
541 + // borderRadius: 8, // 按比例调整 (10 * 0.8 = 8)
542 + // zIndex: 1
543 + // }
544 + ],
545 + }
546 +})
547 +
548 +/**
549 + * 页面加载时初始化
550 + */
551 +onMounted(async () => {
552 + Taro.setNavigationBarTitle({ title: '海报打卡' })
553 +
554 + // 获取页面参数
555 + const instance = Taro.getCurrentInstance()
556 + const params = instance.router?.params || {}
557 +
558 + pageParams.value = {
559 + id: params.id || '',
560 + }
561 +
562 + console.log('海报打卡页面接收到的参数:', pageParams.value)
563 +
564 + // 获取海报详情数据
565 + await fetchPosterDetail()
566 +
567 + // 数据获取完成后检查页面状态
568 + if (pageState.value === 'no-activity') {
569 + // 没有活动信息,显示确认对话框
570 + showNoActivityConfirm()
571 + return
572 + }
573 +
574 + // 页面加载时检查是否需要生成当前海报
575 + generateCurrentPosterIfNeeded()
576 +})
577 +
578 +/**
579 + * 监听背景图变化,重新生成海报
580 + */
581 +watch(
582 + backgroundImages,
583 + (newVal, oldVal) => {
584 + // 只有当前海报的背景图发生变化时才重新生成
585 + const currentIndex = currentPosterIndex.value
586 + const newBgImage = newVal[currentIndex]
587 + const oldBgImage = oldVal?.[currentIndex]
588 +
589 + if (newBgImage !== oldBgImage) {
590 + console.log('背景图发生变化:', { currentIndex, newBgImage, oldBgImage })
591 +
592 + // 标记当前海报需要重新生成
593 + posterGeneratedFlags.value[currentIndex] = false
594 + delete posterConfigHashes.value[currentIndex]
595 +
596 + // 清除当前海报路径
597 + posterPath.value = ''
598 +
599 + // 重新生成海报
600 + generateCurrentPoster()
601 + }
602 + },
603 + { deep: true }
604 +)
605 +
606 +/**
607 + * 监听当前海报索引变化,切换海报
608 + */
609 +watch(currentPosterIndex, (newIndex, oldIndex) => {
610 + // 切换海报时,检查新海报是否已生成
611 + if (newIndex !== oldIndex) {
612 + generateCurrentPosterIfNeeded()
613 + }
614 +})
615 +
616 +/**
617 + * 生成当前海报配置的哈希值
618 + */
619 +const generateConfigHash = config => {
620 + const configStr = JSON.stringify({
621 + backgroundImage: backgroundImages.value[currentPosterIndex.value],
622 + posterIndex: currentPosterIndex.value,
623 + mockData: currentMockData.value,
624 + })
625 + // 简单哈希函数
626 + let hash = 0
627 + for (let i = 0; i < configStr.length; i++) {
628 + const char = configStr.charCodeAt(i)
629 + hash = (hash << 5) - hash + char
630 + hash = hash & hash // 转换为32位整数
631 + }
632 + return hash.toString()
633 +}
634 +
635 +/**
636 + * 检查是否需要生成当前海报
637 + */
638 +const generateCurrentPosterIfNeeded = () => {
639 + const currentIndex = currentPosterIndex.value
640 + const currentHash = generateConfigHash()
641 + const isGenerated = posterGeneratedFlags.value[currentIndex]
642 + const lastHash = posterConfigHashes.value[currentIndex]
643 + const hasCustomBackground = !!backgroundImages.value[currentIndex]
644 +
645 + console.log('检查是否需要生成海报:', {
646 + currentIndex,
647 + isGenerated,
648 + hasCustomBackground,
649 + currentHash,
650 + lastHash,
651 + hashChanged: lastHash !== currentHash,
652 + })
653 +
654 + // 如果海报未生成过,或者配置发生了变化,则需要重新生成
655 + if (!isGenerated || lastHash !== currentHash) {
656 + console.log('需要重新生成海报')
657 + posterConfigHashes.value[currentIndex] = currentHash
658 + generateCurrentPoster()
659 + } else {
660 + // 海报已存在且配置未变化,直接使用缓存的海报
661 + const cachedPoster = posterList.value[currentIndex]
662 + if (cachedPoster && cachedPoster.path) {
663 + console.log('使用缓存的海报:', cachedPoster.path)
664 + posterPath.value = cachedPoster.path
665 + posterGenerateFailed.value = false
666 + } else {
667 + console.log('缓存的海报路径不存在,重新生成')
668 + generateCurrentPoster()
669 + }
670 + }
671 +}
672 +
673 +/**
674 + * 生成当前海报
675 + */
676 +const generateCurrentPoster = () => {
677 + posterPath.value = ''
678 + posterGenerateFailed.value = false
679 + shouldGeneratePoster.value = false
680 +
681 + // 延迟触发生成,确保配置更新
682 + setTimeout(() => {
683 + shouldGeneratePoster.value = true
684 + }, 100)
685 +}
686 +
687 +/**
688 + * 切换到上一张海报
689 + */
690 +const previousPoster = () => {
691 + if (currentPosterIndex.value > 0) {
692 + currentPosterIndex.value = currentPosterIndex.value - 1
693 + }
694 +}
695 +
696 +/**
697 + * 切换到下一张海报
698 + */
699 +const nextPoster = () => {
700 + if (currentPosterIndex.value < posterList.value.length - 1) {
701 + currentPosterIndex.value = currentPosterIndex.value + 1
702 + }
703 +}
704 +
705 +/**
706 + * 选择背景图片
707 + */
708 +const chooseBackgroundImage = () => {
709 + Taro.chooseImage({
710 + count: 1,
711 + sizeType: ['compressed'],
712 + sourceType: ['album', 'camera'],
713 + success: res => {
714 + const tempFile = res.tempFiles[0]
715 + if (tempFile.size > 5 * 1024 * 1024) {
716 + Taro.showToast({
717 + title: '图片大小不能超过5MB',
718 + icon: 'none',
719 + })
720 + return
721 + }
722 +
723 + uploadBackgroundImage(tempFile.path)
724 + },
725 + fail: error => {
726 + console.error('选择图片失败:', error)
727 + // Taro.showToast({
728 + // title: '选择图片失败,请重试',
729 + // icon: 'none'
730 + // })
731 + },
732 + })
733 +}
734 +
735 +/**
736 + * 上传背景图片
737 + */
738 +const uploadBackgroundImage = filePath => {
739 + Taro.showLoading({ title: '上传中...' })
740 +
741 + Taro.uploadFile({
742 + url: BASE_URL + '/admin/?m=srv&a=upload',
743 + filePath,
744 + name: 'file',
745 + success: async uploadRes => {
746 + const data = JSON.parse(uploadRes.data)
747 + if (data.code === 0 && data.data) {
748 + const currentIndex = currentPosterIndex.value
749 + const currentPosterData = posterList.value[currentIndex]
750 +
751 + // 为当前海报设置背景图
752 + backgroundImages.value[currentIndex] = data.data.src
753 +
754 + // 调用保存海报背景接口
755 + try {
756 + const saveResult = await savePosterBackgroundAPI({
757 + detail_id: currentPosterData.checkPointId,
758 + poster_background_url: data.data.src,
759 + })
760 +
761 + if (saveResult.code === 1) {
762 + console.log('海报背景保存成功:', saveResult.data)
763 + } else {
764 + console.warn('海报背景保存失败:', saveResult.msg)
765 + // 即使保存失败,也继续生成海报,不影响用户体验
766 + }
767 + } catch (error) {
768 + console.error('保存海报背景异常:', error)
769 + // 即使保存失败,也继续生成海报,不影响用户体验
770 + }
771 +
772 + // 强制标记当前海报需要重新生成
773 + posterGeneratedFlags.value[currentIndex] = false
774 + delete posterConfigHashes.value[currentIndex]
775 +
776 + // 清除当前海报路径,强制重新生成
777 + posterPath.value = ''
778 +
779 + // 立即重新生成海报
780 + generateCurrentPoster()
781 +
782 + Taro.hideLoading()
783 + Taro.showToast({ title: '上传成功', icon: 'success' })
784 + } else {
785 + Taro.hideLoading()
786 + Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
787 + }
788 + },
789 + fail: error => {
790 + console.error('上传文件失败:', error)
791 + Taro.hideLoading()
792 + Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
793 + },
794 + })
795 +}
796 +
797 +/**
798 + * 海报生成成功
799 + */
800 +const onPosterSuccess = result => {
801 + const currentIndex = currentPosterIndex.value
802 + posterPath.value = result.tempFilePath
803 + posterGenerateFailed.value = false
804 +
805 + // 更新当前海报的路径和生成状态
806 + if (posterList.value[currentIndex]) {
807 + posterList.value[currentIndex].path = result.tempFilePath
808 + }
809 + posterGeneratedFlags.value[currentIndex] = true
810 +
811 + // 保存当前配置的哈希值
812 + posterConfigHashes.value[currentIndex] = generateConfigHash()
813 +
814 + shouldGeneratePoster.value = false
815 +
816 + console.log('海报生成成功:', {
817 + currentIndex,
818 + posterPath: result.tempFilePath,
819 + hasCustomBackground: !!backgroundImages.value[currentIndex],
820 + })
821 +
822 + Taro.showToast({ title: '海报生成成功', icon: 'success' })
823 +}
824 +
825 +/**
826 + * 海报生成失败
827 + */
828 +const onPosterFail = error => {
829 + const currentIndex = currentPosterIndex.value
830 + shouldGeneratePoster.value = false
831 + posterGenerateFailed.value = true
832 + posterPath.value = ''
833 +
834 + // 标记当前海报生成失败,下次仍需重新生成
835 + posterGeneratedFlags.value[currentIndex] = false
836 +
837 + Taro.showToast({ title: '海报生成失败', icon: 'none' })
838 + console.error('海报生成失败:', error)
839 +}
840 +
841 +/**
842 + * 预览海报
843 + */
844 +// const previewPoster = () => {
845 +// const currentPath = currentPoster.value.path
846 +// console.warn('预览海报', currentPath)
847 +// if (!currentPath) return
848 +
849 +// previewImages.value = [{ src: currentPath }]
850 +// previewVisible.value = true
851 +// }
852 +
853 +/**
854 + * 关闭预览
855 + */
856 +const closePreview = () => {
857 + previewVisible.value = false
858 +}
859 +
860 +/**
861 + * 处理海报按钮点击事件
862 + */
863 +const handlePosterAction = () => {
864 + if (posterPath.value) {
865 + // 如果海报已生成,执行保存操作
866 + savePoster()
867 + } else if (posterGenerateFailed.value) {
868 + // 如果生成失败,重新生成海报
869 + generateCurrentPoster()
870 + }
871 +}
872 +
873 +/**
874 + * 保存海报到相册
875 + */
876 +const savePoster = () => {
877 + const currentPath = currentPoster.value.path
878 + if (!currentPath) {
879 + Taro.showToast({ title: '请等待海报生成完成', icon: 'none' })
880 + return
881 + }
882 +
883 + Taro.saveImageToPhotosAlbum({
884 + filePath: currentPath,
885 + success: () => {
886 + Taro.showToast({ title: '保存成功', icon: 'success' })
887 + },
888 + fail: err => {
889 + if (err.errMsg.includes('auth deny')) {
890 + Taro.showModal({
891 + title: '提示',
892 + content: '需要您授权保存图片到相册',
893 + showCancel: false,
894 + confirmText: '去设置',
895 + success: () => {
896 + Taro.openSetting()
897 + },
898 + })
899 + } else {
900 + Taro.showToast({ title: '保存失败', icon: 'none' })
901 + }
902 + },
903 + })
904 +}
905 +
906 +/**
907 + * 显示没有活动信息的确认对话框
908 + */
909 +const showNoActivityConfirm = () => {
910 + Taro.showModal({
911 + title: '温馨提示',
912 + content: '您还没有参加过活动,请先参加活动后再来生成海报',
913 + showCancel: false,
914 + confirmText: '知道了',
915 + success: res => {
916 + if (res.confirm) {
917 + // 返回上一页
918 + Taro.navigateBack({
919 + delta: 1,
920 + })
921 + }
922 + },
923 + })
924 +}
925 +</script>
926 +
927 +<style scoped>
928 +.safe-area-bottom {
929 + padding-bottom: env(safe-area-inset-bottom);
930 +}
931 +
932 +@keyframes spin {
933 + from {
934 + transform: rotate(0deg);
935 + }
936 + to {
937 + transform: rotate(360deg);
938 + }
939 +}
940 +
941 +.animate-spin {
942 + animation: spin 1s linear infinite;
943 +}
944 +</style>
945 +
946 +<style lang="less">
947 +.poster-checkin-page {
948 + .aspect-\[3\/4\] {
949 + aspect-ratio: 3/4;
950 + }
951 +}
952 +</style>