<template>
	<!-- 触摸事件绑定在最外层，确保 cabin 图标不会拦截滑动手势 -->
	<view class="square-container" @touchstart="onBgTouchStart" @touchmove="onBgTouchMove" @touchend="onBgTouchEnd"
		@touchcancel="onBgTouchCancel">
		<!-- 横向无限滚动背景条 -->
		<view class="background-strip" :style="backgroundStripStyle">
			<image v-for="i in 3" :key="i" class="background-tile" :style="{ width: tileWidth + 'px', height: '100%', }"
				src="/static/background/mainbg.png" :ref="'img' + i" />
		</view>

		<!-- Cabin 图标层（与背景同步移动） -->
		<view class="cabin-layer" :style="cabinLayerStyle" v-show="activeTab === 0">
			<view v-for="cabin in visibleCabins" :key="cabin.key" :id="'cabin-' + cabin.key" class="cabin-wrapper"
				:class="{ 'cabin-nickname-mine': cabin.isMine || cabin.nickname === currentUserNickname, 'cabin-slots-zero': cabin.sharedBoothSlotsRemaining === 0 }"
				:style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }"
				@click="handleCabinClick(cabin)">
				<image class="cabin-icon" :src="cabin.src" :style="{ width: cabin.w + 'px', height: cabin.h + 'px' }"
					mode="scaleToFill" />

				<text v-if="cabin.showNickname && cabin.nickname" class="cabin-nickname">{{ cabin.nickname ===
					currentUserNickname ? '我的小屋' : cabin.nickname }}</text>
				<text v-else class="cabin-nickname cabin-nickname--empty">小屋暂无人居住</text>
				<view v-if="cabin.showDialog" class="cabin-slots-dialog">
					<text class="cabin-slots-text text-white">剩余 <text class="text-orange"> {{
						cabin.sharedBoothSlotsRemaining }} </text> 个展位</text>
				</view>
			</view>
		</view>

		<!-- 翻页箭头按钮（仅广场 Tab 显示） -->
		<view v-show="activeTab === 0" class="nav-arrows">
			<view class="arrow-btn arrow-left" @click="scrollPage(-1)">
				<text class="arrow-text">‹</text>
			</view>
			<view class="arrow-btn arrow-right" @click="scrollPage(1)">
				<text class="arrow-text">›</text>
			</view>
		</view>

		<!-- Header组件 -->
		<Header :showBack="activeTab !== 0" :showGuideIcon="activeTab === 0" :showTaskIcon="activeTab === 0" :showStarActivityIcon="activeTab === 0" backIconColor="#e6e6e6" />

		<!-- 轮播图 + 应援活动列表（仅广场 Tab 显示，swiper 切换） -->
		<view v-show="activeTab === 0" class="banner-carousel" @click.stop>
			<swiper class="banner-swiper" :autoplay="true" :interval="4000" :duration="400" :circular="true"
				:indicator-dots="false">
				<swiper-item @click.stop="showRankingModal = true">
					<BannerTop3 />
				</swiper-item>
				<swiper-item v-for="item in bannerActivities" :key="item.id" @click.stop="handleActivityClick(item)">
					<image class="banner-activity-img" :src="item.cover_image || '/static/avatar/1.jpeg'"
						mode="aspectFill" />
				</swiper-item>
			</swiper>
		</view>

		<!-- 页面内容组件 - 使用 v-show 切换 -->
		<StarbookContent v-show="activeTab === 1" :isActive="activeTab === 1" />
		<CastloveContent v-show="activeTab === 2" @back="handleCastloveBack" />
		<StarCityContent v-show="activeTab === 3" />
		<FriendsContent v-show="activeTab === 4" />

		<!-- 蒙层 - 导航栏展开时显示 -->
		<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>

		<!-- 排行榜弹窗 -->
		<RankingModal :visible="showRankingModal" :parent-active="true" :star-id="currentStarId"
			@update:visible="handleRankingModalClose" @visit="handleRankingVisit" />

		<!-- 底部导航栏 -->
		<BottomNav :activeTab="activeTab" :isExpanded="navExpanded" @update:activeTab="handleTabChange"
			@update:isExpanded="navExpanded = $event" />

		<!-- 全局引导遮罩 -->

		<!-- 新手引导开始弹窗 -->
		<GuideStartModal :visible="showGuideStartModal" @start="handleGuideStart" @close="handleGuideStartModalClose" />

		<!-- 全局引导遮罩 -->
		<GuideOverlay />
	</view>
</template>

