<template>
	<view
		v-if="visible"
		class="iv-root"
		:class="{ 'iv-root--closing': animPhase === 'exiting' }"
		:style="{ ...rootStyle, ...animationStyles.root }"
		@click="animPhase !== 'exiting' && onOverlayClick()"
	>
		<view
			class="iv-backdrop"
			:class="{ 'iv-backdrop--expanded': expanded, 'iv-backdrop--closing': animPhase === 'exiting' }"
			:style="animationStyles.backdrop"
			@click.stop
		/>
		<!-- 图片层 -->
		<view
			class="iv-transform-wrap"
			:style="{ ...transformWrapStyle, ...animationStyles.transformWrap }"
			@click.stop="animPhase !== 'exiting' && requestClose()"
		>
			<!-- 单图 / 未展开 / 正在关闭 - 单块内容，多图关闭时也走缩小动画 -->
			<view
				v-if="showSingleContent"
				class="iv-pinch-wrap"
				:style="pinchWrapStyle"
				@touchstart="onPinchTouchStart"
				@touchmove="onPinchTouchMove"
				@touchend="onPinchTouchEnd"
				@touchcancel="onPinchTouchEnd"
			>
				<image 
					:src="currentUrl" 
					class="iv-img" 
					mode="aspectFit"
					@load="onImageLoad(currentIndex)"
					@error="onImageError(currentIndex)"
				/>
				<!-- 加载失败提示 -->
				<view v-if="imageLoadStates[currentIndex] === 'error'" class="iv-error">
					<text class="iv-error-text">图片加载失败</text>
				</view>
			</view>
			<!-- 多图已展开  -->
			<view v-else-if="showSwiper" class="iv-slides-view">
				<view class="iv-slides-track" :style="slidesTrackStyle">
					<view
						v-for="(url, i) in urls"
						:key="i"
						class="iv-slide"
						:style="{ width: slideWidthPx + 'px' }"
						:v-show="isSlideVisible(i)"
					>
						<view
							class="iv-pinch-wrap"
							:style="getPinchWrapStyleForIndex(i)"
							@touchstart="onSlideTouchStart"
							@touchmove="onSlideTouchMove"
							@touchend="onSlideTouchEnd"
							@touchcancel="onSlideTouchEnd"
						>
							<image 
								:src="url" 
								class="iv-slide-img" 
								mode="aspectFit"
								@load="onImageLoad(i)"
								@error="onImageError(i)"
							/>
							<!-- 加载失败提示 -->
							<view v-if="imageLoadStates[i] === 'error'" class="iv-error">
								<text class="iv-error-text">图片加载失败</text>
							</view>
						</view>
					</view>
				</view>
			</view>
		</view>
		<!-- 加载指示器：放 transform-wrap 外侧，避免被 blur 模糊掉，延迟显示避免缓存命中闪一下 -->
		<view
			v-if="expanded && showLoadingOverlay && animPhase !== 'exiting'"
			class="iv-loading"
		>
			<view class="iv-loading-spinner"></view>
		</view>
		<view v-if="showSwiper" class="iv-dots-wrap" @click.stop="requestClose">
			<view class="iv-dots">
				<text class="iv-dots__text">{{ currentIndex + 1 }} / {{ urls.length }}</text>
			</view>
		</view>
	</view>
</template>

<script setup lang="ts">
/**
 * ImageViewer：单图/多图、打开从原图尺寸到全屏、关闭缩回+渐隐。
 * - 单图：一块 pinch 区域，双指缩放/单指拖动。
 * - 多图：自定义滑动条（非原生 swiper），v-show 只显示相邻项；每张图独立管理缩放；未放大时跟手滑动，放大时边缘再切图。
 */
import { ref, computed, watch, inject, nextTick, onUnmounted, reactive } from 'vue'

export type InitialRect = { left: number; top: number; width: number; height: number }

