v1.2.1
1、api增加发布/调试的api域名 2、浏览的图片改为使用jpg格式 3、对联图片加载增加loading提示 4、东直门、弹窗、隆福寺、王府井模块动态加载的图片需要进行预加载处理
|
|
@ -4,7 +4,9 @@
|
|||
*/
|
||||
|
||||
// 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) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,18 @@
|
|||
<div class="couplet-display">
|
||||
<!-- 显示海报图片(后端直接返回) -->
|
||||
<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>
|
||||
|
||||
|
|
@ -25,6 +36,8 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
|
|
@ -40,6 +53,23 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -80,11 +110,53 @@ const emit = defineEmits(['close'])
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.poster-image img {
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 18px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<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 ImagePreloader from '@/utils/preload'
|
||||
|
||||
// 获取 Vue 实例
|
||||
const instance = vueGetCurrentInstance()
|
||||
|
|
@ -19,9 +20,6 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['collect-seal'])
|
||||
|
||||
// 是否收集福印
|
||||
const sealCollected = ref(false)
|
||||
|
||||
// 福字点击区域状态
|
||||
const fuClickAreaVisible = ref(true)
|
||||
const sq3ImageVisible = ref(false)
|
||||
|
|
@ -38,10 +36,15 @@ const handleFuClick = () => {
|
|||
emit('collect-seal')
|
||||
}
|
||||
|
||||
// 动态加载东直门场景图片
|
||||
const getDzmImageUrl = (name) => {
|
||||
return new URL(`/static/dzm/${name}`, import.meta.url).href
|
||||
}
|
||||
|
||||
// 拖拽状态
|
||||
const isDragging = ref(false)
|
||||
const showDuck = ref(true) // 调试时默认显示鸭子
|
||||
const deskImage = ref('/static/dzm/img_desk1.png')
|
||||
const deskImage = ref(getDzmImageUrl('img_desk1.png'))
|
||||
const showGuideElements = ref(true)
|
||||
|
||||
// Canvas 触摸是否禁用
|
||||
|
|
@ -112,6 +115,10 @@ const targetArea = ref({
|
|||
// 动画帧 ID
|
||||
const animationId = ref(null)
|
||||
|
||||
// 图片加载状态
|
||||
const imagesLoaded = ref(false)
|
||||
const isPreloading = ref(false)
|
||||
|
||||
// 初始化 Canvas
|
||||
const initCanvas = () => {
|
||||
console.log('正在初始化 Canvas...')
|
||||
|
|
@ -135,7 +142,7 @@ const initCanvas = () => {
|
|||
// 加载鸭子图片
|
||||
const loadDuckImage = () => {
|
||||
uni.getImageInfo({
|
||||
src: '/static/dzm/img_duck.png',
|
||||
src: getDzmImageUrl('img_duck.png'),
|
||||
success: (res) => {
|
||||
console.log('鸭子图片加载成功:', res)
|
||||
duckImagePath.value = res.path
|
||||
|
|
@ -434,7 +441,7 @@ const handleTouchEnd = () => {
|
|||
if (checkDuckInTarget()) {
|
||||
console.log('成功放入目标区域!')
|
||||
// 更换餐桌图片
|
||||
deskImage.value = '/static/dzm/img_desk2.png'
|
||||
deskImage.value = getDzmImageUrl('img_desk2.png')
|
||||
// 隐藏引导元素
|
||||
showGuideElements.value = false
|
||||
// 隐藏鸭子
|
||||
|
|
@ -460,6 +467,33 @@ const resetDuckPosition = () => {
|
|||
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) => {
|
||||
console.log('开始拖拽', e)
|
||||
|
|
@ -479,6 +513,13 @@ const startDrag = (e) => {
|
|||
handleTouchStart(e)
|
||||
}
|
||||
|
||||
// 监听激活状态变化,当组件激活时预加载图片
|
||||
watch(() => props.active, async (newActive) => {
|
||||
if (newActive) {
|
||||
await preloadImages()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面挂载时的初始化
|
||||
onMounted(() => {
|
||||
// 添加动画类,触发入场动画
|
||||
|
|
@ -487,6 +528,9 @@ onMounted(() => {
|
|||
container.classList.add('animate-in')
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
preloadImages()
|
||||
|
||||
// 调试时自动初始化 Canvas
|
||||
setTimeout(() => {
|
||||
initCanvas()
|
||||
|
|
@ -531,8 +575,8 @@ onUnmounted(() => {
|
|||
|
||||
<!-- 装饰图片 -->
|
||||
<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 v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
|
||||
<image :src="getDzmImageUrl('img_stove1.png')" alt="灶台" class="deco-img stove-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" />
|
||||
|
||||
<!-- Canvas 拖拽区域 -->
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import ImagePreloader from '@/utils/preload'
|
||||
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
|
|
@ -133,7 +136,7 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['lottery', 'couplet', 'restart', 'showCouplet'])
|
||||
const emit = defineEmits(['lottery', 'couplet', 'showCouplet'])
|
||||
|
||||
const handleLottery = () => {
|
||||
// 如果已经提交过,直接提示
|
||||
|
|
@ -157,9 +160,44 @@ const handleCouplet = () => {
|
|||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -170,6 +208,7 @@ const handleRestart = () => {
|
|||
|
||||
.scene-section.active {
|
||||
/* 当前活动场景样式 */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 背景图片层 */
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const handleOverlayClick = () => {
|
|||
height: auto;
|
||||
max-height: 600rpx;
|
||||
object-fit: contain;
|
||||
border-radius: 12rpx;
|
||||
border-radius: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import FuClickArea from './FuClickArea.vue'
|
||||
import ImageGalleryModal from './ImageGalleryModal.vue'
|
||||
import ImagePreloader from '@/utils/preload';
|
||||
|
||||
// 组件属性
|
||||
const props = defineProps({
|
||||
|
|
@ -20,19 +21,45 @@ const props = defineProps({
|
|||
// 组件事件
|
||||
const emit = defineEmits(['collect-seal'])
|
||||
|
||||
// 是否收集福印
|
||||
const sealCollected = ref(false)
|
||||
|
||||
// 福字点击区域状态
|
||||
const fuClickAreaVisible = ref(true)
|
||||
const sq3ImageVisible = ref(false)
|
||||
|
||||
// 图片加载状态
|
||||
const imagesLoaded = ref(false)
|
||||
const isPreloading = ref(false)
|
||||
|
||||
// 计算视差效果的偏移量
|
||||
const parallaxOffset = computed(() => {
|
||||
// 滚动位置的1/10作为视差偏移
|
||||
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 = () => {
|
||||
fuClickAreaVisible.value = false
|
||||
|
|
@ -42,7 +69,7 @@ const handleFuClick = () => {
|
|||
|
||||
// 动态加载图片
|
||||
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) {
|
||||
container.classList.add('animate-in')
|
||||
}
|
||||
|
||||
// 如果组件已经激活,立即预加载图片
|
||||
if (props.active) {
|
||||
preloadImages()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import FuClickArea from './FuClickArea.vue'
|
||||
import ImagePreloader from '@/utils/preload';
|
||||
|
||||
// 组件属性
|
||||
const props = defineProps({
|
||||
|
|
@ -19,19 +20,45 @@ const props = defineProps({
|
|||
// 组件事件
|
||||
const emit = defineEmits(['collect-seal'])
|
||||
|
||||
// 是否收集福印
|
||||
const sealCollected = ref(false)
|
||||
|
||||
// 福字点击区域状态
|
||||
const fuClickAreaVisible = ref(true)
|
||||
const sq3ImageVisible = ref(false)
|
||||
|
||||
// 图片加载状态
|
||||
const imagesLoaded = ref(false)
|
||||
const isPreloading = ref(false)
|
||||
|
||||
// 计算视差效果的偏移量
|
||||
const parallaxOffset = computed(() => {
|
||||
// 滚动位置的1/10作为视差偏移
|
||||
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 = () => {
|
||||
fuClickAreaVisible.value = false
|
||||
|
|
@ -39,12 +66,19 @@ const handleFuClick = () => {
|
|||
emit('collect-seal')
|
||||
}
|
||||
|
||||
// 生成带时间戳的图片URL(避免缓存)
|
||||
const generateImageUrl = (name) => {
|
||||
const url = new URL(`/static/wfj/${name}.jpg`, import.meta.url)
|
||||
// 添加时间戳参数避免浏览器缓存
|
||||
return url.href
|
||||
}
|
||||
|
||||
// 图片浏览数据
|
||||
const images = [
|
||||
{ src: '/static/wfj/img1.png', title: '' },
|
||||
{ src: '/static/wfj/img2.png', title: '' },
|
||||
{ src: '/static/wfj/img3.png', title: '' },
|
||||
{ src: '/static/wfj/img4.png', title: '' }
|
||||
{ src: generateImageUrl('img1'), title: '' },
|
||||
{ src: generateImageUrl('img2'), title: '' },
|
||||
{ src: generateImageUrl('img3'), title: '' },
|
||||
{ src: generateImageUrl('img4'), title: '' }
|
||||
]
|
||||
const currentImageIndex = ref(0)
|
||||
|
||||
|
|
@ -64,6 +98,11 @@ onMounted(() => {
|
|||
if (container) {
|
||||
container.classList.add('animate-in')
|
||||
}
|
||||
|
||||
// 如果组件已经激活,立即预加载图片
|
||||
if (props.active) {
|
||||
preloadImages()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -97,6 +136,9 @@ onMounted(() => {
|
|||
<!-- 图片浏览组件 -->
|
||||
<div class="image-gallery">
|
||||
<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">
|
||||
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
|
||||
</div>
|
||||
|
|
@ -384,7 +426,7 @@ onMounted(() => {
|
|||
.gallery-image {
|
||||
width: 613rpx;
|
||||
object-fit: cover;
|
||||
border-radius: 12rpx;
|
||||
border-radius: 20%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
@ -430,4 +472,34 @@ onMounted(() => {
|
|||
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>
|
||||
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 430 KiB |
BIN
static/logo.png
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 494 KiB |
|
|
@ -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();
|
||||