hldy_app_mini/component/public/donghua.vue

225 lines
6.9 KiB
Vue
Raw Normal View History

2025-12-08 09:49:36 +08:00
<!-- 使用示例 已经全局暴露直接用就行 注意这个组件的性能不如用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 // 不补零
)) -->
2025-11-05 15:59:48 +08:00
<template>
<view>
2026-03-13 16:15:05 +08:00
<image :src="displaySrc" :style="{ width: width, height: height }" :mode="objectFit" @error="handleError"
@load="handleLoad" />
<button v-if="showButton" @click="togglePlaying">
2025-11-05 15:59:48 +08:00
{{ playing ? '停止播放' : '开始播放' }}
</button>
</view>
</template>
2026-03-13 16:15:05 +08:00
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue'
2025-11-05 15:59:48 +08:00
2026-03-13 16:15:05 +08:00
/* ---------------- props ---------------- */
2025-11-05 15:59:48 +08:00
const props = defineProps({
links: {
2026-03-13 16:15:05 +08:00
type: Array as () => string[],
2025-11-05 15:59:48 +08:00
default: () => []
},
2026-03-13 16:15:05 +08:00
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 }
2025-11-05 15:59:48 +08:00
})
const emit = defineEmits(['update:playing'])
2026-03-13 16:15:05 +08:00
/* ---------------- 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++
2025-11-05 15:59:48 +08:00
} else {
2026-03-13 16:15:05 +08:00
// 到尾了,停止播放
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()
2025-11-05 15:59:48 +08:00
} else {
stopPlay()
2026-03-13 16:15:05 +08:00
emit('update:playing', false)
2025-11-05 15:59:48 +08:00
}
}
2026-03-13 16:15:05 +08:00
} else {
// 尝试跳过到下一帧(减少卡顿风险)
if (hasLinks.value) {
if (props.loop || currentIndex.value < props.links.length - 1) {
tickFrame()
} else {
stopPlay()
emit('update:playing', false)
}
}
}
2025-11-05 15:59:48 +08:00
}
2026-03-13 16:15:05 +08:00
function handleLoad() {
// 成功加载清理重试记录(该帧已正常)
const idx = currentIndex.value
frameRetries.delete(idx)
failedFrames.delete(idx)
2025-11-05 15:59:48 +08:00
}
2026-03-13 16:15:05 +08:00
/* ---------------- watch props ---------------- */
// 以 props.playing 为单一真相源;当 props.playing 改变时启动/停止
2025-11-05 15:59:48 +08:00
watch(() => props.playing, (val) => {
if (val) {
2026-03-13 16:15:05 +08:00
// reset index 到 0可选如果你希望保留进度可以去掉
// currentIndex.value = 0
2025-11-05 15:59:48 +08:00
startPlay()
} else {
stopPlay()
2026-03-13 16:15:05 +08:00
// 保证视觉上复位到首帧(可选)
setTimeout(() => {
currentIndex.value = 0
}, 50)
2025-11-05 15:59:48 +08:00
}
})
2026-03-13 16:15:05 +08:00
// 监听 links 的引用变化(但不要 deep watch
// 当父组件传入新的数组对象时触发:重置状态,清理 retry/failed 信息
watch(() => props.links, (newLinks, oldLinks) => {
// 如果引用相同且长度相同,尽量不重置(减少不必要抖动)
if (newLinks === oldLinks) return
// 重置索引和错误记录
2025-11-05 15:59:48 +08:00
currentIndex.value = 0
2026-03-13 16:15:05 +08:00
frameRetries.clear()
failedFrames.clear()
// 如果当前 props.playing 为 true重启播放先清理 timer 再开始)
if (props.playing) {
2025-11-05 15:59:48 +08:00
stopPlay()
2026-03-13 16:15:05 +08:00
// 延迟一点点再启动,避免连续多次触发时重复 schedule
setTimeout(() => {
if (props.playing) startPlay()
}, 30)
2025-11-05 15:59:48 +08:00
}
2026-03-13 16:15:05 +08:00
}, { immediate: false })
/* ---------------- toggle via internal button ---------------- */
function togglePlaying() {
emit('update:playing', !props.playing)
}
2025-11-05 15:59:48 +08:00
2026-03-13 16:15:05 +08:00
/* ---------------- cleanup ---------------- */
2025-11-05 15:59:48 +08:00
onUnmounted(() => {
2026-03-13 16:15:05 +08:00
clearTimer()
2025-11-05 15:59:48 +08:00
})
</script>