// ==================== 配置常量 ====================
/** 最小缩放比例 */
const MIN_PINCH_SCALE = 1
/** 最大缩放比例 */
const MAX_PINCH_SCALE = 8
/** 双指最小距离（像素），小于此距离不响应缩放 */
const MIN_PINCH_DIST = 24
/** 边缘滑动切换图片的阈值（像素） */
const EDGE_OVERFLOW_THRESHOLD = 28
/** 边缘检测的边距（像素） */
const EDGE_MARGIN = 2
/** 滑动切换图片的拖拽阈值（像素） */
const SLIDE_DRAG_THRESHOLD = 60
/** 滑动动画时长（毫秒） */
const SLIDE_ANIMATION_MS = 200
/** 滑动动画缓动函数（快进慢出） */
const SLIDE_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'
/** 滑动拖拽系数，>1 更轻便，<1 更阻尼 */
const SLIDE_DRAG_FACTOR = 1.35
/** 打开时模糊+透明度动画时长（秒） */
const OPEN_BLUR_S = 0.25
/** 打开时最大模糊值（像素） */
const OPEN_BLUR_MAX = 20
/** 打开动画缓动函数 */
const OPEN_ANIMATION_EASE = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
/** 关闭时模糊+透明度动画时长（秒） */
const EXIT_BLUR_S = 0.4
/** 关闭时最大模糊值（像素） */
const EXIT_BLUR_MAX = 100
/** 加载指示器延迟显示（毫秒），避免缓存命中时闪一下 */
const LOADING_INDICATOR_DELAY_MS = 100

const props = withDefaults(
	defineProps<{
		visible: boolean
		urls: string[]
		current?: number
		initialRect?: InitialRect | null
		zIndex?: number
	}>(),
	{ current: 0, initialRect: null, zIndex: 9999 }
)

const emit = defineEmits<{
	close: []
}>()

const tabRootBackPress = inject<{
	register: (h: { check: () => boolean; close: () => void }) => () => void
}>('tabRootBackPress', undefined as any)

// ==================== 动画状态机 ====================
/**
 * 动画流程：启动动画 -> 模糊按加载状态 -> 加载完成后模糊动画 -> 退出动画
 * - entering: 根/背景/内容 opacity 0→1，内容保持 blur(20)
 * - displaying: 透明度已到 1，内容 blur 由加载状态决定：loading 保持 20px，loaded 时 blur 20→0
 * - exiting: blur(100) + opacity 0
 * 注意：首帧必须先渲染 opacity:0，下一帧再应用 opacity:1+transition，否则浏览器不会播放透明度过渡。
 */
type AnimPhase = 'entering' | 'displaying' | 'exiting'
const animPhase = ref<AnimPhase | null>(null)
/** 进入阶段是否已过首帧，用于延迟应用「目标样式」以触发 CSS transition */
const enterPaintReady = ref(false)
/** 目标样式首帧已绘制后为 true，此后 blur 随 loaded 变化，保证跟手 */
const enterTargetPainted = ref(false)
let enterCompleteTimer: ReturnType<typeof setTimeout> | null = null
let exitCompleteTimer: ReturnType<typeof setTimeout> | null = null

function clearAnimTimers() {
	if (enterCompleteTimer) {
		clearTimeout(enterCompleteTimer)
		enterCompleteTimer = null
	}
	if (exitCompleteTimer) {
		clearTimeout(exitCompleteTimer)
		exitCompleteTimer = null
	}
}

/** 下一帧执行（小程序等环境无 requestAnimationFrame，用 setTimeout 兜底） */
function nextFrame(fn: () => void) {
	if (typeof requestAnimationFrame !== 'undefined') {
		requestAnimationFrame(fn)
	} else {
		setTimeout(fn, 0)
	}
}

function startEnterPhase() {
	animPhase.value = 'entering'
	enterPaintReady.value = false
	enterTargetPainted.value = false
	clearAnimTimers()
	nextFrame(() => {
		enterPaintReady.value = true
		nextFrame(() => {
			enterTargetPainted.value = true
		})
	})
	enterCompleteTimer = setTimeout(() => {
		enterCompleteTimer = null
		animPhase.value = 'displaying'
	}, OPEN_BLUR_S * 1000)
}

function startExitPhase(onComplete: () => void) {
	animPhase.value = 'exiting'
	clearAnimTimers()
	exitCompleteTimer = setTimeout(() => {
		exitCompleteTimer = null
		onComplete()
	}, EXIT_BLUR_S * 1000)
}

// ==================== 响应式状态 ====================
const expanded = ref(false)
const currentIndex = ref(0)
const windowSize = ref({ width: 375, height: 667 })
let unregisterBack: (() => void) | null = null

// ==================== 图片加载状态 ====================
/** 每张图片的加载状态：loading | loaded | error */
const imageLoadStates = reactive<Record<number, 'loading' | 'loaded' | 'error'>>({})

/** 当前显示的图片是否已加载完成 */
const isCurrentImageLoaded = computed(() => {
	return imageLoadStates[currentIndex.value] === 'loaded'
})

/** 当前有内容可展示（加载中/已加载/错误），用于显示内容区，否则父级 opacity:0 会隐藏加载动画 */
const hasContentToShow = computed(() => {
	const state = imageLoadStates[currentIndex.value]
	return state === 'loading' || state === 'loaded' || state === 'error'
})