<script setup>
import {
	ref,
	reactive,
	computed,
	shallowRef,
	watch,
	onMounted,
	onUnmounted
} from "vue";
import {
	onLoad,
	onShow,
} from "@dcloudio/uni-app";
import {
	useStore
} from "vuex";
import Header from "../components/Header.vue";
import BottomNav from "../components/BottomNav.vue";
import CastloveContent from "../components/CastloveContent.vue";
import StarbookContent from "../components/StarbookContent.vue";
import StarCityContent from "../components/StarCityContent.vue";
import FriendsContent from "../components/FriendsContent.vue";
import GuideStartModal from "@/components/GuideStartModal.vue";
import GuideOverlay from "@/components/GuideOverlay.vue";
import {
	getRandomUsersApi,
	getActivityListApi,
	getOssPresignedUrlApi,
} from "@/utils/api.js";
import BannerTop3 from "../components/BannerTop3.vue";
import RankingModal from "../components/RankingModal.vue";
import {
	shouldShowGuideStartModal
} from "@/utils/guideConfig.js";

const activeTab = ref(0);

const navExpanded = ref(false);

// 排行榜弹窗
const showRankingModal = ref(false);
const currentStarId = ref(uni.getStorageSync("star_id") || null);

// 新手引导弹窗
const showGuideStartModal = ref(false);

// --- Banner 活动图片（取前3条） ---
const bannerActivities = ref([]);

const loadBannerActivities = async () => {
	try {
		const starId = uni.getStorageSync("star_id") || null;
		const res = await getActivityListApi(starId, "active", 1, 3);
		if (res.code === 200 && res.data?.activities) {
			bannerActivities.value = await Promise.all(
				res.data.activities.map(async (item) => {
					if (!item.cover_image) return item;
					try {
						const r = await getOssPresignedUrlApi(
							item.cover_image,
							3600,
							"avatar",
						);
						if (r?.code === 200 && r.data?.url)
							return {
								...item,
								cover_image: r.data.url,
							};
					} catch (e) { }
					return item;
				}),
			);
		}
	} catch (e) {
		console.error("[Square] 加载 banner 活动失败", e?.message ?? e);
	}
};

const handleActivityClick = (item) => {
	uni.navigateTo({
		url: `/pages/support-activity/index?id=${item.id}`,
	});
};

// 处理排行榜弹窗中的拜访小屋点击
const handleRankingVisit = (userId) => {
	showRankingModal.value = false;
	uni.navigateTo({
		url: `/pages/exhibition/exhibition?target_uid=${userId}`,
	});
};

// 处理排行榜弹窗关闭（引导中使用）
const handleRankingModalClose = (visible) => {
	showRankingModal.value = visible;
	if (!visible && store.state.guide.isActive) {
		// 引导中关闭组件，触发下一步
		uni.$emit('guide:closeComponent')
	}
};

// --- 背景横向无限滚动 ---
// 背景图原始尺寸，用于按屏幕高度等比计算 tileWidth（一个 tile 的宽度）
const IMAGE_W = 2012;
const IMAGE_H = 1918;

const tileWidth = ref(375);
const screenWidth = ref(375);
const screenHeight = ref(812);
// bgOffsetX：当前背景的横向偏移量，始终被 clampOffset 限制在 [-tileWidth, 0) 范围内。
// 背景由 3 张图片并排（strip），初始 translateX = -tileWidth，使中间那张居中显示。
// 滑动时只改变 bgOffsetX，CSS transform 实时跟随，视觉上形成无限滚动效果。
const bgOffsetX = ref(0);
let touchStartX = 0;
let lastMoveX = 0;
let lastMoveTime = 0;
let velocity = 0;
let inertiaRaf = null;

// --- Cabin 坐标 ---
// 每个元素 [x, y] 是背景图片的小平台中心点坐标，也是 cabin 在背景原图（2012×1918）中的像素坐标。
// 运行时会按 windowHeight/IMAGE_H 等比缩放为屏幕坐标（见 onMounted 中的 scaledCoords）。
// 如需调整某个 cabin 的显示位置，修改对应的 [x, y] 值即可；
// 坐标顺序即为每页用户数据的排列顺序（第 i 个坐标对应第 i 个用户）。
// 原图 2293×4281 → 新图 2012×1918，按比例 x*0.8775 y*0.4481 换算
const CABIN_COORDS = [
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[0, 0],
	[-260, -20],
	[235, -20],
	[750, -20],
	[1265, -20],
	[0, 150],
	[510, 150],
	[1010, 150],
	[1505, 150],
	[-260, 350],
	[235, 350],
	[750, 350],
	[1265, 350],
	[0, 530],
	[510, 530],
	[1010, 530],
	[1505, 520],
	[-260, 730],
	[235, 730],
	[750, 730],
	[1265, 730],
	[0, 910],
	[510, 910],
	[1010, 910],
	[1505, 910],
	[-260, 1115],
	[235, 1115],
	[750, 1115],
	[1265, 1115],
	[0, 1300],
	[510, 1300],
	[1010, 1300],
	[1505, 1300],
	[-260, 1510],
	[235, 1510],
	[750, 1510],
	[1265, 1510],
	[0, 1690],
	[510, 1690],
	[1010, 1690],
	[1505, 1690],
	[-260, 1910],
	[235, 1910],
	[750, 1910],
	[1265, 1910],
];

