ufutx-love-h5-public/src/views/activity/activityPoster.vue
2026-04-15 18:27:47 +08:00

828 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="poster-generator-container">
<div class="input-card">
<div class="card-header">
<span class="header-icon">📋</span>
<span class="header-title">选择活动</span>
</div>
<div class="activity-selector" @click="showActivityPicker = true">
<div class="selector-label">活动名称</div>
<div class="selector-value" :class="{ placeholder: !selectedActivityTitle }">
<span class="selector-text">{{ selectedActivityTitle || '点击选择活动' }}</span>
</div>
<div class="selector-arrow">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 活动选择弹窗 -->
<van-popup v-model:show="showActivityPicker" position="bottom" round class="activity-popup">
<div class="activity-picker-header">
<span class="cancel-btn" @click="showActivityPicker = false">取消</span>
<span class="title">选择活动</span>
<span class="confirm-btn" @click="showActivityPicker = false">确定</span>
</div>
<div class="scroll-wrapper">
<van-list
v-model:loading="listLoading"
:finished="listFinished"
finished-text="没有更多活动了"
@load="onLoadMore">
<div
v-for="item in activityList"
:key="item.id"
class="activity-item"
:class="{ active: selectedActivity?.id === item.id }"
@click="onSelectActivity(item)">
<div class="activity-title">{{ item.title }}</div>
<div class="activity-time">{{ item.Subtitle }} | {{ formatDateToShow(item.end_time) }}</div>
</div>
</van-list>
</div>
</van-popup>
<!-- 隐藏海报区域(样式保持原样) -->
<div ref="posterRef" class="poster-wrapper hidden-poster">
<div class="poster-card">
<div class="poster-content">
<img src="https://images.health.ufutx.com/202604/15/a90d31b444dd5f8f974b502b42bd4872.jpeg" alt="海报背景图" />
<div class="poster-content-wrapper">
<div class="poster-title">
<p class="poster-title-text">{{ formattedTitle }}</p>
</div>
<div class="poster-subtitle">
<p class="poster-subtitle-text">{{ selectedActivity?.Subtitle || '河南·襄城站' }}</p>
</div>
<div class="sponsor-name">
<p class="name">主办:{{ sponsorName }}</p>
</div>
<div class="poster-qrcode">
<canvas ref="qrcodeCanvas" class="poster-qrcode-image"></canvas>
</div>
<div class="poster-time-wrap">
<div class="poster-date">{{ formatDateToChinese(selectedActivity?.end_time) }}</div>
<div class="poster-time">
<span class="start-time">{{ formatTime(selectedActivity?.start_time) }}</span>
<span class="end-time">{{ formatTime(selectedActivity?.end_time) }}</span>
</div>
</div>
<div class="poster-address">
<div class="_text-warp">
<div class="_title">活动地点:</div>
<div class="_text">{{ selectedActivity?.address || '广东省深圳市南山区阳光科创' }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮组 -->
<div class="btn-group">
<button class="btn btn-primary" :disabled="isGenerating" @click="generatePoster">
<van-loading v-if="isGenerating" size="20px" color="#fff" />
<span v-else>✨ 生成海报</span>
</button>
<button v-if="posterImg && !isWeChatEnv" class="btn btn-secondary" @click="downloadPoster">📥 下载海报</button>
</div>
<!-- 生成预览区域 -->
<div v-if="posterImg" class="generated-poster-preview">
<div class="preview-header">
<div class="preview-icon">🎉</div>
<h4 class="preview-title">{{ isWeChatEnv ? '长按图片保存到手机' : '点击图片预览大图' }}</h4>
</div>
<img :src="posterImg" alt="生成的海报" class="preview-img" @click="showImagePreviewFn(posterImg)" />
</div>
</div>
</template>
<script>
import html2canvas from 'html2canvas'
import { ImagePreview } from 'vant'
import { $toastText, $toastSuccess, $toastLoading, $toastClear } from '@/config/toast'
import requestApp from '@/utils/request'
import QRCode from 'qrcode'
export default {
name: 'ActivityPoster',
data() {
return {
// 响应式数据 (原 ref 和 reactive)
sponsorName: '',
posterImg: '',
isGenerating: false,
isWeChatEnv: false,
activityList: [],
selectedActivity: null,
selectedActivityTitle: '',
showActivityPicker: false,
listLoading: false,
listFinished: true,
openId: ''
// 注意qrcodeCanvas 作为 DOM 引用,通常在 mounted 中通过 this.$refs 访问
}
},
mounted() {
// 替代 onMounted
this.detectWeChatEnv()
this.getData()
console.log('33')
},
computed: {
formattedTitle() {
if (!this.selectedActivity?.title) return ''
const parts = this.selectedActivity.title.split('•')
console.log('computed parts:', parts)
const lastPart = parts[parts.length - 1].trim()
console.log('computed formattedTitle:', lastPart)
return lastPart
}
},
methods: {
// 格式化标题:切割 · 符号,取最后一个
// formatTitle(title) {
// if (!title) return ''
// // 按 · 分割,取最后一部分,并去除首尾空格
// debugger
// const parts = title.split('·')
// console.log(parts)
// debugger
// const lastPart = parts[parts.length - 1].trim()
// return lastPart
// },
// 替代 script setup 中的函数
async getData() {
console.log('33455')
const vm = this
this.listLoading = true
// 注意weXinShare 需要确保在 Vue 2 实例上下文中可用
vm.$nextTick(() => {
setTimeout(() => {
// if (vm.$store.state.app.configData) {
const urls = `${vm.$shareCallback}/api/official/live/wechat/FamilyAuth?from_openid=${localStorage.getItem('openid')}&merchant_id=${vm.$store.state.app.merchant_id}&spread_merchant_id=${vm.$store.state.app.spread_merchant_id}&url=` + encodeURIComponent(`${vm.$shareCallback}/pu/#/activityPoster`)
vm.$shareList(
'https://image.fulllinkai.com/202310/28/88e931a50ec0a8094fb46191b389457e.png?x-oss-process=image/resize,w_200,h_200',
urls,
'查看详情',
'活动海报生成「saas」'
)
// }
}, 300)
})
// vm.$shareList('https://image.fulllinkai.com/202310/28/88e931a50ec0a8094fb46191b389457e.png',
// `https://health.ufutx.cn/go_html/role_apply#/activityPoster`,
// '查看详情', '活动海报生成「saas」')
try {
const res = await requestApp({
url: '/s/h5/uftx/community/activity/list',
method: 'get'
})
console.log(res, 'res---')
if (Array.isArray(res)) {
this.activityList = res
} else {
$toastText('活动列表获取失败')
}
console.log(this.activityList, ' this.activityList')
} catch (err) {
console.error('获取活动列表失败:', err)
$toastText('活动列表获取失败,请重试')
} finally {
this.listLoading = false
this.listFinished = true
$toastClear()
}
},
detectWeChatEnv() {
const userAgent = navigator.userAgent.toLowerCase()
this.isWeChatEnv = /micromessenger/.test(userAgent)
},
onSelectActivity(item) {
this.selectedActivity = item
this.selectedActivityTitle = item.title
this.showActivityPicker = false
this.generatePoster()
},
onLoadMore() {
this.listLoading = false
this.listFinished = true
},
formatDateToShow(timeStr) {
if (!timeStr) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}${month}${day}`
},
formatDateToChinese(timeStr) {
if (!timeStr) {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}/${month}/${day}`
}
const [datePart] = timeStr.split(' ')
const [year, month, day] = datePart.split('-')
return `${year}/${month}/${day}`
},
formatTime(timeStr) {
if (!timeStr) return '14:00'
const [, timePart] = timeStr.split(' ')
return timePart.slice(0, 5)
},
showImagePreviewFn(img) {
return ImagePreview({
images: [img],
showIndex: true
})
},
async generatePoster() {
try {
console.log(this.selectedActivity, 'pp--')
if (!this.selectedActivity) {
console.log('fd')
$toastText('请先选择活动!')
return
}
this.isGenerating = true
$toastLoading('海报生成中...')
// Vue 2 中使用 $nextTick
await this.$nextTick()
// 注意:原代码中使用了 ref="posterRef",这里通过 this.$refs 访问
const posterElement = this.$refs.posterRef
if (!posterElement) {
this.isGenerating = false
$toastClear()
return
}
const canvas = await html2canvas(posterElement, {
useCORS: true,
scale: 3,
backgroundColor: null,
logging: false,
imageTimeout: 10000
})
this.posterImg = canvas.toDataURL('image/png', 1.0)
$toastClear()
$toastSuccess('海报生成成功')
} catch (err) {
console.error('生成海报失败:', err)
$toastText('生成海报失败,请重试!')
} finally {
this.isGenerating = false
setTimeout(() => {
$toastClear()
}, 1200)
}
},
downloadPoster() {
if (!this.posterImg) return
const link = document.createElement('a')
link.href = this.posterImg
const fileName = `[${this.selectedActivity?.title || '活动'}]海报.png`
link.download = fileName.replace(/[\\/:*?"<>|]/g, '')
link.click()
URL.revokeObjectURL(link.href)
}
},
watch: {
// 监听器 (替代 watch(selectedActivity, ...))
selectedActivity: {
handler(newVal) {
if (newVal && newVal.id) {
// 自动从活动数据中读取主办方
this.sponsorName = newVal.sponsor || ''
this.openId = localStorage.getItem('openid')
// 注意Vue 2 中通常通过 this.$refs 访问 DOM 元素
const canvas = this.$refs.qrcodeCanvas
if (canvas) {
const link = `https://love.ufutx.cn/api/official/live/wechat/FamilyAuth?merchant_id=44&serve_tab=&from_openid=${this.openId}&url=https%3A%2F%2Flove.ufutx.cn%2Fpu%2F%23%2FactivityDetails%2F${newVal.id}`
QRCode.toCanvas(canvas, link, {
width: 76,
margin: 1,
color: { dark: '#000000', light: '#ffffff' }
}).catch(err => {
console.error('生成二维码失败:', err)
})
}
}
},
immediate: true // 如果需要立即执行一次,设置 immediate
}
}
}
</script>
<style scoped>
img {
display: inline-block !important;
max-width: 100%;
max-height: 100%;
}
.poster-wrapper {
width: 375px;
height: 788px;
overflow: hidden;
}
.hidden-poster {
position: absolute;
left: -9999px;
top: -9999px;
z-index: -1;
pointer-events: none;
}
.poster-card {
width: 100%;
height: 100%;
color: #ffffff;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #333333;
position: relative;
}
.poster-content img {
object-fit: cover;
}
.poster-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}
.poster-title {
position: absolute;
left: 0;
top: 130px;
width: 100%;
letter-spacing: 4px;
font-size: 32px;
font-weight: 900;
display: flex;
justify-content: center;
padding: 0;
}
.poster-title .poster-title-text {
text-align: center;
width: 300px;
color: #0d2f73;
line-height: 1.6;
padding: 2px 0;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.poster-subtitle {
position: absolute;
left: 0;
top: 262px;
width: 100%;
letter-spacing: 4px;
font-size: 20px;
font-weight: 400;
display: flex;
justify-content: center;
padding: 0;
}
.poster-subtitle .poster-subtitle-text {
text-align: center;
width: 300px;
color: #0d2f73;
line-height: 1.6;
padding: 2px 0;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.sponsor-name {
position: absolute;
left: 0;
top: 446px;
width: 100%;
letter-spacing: 0.8px;
display: flex;
justify-content: center;
padding: 0;
}
.sponsor-name .name {
font-size: 14px;
color: #0e2965;
}
.poster-qrcode {
position: absolute;
left: 0;
top: 532px;
width: 100%;
display: flex;
justify-content: center;
padding: 0;
}
.poster-qrcode-image {
width: 76px;
height: 76px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
background-color: #ffffff;
padding: 3px;
}
.poster-time-wrap {
position: absolute;
left: 0;
top: 630px;
width: 100%;
text-align: center;
justify-content: center;
letter-spacing: 1px;
}
.poster-date {
font-size: 24px;
font-weight: 500;
color: #0e2866;
}
.poster-time {
font-weight: 400;
font-size: 22px;
color: #0e2866;
margin-top: 14px;
}
.start-time {
margin-right: 30px;
}
.poster-address {
position: absolute;
left: 0;
top: 720px;
width: 100%;
max-height: 50px;
display: flex;
text-align: center;
justify-content: center;
letter-spacing: 1px;
}
.poster-address ._text-warp {
max-width: 80%;
font-size: 14px;
color: #0f2967;
font-weight: 500;
background: white;
border-radius: 32px;
padding: 4px 16px;
display: flex;
justify-content: space-between;
}
.poster-address ._title {
width: 76px;
flex-shrink: 0;
}
.poster-address ._text {
text-align: left;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* ========== 以下为美化后的外部样式(选择器、按钮、预览区域) ========== */
.poster-generator-container {
min-height: 100vh;
padding: 20px 16px 40px;
background: linear-gradient(135deg, #f5f7fa 0%, #e9eef3 100%);
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 输入卡片 */
.input-card {
width: 100%;
max-width: 400px;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.input-card:hover {
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 20px 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.header-icon {
font-size: 20px;
}
.header-title {
background: linear-gradient(135deg, #18ca6e, #0f9d58);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.activity-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px 12px 20px;
cursor: pointer;
transition: background 0.2s;
}
.activity-selector:active {
background: #f8fafc;
}
.selector-label {
font-size: 14px;
color: #6b7280;
font-weight: 500;
flex-shrink: 0;
}
.selector-value {
flex: 1;
margin: 0 12px;
font-size: 15px;
font-weight: 500;
color: #1f2937;
text-align: right;
display: flex;
justify-content: flex-end;
align-items: center;
min-height: 42px;
}
.selector-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
max-width: 100%;
line-height: 1.4;
}
.selector-value.placeholder .selector-text {
color: #9ca3af;
font-weight: 400;
}
.selector-arrow {
color: #9ca3af;
font-size: 14px;
flex-shrink: 0;
width: 16px;
height: 16px;
}
.selector-arrow .van-icon {
width: 16px;
height: 16px;
}
/* 弹窗样式 */
.activity-popup {
height: 85vh;
display: flex;
flex-direction: column;
border-radius: 20px 20px 0 0;
overflow: hidden;
}
.activity-picker-header {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.cancel-btn {
font-size: 15px;
color: #9ca3af;
cursor: pointer;
}
.title {
font-size: 17px;
font-weight: 600;
color: #1f2937;
}
.confirm-btn {
font-size: 15px;
color: #18ca6e;
font-weight: 600;
cursor: pointer;
}
.scroll-wrapper {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
.activity-item {
padding: 16px 20px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: background 0.2s;
}
.activity-item:active {
background: #f8fafc;
}
.activity-item.active {
background: #f0fdf4;
border-left: 3px solid #18ca6e;
}
.activity-title {
font-size: 15px;
font-weight: 600;
color: #1f2937;
margin-bottom: 6px;
}
.activity-time {
font-size: 12px;
color: #9ca3af;
}
/* 按钮组 */
.btn-group {
display: flex;
gap: 16px;
width: 100%;
max-width: 400px;
}
.btn {
flex: 1;
padding: 14px 20px;
border: none;
border-radius: 60px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.25s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #18ca6e, #0f9d58);
color: white;
box-shadow: 0 4px 12px rgba(24, 202, 110, 0.3);
}
.btn-primary:active {
transform: scale(0.97);
box-shadow: 0 2px 6px rgba(24, 202, 110, 0.3);
}
.btn-primary:disabled {
opacity: 0.7;
transform: none;
cursor: not-allowed;
}
.btn-secondary {
background: #2c3e50;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-secondary:active {
transform: scale(0.97);
}
/* 预览区域 */
.generated-poster-preview {
width: 100%;
max-width: 400px;
background: white;
border-radius: 28px;
padding: 20px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
box-sizing: border-box;
}
.preview-header {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
}
.preview-icon {
font-size: 22px;
}
.preview-title {
font-size: 15px;
font-weight: 500;
color: #374151;
margin: 0;
}
.preview-img {
width: 100%;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.preview-img:active {
transform: scale(0.99);
}
/* 适配小屏幕 */
@media (max-width: 420px) {
.poster-generator-container {
padding: 16px 12px 32px;
}
.btn {
padding: 12px 16px;
font-size: 14px;
}
}
</style>