hookehuyr

feat: 添加海报打卡页面并更新默认海报图

添加新的海报打卡功能页面,包含海报生成、预览和保存功能
更新活动封面页面的默认海报图URL
...@@ -33,6 +33,7 @@ export default { ...@@ -33,6 +33,7 @@ export default {
33 'pages/PointsList/index', 33 'pages/PointsList/index',
34 'pages/UploadMedia/index', 34 'pages/UploadMedia/index',
35 'pages/FamilyRank/index', 35 'pages/FamilyRank/index',
36 + 'pages/PosterCheckin/index',
36 ], 37 ],
37 window: { 38 window: {
38 backgroundTextStyle: 'light', 39 backgroundTextStyle: 'light',
......
1 <!-- 1 <!--
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-02 21:39:49 4 + * @LastEditTime: 2025-09-03 13:31:52
5 * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue 5 * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue
6 * @Description: 活动海报页面 - 展示活动信息并处理定位授权 6 * @Description: 活动海报页面 - 展示活动信息并处理定位授权
7 --> 7 -->
...@@ -94,7 +94,7 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue' ...@@ -94,7 +94,7 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue'
94 import { getMyFamiliesAPI } from '@/api/family' 94 import { getMyFamiliesAPI } from '@/api/family'
95 95
96 // 默认海报图 96 // 默认海报图
97 -const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome.png'; 97 +const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png';
98 /** 98 /**
99 * 活动海报页面组件 99 * 活动海报页面组件
100 * 功能:展示活动信息、处理定位授权、跳转到活动页面 100 * 功能:展示活动信息、处理定位授权、跳转到活动页面
......
1 +<template>
2 + <view class="poster-checkin-page bg-gray-50 min-h-screen pb-20">
3 + <!-- 海报预览区域 -->
4 + <view class="mx-4 mt-4 bg-white rounded-lg shadow-sm overflow-hidden">
5 + <view class="aspect-[3/4] relative bg-gray-100 flex items-center justify-center">
6 + <view v-if="posterPath" class="w-full h-full relative">
7 + <image
8 + :src="posterPath"
9 + mode="aspectFit"
10 + class="w-full h-full"
11 + @click="previewPoster"
12 + />
13 + <!-- 点击预览提示 -->
14 + <view class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
15 + 点击预览
16 + </view>
17 + </view>
18 + <view v-else class="text-center text-gray-500">
19 + <view class="text-4xl mb-2">🎨</view>
20 + <text class="text-sm">海报生成中...</text>
21 + <view class="mt-2">
22 + <view class="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto"></view>
23 + </view>
24 + </view>
25 + </view>
26 + </view>
27 +
28 + <!-- 底部操作按钮 -->
29 + <view class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4 safe-area-bottom">
30 + <view class="flex gap-4">
31 + <button
32 + 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"
33 + @click="chooseBackgroundImage"
34 + >
35 + <text class="text-lg">📷</text>
36 + <text>{{ backgroundImage ? '更改照片' : '上传照片' }}</text>
37 + </button>
38 + <button
39 + 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"
40 + :class="posterPath ? 'bg-gradient-to-r from-green-400 to-green-500' : 'bg-gray-400'"
41 + @click="savePoster"
42 + :disabled="!posterPath"
43 + >
44 + <text class="text-lg">💾</text>
45 + <text>保存海报</text>
46 + </button>
47 + </view>
48 + </view>
49 +
50 + <!-- 海报生成组件 -->
51 + <PosterBuilder
52 + v-if="shouldGeneratePoster"
53 + :config="posterConfig"
54 + :show-loading="true"
55 + @success="onPosterSuccess"
56 + @fail="onPosterFail"
57 + />
58 +
59 + <!-- 图片预览 -->
60 + <nut-image-preview
61 + v-model:show="previewVisible"
62 + :images="previewImages"
63 + :init-no="0"
64 + @close="closePreview"
65 + />
66 + </view>
67 +</template>
68 +
69 +<style scoped>
70 +.safe-area-bottom {
71 + padding-bottom: env(safe-area-inset-bottom);
72 +}
73 +
74 +@keyframes spin {
75 + from {
76 + transform: rotate(0deg);
77 + }
78 + to {
79 + transform: rotate(360deg);
80 + }
81 +}
82 +
83 +.animate-spin {
84 + animation: spin 1s linear infinite;
85 +}
86 +</style>
87 +
88 +<script setup>
89 +import { ref, onMounted, computed, watch } from 'vue'
90 +import Taro from '@tarojs/taro'
91 +import { Left } from '@nutui/icons-vue-taro'
92 +import PosterBuilder from '@/components/PosterBuilder/index.vue'
93 +import BASE_URL from '@/utils/config'
94 +
95 +// 页面状态
96 +const posterPath = ref('') // 生成的海报路径
97 +const backgroundImage = ref('') // 用户上传的背景图
98 +const shouldGeneratePoster = ref(false) // 是否应该生成海报
99 +
100 +// 图片预览相关
101 +const previewVisible = ref(false)
102 +const previewImages = ref([])
103 +
104 +// Mock数据
105 +const mockData = ref({
106 + user: {
107 + avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
108 + nickname: '张大爷'
109 + },
110 + family: {
111 + name: '幸福之家',
112 + description: '一家人整整齐齐最重要'
113 + },
114 + activity: {
115 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/activity-logo.png',
116 + name: '南京路时尚citywalk'
117 + },
118 + level: {
119 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/level-badge.png',
120 + name: '第一关卡'
121 + },
122 + qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
123 + qrcodeDesc: '长按识别小程序码\n送我一朵小红花为我助力吧~'
124 +})
125 +
126 +// 默认背景图
127 +const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
128 +
129 +// 海报配置
130 +const posterConfig = computed(() => {
131 + const bgImage = backgroundImage.value || defaultBackground
132 +
133 + return {
134 + width: 750,
135 + height: 1334,
136 + backgroundColor: '#f5f5f5',
137 + debug: false,
138 + images: [
139 + // 背景图
140 + {
141 + x: 0,
142 + y: 0,
143 + width: 750,
144 + height: 800,
145 + url: bgImage,
146 + zIndex: 0
147 + },
148 + // 用户头像
149 + {
150 + x: 40,
151 + y: 40,
152 + width: 80,
153 + height: 80,
154 + url: mockData.value.user.avatar,
155 + borderRadius: 40,
156 + zIndex: 2
157 + },
158 + // 活动logo
159 + {
160 + x: 580,
161 + y: 40,
162 + width: 120,
163 + height: 80,
164 + url: mockData.value.activity.logo,
165 + zIndex: 2
166 + },
167 + // 关卡徽章
168 + {
169 + x: 580,
170 + y: 680,
171 + width: 120,
172 + height: 80,
173 + url: mockData.value.level.logo,
174 + zIndex: 2
175 + },
176 + // 小程序码
177 + {
178 + x: 50,
179 + y: 850,
180 + width: 200,
181 + height: 200,
182 + url: mockData.value.qrcode,
183 + zIndex: 1
184 + }
185 + ],
186 + texts: [
187 + // 家庭名称
188 + {
189 + x: 140,
190 + y: 50,
191 + text: mockData.value.family.name,
192 + fontSize: 36,
193 + color: '#ffffff',
194 + fontWeight: 'bold',
195 + textAlign: 'left',
196 + zIndex: 2
197 + },
198 + // 家庭描述
199 + {
200 + x: 140,
201 + y: 100,
202 + text: mockData.value.family.description,
203 + fontSize: 26,
204 + color: '#ffffff',
205 + textAlign: 'left',
206 + zIndex: 2
207 + },
208 + // 小程序码描述
209 + {
210 + x: 280,
211 + y: 900,
212 + text: mockData.value.qrcodeDesc,
213 + fontSize: 28,
214 + color: '#333333',
215 + lineHeight: 40,
216 + lineNum: 2,
217 + width: 400,
218 + textAlign: 'left',
219 + zIndex: 1
220 + }
221 + ],
222 + blocks: [
223 + // 下半部分白色背景
224 + {
225 + x: 0,
226 + y: 800,
227 + width: 750,
228 + height: 534,
229 + backgroundColor: '#ffffff',
230 + zIndex: 0
231 + },
232 + // 用户信息背景遮罩
233 + {
234 + x: 30,
235 + y: 30,
236 + width: 520,
237 + height: 120,
238 + backgroundColor: 'rgba(0,0,0,0.3)',
239 + borderRadius: 10,
240 + zIndex: 1
241 + }
242 + ]
243 + }
244 +})
245 +
246 +/**
247 + * 页面加载时初始化
248 + */
249 +onMounted(() => {
250 + Taro.setNavigationBarTitle({ title: '海报打卡' })
251 + // 页面加载时生成海报
252 + generatePoster()
253 +})
254 +
255 +/**
256 + * 监听背景图变化,重新生成海报
257 + */
258 +watch(backgroundImage, () => {
259 + if (backgroundImage.value) {
260 + generatePoster()
261 + }
262 +})
263 +
264 +/**
265 + * 返回上一页
266 + */
267 +const goBack = () => {
268 + Taro.navigateBack()
269 +}
270 +
271 +/**
272 + * 生成海报
273 + */
274 +const generatePoster = () => {
275 + posterPath.value = ''
276 + shouldGeneratePoster.value = false
277 +
278 + // 延迟触发生成,确保配置更新
279 + setTimeout(() => {
280 + shouldGeneratePoster.value = true
281 + }, 100)
282 +}
283 +
284 +/**
285 + * 选择背景图片
286 + */
287 +const chooseBackgroundImage = () => {
288 + Taro.chooseImage({
289 + count: 1,
290 + sizeType: ['compressed'],
291 + sourceType: ['album', 'camera'],
292 + success: (res) => {
293 + const tempFile = res.tempFiles[0]
294 + if (tempFile.size > 5 * 1024 * 1024) {
295 + Taro.showToast({
296 + title: '图片大小不能超过5MB',
297 + icon: 'none'
298 + })
299 + return
300 + }
301 +
302 + uploadBackgroundImage(tempFile.path)
303 + },
304 + fail: () => {
305 + Taro.showToast({
306 + title: '选择图片失败',
307 + icon: 'none'
308 + })
309 + }
310 + })
311 +}
312 +
313 +/**
314 + * 上传背景图片
315 + */
316 +const uploadBackgroundImage = (filePath) => {
317 + Taro.showLoading({ title: '上传中...' })
318 +
319 + Taro.uploadFile({
320 + url: BASE_URL + '/admin/?m=srv&a=upload',
321 + filePath,
322 + name: 'file',
323 + success: (uploadRes) => {
324 + Taro.hideLoading()
325 + const data = JSON.parse(uploadRes.data)
326 + if (data.code === 0) {
327 + backgroundImage.value = data.data.src
328 + Taro.showToast({ title: '上传成功', icon: 'success' })
329 + } else {
330 + Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
331 + }
332 + },
333 + fail: () => {
334 + Taro.hideLoading()
335 + Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
336 + }
337 + })
338 +}
339 +
340 +/**
341 + * 海报生成成功
342 + */
343 +const onPosterSuccess = (result) => {
344 + posterPath.value = result.tempFilePath
345 + shouldGeneratePoster.value = false
346 + Taro.showToast({ title: '海报生成成功', icon: 'success' })
347 +}
348 +
349 +/**
350 + * 海报生成失败
351 + */
352 +const onPosterFail = (error) => {
353 + shouldGeneratePoster.value = false
354 + Taro.showToast({ title: '海报生成失败', icon: 'none' })
355 + console.error('海报生成失败:', error)
356 +}
357 +
358 +/**
359 + * 预览海报
360 + */
361 +const previewPoster = () => {
362 + if (!posterPath.value) return
363 +
364 + previewImages.value = [{ src: posterPath.value }]
365 + previewVisible.value = true
366 +}
367 +
368 +/**
369 + * 关闭预览
370 + */
371 +const closePreview = () => {
372 + previewVisible.value = false
373 +}
374 +
375 +/**
376 + * 保存海报到相册
377 + */
378 +const savePoster = () => {
379 + if (!posterPath.value) {
380 + Taro.showToast({ title: '请等待海报生成完成', icon: 'none' })
381 + return
382 + }
383 +
384 + Taro.saveImageToPhotosAlbum({
385 + filePath: posterPath.value,
386 + success: () => {
387 + Taro.showToast({ title: '保存成功', icon: 'success' })
388 + },
389 + fail: (err) => {
390 + if (err.errMsg.includes('auth deny')) {
391 + Taro.showModal({
392 + title: '提示',
393 + content: '需要您授权保存图片到相册',
394 + showCancel: false,
395 + confirmText: '去设置',
396 + success: () => {
397 + Taro.openSetting()
398 + }
399 + })
400 + } else {
401 + Taro.showToast({ title: '保存失败', icon: 'none' })
402 + }
403 + }
404 + })
405 +}
406 +</script>
407 +
408 +<style lang="less" scoped>
409 +.poster-checkin-page {
410 + .aspect-\[3\/4\] {
411 + aspect-ratio: 3/4;
412 + }
413 +}
414 +</style>