// --- 无限翻页：数据分页与缓存 ---
// 实现原理：
//   背景图片循环平铺（3 张并排 + clampOffset 回绕），视觉上可无限横向滚动。
//   每滑过一个 tile 宽度，currentPage 递增/递减，通过 wrapPage 在 [1, maxPage] 环形回绕，
//   实现"最后一页 → 第一页"的无缝循环。
//   ensurePages 始终保证当前可见的 5 个 tile 对应的页数据已缓存，
//   超出范围的页缓存会被自动清除以节省内存。
const PAGE_SIZE = CABIN_COORDS.length;
const currentPage = ref(1);
const totalUsers = ref(0);
const pageCache = shallowRef(new Map());
// pageCache 内容变化时递增，用于驱动 watch（shallowRef 的 triggerRef 不触发普通 watch）
const pageCacheVersion = ref(0);
// 正在进行中的请求页码集合，清除缓存时跳过这些页，防止"刚加载的数据被立即删除"
const pendingPages = new Set();

const maxPage = computed(() =>
	Math.max(1, Math.ceil(totalUsers.value / PAGE_SIZE)),
);

// 将页码环形回绕到 [1, maxPage] 范围，使翻页可无限循环
const wrapPage = (p) => {
	const max = maxPage.value;
	return ((p - 1 + max) % max) + 1;
};

const fetchPage = async (page) => {
	if (pageCache.value.has(page)) {
		return;
	}
	if (pendingPages.has(page)) {
		return;
	}
	const t0 = Date.now();
	pendingPages.add(page);
	try {
		const res = await getRandomUsersApi(page, PAGE_SIZE);
		if (res.code === 200 && res.data) {
			if (totalUsers.value === 0) totalUsers.value = res.data.total || 0;
			pageCache.value.set(page, (() => {
				const users = res.data.users || [];
				if (users.length >= 3) [users[0], users[2]] = [users[2], users[0]];
				return users;
			})());
			pageCacheVersion.value++;
		} else { }
	} catch (e) {
		console.error(
			`[Square] fetchPage: page=${page} 请求失败 耗时=${Date.now() - t0}ms`,
			e?.message ?? e,
		);
	} finally {
		pendingPages.delete(page);
	}
};

const ensurePages = (center) => {
	// 渲染范围 n=[0,4] 对应页码 center-1 ~ center+3，需全部预加载
	const keep = new Set();
	for (let n = 0; n <= 4; n++) keep.add(wrapPage(center + n - 1));

	const pages = [...keep];
	pages.forEach(fetchPage);

	// 清除不在渲染范围内的页缓存，跳过正在请求中的页
	const evicted = [];
	for (const key of pageCache.value.keys()) {
		if (!keep.has(key) && !pendingPages.has(key)) {
			evicted.push(key);
			pageCache.value.delete(key);
		}
	}
	if (evicted.length) {
		pageCacheVersion.value++;
	}
};

// 每种 cabin 的图片路径、原图尺寸、锚点（需要cabin底座中心点对准背景小平台中心点坐标的像素位置）
const CABIN_DEFS = [{
	src: "/static/components/cabin1.png",
	imgW: 1000,
	imgH: 1000,
	anchorX: 500,
	anchorY: 680,
},
{
	src: "/static/components/cabin2.png",
	imgW: 1000,
	imgH: 1000,
	anchorX: 500,
	anchorY: 680,
},
{
	src: "/static/components/cabin3.png",
	imgW: 1000,
	imgH: 1351,
	anchorX: 500,
	anchorY: 965,
},
{
	src: "/static/components/cabin4.png",
	imgW: 1000,
	imgH: 1223,
	anchorX: 500,
	anchorY: 875,
},
];

// 根据用户等级返回 cabin 类型索引（0=cabin1, 1=cabin2, 2=cabin3, 3=cabin4）
const cabinTypeByLevel = (level) => {
	if (level >= 7) return 3;
	if (level >= 5) return 2;
	if (level >= 3) return 1;
	return 0;
};

// 缩放后的坐标（ref，onMounted 赋值后触发 visibleCabins 重新计算）
const scaledCoords = ref([]);
// 每种 cabin 渲染后的宽高和锚点偏移（px），onMounted 中计算
let cabinRenderDefs = [];

// uni-app 环境下使用 uni.requestAnimationFrame 替代 requestAnimationFrame
const rafFn = (cb) =>
	uni.requestAnimationFrame ?
		uni.requestAnimationFrame(cb) :
		setTimeout(cb, 16);
const cafFn = (id) =>
	uni.cancelAnimationFrame ? uni.cancelAnimationFrame(id) : clearTimeout(id);

