hookehuyr

fix(海报生成): 修复二维码加载问题并优化加载状态处理

替换 axios 为 fetch 加载二维码以避免拦截器导致的 CORS 问题
添加加载状态处理防止未准备好时渲染组件
同时监听二维码 URL 变化触发重新生成
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
21 <script setup> 21 <script setup>
22 import { ref, watch, nextTick, onMounted } from 'vue' 22 import { ref, watch, nextTick, onMounted } from 'vue'
23 import { showToast } from 'vant' 23 import { showToast } from 'vant'
24 +// import request from '@/utils/axios' // 移除 axios 依赖,改用 fetch
24 25
25 const props = defineProps({ 26 const props = defineProps({
26 bgUrl: { 27 bgUrl: {
...@@ -150,13 +151,37 @@ const generatePoster = async () => { ...@@ -150,13 +151,37 @@ const generatePoster = async () => {
150 ctx.fillStyle = '#ffffff' 151 ctx.fillStyle = '#ffffff'
151 ctx.fillRect(0, 0, width, height) 152 ctx.fillRect(0, 0, width, height)
152 153
154 + // 处理 API 形式的二维码 URL (如 /admin/?m=srv...)
155 + // 如果是相对路径或特定API,使用 fetch 获取 blob 以通过鉴权并避免跨域/拦截器问题
156 + let qrUrlToLoad = props.qrUrl
157 + let qrBlobUrl = null
158 + // 兼容完整 URL 和相对路径,只要包含特定特征或是相对路径
159 + if (qrUrlToLoad && (qrUrlToLoad.startsWith('/') || qrUrlToLoad.includes('m=srv') || qrUrlToLoad.includes('http'))) {
160 + try {
161 + // 使用 fetch 替代 axios,避免拦截器自动添加 header 导致 CORS 预检失败
162 + const res = await fetch(qrUrlToLoad)
163 + if (res.ok) {
164 + const blob = await res.blob()
165 + qrBlobUrl = URL.createObjectURL(blob)
166 + qrUrlToLoad = qrBlobUrl
167 + }
168 + } catch (err) {
169 + console.warn('QR Code API fetch failed, falling back to direct load', err)
170 + }
171 + }
172 +
153 // 2. 并行加载所有图片资源 173 // 2. 并行加载所有图片资源
154 const [bgImg, logoImg, qrImg] = await Promise.all([ 174 const [bgImg, logoImg, qrImg] = await Promise.all([
155 loadImage(props.bgUrl), 175 loadImage(props.bgUrl),
156 loadImage(props.logoUrl), 176 loadImage(props.logoUrl),
157 - loadImage(props.qrUrl) 177 + loadImage(qrUrlToLoad)
158 ]) 178 ])
159 179
180 + // 清理 Blob URL
181 + if (qrBlobUrl) {
182 + URL.revokeObjectURL(qrBlobUrl)
183 + }
184 +
160 // 3. 绘制背景图 (Object-Cover 效果) 185 // 3. 绘制背景图 (Object-Cover 效果)
161 if (bgImg) { 186 if (bgImg) {
162 // 计算裁剪 187 // 计算裁剪
...@@ -321,7 +346,7 @@ const generatePoster = async () => { ...@@ -321,7 +346,7 @@ const generatePoster = async () => {
321 } 346 }
322 } 347 }
323 348
324 -watch(() => props.bgUrl, () => { 349 +watch(() => [props.bgUrl, props.qrUrl], () => {
325 generatePoster() 350 generatePoster()
326 }) 351 })
327 352
......
1 <!-- 1 <!--
2 * @Date: 2025-12-23 15:50:59 2 * @Date: 2025-12-23 15:50:59
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-12-24 20:35:33 4 + * @LastEditTime: 2025-12-25 13:07:01
5 * @FilePath: /mlaj/src/views/recall/PosterPage.vue 5 * @FilePath: /mlaj/src/views/recall/PosterPage.vue
6 * @Description: 分享海报页面 6 * @Description: 分享海报页面
7 --> 7 -->
...@@ -11,11 +11,15 @@ ...@@ -11,11 +11,15 @@
11 <!-- Poster Container (Scrollable Area) --> 11 <!-- Poster Container (Scrollable Area) -->
12 <div class="flex-1 overflow-y-auto px-6 pt-6 pb-32 flex flex-col items-center"> 12 <div class="flex-1 overflow-y-auto px-6 pt-6 pb-32 flex flex-col items-center">
13 <RecallPoster 13 <RecallPoster
14 + v-if="isReady"
14 :bg-url="posterBg" 15 :bg-url="posterBg"
15 :title="title" 16 :title="title"
16 :logo-url="logoUrl" 17 :logo-url="logoUrl"
17 :qr-url="qrCodeUrl" 18 :qr-url="qrCodeUrl"
18 /> 19 />
20 + <div v-else class="w-full h-[62vh] min-h-[400px] flex items-center justify-center">
21 + <van-loading color="#ffffff" vertical>加载中...</van-loading>
22 + </div>
19 </div> 23 </div>
20 24
21 <!-- Buttons (Fixed at Bottom) --> 25 <!-- Buttons (Fixed at Bottom) -->
...@@ -52,10 +56,11 @@ useTitle('分享海报'); ...@@ -52,10 +56,11 @@ useTitle('分享海报');
52 // Assets 56 // Assets
53 const defaultBg = 'https://cdn.ipadbiz.cn/mlaj/images/test-bgg03.jpg?imageMogr2/thumbnail/800x/strip/quality/80' 57 const defaultBg = 'https://cdn.ipadbiz.cn/mlaj/images/test-bgg03.jpg?imageMogr2/thumbnail/800x/strip/quality/80'
54 const logoUrl = 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png' 58 const logoUrl = 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png'
55 -const qrCodeUrl = 'https://cdn.ipadbiz.cn/mlaj/recall/poster/%E4%BA%8C%E7%BB%B4%E7%A0%81@2x.png'
56 59
57 // State 60 // State
58 const posterBg = ref(defaultBg) 61 const posterBg = ref(defaultBg)
62 +const qrCodeUrl = ref('')
63 +const isReady = ref(false)
59 64
60 /** 65 /**
61 * 获取文件哈希(与七牛云ETag一致) 66 * 获取文件哈希(与七牛云ETag一致)
...@@ -163,15 +168,19 @@ const title = ref($route.query.title || '活动海报') ...@@ -163,15 +168,19 @@ const title = ref($route.query.title || '活动海报')
163 168
164 onMounted(async () => { 169 onMounted(async () => {
165 if (stu_uid.value && campaign_id.value) { 170 if (stu_uid.value && campaign_id.value) {
166 - const { data } = await getPosterAPI({ 171 + const { code, data } = await getPosterAPI({
167 stu_uid: stu_uid.value, 172 stu_uid: stu_uid.value,
168 campaign_id: campaign_id.value 173 campaign_id: campaign_id.value
169 }) 174 })
170 175
171 - if (data) { 176 + if (code) {
172 posterBg.value = data.background_image + '?imageMogr2/thumbnail/800x/strip/quality/80' || defaultBg 177 posterBg.value = data.background_image + '?imageMogr2/thumbnail/800x/strip/quality/80' || defaultBg
173 - qrCodeUrl.value = data.qrcode 178 + qrCodeUrl.value = data?.qrcode || ''
179 + isReady.value = true
174 } 180 }
181 + } else {
182 + // 如果没有参数,也设置为 ready,显示默认背景
183 + isReady.value = true
175 } 184 }
176 }) 185 })
177 </script> 186 </script>
......