/** 加载指示器是否显示：进入 loading 后延迟 0.1s 再显示，避免重新点开时闪一下 */
const showLoadingOverlay = ref(false)
let loadingIndicatorTimer: ReturnType<typeof setTimeout> | null = null

function clearLoadingIndicatorTimer() {
	if (loadingIndicatorTimer) {
		clearTimeout(loadingIndicatorTimer)
		loadingIndicatorTimer = null
	}
}

// 双指缩放与单指平移
const pinchScale = ref(1)
const pinchX = ref(0)
const pinchY = ref(0)
// ==================== 状态管理 ====================
/** 单指拖动目标位置（用于状态同步） */
let targetPanX = 0
let targetPanY = 0
let targetDragOffset = 0

const DEFAULT_PINCH = { scale: 1, x: 0, y: 0 } as const

// ==================== 手势状态 ====================
/** 双指缩放开始状态 */
let pinchStart: {
	distance: number
	centerX: number
	centerY: number
	baseScale: number
	baseX: number
	baseY: number
	lastCenterX?: number
} | null = null

/** 单指拖动开始状态 */
let panStart: {
	startX: number
	startY: number
	baseX: number
	baseY: number
	lastX: number
	lastY: number
	baseDragOffset?: number
} | null = null

/** 边缘滑动溢出累计值 */
let edgeOverflow = 0

// ==================== 多图状态管理 ====================
/** 每张图独立的缩放/平移状态 */
const pinchStateByIndex = reactive<Record<number, { scale: number; x: number; y: number }>>({})

function getPinchState(i: number) {
	return pinchStateByIndex[i] ?? DEFAULT_PINCH
}

function setPinchState(i: number, s: { scale: number; x: number; y: number }) {
	pinchStateByIndex[i] = { ...s }
}

// ==================== 滑动条状态 ====================
/** 滑动条拖拽偏移量（像素） */
const dragOffsetPx = ref(0)
/** 是否启用滑动过渡动画 */
const slideTransitionOn = ref(false)