// 累计未归一化的总偏移量（px），用于检测跨越了多少个 tile
let rawOffsetX = 0;
// 是否处于惯性滚动阶段（touchEnd 后），用于决定是否防抖
let isInertiaPhase = false;
// 防止 ensurePages 在惯性滚动中被高频调用（仅惯性阶段使用）
let ensureTimer = null;
const debouncedEnsurePages = (center) => {
	if (ensureTimer) clearTimeout(ensureTimer);
	ensureTimer = setTimeout(() => {
		ensureTimer = null;
		ensurePages(center);
	}, 100);
};

// 将 offsetX 归一化到 [-tileWidth, 0) 范围，实现无缝回绕
const clampOffset = (offset) => {
	const w = tileWidth.value;
	return (((offset % w) + w) % w) - w;
};

// 滑动时使用：归一化 + 通过累计原始偏移量检测跨越 tile 边界，更新 currentPage
const normalizeOffset = (offset) => {
	const w = tileWidth.value;
	const normalized = clampOffset(offset);
	const prevTileN = Math.floor(-rawOffsetX / w);
	rawOffsetX += offset - bgOffsetX.value;
	const nextTileN = Math.floor(-rawOffsetX / w);
	const delta = nextTileN - prevTileN;
	if (delta !== 0) {
		const newPage = wrapPage(currentPage.value + delta);
		currentPage.value = newPage;
		// 手动触摸阶段立即触发，惯性阶段防抖避免高频调用
		if (isInertiaPhase) {
			debouncedEnsurePages(currentPage.value);
		} else {
			ensurePages(currentPage.value);
		}
	}
	return normalized;
};

const backgroundStripStyle = computed(() => ({
	width: `${tileWidth.value * 3}px`,
	transform: `translateX(${-tileWidth.value + (screenWidth.value - tileWidth.value) / 2 + bgOffsetX.value}px)`
	// transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`,
	// transform: `translateX(${-tileWidth.value + (screenWidth.value - tileWidth.value) / 2 + bgOffsetX.value}px) rotateX(45deg) translateY(${(-tileWidth.value + (screenWidth.value - tileWidth.value) / 2 + bgOffsetX.value) / 4}px)`,
}));

// --- Cabin 层 ---
const cabinLayerStyle = computed(() => ({
	// transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`,
transform: `translateX(${-tileWidth.value + (screenWidth.value - tileWidth.value) / 2 + bgOffsetX.value}px) `
}));

// visibleCabins 是稳定的对象数组，DOM 节点始终复用，避免图片重载闪烁。
// 更新分两条路径：
//   - 翻页 / 坐标初始化（scaledCoords 变化）：重建整个数组（位置 + 数据全量更新）
//   - 数据加载完成（pageCacheVersion 变化）：只原地更新数据字段，不替换数组引用
// cabin 的屏幕位置通过 cabinLayerStyle 的 translateX 整体平移，无需每帧重建列表。
const visibleCabins = ref([]);

