1、api增加发布/调试的api域名
2、浏览的图片改为使用jpg格式
3、对联图片加载增加loading提示
4、东直门、弹窗、隆福寺、王府井模块动态加载的图片需要进行预加载处理
This commit is contained in:
Wenzhe 2026-02-03 13:50:41 +08:00
parent 1934cee57a
commit 4b25967a95
24 changed files with 419 additions and 30 deletions

View File

@ -4,7 +4,9 @@
*/ */
// API 基础配置 // API 基础配置
const BASE_URL = 'http://xcsq.wxinh5.host' const BASE_URL = process.env.NODE_ENV === 'production'
? 'https://xcsq.wxinh5.com'
: 'http://xcsq.wxinh5.host'
// 请求拦截器 // 请求拦截器
const requestInterceptor = (config) => { const requestInterceptor = (config) => {

View File

@ -6,7 +6,18 @@
<div class="couplet-display"> <div class="couplet-display">
<!-- 显示海报图片后端直接返回 --> <!-- 显示海报图片后端直接返回 -->
<div class="poster-image" v-if="couplet.image_url"> <div class="poster-image" v-if="couplet.image_url">
<img :src="couplet.image_url" alt="春联海报" style="width: 100%; max-width: 300px; height: auto;" /> <!-- 加载提示 -->
<div class="image-loading" v-if="isLoading">
<div class="loading-spinner"></div>
<div class="loading-text">海报加载中...</div>
</div>
<img
:src="couplet.image_url"
alt="春联海报"
style="width: 100%; max-width: 300px; height: auto;"
@load="isLoading = false"
@error="isLoading = false"
/>
</div> </div>
</div> </div>
@ -25,6 +36,8 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
@ -40,6 +53,23 @@ const props = defineProps({
}) })
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
//
const isLoading = ref(true)
// couplet
watch(() => props.couplet.image_url, (newImageUrl) => {
if (newImageUrl) {
isLoading.value = true
}
})
// visible
watch(() => props.visible, (newVisible) => {
if (newVisible && props.couplet.image_url) {
isLoading.value = true
}
})
</script> </script>
<style scoped> <style scoped>
@ -80,11 +110,53 @@ const emit = defineEmits(['close'])
text-align: center; text-align: center;
} }
.poster-image {
position: relative;
display: inline-block;
}
.poster-image img { .poster-image img {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
/* 图片加载提示 */
.image-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
font-size: 14px;
color: #333;
text-align: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.share-hint { .share-hint {
margin-bottom: 10rpx; margin-bottom: 10rpx;
font-size: 18px; font-size: 18px;

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue' import { ref, onMounted, onUnmounted, computed, getCurrentInstance as vueGetCurrentInstance, watch } from 'vue'
import FuClickArea from './FuClickArea.vue' import FuClickArea from './FuClickArea.vue'
import ImagePreloader from '@/utils/preload'
// Vue // Vue
const instance = vueGetCurrentInstance() const instance = vueGetCurrentInstance()
@ -19,9 +20,6 @@ const props = defineProps({
const emit = defineEmits(['collect-seal']) const emit = defineEmits(['collect-seal'])
//
const sealCollected = ref(false)
// //
const fuClickAreaVisible = ref(true) const fuClickAreaVisible = ref(true)
const sq3ImageVisible = ref(false) const sq3ImageVisible = ref(false)
@ -38,10 +36,15 @@ const handleFuClick = () => {
emit('collect-seal') emit('collect-seal')
} }
//
const getDzmImageUrl = (name) => {
return new URL(`/static/dzm/${name}`, import.meta.url).href
}
// //
const isDragging = ref(false) const isDragging = ref(false)
const showDuck = ref(true) // const showDuck = ref(true) //
const deskImage = ref('/static/dzm/img_desk1.png') const deskImage = ref(getDzmImageUrl('img_desk1.png'))
const showGuideElements = ref(true) const showGuideElements = ref(true)
// Canvas // Canvas
@ -112,6 +115,10 @@ const targetArea = ref({
// ID // ID
const animationId = ref(null) const animationId = ref(null)
//
const imagesLoaded = ref(false)
const isPreloading = ref(false)
// Canvas // Canvas
const initCanvas = () => { const initCanvas = () => {
console.log('正在初始化 Canvas...') console.log('正在初始化 Canvas...')
@ -135,7 +142,7 @@ const initCanvas = () => {
// //
const loadDuckImage = () => { const loadDuckImage = () => {
uni.getImageInfo({ uni.getImageInfo({
src: '/static/dzm/img_duck.png', src: getDzmImageUrl('img_duck.png'),
success: (res) => { success: (res) => {
console.log('鸭子图片加载成功:', res) console.log('鸭子图片加载成功:', res)
duckImagePath.value = res.path duckImagePath.value = res.path
@ -434,7 +441,7 @@ const handleTouchEnd = () => {
if (checkDuckInTarget()) { if (checkDuckInTarget()) {
console.log('成功放入目标区域!') console.log('成功放入目标区域!')
// //
deskImage.value = '/static/dzm/img_desk2.png' deskImage.value = getDzmImageUrl('img_desk2.png')
// //
showGuideElements.value = false showGuideElements.value = false
// //
@ -460,6 +467,33 @@ const resetDuckPosition = () => {
console.log('鸭子已回归原始位置:', duckX.value, duckY.value) console.log('鸭子已回归原始位置:', duckX.value, duckY.value)
} }
//
const preloadImages = async () => {
if (imagesLoaded.value || isPreloading.value) return
isPreloading.value = true
try {
//
const imageUrls = [
getDzmImageUrl('img_duck.png'),
getDzmImageUrl('img_desk1.png'),
getDzmImageUrl('img_desk2.png'),
getDzmImageUrl('img_stove1.png'),
getDzmImageUrl('img_line.png'),
'/static/images/icon_hand.png'
]
// 使 ImagePreloader
await ImagePreloader.preloadAll(imageUrls)
imagesLoaded.value = true
console.log('东直门场景图片预加载完成')
} catch (error) {
console.error('图片预加载失败:', error)
} finally {
isPreloading.value = false
}
}
// //
const startDrag = (e) => { const startDrag = (e) => {
console.log('开始拖拽', e) console.log('开始拖拽', e)
@ -479,6 +513,13 @@ const startDrag = (e) => {
handleTouchStart(e) handleTouchStart(e)
} }
//
watch(() => props.active, async (newActive) => {
if (newActive) {
await preloadImages()
}
})
// //
onMounted(() => { onMounted(() => {
// //
@ -487,6 +528,9 @@ onMounted(() => {
container.classList.add('animate-in') container.classList.add('animate-in')
} }
//
preloadImages()
// Canvas // Canvas
setTimeout(() => { setTimeout(() => {
initCanvas() initCanvas()
@ -531,8 +575,8 @@ onUnmounted(() => {
<!-- 装饰图片 --> <!-- 装饰图片 -->
<image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" /> <image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
<image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" /> <image :src="getDzmImageUrl('img_stove1.png')" alt="灶台" class="deco-img stove-img" mode="widthFix" />
<image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" /> <image v-if="showGuideElements" :src="getDzmImageUrl('img_line.png')" alt="线条" class="deco-img line-img" mode="widthFix" />
<image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" /> <image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" />
<!-- Canvas 拖拽区域 --> <!-- Canvas 拖拽区域 -->
@ -659,4 +703,4 @@ onUnmounted(() => {
height: 100rpx; height: 100rpx;
z-index: 30; z-index: 30;
} }
</style> </style>

View File

@ -92,6 +92,9 @@
</template> </template>
<script setup> <script setup>
import { onMounted } from 'vue'
import ImagePreloader from '@/utils/preload'
const props = defineProps({ const props = defineProps({
isActive: { isActive: {
type: Boolean, type: Boolean,
@ -133,7 +136,7 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['lottery', 'couplet', 'restart', 'showCouplet']) const emit = defineEmits(['lottery', 'couplet', 'showCouplet'])
const handleLottery = () => { const handleLottery = () => {
// //
@ -157,9 +160,44 @@ const handleCouplet = () => {
emit('couplet') emit('couplet')
} }
const handleRestart = () => {
emit('restart')
//
const preloadImages = async () => {
try {
const imageUrls = [
// EndPage
'/static/bg/bg_finish.jpg',
'/static/images/finish_title2.png',
'/static/images/finish_title.png',
'/static/images/gift1.png',
'/static/images/gift2.png',
'/static/images/gift3.png',
'/static/images/gift4.png',
'/static/images/gift5.png',
'/static/images/btn_lottery.png',
'/static/images/btn_ai.png',
// AICoupletForm
'/static/info/couplet_info_box.png',
'/static/images/btn_submit.png',
'/static/images/btn_close.png',
// LotteryFormModal
'/static/info/info_bg.jpg',
'/static/images/btn_back.png',
'/static/info/info_title.png',
'/static/info/info_tips.png'
]
await ImagePreloader.preloadAll(imageUrls)
console.log('EndPage 图片预加载完成')
} catch (error) {
console.error('EndPage 图片预加载失败:', error)
}
} }
//
onMounted(() => {
preloadImages()
})
</script> </script>
<style scoped> <style scoped>
@ -170,6 +208,7 @@ const handleRestart = () => {
.scene-section.active { .scene-section.active {
/* 当前活动场景样式 */ /* 当前活动场景样式 */
opacity: 1;
} }
/* 背景图片层 */ /* 背景图片层 */

View File

@ -168,7 +168,7 @@ const handleOverlayClick = () => {
height: auto; height: auto;
max-height: 600rpx; max-height: 600rpx;
object-fit: contain; object-fit: contain;
border-radius: 12rpx; border-radius: 20rpx;
display: block; display: block;
} }

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import FuClickArea from './FuClickArea.vue' import FuClickArea from './FuClickArea.vue'
import ImageGalleryModal from './ImageGalleryModal.vue' import ImageGalleryModal from './ImageGalleryModal.vue'
import ImagePreloader from '@/utils/preload';
// //
const props = defineProps({ const props = defineProps({
@ -20,19 +21,45 @@ const props = defineProps({
// //
const emit = defineEmits(['collect-seal']) const emit = defineEmits(['collect-seal'])
//
const sealCollected = ref(false)
// //
const fuClickAreaVisible = ref(true) const fuClickAreaVisible = ref(true)
const sq3ImageVisible = ref(false) const sq3ImageVisible = ref(false)
//
const imagesLoaded = ref(false)
const isPreloading = ref(false)
// //
const parallaxOffset = computed(() => { const parallaxOffset = computed(() => {
// 1/10 // 1/10
return props.scrollPosition * 0.1 return props.scrollPosition * 0.1
}) })
//
const preloadImages = async () => {
if (imagesLoaded.value || isPreloading.value) return
isPreloading.value = true
try {
//
const imageUrls = lfsImages.map(img => img.src)
// 使ImagePreloader
await ImagePreloader.preloadAll(imageUrls)
imagesLoaded.value = true
} catch (error) {
console.error('图片预加载失败:', error)
} finally {
isPreloading.value = false
}
}
//
watch(() => props.active, async (newActive) => {
if (newActive) {
await preloadImages()
}
})
// //
const handleFuClick = () => { const handleFuClick = () => {
fuClickAreaVisible.value = false fuClickAreaVisible.value = false
@ -42,7 +69,7 @@ const handleFuClick = () => {
// //
const getImageUrl = (name) => { const getImageUrl = (name) => {
return new URL(`/static/lfs/${name}.png`, import.meta.url).href return new URL(`/static/lfs/${name}.jpg`, import.meta.url).href
} }
// //
@ -74,6 +101,11 @@ onMounted(() => {
if (container) { if (container) {
container.classList.add('animate-in') container.classList.add('animate-in')
} }
//
if (props.active) {
preloadImages()
}
}) })
</script> </script>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import FuClickArea from './FuClickArea.vue' import FuClickArea from './FuClickArea.vue'
import ImagePreloader from '@/utils/preload';
// //
const props = defineProps({ const props = defineProps({
@ -19,19 +20,45 @@ const props = defineProps({
// //
const emit = defineEmits(['collect-seal']) const emit = defineEmits(['collect-seal'])
//
const sealCollected = ref(false)
// //
const fuClickAreaVisible = ref(true) const fuClickAreaVisible = ref(true)
const sq3ImageVisible = ref(false) const sq3ImageVisible = ref(false)
//
const imagesLoaded = ref(false)
const isPreloading = ref(false)
// //
const parallaxOffset = computed(() => { const parallaxOffset = computed(() => {
// 1/10 // 1/10
return props.scrollPosition * 0.1 return props.scrollPosition * 0.1
}) })
//
const preloadImages = async () => {
if (imagesLoaded.value || isPreloading.value) return
isPreloading.value = true
try {
//
const imageUrls = images.map(img => img.src)
// 使ImagePreloader
await ImagePreloader.preloadAll(imageUrls)
imagesLoaded.value = true
} catch (error) {
console.error('图片预加载失败:', error)
} finally {
isPreloading.value = false
}
}
//
watch(() => props.active, async (newActive) => {
if (newActive) {
await preloadImages()
}
})
// //
const handleFuClick = () => { const handleFuClick = () => {
fuClickAreaVisible.value = false fuClickAreaVisible.value = false
@ -39,12 +66,19 @@ const handleFuClick = () => {
emit('collect-seal') emit('collect-seal')
} }
// URL
const generateImageUrl = (name) => {
const url = new URL(`/static/wfj/${name}.jpg`, import.meta.url)
//
return url.href
}
// //
const images = [ const images = [
{ src: '/static/wfj/img1.png', title: '' }, { src: generateImageUrl('img1'), title: '' },
{ src: '/static/wfj/img2.png', title: '' }, { src: generateImageUrl('img2'), title: '' },
{ src: '/static/wfj/img3.png', title: '' }, { src: generateImageUrl('img3'), title: '' },
{ src: '/static/wfj/img4.png', title: '' } { src: generateImageUrl('img4'), title: '' }
] ]
const currentImageIndex = ref(0) const currentImageIndex = ref(0)
@ -64,6 +98,11 @@ onMounted(() => {
if (container) { if (container) {
container.classList.add('animate-in') container.classList.add('animate-in')
} }
//
if (props.active) {
preloadImages()
}
}) })
</script> </script>
@ -97,6 +136,9 @@ onMounted(() => {
<!-- 图片浏览组件 --> <!-- 图片浏览组件 -->
<div class="image-gallery"> <div class="image-gallery">
<div class="gallery-image-wrapper"> <div class="gallery-image-wrapper">
<div class="loading-overlay" v-if="isPreloading">
<div class="loading-spinner"></div>
</div>
<div class="nav-btn prev-btn" @click="prevImage"> <div class="nav-btn prev-btn" @click="prevImage">
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" /> <img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
</div> </div>
@ -384,7 +426,7 @@ onMounted(() => {
.gallery-image { .gallery-image {
width: 613rpx; width: 613rpx;
object-fit: cover; object-fit: cover;
border-radius: 12rpx; border-radius: 20%;
display: block; display: block;
} }
@ -430,4 +472,34 @@ onMounted(() => {
height: 80px; height: 80px;
} }
} }
/* 加载遮罩层 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
border-radius: 12rpx;
}
/* 加载动画 */
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style> </style>

BIN
static/lfs/img1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

BIN
static/lfs/img2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

BIN
static/lfs/img3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
static/wfj/img1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 KiB

BIN
static/wfj/img2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

BIN
static/wfj/img3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 KiB

BIN
static/wfj/img4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

128
utils/preload.js Normal file
View File

@ -0,0 +1,128 @@
// utils/preload.js
export class ImagePreloader {
constructor() {
this.cache = new Map();
}
/**
* 预加载单张图片
* @param {string} src - 图片地址
* @returns {Promise<boolean>}
*/
async preload(src) {
if (!src) return false;
// 检查缓存
if (this.cache.has(src)) {
return true;
}
// data URL 直接返回
if (src.startsWith('data:')) {
this.cache.set(src, true);
return true;
}
try {
// 本地图片和网络图片都需要真正加载
if (src.startsWith('/')) {
// 对于本地图片使用Image对象加载
await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = reject;
img.src = src;
});
this.cache.set(src, true);
console.log('本地图片预加载成功:', src);
return true;
} else {
// 对于网络图片使用uni.getImageInfo
await uni.getImageInfo({ src });
this.cache.set(src, true);
console.log('网络图片预加载成功:', src);
return true;
}
} catch (error) {
console.warn('图片预加载失败:', src, error);
this.cache.set(src, false);
return false;
}
}
/**
* 批量预加载图片
* @param {string[]} srcList - 图片地址数组
* @param {number} concurrency - 并发数
* @returns {Promise<Array<{src: string, success: boolean}>>}
*/
async preloadAll(srcList, concurrency = 3) {
const results = [];
const queue = [...srcList];
// 执行并发下载
const workers = [];
for (let i = 0; i < concurrency; i++) {
workers.push(this.worker(queue, results));
}
await Promise.all(workers);
return results;
}
async worker(queue, results) {
while (queue.length > 0) {
const src = queue.shift();
if (src) {
const success = await this.preload(src);
results.push({ src, success });
}
}
}
/**
* 清理缓存
*/
clearCache() {
this.cache.clear();
}
/**
* 预加载图片并设置到 img 标签
* @param {string} src - 图片地址
* @param {number} timeout - 超时时间
* @returns {Promise<string>} 返回图片临时路径
*/
async preloadToTemp(src, timeout = 10000) {
if (src.startsWith('/') || src.startsWith('data:')) {
return src;
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('图片下载超时'));
}, timeout);
uni.downloadFile({
url: src,
success: (res) => {
clearTimeout(timer);
if (res.statusCode === 200) {
const tempPath = res.tempFilePath;
this.cache.set(src, tempPath);
resolve(tempPath);
} else {
reject(new Error(`下载失败: ${res.statusCode}`));
}
},
fail: (error) => {
clearTimeout(timer);
reject(error);
}
});
});
}
}
// 创建单例
export default new ImagePreloader();