225 lines
6.9 KiB
Vue
225 lines
6.9 KiB
Vue
<!-- 使用示例 已经全局暴露直接用就行 注意!这个组件的性能不如用AE写的动画-->
|
||
<!-- <donghua :width="`1300rpx`" :height="`900rpx`" :links="blueArray" :playing="photoplay" :loop="true" :interval="120" /> -->
|
||
<!-- 注意看参数是什么意思 -->
|
||
<!-- 通用的生成函数 这个方法可以快速让你写出图片数组
|
||
function genPaths(base, prefix, count, ext = 'png', startIndex = 0, pad = false) {
|
||
return Array.from({ length: count }, (_, i) => {
|
||
const idx = pad
|
||
? String(i + startIndex).padStart(2, '0')
|
||
: i + startIndex
|
||
return `${base}/${prefix}${idx}.${ext}`
|
||
})
|
||
} -->
|
||
<!-- 数组的示例
|
||
const leftArray = ref(genPaths(
|
||
'/static/index/newindex/leftmenu',地址
|
||
'',图片前缀
|
||
3, // 一共加一起多少张图片
|
||
'png', 类型
|
||
0, // 起始索引
|
||
false // 不补零
|
||
)) -->
|
||
<template>
|
||
<view>
|
||
<image :src="displaySrc" :style="{ width: width, height: height }" :mode="objectFit" @error="handleError"
|
||
@load="handleLoad" />
|
||
<button v-if="showButton" @click="togglePlaying">
|
||
{{ playing ? '停止播放' : '开始播放' }}
|
||
</button>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||
|
||
/* ---------------- props ---------------- */
|
||
const props = defineProps({
|
||
links: {
|
||
type: Array as () => string[],
|
||
default: () => []
|
||
},
|
||
width: { type: String, default: '65rpx' },
|
||
height: { type: String, default: '65rpx' },
|
||
objectFit: { type: String, default: 'aspectFill' },
|
||
defaultImage: { type: String, default: '' },
|
||
interval: { type: Number, default: 80 }, // ms
|
||
playing: { type: Boolean, default: false },
|
||
showButton: { type: Boolean, default: false },
|
||
loop: { type: Boolean, default: false },
|
||
// 可选:每帧最大重试次数(失败后跳过)
|
||
maxRetryPerFrame: { type: Number, default: 1 }
|
||
})
|
||
|
||
const emit = defineEmits(['update:playing'])
|
||
|
||
/* ---------------- local state ---------------- */
|
||
const currentIndex = ref(0)
|
||
const internalPlaying = ref(false) // 与 props.playing 同步(主要用于内部判断)
|
||
const timer : { id : number | null } = { id: null } // 使用对象包装,方便类型
|
||
// 记录每帧失败次数
|
||
const frameRetries = new Map<number, number>()
|
||
// 记录已经判定为“失败很严重、需要显示默认图”的帧
|
||
const failedFrames = new Set<number>()
|
||
|
||
/* ---------------- helpers / computed ---------------- */
|
||
const hasLinks = computed(() => Array.isArray(props.links) && props.links.length > 0)
|
||
const displaySrc = computed(() => {
|
||
if (!hasLinks.value) {
|
||
return props.defaultImage || ''
|
||
}
|
||
// 如果该帧被标为失败则显示 defaultImage(兜底)
|
||
if (failedFrames.has(currentIndex.value) && props.defaultImage) {
|
||
return props.defaultImage
|
||
}
|
||
// 正常显示链接(或 defaultImage 如果索引越界)
|
||
const idx = currentIndex.value
|
||
return props.links[idx] || props.defaultImage || ''
|
||
})
|
||
|
||
/* ---------------- play control (使用递归 setTimeout) ---------------- */
|
||
function clearTimer() {
|
||
if (timer.id !== null) {
|
||
clearTimeout(timer.id)
|
||
timer.id = null
|
||
}
|
||
}
|
||
|
||
function scheduleNextTick() {
|
||
// 防止重复 schedule
|
||
if (timer.id !== null) return
|
||
timer.id = setTimeout(() => {
|
||
timer.id = null
|
||
tickFrame()
|
||
// 继续循环(如果仍在播放)
|
||
if (props.playing) {
|
||
// 若没有 links 则不再 schedule
|
||
if (hasLinks.value) {
|
||
scheduleNextTick()
|
||
}
|
||
}
|
||
}, Math.max(16, props.interval)) as unknown as number
|
||
}
|
||
|
||
function startPlay() {
|
||
if (!hasLinks.value) return
|
||
if (timer.id !== null) return
|
||
internalPlaying.value = true
|
||
scheduleNextTick()
|
||
}
|
||
|
||
function stopPlay() {
|
||
internalPlaying.value = false
|
||
clearTimer()
|
||
}
|
||
|
||
function tickFrame() {
|
||
// 当没有帧时什么也不做
|
||
if (!hasLinks.value) return
|
||
|
||
const len = props.links.length
|
||
if (len === 0) return
|
||
|
||
if (props.loop) {
|
||
// 循环播放
|
||
currentIndex.value = (currentIndex.value + 1) % len
|
||
} else {
|
||
// 非循环播放:到末尾停止并通知父组件
|
||
if (currentIndex.value < len - 1) {
|
||
currentIndex.value++
|
||
} else {
|
||
// 到尾了,停止播放
|
||
stopPlay()
|
||
// 告知父组件(保持 playing 为单一真相)
|
||
emit('update:playing', false)
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---------------- image event handlers ---------------- */
|
||
function handleError() {
|
||
// 记录重试次数并决定是否跳过该帧
|
||
const idx = currentIndex.value
|
||
const prev = frameRetries.get(idx) || 0
|
||
const nextCount = prev + 1
|
||
frameRetries.set(idx, nextCount)
|
||
|
||
if (nextCount > (props.maxRetryPerFrame || 0)) {
|
||
// 标记为失败帧,用 defaultImage 兜底并尝试跳到下一帧(避免卡住)
|
||
failedFrames.add(idx)
|
||
// 立即跳到下一帧(但不用强制立即 schedule — 因为 scheduleNextTick 会继续)
|
||
// 若你想立即切换也可以直接调用 tickFrame()
|
||
if (hasLinks.value) {
|
||
// 如果是 loop 模式或尚未到尾(非 loop),直接 advance,否则 stop
|
||
if (props.loop || currentIndex.value < props.links.length - 1) {
|
||
// 立即 advance 一帧,但不要重复 schedule(tickFrame 内部不依赖 timer)
|
||
tickFrame()
|
||
} else {
|
||
stopPlay()
|
||
emit('update:playing', false)
|
||
}
|
||
}
|
||
} else {
|
||
// 尝试跳过到下一帧(减少卡顿风险)
|
||
if (hasLinks.value) {
|
||
if (props.loop || currentIndex.value < props.links.length - 1) {
|
||
tickFrame()
|
||
} else {
|
||
stopPlay()
|
||
emit('update:playing', false)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleLoad() {
|
||
// 成功加载清理重试记录(该帧已正常)
|
||
const idx = currentIndex.value
|
||
frameRetries.delete(idx)
|
||
failedFrames.delete(idx)
|
||
}
|
||
|
||
/* ---------------- watch props ---------------- */
|
||
// 以 props.playing 为单一真相源;当 props.playing 改变时启动/停止
|
||
watch(() => props.playing, (val) => {
|
||
if (val) {
|
||
// reset index 到 0(可选,如果你希望保留进度可以去掉)
|
||
// currentIndex.value = 0
|
||
startPlay()
|
||
} else {
|
||
stopPlay()
|
||
// 保证视觉上复位到首帧(可选)
|
||
setTimeout(() => {
|
||
currentIndex.value = 0
|
||
}, 50)
|
||
}
|
||
})
|
||
|
||
// 监听 links 的引用变化(但不要 deep watch)
|
||
// 当父组件传入新的数组对象时触发:重置状态,清理 retry/failed 信息
|
||
watch(() => props.links, (newLinks, oldLinks) => {
|
||
// 如果引用相同且长度相同,尽量不重置(减少不必要抖动)
|
||
if (newLinks === oldLinks) return
|
||
// 重置索引和错误记录
|
||
currentIndex.value = 0
|
||
frameRetries.clear()
|
||
failedFrames.clear()
|
||
// 如果当前 props.playing 为 true,重启播放(先清理 timer 再开始)
|
||
if (props.playing) {
|
||
stopPlay()
|
||
// 延迟一点点再启动,避免连续多次触发时重复 schedule
|
||
setTimeout(() => {
|
||
if (props.playing) startPlay()
|
||
}, 30)
|
||
}
|
||
}, { immediate: false })
|
||
|
||
/* ---------------- toggle via internal button ---------------- */
|
||
function togglePlaying() {
|
||
emit('update:playing', !props.playing)
|
||
}
|
||
|
||
/* ---------------- cleanup ---------------- */
|
||
onUnmounted(() => {
|
||
clearTimer()
|
||
})
|
||
</script> |