// cabin 层整体有 -tileWidth 的初始偏移（cabinLayerStyle），
// 因此 n=1 的 tile 才是视觉上的"当前页"（屏幕中央）。
// 页码映射：tile n → wrapPage(page + n - 1)，使 n=1 对应 currentPage。
// 渲染范围 n=[0,4]，覆盖左右各 1 个 tile 的过渡区域。
const buildVisibleCabins = (page, cache, coords) => {
	if (!coords.length || !cabinRenderDefs.length) return [];
	const w = tileWidth.value;
	const result = [];

	// 轮播图底部边界：top(184rpx) + height(312rpx) = 496rpx，转换为 px
	// 额外加一个 cabin 高度的缓冲，确保被 banner 部分遮挡的小屋也不显示昵称
	const rpxToPx = screenWidth.value / 750;
	const cabinH = cabinRenderDefs.length ? cabinRenderDefs[0].renderedH : 0;
	const bannerBottom = 496 * rpxToPx + cabinH;

	// 第一遍：收集上方有用户数据的 cabin（待移位），以及下方空位坐标（无用户数据）
	// 结构：{ i, n, user, typeIdx, def, render }
	const toRelocate = []; // 上方有数据需要移位的
	const emptySlots = []; // 下方无数据的空位 { sx, sy, n }

	for (let i = 0; i < coords.length; i++) {
		const {
			sx,
			sy
		} = coords[i];
		for (let n = 0; n <= 4; n++) {
			const p = wrapPage(page + n - 1);
			const users = cache.get(p) || [];
			const user = users[i] || null;
			const isAbove = sy < bannerBottom;
			if (isAbove && user) {
				const typeIdx = cabinTypeByLevel(user.level);
				toRelocate.push({
					i,
					n,
					user,
					typeIdx,
					sx,
					sy
				});
			} else if (!isAbove && !user) {
				emptySlots.push({
					sx,
					sy,
					n
				});
			}
		}
	}

	// 第二遍：正常渲染，跳过"被移位占用的空位"和"已移位的上方 cabin"
	// 记录哪些空位被占用（按 index）
	const occupiedSlots = new Set();
	for (let k = 0; k < toRelocate.length && k < emptySlots.length; k++) {
		occupiedSlots.add(k);
	}

	for (let i = 0; i < coords.length; i++) {
		const {
			sx,
			sy
		} = coords[i];
		for (let n = 0; n <= 4; n++) {
			const p = wrapPage(page + n - 1);
			const users = cache.get(p) || [];
			const user = users[i] || null;
			const isAbove = sy < bannerBottom;

			// 跳过上方有数据的（会被移位渲染）
			if (isAbove && user) continue;

			// 检查这个空位是否被移位 cabin 占用
			const slotIdx = emptySlots.findIndex(
				(s) => s.sx === sx && s.sy === sy && s.n === n,
			);
			if (slotIdx !== -1 && occupiedSlots.has(slotIdx)) continue;

			const typeIdx = cabinTypeByLevel(user ? user.level : 0);
			const def = CABIN_DEFS[typeIdx];
			const render = cabinRenderDefs[typeIdx];
			const cabinY = sy - render.offsetY;
			result.push(
				reactive({
					key: `${i}-${n}`,
					x: sx + n * w - render.offsetX,
					y: cabinY,
					w: render.renderedW,
					h: render.renderedH,
					src: def.src,
					userId: user ? user.user_id : null,
					galleryOwnerId: user ? user.gallery_owner_id : null,
					nickname: user ? user.nickname : null,
					sharedBoothSlotsRemaining: user ? user.shared_booth_slots_remaining : null,
					showNickname: !isAbove,
					isMine: user ? user.nickname === currentUserNickname.value : false,
					showDialog: false,
				}),
			);
		}
	}

	// 第三遍：把移位的 cabin 放到对应空位坐标上
	for (let k = 0; k < toRelocate.length; k++) {
		const {
			n,
			user,
			typeIdx
		} = toRelocate[k];
		const def = CABIN_DEFS[typeIdx];
		const render = cabinRenderDefs[typeIdx];

		let targetSx, targetSy;
		if (k < emptySlots.length) {
			// 放到空位坐标
			targetSx = emptySlots[k].sx;
			targetSy = emptySlots[k].sy;
		} else {
			// 空位不够，跳过不渲染
			continue;
		}

		result.push(
			reactive({
				key: `reloc-${k}-${n}`,
				x: targetSx + n * w - render.offsetX,
				y: targetSy - render.offsetY,
				w: render.renderedW,
				h: render.renderedH,
				src: def.src,
				userId: user.user_id,
				galleryOwnerId: user.gallery_owner_id,
				nickname: user.nickname,
				sharedBoothSlotsRemaining: user.shared_booth_slots_remaining,
				showNickname: true,
				isMine: user.nickname === currentUserNickname.value,
				showDialog: false,
			}),
		);
	}

	return result;
};

// 仅更新数据字段（src / userId / galleryOwnerId / nickname），
// 保持数组引用和 DOM 节点稳定，防止图片重载闪烁。
const patchVisibleCabinsData = (page, cache) => {
	const list = visibleCabins.value;
	if (!list.length) return;
	let idx = 0;
	for (let i = 0; i < CABIN_COORDS.length; i++) {
		for (let n = 0; n <= 4; n++) {
			const p = wrapPage(page + n - 1);
			const users = cache.get(p) || [];
			const user = users[i] || null;
			const typeIdx = cabinTypeByLevel(user ? user.level : 0);
			const item = list[idx++];
			item.src = CABIN_DEFS[typeIdx].src;
			item.userId = user ? user.user_id : null;
			item.galleryOwnerId = user ? user.gallery_owner_id : null;
			item.nickname = user ? user.nickname : null;
			item.sharedBoothSlotsRemaining = user ? user.shared_booth_slots_remaining : null;
		}
	}
};

// 翻页或坐标初始化时全量重建（位置变了，必须重建）
watch(
	[currentPage, scaledCoords],
	() => {
		visibleCabins.value = buildVisibleCabins(
			currentPage.value,
			pageCache.value,
			scaledCoords.value,
		);
	}, {
	immediate: true,
},
);

// 数据加载完成时重建（因为移位逻辑依赖用户数据，需要全量重建）
watch(pageCacheVersion, () => {
	visibleCabins.value = buildVisibleCabins(
		currentPage.value,
		pageCache.value,
		scaledCoords.value,
	);
	// cabin 数据加载完成后，如果引导处于激活状态，重新计算位置
	if (store.state.guide.isActive) {
		const guide = store.state.guide.currentGuide
		const currentStep = store.state.guide.currentStep
		const stepConfig = guide?.steps?.[currentStep]
		// 检查当前步骤是否在当前页面
		if (stepConfig?.page === '/pages/square/square') {
			setTimeout(() => {
				uni.$emit('guide:recalculatePosition')
			}, 100)
		}
	}
});

