hookehuyr

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

添加新的海报打卡功能页面,包含海报生成、预览和保存功能
更新活动封面页面的默认海报图URL
......@@ -33,6 +33,7 @@ export default {
'pages/PointsList/index',
'pages/UploadMedia/index',
'pages/FamilyRank/index',
'pages/PosterCheckin/index',
],
window: {
backgroundTextStyle: 'light',
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 21:39:49
* @LastEditTime: 2025-09-03 13:31:52
* @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue
* @Description: 活动海报页面 - 展示活动信息并处理定位授权
-->
......@@ -94,7 +94,7 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue'
import { getMyFamiliesAPI } from '@/api/family'
// 默认海报图
const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome.png';
const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png';
/**
* 活动海报页面组件
* 功能:展示活动信息、处理定位授权、跳转到活动页面
......
<template>
<view class="poster-checkin-page bg-gray-50 min-h-screen pb-20">
<!-- 海报预览区域 -->
<view class="mx-4 mt-4 bg-white rounded-lg shadow-sm overflow-hidden">
<view class="aspect-[3/4] relative bg-gray-100 flex items-center justify-center">
<view v-if="posterPath" class="w-full h-full relative">
<image
:src="posterPath"
mode="aspectFit"
class="w-full h-full"
@click="previewPoster"
/>
<!-- 点击预览提示 -->
<view class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
点击预览
</view>
</view>
<view v-else class="text-center text-gray-500">
<view class="text-4xl mb-2">🎨</view>
<text class="text-sm">海报生成中...</text>
<view class="mt-2">
<view class="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto"></view>
</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4 safe-area-bottom">
<view class="flex gap-4">
<button
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 class="text-lg">📷</text>
<text>{{ backgroundImage ? '更改照片' : '上传照片' }}</text>
</button>
<button
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' : 'bg-gray-400'"
@click="savePoster"
:disabled="!posterPath"
>
<text class="text-lg">💾</text>
<text>保存海报</text>
</button>
</view>
</view>
<!-- 海报生成组件 -->
<PosterBuilder
v-if="shouldGeneratePoster"
:config="posterConfig"
:show-loading="true"
@success="onPosterSuccess"
@fail="onPosterFail"
/>
<!-- 图片预览 -->
<nut-image-preview
v-model:show="previewVisible"
:images="previewImages"
:init-no="0"
@close="closePreview"
/>
</view>
</template>
<style scoped>
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import Taro from '@tarojs/taro'
import { Left } from '@nutui/icons-vue-taro'
import PosterBuilder from '@/components/PosterBuilder/index.vue'
import BASE_URL from '@/utils/config'
// 页面状态
const posterPath = ref('') // 生成的海报路径
const backgroundImage = ref('') // 用户上传的背景图
const shouldGeneratePoster = ref(false) // 是否应该生成海报
// 图片预览相关
const previewVisible = ref(false)
const previewImages = ref([])
// Mock数据
const mockData = ref({
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '张大爷'
},
family: {
name: '幸福之家',
description: '一家人整整齐齐最重要'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/activity-logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/level-badge.png',
name: '第一关卡'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '长按识别小程序码\n送我一朵小红花为我助力吧~'
})
// 默认背景图
const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
// 海报配置
const posterConfig = computed(() => {
const bgImage = backgroundImage.value || defaultBackground
return {
width: 750,
height: 1334,
backgroundColor: '#f5f5f5',
debug: false,
images: [
// 背景图
{
x: 0,
y: 0,
width: 750,
height: 800,
url: bgImage,
zIndex: 0
},
// 用户头像
{
x: 40,
y: 40,
width: 80,
height: 80,
url: mockData.value.user.avatar,
borderRadius: 40,
zIndex: 2
},
// 活动logo
{
x: 580,
y: 40,
width: 120,
height: 80,
url: mockData.value.activity.logo,
zIndex: 2
},
// 关卡徽章
{
x: 580,
y: 680,
width: 120,
height: 80,
url: mockData.value.level.logo,
zIndex: 2
},
// 小程序码
{
x: 50,
y: 850,
width: 200,
height: 200,
url: mockData.value.qrcode,
zIndex: 1
}
],
texts: [
// 家庭名称
{
x: 140,
y: 50,
text: mockData.value.family.name,
fontSize: 36,
color: '#ffffff',
fontWeight: 'bold',
textAlign: 'left',
zIndex: 2
},
// 家庭描述
{
x: 140,
y: 100,
text: mockData.value.family.description,
fontSize: 26,
color: '#ffffff',
textAlign: 'left',
zIndex: 2
},
// 小程序码描述
{
x: 280,
y: 900,
text: mockData.value.qrcodeDesc,
fontSize: 28,
color: '#333333',
lineHeight: 40,
lineNum: 2,
width: 400,
textAlign: 'left',
zIndex: 1
}
],
blocks: [
// 下半部分白色背景
{
x: 0,
y: 800,
width: 750,
height: 534,
backgroundColor: '#ffffff',
zIndex: 0
},
// 用户信息背景遮罩
{
x: 30,
y: 30,
width: 520,
height: 120,
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 10,
zIndex: 1
}
]
}
})
/**
* 页面加载时初始化
*/
onMounted(() => {
Taro.setNavigationBarTitle({ title: '海报打卡' })
// 页面加载时生成海报
generatePoster()
})
/**
* 监听背景图变化,重新生成海报
*/
watch(backgroundImage, () => {
if (backgroundImage.value) {
generatePoster()
}
})
/**
* 返回上一页
*/
const goBack = () => {
Taro.navigateBack()
}
/**
* 生成海报
*/
const generatePoster = () => {
posterPath.value = ''
shouldGeneratePoster.value = false
// 延迟触发生成,确保配置更新
setTimeout(() => {
shouldGeneratePoster.value = true
}, 100)
}
/**
* 选择背景图片
*/
const chooseBackgroundImage = () => {
Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFile = res.tempFiles[0]
if (tempFile.size > 5 * 1024 * 1024) {
Taro.showToast({
title: '图片大小不能超过5MB',
icon: 'none'
})
return
}
uploadBackgroundImage(tempFile.path)
},
fail: () => {
Taro.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
}
/**
* 上传背景图片
*/
const uploadBackgroundImage = (filePath) => {
Taro.showLoading({ title: '上传中...' })
Taro.uploadFile({
url: BASE_URL + '/admin/?m=srv&a=upload',
filePath,
name: 'file',
success: (uploadRes) => {
Taro.hideLoading()
const data = JSON.parse(uploadRes.data)
if (data.code === 0) {
backgroundImage.value = data.data.src
Taro.showToast({ title: '上传成功', icon: 'success' })
} else {
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
}
},
fail: () => {
Taro.hideLoading()
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
}
})
}
/**
* 海报生成成功
*/
const onPosterSuccess = (result) => {
posterPath.value = result.tempFilePath
shouldGeneratePoster.value = false
Taro.showToast({ title: '海报生成成功', icon: 'success' })
}
/**
* 海报生成失败
*/
const onPosterFail = (error) => {
shouldGeneratePoster.value = false
Taro.showToast({ title: '海报生成失败', icon: 'none' })
console.error('海报生成失败:', error)
}
/**
* 预览海报
*/
const previewPoster = () => {
if (!posterPath.value) return
previewImages.value = [{ src: posterPath.value }]
previewVisible.value = true
}
/**
* 关闭预览
*/
const closePreview = () => {
previewVisible.value = false
}
/**
* 保存海报到相册
*/
const savePoster = () => {
if (!posterPath.value) {
Taro.showToast({ title: '请等待海报生成完成', icon: 'none' })
return
}
Taro.saveImageToPhotosAlbum({
filePath: posterPath.value,
success: () => {
Taro.showToast({ title: '保存成功', icon: 'success' })
},
fail: (err) => {
if (err.errMsg.includes('auth deny')) {
Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
showCancel: false,
confirmText: '去设置',
success: () => {
Taro.openSetting()
}
})
} else {
Taro.showToast({ title: '保存失败', icon: 'none' })
}
}
})
}
</script>
<style lang="less" scoped>
.poster-checkin-page {
.aspect-\[3\/4\] {
aspect-ratio: 3/4;
}
}
</style>