+
-
-
![slide]()
+
@@ -112,28 +99,21 @@
-
+
{{ formatNumber(currentIndex + 1) }}
/
{{ formatNumber(photos.length) }}
-
-
-
+
+
@@ -152,21 +132,19 @@ const emit = defineEmits(['exit', 'toggleFullscreen', 'update:index'])
// --- 状态管理 ---
const currentIndex = ref(props.initialIndex)
const isPlaying = ref(true)
-const isBuffering = ref(true) // 初始缓冲
+const isBuffering = ref(false)
+const isGlobalLoading = ref(true) // 全局加载状态
+const loadingProgress = ref(0)
+const loadedCount = ref(0)
+const totalCount = computed(() => props.photos.length)
+
const duration = ref(5000)
const progress = ref(0)
const showControls = ref(true)
const showSettings = ref(false)
const animationType = ref('random')
-const animLabels = {
- 'random': '随机特效',
- 'fade': '淡入淡出',
- 'zoom-fade': '镜头推进',
- 'slide-left': '左侧推入',
- 'wipe': '线性擦除',
- 'circle': '圆形揭示'
-}
+const animLabels = { 'random': '随机特效', 'fade': '淡入淡出', 'zoom-fade': '镜头推进', 'slide-left': '左侧推入', 'wipe': '线性擦除', 'circle': '圆形揭示' }
let lastFrameTime = 0
let accumulatedTime = 0
@@ -175,7 +153,8 @@ let controlsTimer = null
let isSwitching = false
const currentPhoto = computed(() => props.photos[currentIndex.value])
-const currentBg = computed(() => currentPhoto.value ? getImgUrl(currentPhoto.value.compressed) : '')
+// 加载期间给个默认背景或黑屏,避免报错
+const currentBg = computed(() => (currentPhoto.value && !isGlobalLoading.value) ? getImgUrl(currentPhoto.value.compressed) : '')
const pptTransitions = ['ppt-fade', 'ppt-zoom-fade', 'ppt-slide-left', 'ppt-wipe', 'ppt-circle']
const currentTransitionName = computed(() => {
@@ -194,17 +173,16 @@ const getCurrentAnimationClass = (index) => {
return 'anim-ken-burns-4'
}
-// const getImgUrl = (path) => path ? `http://i.nailaoyun.cn${path}` : ''
+// 图片路径修正
const getImgUrl = (path) => path ? `/t-oss${path}` : ''
const formatNumber = (num) => num < 10 ? `0${num}` : num
const formatDate = (dateStr) => {
if(!dateStr) return ''
return new Date(dateStr).toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' })
}
-
const getSliderPercent = (val, min, max) => ((val - min) / (max - min)) * 100
-// --- 核心:高级图片预加载器 ---
+// --- 预加载器 ---
const loadImage = (url) => {
return new Promise((resolve) => {
const img = new Image()
@@ -222,6 +200,27 @@ const loadImage = (url) => {
})
}
+// 全量预加载
+const preloadAllImages = async () => {
+ const promises = props.photos.map(photo => {
+ const url = getImgUrl(photo.original || photo.compressed)
+ return loadImage(url).then(() => {
+ loadedCount.value++
+ loadingProgress.value = Math.floor((loadedCount.value / totalCount.value) * 100)
+ })
+ })
+
+ await Promise.all(promises)
+
+ // 全部加载完成后,稍微延迟进入
+ setTimeout(() => {
+ isGlobalLoading.value = false
+ startPlay() // 开始播放
+ handleUserInteraction()
+ }, 500)
+}
+
+// 动态预加载队列 (用于播放中)
const preloadQueue = (index) => {
const range = [1, 2]
range.forEach(offset => {
@@ -233,13 +232,13 @@ const preloadQueue = (index) => {
})
}
-// --- 播放控制 ---
+// --- 播放逻辑 ---
const loop = (timestamp) => {
if (!lastFrameTime) lastFrameTime = timestamp
const deltaTime = timestamp - lastFrameTime
lastFrameTime = timestamp
- if (isPlaying.value && !isBuffering.value && !isSwitching) {
+ if (isPlaying.value && !isBuffering.value && !isSwitching && !isGlobalLoading.value) {
accumulatedTime += deltaTime
progress.value = Math.min((accumulatedTime / duration.value) * 100, 100)
if (accumulatedTime >= duration.value) {
@@ -252,13 +251,13 @@ const loop = (timestamp) => {
const handleAutoSwitch = async () => {
isSwitching = true
const nextIdx = (currentIndex.value + 1) % props.photos.length
+
+ // 即使预加载过,也检查一下(通常会立即返回)
const nextPhoto = props.photos[nextIdx]
const nextUrl = getImgUrl(nextPhoto.original || nextPhoto.compressed)
- const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 50)
-
+ const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 100)
await loadImage(nextUrl)
-
clearTimeout(bufferTimeout)
isBuffering.value = false
@@ -272,14 +271,8 @@ const startPlay = () => {
if (!rafId) rafId = requestAnimationFrame(loop)
}
-const pausePlay = () => {
- isPlaying.value = false
-}
-
-const togglePlay = () => {
- if (isPlaying.value) pausePlay()
- else startPlay()
-}
+const pausePlay = () => { isPlaying.value = false }
+const togglePlay = () => { isPlaying.value ? pausePlay() : startPlay() }
const nextSlide = (auto = false) => {
accumulatedTime = 0
@@ -295,12 +288,9 @@ const prevSlide = async () => {
const prevIdx = (currentIndex.value - 1 + props.photos.length) % props.photos.length
isSwitching = true
+ isBuffering.value = true
const prevPhoto = props.photos[prevIdx]
- const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 50)
-
await loadImage(getImgUrl(prevPhoto.original || prevPhoto.compressed))
-
- clearTimeout(bufferTimeout)
isBuffering.value = false
isSwitching = false
@@ -308,53 +298,30 @@ const prevSlide = async () => {
emit('update:index', currentIndex.value)
}
-const onDurationChange = () => {
- accumulatedTime = 0
- progress.value = 0
-}
+const onDurationChange = () => { accumulatedTime = 0; progress.value = 0 }
const handleUserInteraction = () => {
showControls.value = true
clearTimeout(controlsTimer)
-
if (isPlaying.value && !showSettings.value) {
- controlsTimer = setTimeout(() => {
- showControls.value = false
- }, 3000)
+ controlsTimer = setTimeout(() => { showControls.value = false }, 3000)
}
}
-const toggleSettings = () => {
- showSettings.value = !showSettings.value
- handleUserInteraction()
-}
-
-const closeSettings = () => {
- showSettings.value = false
-}
+const toggleSettings = () => { showSettings.value = !showSettings.value; handleUserInteraction() }
+const closeSettings = () => { showSettings.value = false }
const vClickOutside = {
mounted(el, binding) {
- el.clickOutsideEvent = (event) => {
- if (!(el === event.target || el.contains(event.target))) binding.value(event)
- }
+ el.clickOutsideEvent = (event) => { if (!(el === event.target || el.contains(event.target))) binding.value(event) }
document.body.addEventListener('click', el.clickOutsideEvent)
},
- unmounted(el) {
- document.body.removeEventListener('click', el.clickOutsideEvent)
- }
+ unmounted(el) { document.body.removeEventListener('click', el.clickOutsideEvent) }
}
-onMounted(async () => {
- if (props.photos.length > 0) {
- const curPhoto = props.photos[currentIndex.value]
- await loadImage(getImgUrl(curPhoto.original || curPhoto.compressed))
- }
- isBuffering.value = false
-
- rafId = requestAnimationFrame(loop)
- handleUserInteraction()
- preloadQueue(currentIndex.value)
+onMounted(() => {
+ // 不直接 startPlay,而是先 preloadAll
+ preloadAllImages()
})
onUnmounted(() => {
@@ -362,103 +329,68 @@ onUnmounted(() => {
clearTimeout(controlsTimer)
})
-watch(() => isPlaying.value, (val) => {
- handleUserInteraction()
-})
+watch(() => isPlaying.value, (val) => { handleUserInteraction() })
diff --git a/src/components/PhotoPlanet.vue b/src/components/PhotoPlanet.vue
index 7609aa2..4248dd7 100644
--- a/src/components/PhotoPlanet.vue
+++ b/src/components/PhotoPlanet.vue
@@ -6,15 +6,18 @@
:class="{ 'visible': isReady }"
>
-
+
-
+
+
+
- 正在构建影像宇宙... {{ loadingProgress }}%
-
已加载 {{ loadedCount }} / {{ totalCount }} 碎片
+ 初始化 宇宙...
+
{{ loadingProgress }}%
+
已装载 {{ loadedCount }} / {{ totalCount }} 影像碎片
@@ -78,7 +81,7 @@ let animationId
let photoMeshes = []
let particleSystem
-// const getImgUrl = (path) => path ? `http://i.nailaoyun.cn${path}` : ''
+// 更新图片路径前缀
const getImgUrl = (path) => path ? `/t-oss${path}` : ''
// --- 核心优化:图片压缩与纹理生成 (带 CORS 代理回退) ---
@@ -202,7 +205,7 @@ const initThree = async () => {
// 全部加载完成后,标记为就绪,触发淡入动画
setTimeout(() => {
isReady.value = true
- }, 500) // 稍微延迟一点,让进度条显示 100% 更有满足感
+ }, 500)
window.addEventListener('click', onMouseClick)
window.addEventListener('resize', onWindowResize)
@@ -210,63 +213,49 @@ const initThree = async () => {
animate()
}
-// --- 构建照片球体 (逻辑修复:真实进度追踪) ---
+// --- 构建照片球体 ---
const createPhotoSphere = async () => {
const vector = new THREE.Vector3()
const total = props.photos.length
-
- // 纹理加载 Promise 队列
const texturePromises = []
for (let i = 0; i < total; i++) {
const photo = props.photos[i]
-
- // 1. 创建 Mesh (先用白色占位,位置定好)
const phi = Math.acos(-1 + (2 * i) / total)
const theta = Math.sqrt(total * Math.PI) * phi
-
const radius = isMobile.value ? 300 : 380
const cardSize = isMobile.value ? 35 : 45
vector.setFromSphericalCoords(radius, phi, theta)
const geometry = new THREE.PlaneGeometry(cardSize, cardSize)
-
- // 初始透明,加载好纹理再显示
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
map: null,
transparent: true,
- opacity: 0 // 技巧:初始不可见
+ opacity: 0 // 初始不可见
})
const mesh = new THREE.Mesh(geometry, material)
mesh.position.copy(vector)
mesh.lookAt(new THREE.Vector3(0, 0, 0))
-
mesh.userData = { id: i, isPhoto: true }
scene.add(mesh)
photoMeshes.push(mesh)
- // 2. 发起异步纹理加载,并追踪进度
const p = createCompressedTexture(getImgUrl(photo.compressed)).then(texture => {
mesh.material.map = texture
- mesh.material.opacity = 1 // 纹理就绪,显示 Mesh
+ mesh.material.opacity = 1
mesh.material.needsUpdate = true
-
- // 更新真实进度
loadedCount.value++
loadingProgress.value = Math.floor((loadedCount.value / total) * 100)
})
texturePromises.push(p)
-
- // 分批处理 Mesh 创建,避免 UI 冻结
if (i % 10 === 0) await new Promise(r => setTimeout(r, 0))
}
- // 3. 等待所有纹理加载完毕
await Promise.all(texturePromises)
loadingProgress.value = 100
}
@@ -275,7 +264,6 @@ const createPhotoSphere = async () => {
const createParticles = () => {
const geometry = new THREE.BufferGeometry()
const vertices = []
-
for (let i = 0; i < 1500; i++) {
vertices.push(
THREE.MathUtils.randFloatSpread(1500),
@@ -283,9 +271,7 @@ const createParticles = () => {
THREE.MathUtils.randFloatSpread(1500)
)
}
-
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
-
const material = new THREE.PointsMaterial({
color: 0xe63946,
size: 2,
@@ -293,24 +279,17 @@ const createParticles = () => {
transparent: true,
opacity: 0.5
})
-
particleSystem = new THREE.Points(geometry, material)
scene.add(particleSystem)
}
-// --- 动画循环 ---
const animate = () => {
animationId = requestAnimationFrame(animate)
controls.update()
-
- if (particleSystem) {
- particleSystem.rotation.y += 0.0003
- }
-
+ if (particleSystem) particleSystem.rotation.y += 0.0003
renderer.render(scene, camera)
}
-// --- 交互事件 ---
const onWindowResize = () => {
if (!camera || !renderer) return
isMobile.value = window.innerWidth < 768
@@ -322,10 +301,8 @@ const onWindowResize = () => {
const onMouseClick = (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
-
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(photoMeshes)
-
if (intersects.length > 0) {
const target = intersects[0].object
if (target.userData.isPhoto) {
@@ -346,136 +323,66 @@ onUnmounted(() => {
window.removeEventListener('click', onMouseClick)
window.removeEventListener('resize', onWindowResize)
if (animationId) cancelAnimationFrame(animationId)
-
- if (scene) {
- scene.traverse((object) => {
- if (object.geometry) object.geometry.dispose()
- if (object.material) {
- if (object.material.map) object.material.map.dispose()
- object.material.dispose()
- }
- })
- }
+ if (scene) scene.clear()
if (renderer) renderer.dispose()
})