// 展位提示框随机轮换定时器
let dialogRotationTimer = null;

// 计算 cabin 是否在可视区域内（通过 DOM boundingClientRect 判断，漏出区域大于70%）
const isCabinInViewport = (cabin) => {
	return new Promise((resolve) => {
		const query = uni.createSelectorQuery();
		query.select(`#cabin-${cabin.key}`).boundingClientRect((rect) => {
			if (!rect) {
				resolve(false);
				return;
			}
			// 计算漏出区域百分比
			const visibleTop = Math.max(0, rect.top);
			const visibleBottom = Math.min(screenHeight.value, rect.bottom);
			const visibleLeft = Math.max(0, rect.left);
			const visibleRight = Math.min(screenWidth.value, rect.right);
			const visibleWidth = Math.max(0, visibleRight - visibleLeft);
			const visibleHeight = Math.max(0, visibleBottom - visibleTop);
			const visibleArea = visibleWidth * visibleHeight;
			const totalArea = rect.width * rect.height;
			const visiblePercent = totalArea > 0 ? visibleArea / totalArea : 0;
			const inViewport = visiblePercent > 0.7;
			resolve(inViewport);
		}).exec();
	});
};

// 随机轮换展位提示框可见性
const rotateDialogVisibility = async () => {
	const cabins = visibleCabins.value;
	if (!cabins.length) {
		return;
	}

	// 先全部重置为 false
	cabins.forEach(c => c.showDialog = false);

	// 筛选符合条件的 cabin：有昵称、有展位剩余
	const hasData = cabins.filter(c => c.nickname && c.sharedBoothSlotsRemaining !== null);

	// 批量检查所有 cabin 是否在可视区域内
	const viewportChecks = await Promise.all(hasData.map(c => isCabinInViewport(c)));
	const eligible = hasData.filter((c, i) => viewportChecks[i]);

	if (eligible.length === 0) return;

	// 随机选择 2-3 个
	const count = Math.min(Math.floor(Math.random() * 2) + 2, eligible.length, 3);
	const shuffled = eligible.sort(() => Math.random() - 0.5);
	for (let i = 0; i < count; i++) {
		shuffled[i].showDialog = true;
	}
};

// 启动定时器（2-3秒随机间隔）
const startDialogRotation = () => {
	stopDialogRotation();
	const interval = Math.floor(Math.random() * 1000) + 2000; // 2000-3000ms
	dialogRotationTimer = setTimeout(() => {
		rotateDialogVisibility();
		startDialogRotation(); // 继续下一轮
	}, interval);
};

const stopDialogRotation = () => {
	if (dialogRotationTimer) {
		clearTimeout(dialogRotationTimer);
		dialogRotationTimer = null;
	}
};

// visibleCabins 更新时重启轮换
watch(visibleCabins, () => {
	console.log('[Square] visibleCabins watch triggered, cabins:', visibleCabins.value.length);
	if (visibleCabins.value.some(c => c.nickname && c.sharedBoothSlotsRemaining !== null)) {
		console.log('[Square] starting dialog rotation');
		startDialogRotation();
	} else {
		stopDialogRotation();
	}
}, { immediate: true });

// 翻页动画：direction = -1 向左，1 向右，精确移动 1 个 tileWidth
const scrollPage = (direction) => {
	// 立即更新 currentPage，触发数据加载，不等动画结束
	const newPage = wrapPage(currentPage.value + direction);
	currentPage.value = newPage;
	ensurePages(newPage);

	stopInertia();
	const DURATION = 300;
	const FRAME = 16;
	const totalFrames = Math.round(DURATION / FRAME);
	// 精确移动 1 个 tile 宽度，确保视觉上跨越一个 tile
	const totalDelta = tileWidth.value * direction * -1;
	let frame = 0;

	const step = () => {
		frame++;
		const progress = frame / totalFrames;
		const eased = 1 - Math.pow(1 - progress, 3);
		const prevEased =
			frame === 1 ? 0 : 1 - Math.pow(1 - (frame - 1) / totalFrames, 3);
		const delta = totalDelta * (eased - prevEased);
		// 箭头翻页使用 clampOffset，不触发 normalizeOffset 里的 tile 边界检测
		bgOffsetX.value = clampOffset(bgOffsetX.value + delta);
		rawOffsetX += delta;
		if (frame < totalFrames) {
			inertiaRaf = rafFn(step);
		} else { }
	};

	inertiaRaf = rafFn(step);
};

const stopInertia = () => {
	if (inertiaRaf) {
		cafFn(inertiaRaf);
		inertiaRaf = null;
	}
};

// 重置广场到初始状态（位置、数据全部清空并重新加载第 1 页）
const resetSquare = async () => {
	stopInertia();
	bgOffsetX.value = 0;
	rawOffsetX = 0;
	velocity = 0;
	currentPage.value = 1;
	totalUsers.value = 0;
	pageCache.value = new Map();
	pageCacheVersion.value++;
	await fetchPage(1);
	ensurePages(1);
};

