hldy_app_mini/component/public/donghua.vue

225 lines
6.9 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.

<!-- 使用示例 已经全局暴露直接用就行 注意这个组件的性能不如用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 一帧,但不要重复 scheduletickFrame 内部不依赖 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>