const slideWidthPx = computed(() => windowSize.value.width)
const slidesTrackStyle = computed(() => {
	const w = windowSize.value.width
	const n = props.urls?.length ?? 0
	const tx = n <= 0 ? 0 : -currentIndex.value * w + dragOffsetPx.value
	const trans = slideTransitionOn.value ? `transform ${SLIDE_ANIMATION_MS}ms ${SLIDE_EASE}` : 'none'
	return {
		width: `${n * w}px`,
		transform: `translateX(${tx}px)`,
		transition: trans
	}
})
function isSlideVisible(i: number) {
	const c = currentIndex.value
	return i >= c - 1 && i <= c + 1
}
function getPinchWrapStyleForIndex(i: number) {
	const s = getPinchState(i)
	return { transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale})` }
}

const pinchWrapStyle = computed(() => ({
	transform: `translate(${pinchX.value}px, ${pinchY.value}px) scale(${pinchScale.value})`
}))

// ==================== 边界计算 ====================
/** 根据缩放与视口算平移边界（单图/多图共用） */
function getBounds(scale: number, w: number, h: number) {
	const maxX = (w * (scale - 1)) / 2
	const maxY = (h * (scale - 1)) / 2
	return { maxX: Math.max(0, maxX), maxY: Math.max(0, maxY) }
}

function getPinchBounds() {
	const { width: w, height: h } = windowSize.value
	return getBounds(pinchScale.value, w, h)
}

/** 缩放变化后把平移限制在合法范围内，避免缩小后卡住 */
function clampTranslateToBounds() {
	const s = pinchScale.value
	if (s <= 1) {
		pinchX.value = 0
		pinchY.value = 0
		return
	}
	const { maxX, maxY } = getPinchBounds()
	pinchX.value = Math.max(-maxX, Math.min(maxX, pinchX.value))
	pinchY.value = Math.max(-maxY, Math.min(maxY, pinchY.value))
}

function getDistance(touches: { clientX: number; clientY: number }[]) {
	if (touches.length < 2) return 0
	const dx = touches[1].clientX - touches[0].clientX
	const dy = touches[1].clientY - touches[0].clientY
	return Math.hypot(dx, dy) || 1
}
function getCenter(touches: { clientX: number; clientY: number }[]) {
	if (touches.length < 2) return { x: touches[0].clientX, y: touches[0].clientY }
	return {
		x: (touches[0].clientX + touches[1].clientX) / 2,
		y: (touches[0].clientY + touches[1].clientY) / 2
	}
}

// ==================== 工具函数 ====================
/** 限制值在范围内 */
function clamp(value: number, min: number, max: number): number {
	return Math.max(min, Math.min(max, value))
}

/** 计算并限制平移位置在边界内 */
function clampPosition(x: number, y: number, maxX: number, maxY: number): { x: number; y: number } {
	return {
		x: clamp(x, -maxX, maxX),
		y: clamp(y, -maxY, maxY)
	}
}

/** 边缘滑动切图：在贴边且朝切图方向滑动时累加，达到阈值返回 'prev'|'next'，否则重置并返回 null */
function tryEdgeSwitch(newX: number, maxX: number, deltaX: number): 'prev' | 'next' | null {
	if (maxX <= EDGE_MARGIN) return null
	if (newX >= maxX - EDGE_MARGIN && deltaX < 0) {
		edgeOverflow += Math.abs(deltaX)
		if (edgeOverflow >= EDGE_OVERFLOW_THRESHOLD) {
			edgeOverflow = 0
			return 'next'
		}
		return null
	}
	if (newX <= -maxX + EDGE_MARGIN && deltaX > 0) {
		edgeOverflow += Math.abs(deltaX)
		if (edgeOverflow >= EDGE_OVERFLOW_THRESHOLD) {
			edgeOverflow = 0
			return 'prev'
		}
		return null
	}
	edgeOverflow = 0
	return null
}

// ==================== 手势处理 ====================
/** 处理双指缩放（以双指中心为缩放中心） */
function handlePinchZoom(touches: any[], isMultiMode: boolean, curIdx?: number) {
	if (!pinchStart) return
	
	const dist = Math.max(getDistance(touches), MIN_PINCH_DIST)
	const center = getCenter(touches)
	const baseDist = Math.max(pinchStart.distance, MIN_PINCH_DIST)
	const scale = clamp((dist / baseDist) * pinchStart.baseScale, MIN_PINCH_SCALE, MAX_PINCH_SCALE)
	
	const { width: w, height: h } = windowSize.value
	const scaleRatio = scale / pinchStart.baseScale
	
	// 计算以双指中心为缩放中心的新位置
	// 缩放前：图片中心在 baseX, baseY，双指中心在 centerX, centerY
	// 缩放后：要保持双指中心在图片上的相对位置不变
	// newX = baseX * scaleRatio + (centerX - w/2) * (1 - scaleRatio) + (centerX - startCenterX)
	const wHalf = w / 2
	const hHalf = h / 2
	
	// 双指中心的移动量
	const deltaCenterX = center.x - pinchStart.centerX
	const deltaCenterY = center.y - pinchStart.centerY
	
	// 以双指中心为缩放中心：缩放时，双指中心在图片上的相对位置保持不变
	// 缩放前双指中心相对于图片中心的偏移：(centerX - w/2 - baseX)
	// 缩放后这个偏移应该保持不变，所以：newX = centerX - w/2 - (centerX - w/2 - baseX) * scaleRatio
	// newX = centerX - w/2 - centerX * scaleRatio + w/2 * scaleRatio + baseX * scaleRatio
	//      newX = centerX * (1 - scaleRatio) + baseX * scaleRatio + w/2 * (scaleRatio - 1)
	//      newX = baseX * scaleRatio + (centerX - w/2) * (1 - scaleRatio)
	const newX = pinchStart.baseX * scaleRatio + (pinchStart.centerX - wHalf) * (1 - scaleRatio) + deltaCenterX
	const newY = pinchStart.baseY * scaleRatio + (pinchStart.centerY - hHalf) * (1 - scaleRatio) + deltaCenterY
	
	const { maxX, maxY } = getBounds(scale, w, h)
	const { x, y } = clampPosition(newX, newY, maxX, maxY)
	
	if (isMultiMode && curIdx !== undefined) {
		setPinchState(curIdx, { scale, x, y })
		if (scale > 1 && pinchStart.lastCenterX != null) {
			const dc = center.x - pinchStart.lastCenterX
			pinchStart.lastCenterX = center.x
			const dir = tryEdgeSwitch(x, maxX, dc)
			if (dir === 'prev') goToSlide(-1)
			else if (dir === 'next') goToSlide(1)
		}
	} else {
		pinchScale.value = scale
		pinchX.value = x
		pinchY.value = y
		targetPanX = x
		targetPanY = y
		clampTranslateToBounds()
	}
}

/** 处理单指拖动（放大状态） */
function handlePanDrag(t: { clientX: number; clientY: number }, isMultiMode: boolean, curIdx?: number): number {
	if (!panStart) return 0
	
	const deltaDx = t.clientX - panStart.lastX
	panStart.lastX = t.clientX
	panStart.lastY = t.clientY
	
	if (isMultiMode && curIdx !== undefined) {
		const curState = getPinchState(curIdx)
		if (curState.scale <= 1) return deltaDx
		
		const { width: w, height: h } = windowSize.value
		const { maxX, maxY } = getBounds(curState.scale, w, h)
		const rawX = panStart.baseX + (t.clientX - panStart.startX)
		const rawY = panStart.baseY + (t.clientY - panStart.startY)
		const { x, y } = clampPosition(rawX, rawY, maxX, maxY)
		
		setPinchState(curIdx, { ...curState, x, y })
		return deltaDx
	} else {
		const scale = pinchScale.value
		if (scale <= 1) return deltaDx
		
		const { maxX, maxY } = getPinchBounds()
		const rawX = panStart.baseX + (t.clientX - panStart.startX)
		const rawY = panStart.baseY + (t.clientY - panStart.startY)
		const { x, y } = clampPosition(rawX, rawY, maxX, maxY)
		
		pinchX.value = x
		pinchY.value = y
		targetPanX = x
		targetPanY = y
		return deltaDx
	}
}

/** 处理单指滑动（未放大状态，多图模式） */
function handleSlideDrag(t: { clientX: number; clientY: number }): void {
	if (!panStart) return
	
	const dx = t.clientX - panStart.startX
	const w = windowSize.value.width
	const raw = (panStart.baseDragOffset ?? 0) + dx * SLIDE_DRAG_FACTOR
	dragOffsetPx.value = clamp(raw, -w, w)
	targetDragOffset = dragOffsetPx.value
}

function resetPinch() {
	pinchScale.value = 1
	pinchX.value = 0
	pinchY.value = 0
	targetPanX = 0
	targetPanY = 0
	targetDragOffset = 0
	pinchStart = null
	panStart = null
	edgeOverflow = 0
}

/** 清空每张图独立的缩放/平移状态，退出预览时调用 */
function resetPinchStateByIndex() {
	for (const key of Object.keys(pinchStateByIndex)) delete pinchStateByIndex[Number(key)]
}

function goToSlide(delta: -1 | 1) {
	const n = props.urls?.length || 0
	if (n <= 1) return
	const oldIdx = currentIndex.value
	const nextIdx = delta < 0 ? Math.max(0, oldIdx - 1) : Math.min(n - 1, oldIdx + 1)
	if (nextIdx === oldIdx) return
	currentIndex.value = nextIdx
	setPinchState(oldIdx, { ...getPinchState(oldIdx), x: 0, y: 0 })
		slideTransitionOn.value = true
	dragOffsetPx.value = 0
	targetDragOffset = 0
	setTimeout(() => { slideTransitionOn.value = false }, SLIDE_ANIMATION_MS)
}

// ==================== 触摸事件处理 ====================
/** 创建单指拖动开始状态 */
function createPanStart(
	t: { clientX: number; clientY: number },
	baseX: number,
	baseY: number,
	baseDragOffset?: number
) {
	return {
		startX: t.clientX,
		startY: t.clientY,
		baseX,
		baseY,
		lastX: t.clientX,
		lastY: t.clientY,
		baseDragOffset
	}
}

/** 单图 */
function ensurePanStart(t: { clientX: number; clientY: number }) {
	if (panStart) return
	panStart = createPanStart(t, pinchX.value, pinchY.value)
}

function onPinchTouchStart(e: any) {
	const touches = e.touches
	if (touches.length >= 2) {
		e.stopPropagation()
		panStart = null
		edgeOverflow = 0
		const dist = Math.max(getDistance(touches), MIN_PINCH_DIST)
		const center = getCenter(touches)
		pinchStart = {
			distance: dist,
			centerX: center.x,
			centerY: center.y,
			baseScale: pinchScale.value,
			baseX: pinchX.value,
			baseY: pinchY.value
		}
	} else if (touches.length === 1) {
		ensurePanStart(touches[0])
	}
}

function onPinchTouchMove(e: any) {
	const touches = e.touches
	
	if (touches.length >= 2 && pinchStart) {
		e.preventDefault()
		e.stopPropagation()
		panStart = null
		edgeOverflow = 0
		handlePinchZoom(touches, false)
		return
	}
	
	if (touches.length === 1 && !pinchStart) {
		ensurePanStart(touches[0])
		const t = touches[0]
		const scale = pinchScale.value
		
		if (scale > 1) {
			e.preventDefault()
			e.stopPropagation()
			const deltaDx = handlePanDrag(t, false)
			if (isMulti.value) {
				const { maxX } = getPinchBounds()
				const dir = tryEdgeSwitch(pinchX.value, maxX, deltaDx)
				if (dir === 'prev') goToSlide(-1)
				else if (dir === 'next') goToSlide(1)
			}
		}
	}
}

/** 多图  */
function onSlideTouchStart(e: any) {
	const touches = e.touches
	if (touches.length >= 2) {
		e.stopPropagation()
		panStart = null
		edgeOverflow = 0
		const cur = getPinchState(currentIndex.value)
		const dist = Math.max(getDistance(touches), MIN_PINCH_DIST)
		const center = getCenter(touches)
		pinchStart = {
			distance: dist,
			centerX: center.x,
			centerY: center.y,
			baseScale: cur.scale,
			baseX: cur.x,
			baseY: cur.y,
			lastCenterX: center.x
		}
	} else if (touches.length === 1) {
		const cur = getPinchState(currentIndex.value)
		panStart = createPanStart(touches[0], cur.x, cur.y, dragOffsetPx.value)
	}
}

function onSlideTouchMove(e: any) {
	const touches = e.touches
	const curIdx = currentIndex.value
	const curState = getPinchState(curIdx)

	if (touches.length >= 2 && pinchStart) {
		e.preventDefault()
		e.stopPropagation()
		panStart = null
		handlePinchZoom(touches, true, curIdx)
		return
	}
	
	if (touches.length === 1 && !pinchStart) {
		const t = touches[0]
		if (!panStart) panStart = createPanStart(t, curState.x, curState.y, dragOffsetPx.value)
		
		if (curState.scale > 1) {
			e.preventDefault()
			e.stopPropagation()
			const deltaDx = handlePanDrag(t, true, curIdx)
			const { maxX } = getBounds(curState.scale, windowSize.value.width, windowSize.value.height)
			const newX = getPinchState(curIdx).x
			const dir = tryEdgeSwitch(newX, maxX, deltaDx)
			if (dir === 'prev') goToSlide(-1)
			else if (dir === 'next') goToSlide(1)
		} else {
			e.preventDefault()
			handleSlideDrag(t)
		}
	}
}

function onSlideTouchEnd(e: any) {
	if (e.touches.length < 2) pinchStart = null
	if (e.touches.length === 0) {
		const n = props.urls?.length ?? 0
		if (n <= 1) { panStart = null; edgeOverflow = 0; return }
		
		const curIdx = currentIndex.value
		const curState = getPinchState(curIdx)
		
		if (curState.scale <= 1) {
			targetDragOffset = dragOffsetPx.value
			const th = SLIDE_DRAG_THRESHOLD
			if (dragOffsetPx.value > th && curIdx > 0) goToSlide(-1)
			else if (dragOffsetPx.value < -th && curIdx < n - 1) goToSlide(1)
			else {
				slideTransitionOn.value = true
				dragOffsetPx.value = 0
				targetDragOffset = 0
				setTimeout(() => { slideTransitionOn.value = false }, SLIDE_ANIMATION_MS)
			}
		}
		panStart = null
		edgeOverflow = 0
	} else if (e.touches.length === 1 && panStart) {
		const cur = getPinchState(currentIndex.value)
		panStart = { ...panStart, baseX: cur.x, baseY: cur.y, baseDragOffset: dragOffsetPx.value }
	}
}

function onPinchTouchEnd(e: any) {
	if (e.touches.length < 2) pinchStart = null
	if (e.touches.length === 0) {
		panStart = null
		edgeOverflow = 0
		targetPanX = pinchX.value
		targetPanY = pinchY.value
	} else if (e.touches.length === 1) {
		ensurePanStart(e.touches[0])
	}
}

const isMulti = computed(() => (props.urls?.length || 0) > 1)
const currentUrl = computed(() => {
	const list = props.urls || []
	const i = Math.max(0, Math.min(currentIndex.value, list.length - 1))
	return list[i] || ''
})
const showSwiper = computed(() => expanded.value && isMulti.value && animPhase.value !== 'exiting')
const showSingleContent = computed(
	() => currentUrl.value && (!isMulti.value || !expanded.value || animPhase.value === 'exiting')
)

const rootStyle = computed(() => ({ zIndex: props.zIndex }))

// ==================== 动画样式（按状态机统一输出） ====================
const animationStyles = computed(() => {
	const phase = animPhase.value
	const transOpen = `opacity ${OPEN_BLUR_S}s ${OPEN_ANIMATION_EASE}`
	const transOpenFilter = `opacity ${OPEN_BLUR_S}s ${OPEN_ANIMATION_EASE}, filter ${OPEN_BLUR_S}s ${OPEN_ANIMATION_EASE}`
	const transExit = `opacity ${EXIT_BLUR_S}s ease-out, filter ${EXIT_BLUR_S}s ease-out`

	if (phase === 'exiting') {
		return {
			root: {
				transition: transExit,
				filter: `blur(${EXIT_BLUR_MAX}px)`,
				opacity: 0,
				pointerEvents: 'none' as const
			},
			backdrop: { transition: `opacity ${EXIT_BLUR_S}s ease-out`, opacity: 0 },
			transformWrap: {
				transition: transExit,
				filter: `blur(${EXIT_BLUR_MAX}px)`,
				opacity: 0,
				pointerEvents: 'none' as const
			}
		}
	}
	if (phase === 'entering' || phase === 'displaying') {
		const loaded = isCurrentImageLoaded.value
		const hasContent = hasContentToShow.value
		const applyTarget = phase === 'displaying' || (phase === 'entering' && enterPaintReady.value)
		if (!applyTarget) {
			return {
				root: { opacity: 0 },
				backdrop: { opacity: 0 },
				transformWrap: {}
			}
		}
		// 仅目标样式首帧强制模糊，之后随 loaded 变化，既保证有过渡又跟手
		const contentBlurred = phase === 'entering' && !enterTargetPainted.value ? true : !loaded
		return {
			root: hasContent ? { transition: transOpen, opacity: 1 } : {},
			backdrop: { transition: transOpen, opacity: 1 },
			transformWrap: hasContent
				? {
						transition: transOpenFilter,
						filter: contentBlurred ? `blur(${OPEN_BLUR_MAX}px)` : 'blur(0px)',
						opacity: 1
					}
				: {}
		}
	}
	return { root: {}, backdrop: {}, transformWrap: {} }
})

/** 图片容器基础样式：始终全屏 */
const transformWrapStyle = computed(() => {
	const { width: w, height: h } = windowSize.value
	return {
		left: '0',
		top: '0',
		width: `${w}px`,
		height: `${h}px`,
		filter: `blur(${OPEN_BLUR_MAX}px)`,
		opacity: 0
	}
})

// ==================== 图片加载处理 ====================
/** 图片加载成功 */
function onImageLoad(index: number) {
	imageLoadStates[index] = 'loaded'
}

/** 图片加载失败 */
function onImageError(index: number) {
	imageLoadStates[index] = 'error'
}

/** 初始化图片加载状态 */
function initImageLoadStates() {
	const urls = props.urls || []
	urls.forEach((url, index) => {
		if (url) {
			imageLoadStates[index] = 'loading'
		}
	})
}

/** 重置图片加载状态 */
function resetImageLoadStates() {
	Object.keys(imageLoadStates).forEach(key => {
		delete imageLoadStates[Number(key)]
	})
}

function registerBack() {
	if (tabRootBackPress) {
		unregisterBack = tabRootBackPress.register({
			check: () => props.visible,
			close
		})
	}
}

function unregisterBackHandler() {
	if (unregisterBack) {
		unregisterBack()
		unregisterBack = null
	}
}

watch(
	() => props.visible,
	(v) => {
		if (v) {
			try {
				const sys = uni.getSystemInfoSync()
				windowSize.value = {
					width: sys.windowWidth ?? sys.screenWidth ?? 375,
					height: sys.windowHeight ?? sys.screenHeight ?? 667
				}
			} catch {
				windowSize.value = { width: 375, height: 667 }
			}
			currentIndex.value = Math.max(0, Math.min(props.current, (props.urls?.length || 1) - 1))
			// 初始化图片加载状态
			initImageLoadStates()
			expanded.value = !props.initialRect || props.initialRect.width <= 0 || props.initialRect.height <= 0
			registerBack()
			if (props.initialRect && props.initialRect.width > 0 && props.initialRect.height > 0) {
				// 先渲染一帧“缩小在点击位置”的状态，再开启动画，避免动画不生效
				setTimeout(() => {
					expanded.value = true
					startEnterPhase()
				}, 50)
			} else {
				expanded.value = true
				startEnterPhase()
			}
		} else {
			expanded.value = false
			animPhase.value = null
			enterPaintReady.value = false
			enterTargetPainted.value = false
			clearAnimTimers()
			resetPinch()
			resetPinchStateByIndex()
			resetImageLoadStates()
			clearLoadingIndicatorTimer()
			showLoadingOverlay.value = false
			dragOffsetPx.value = 0
			slideTransitionOn.value = false
			unregisterBackHandler()
		}
	},
	{ immediate: true }
)

watch(
	() => props.current,
	(v) => {
		currentIndex.value = Math.max(0, Math.min(v, (props.urls?.length || 1) - 1))
	}
)

/** 切换图片时，如果新图片未加载，初始化加载状态 */
watch(
	() => currentIndex.value,
	(newIndex) => {
		if (!imageLoadStates[newIndex] && props.urls?.[newIndex]) {
			imageLoadStates[newIndex] = 'loading'
		}
	}
)

/** 加载指示器延迟显示：进入 loading 后 0.1s 再显示，离开 loading 立即隐藏 */
const currentLoadState = computed(() => imageLoadStates[currentIndex.value])
watch(
	[expanded, currentIndex, currentLoadState],
	([exp, , state]) => {
		clearLoadingIndicatorTimer()
		showLoadingOverlay.value = false
		if (exp && state === 'loading') {
			loadingIndicatorTimer = setTimeout(() => {
				loadingIndicatorTimer = null
				if (expanded.value && imageLoadStates[currentIndex.value] === 'loading') {
					showLoadingOverlay.value = true
				}
			}, LOADING_INDICATOR_DELAY_MS)
		}
	}
)

onUnmounted(() => {
	clearAnimTimers()
	clearLoadingIndicatorTimer()
	unregisterBackHandler()
	resetPinch()
	resetPinchStateByIndex()
})


function onOverlayClick() {
	requestClose()
}

/** 先播退出动画（模糊+透明度渐隐），再通知关闭 */
function requestClose() {
	if (animPhase.value === 'exiting') return
	if (isMulti.value) {
		const s = getPinchState(currentIndex.value)
		pinchScale.value = s.scale
		pinchX.value = s.x
		pinchY.value = s.y
	}
	startExitPhase(() => {
		expanded.value = false
		animPhase.value = null
		emit('close')
	})
}

function close() {
	requestClose()
}
</script>

<style scoped>
.iv-root {
	position: fixed;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

.iv-root--closing {
	opacity: 0;
}

.iv-backdrop {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	background: rgba(0, 0, 0, 0.4);
	opacity: 0;
	transition: opacity 0.3s ease;
}

.iv-backdrop--expanded {
	background: rgba(0, 0, 0, 0.94);
	opacity: 1;
}

.iv-backdrop--closing {
	opacity: 0;
}

.iv-transform-wrap {
	position: fixed;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	z-index: 1;
}

.iv-pinch-wrap {
	width: 100%;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	transform-origin: center center;
	touch-action: none;
}

.iv-img {
	width: 100%;
	height: 100%;
	display: block;
}

.iv-slides-view {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
}

.iv-slides-track {
	display: flex;
	height: 100%;
	flex-shrink: 0;
	will-change: transform;
}

.iv-slide {
	flex: 0 0 auto;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

.iv-slide .iv-pinch-wrap {
	touch-action: none;
}

.iv-slide-img {
	width: 100%;
	height: 100%;
	display: block;
}

.iv-dots-wrap {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 2;
	display: flex;
	justify-content: center;
	align-items: flex-end;
	padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
	pointer-events: none;
}

.iv-dots {
	display: flex;
	justify-content: center;
}

.iv-dots__text {
	font-size: 28rpx;
	color: rgba(255, 255, 255, 0.9);
	background: rgba(0, 0, 0, 0.4);
	padding: 8rpx 24rpx;
	border-radius: 24rpx;
}

/* ==================== 加载指示器（样式参考 PullRefresh 刷新圈） ==================== */
.iv-loading {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	background: rgba(0, 0, 0, 0.3);
	z-index: 10;
}

.iv-loading-spinner {
	width: 36rpx;
	height: 36rpx;
	border: 6rpx solid rgba(255, 255, 255, 0.3);
	border-top-color: rgba(255, 255, 255, 0.9);
	border-radius: 50%;
	animation: iv-spin 0.7s linear infinite;
}

.iv-error {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	background: rgba(0, 0, 0, 0.5);
	z-index: 10;
}

.iv-error-text {
	font-size: 28rpx;
	color: rgba(255, 255, 255, 0.9);
}

@keyframes iv-spin {
	to { transform: rotate(360deg); }
}
</style>