onMounted(async () => {
	const info = uni.getSystemInfoSync();
	screenWidth.value = info.windowWidth;
	screenHeight.value = info.windowHeight;
	tileWidth.value = Math.round(info.windowHeight * (IMAGE_W / IMAGE_H));

	const baseH = Math.round(info.windowWidth * 0.2 * 1.3);
	cabinRenderDefs = CABIN_DEFS.map(({
		imgW,
		imgH,
		anchorX,
		anchorY
	}) => {
		const renderedH = baseH;
		const renderedW = Math.round(renderedH * (imgW / imgH));
		return {
			renderedW,
			renderedH,
			offsetX: Math.round(renderedW * (anchorX / imgW)),
			offsetY: Math.round(renderedH * (anchorY / imgH)),
		};
	});

	const scale = info.windowHeight / IMAGE_H;
	scaledCoords.value = CABIN_COORDS.map(([x, y]) => ({
		sx: x * scale,
		sy: y * scale,
	}));
	await fetchPage(1);
	ensurePages(1);
	loadBannerActivities();
});

// 从其他路由页面返回时重置
onShow(() => {
	if (activeTab.value === 0) resetSquare();
});

// 页面加载完成后初始化引导
const store = useStore();
const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname || '');

// 监听引导打开组件事件
uni.$on('guide:openComponent', (componentName) => {
	if (componentName === 'RankingModal') {
		showRankingModal.value = true;
	}
});

onUnmounted(() => {
	uni.$off('guide:openComponent');
	stopDialogRotation();
});

onShow(() => {
	// 检查是否需要显示引导开始弹窗
	if (shouldShowGuideStartModal()) {
		showGuideStartModal.value = true;
		console.log("[Square] 显示引导开始弹窗");
	}
	// 不再自动触发引导，由用户从引导列表选择后触发
});

// 处理引导开始
const handleGuideStart = () => {
	showGuideStartModal.value = false;
};

// 关闭引导开始弹窗
const handleGuideStartModalClose = () => {
	showGuideStartModal.value = false;
};

// 领取奖励成功
const handleClaimSuccess = (reward) => {
	// 可以在这里刷新用户信息
	console.log("[Square] 领取奖励成功:", reward);
};

// banner 区域底部边界（px），触摸起点在此范围内时跳过背景滑动处理
const getBannerBottom = () => (screenWidth.value / 750) * 496;
let touchInBanner = false;

const onBgTouchStart = (e) => {
	if (activeTab.value !== 0) return;
	const touchY = e.touches[0].clientY;
	touchInBanner = touchY < getBannerBottom();
	if (touchInBanner) return;
	stopInertia();
	isInertiaPhase = false;
	touchStartX = e.touches[0].clientX;
	lastMoveX = touchStartX;
	lastMoveTime = Date.now();
	velocity = 0;
};

const onBgTouchMove = (e) => {
	// 非 tab 0 时不阻止，让 scroll-view 正常滚动
	if (activeTab.value !== 0) return;
	// tab 0 时如果触摸在 banner 区域也不处理，让 banner 自己处理
	if (touchInBanner) return;
	// tab 0 且不在 banner 区域时，阻止默认行为以实现横向背景滑动
	e.preventDefault();

	const currentX = e.touches[0].clientX;
	const now = Date.now();
	const dt = now - lastMoveTime || 1;
	velocity = (currentX - lastMoveX) / dt;
	lastMoveX = currentX;
	lastMoveTime = now;
	bgOffsetX.value = normalizeOffset(bgOffsetX.value + (currentX - touchStartX));
	touchStartX = currentX;
};

const onBgTouchEnd = () => {
	if (activeTab.value !== 0 || touchInBanner) {
		touchInBanner = false;
		return;
	}
	touchInBanner = false;
	isInertiaPhase = true;
	const FRICTION = 0.8; // 惯性滚动的速度衰减系数
	const MIN_VELOCITY = 0.2; // 惯性滚动速度小于此值时停止滚动

	const step = () => {
		velocity *= FRICTION;
		if (Math.abs(velocity) < MIN_VELOCITY) {
			isInertiaPhase = false;
			return;
		}
		bgOffsetX.value = normalizeOffset(bgOffsetX.value + velocity * 16);
		inertiaRaf = rafFn(step);
	};

	inertiaRaf = rafFn(step);
};

const onBgTouchCancel = () => {
	touchInBanner = false;
	stopInertia();
	isInertiaPhase = false;
	velocity = 0;
};

// --- 小屋点击跳转 ---
const handleCabinClick = (cabin) => {
	// 惯性速度残留时说明是滑动结束，不是真实点击
	if (Math.abs(velocity) > 0.5) return;
	// 无用户、或被 banner 遮住（showNickname=false）的小屋不响应点击
	if (!cabin.userId || !cabin.showNickname) return;
	// 如果是自己的小屋，不需要 target_uid 参数
	uni.navigateTo({
		url: cabin.isMine ? '/pages/exhibition/exhibition' : `/pages/exhibition/exhibition?target_uid=${cabin.userId}`,
	});
};

// --- Tab 切换 ---
const handleTabChange = (newTab) => {
	const prevTab = activeTab.value;
	activeTab.value = newTab;
	navExpanded.value = false;
	if (prevTab !== 0 && newTab === 0) resetSquare();
};

const handleCastloveBack = () => {
	handleTabChange(0);
};

onLoad((options) => {
	// 调试模式：读取 guide_debug 参数并设置存储
	if (options && 'guide_debug' in options) {
		const debugValue = options.guide_debug
		const isDebug = debugValue === '1' || debugValue === 'true'
		if (isDebug) {
			uni.setStorageSync('guide_debug_mode', true)
			uni.setStorageSync('is_new_user', true)
			console.log('[Guide] 调试模式已开启')
		} else {
			uni.setStorageSync('guide_debug_mode', false)
			uni.removeStorageSync('is_new_user')
			console.log('[Guide] 调试模式已关闭')
		}
	}

	if (options && options.tab) {
		const tabIndex = parseInt(options.tab);
		if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex <= 4) {
			activeTab.value = tabIndex;
		}
	}
});
</script>

<style scoped>
.square-container {
	position: relative;
	width: 100vw;
	height: 100vh;
	min-height: 100vh;
	overflow: hidden;
}

/* 3 张图并排，translateX 由 JS 控制（含初始 -tileWidth 偏移） */
.background-strip {
	position: absolute;
	top: 0;
	left: 0;
	height: 150%;
	display: flex;
	z-index: 0;
	will-change: transform;
}

.background-tile {
	flex-shrink: 0;
	height: 100%;
}

.cabin-layer {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	z-index: 1;
	pointer-events: none;
}

.cabin-wrapper {
	position: absolute;
	display: flex;
	flex-direction: column;
	align-items: center;
	pointer-events: auto;
}

.cabin-icon {
	display: block;
}

.cabin-slots-zero .cabin-icon {
	filter: grayscale(100%);
	opacity: 0.6;
}

.cabin-nickname {
	font-size: 20rpx;
	color: #ffffff;
	text-align: center;
	text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
	margin-top: 4rpx;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cabin-nickname--empty {
	color: rgba(255, 255, 255, 0.75);
	font-style: italic;
}

.cabin-slots-dialog {
	position: absolute;
	top: -50rpx;
	left: 50%;
	transform: translateX(-50%);
	background-image: url('/static/icon/tips-bg.png');
	background-size: 100% 100%;
	background-repeat: no-repeat;
	padding: 8rpx 24rpx 18rpx;
	width: max-content;
	z-index: 10;
}

.cabin-slots-text {
	font-size: 18rpx;
	font-weight: 600;
	white-space: nowrap;
	text-align: center;
}

.text-white {
	color: #ffffff;
}

.text-orange {
	color: #FFB800;
	font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
	text-shadow:
		0 0 10rpx rgba(255, 184, 0, 0.8),
		0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}

.nav-arrows {
	position: fixed;
	top: 50%;
	left: 24rpx;
	right: 24rpx;
	transform: translateY(-50%);
	display: flex;
	justify-content: space-between;
	align-items: center;
	pointer-events: none;
	z-index: 10;
}

.arrow-btn {
	width: 56rpx;
	height: 96rpx;
	display: flex;
	align-items: center;
	justify-content: center;
	background: linear-gradient(165deg,
			#f0e4b1 0%,
			#f08399 50%,
			#b94e73 90%,
			#834b9e 100%);
	border-radius: 12rpx;
	pointer-events: auto;
	cursor: pointer;
	box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.25);
}

.arrow-left {
	border-radius: 12rpx;
}

.arrow-right {
	border-radius: 12rpx;
}

.arrow-btn:active {
	opacity: 0.8;
}

.arrow-text {
	font-size: 56rpx;
	color: #ffffff;
	font-weight: 600;
	line-height: 1;
	margin-top: -6rpx;
}

/* 轮播图 */
.banner-carousel {
	position: fixed;
	top: 216rpx;
	left: 0;
	right: 0;
	width: 100%;
	z-index: 100;
	padding: 0 8rpx;
	box-sizing: border-box;
}

.banner-swiper {
	width: 100%;
	height: 312rpx;
	border-radius: 24rpx;
	overflow: hidden;
}

/* 防止 swiper-item 内容溢出露边 */
:deep(.uni-swiper-slide) {
	overflow: hidden;
}

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

.nav-mask {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	background: rgba(0, 0, 0, 0.3);
	z-index: 999;
	animation: fadeIn 0.3s ease-out;
}

@keyframes fadeIn {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}
</style>