import { ref, reactive, watch, shallowRef } from 'vue' import { generateGridCoordinates, CABIN_DEFS, cabinTypeByLevel } from '../config/cabin.js' import { getRandomUsersApi } from '@/utils/api.js' export function useCabin() { const visibleCabins = ref([]) const currentPage = ref(1) const totalUsers = ref(0) const scaledCoords = ref([]) const pageCache = shallowRef(new Map()) const pageCacheVersion = ref(0) const pendingPages = new Set() let cabinRenderDefs = [] let tileWidth = 375 let screenWidth = 375 let bannerBottom = 0 const PAGE_SIZE = generateGridCoordinates().length const maxPage = () => Math.max(1, Math.ceil(totalUsers.value / PAGE_SIZE)) const wrapPage = (p) => { const max = maxPage() return ((p - 1 + max) % max) + 1 } const fetchPage = async (page) => { if (pageCache.value.has(page) || pendingPages.has(page)) { return } 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 const users = res.data.users || [] // 交换第 1 和第 3 个用户位置 if (users.length >= 3) { [users[0], users[20]] = [users[20], users[0]] } pageCache.value.set(page, users) pageCacheVersion.value++ } } catch (e) { console.error(`[useCabin] fetchPage: page=${page} 请求失败`, e?.message ?? e) } finally { pendingPages.delete(page) } } const ensurePages = (center, isInertia = false) => { 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++ } } const buildVisibleCabins = (page, cache, coords) => { if (!coords.length || !cabinRenderDefs.length) return [] const w = tileWidth const result = [] // 第一遍:收集上方有用户数据的 cabin(待移位),以及下方空位坐标 const toRelocate = [] const emptySlots = [] 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" 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: 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] if (k >= emptySlots.length) continue const targetSx = emptySlots[k].sx const targetSy = emptySlots[k].sy 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: false, showDialog: false, })) } return result } // 翻页或坐标初始化时全量重建 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) }) const initCabin = ({ screenW, tileW, imageH, currentUserNickname }) => { screenWidth = screenW tileWidth = tileW // 计算 banner 底部边界 const rpxToPx = screenWidth / 750 bannerBottom = 496 * rpxToPx // 计算 cabin 渲染尺寸 const baseH = Math.round(screenWidth * 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 = imageH / 1918 const coords = generateGridCoordinates() scaledCoords.value = coords.map(({ x, y }) => ({ sx: x * scale, sy: y * scale, })) } const updateCurrentUserNickname = (nickname) => { visibleCabins.value.forEach(cabin => { if (cabin.nickname === nickname) { cabin.isMine = true } }) } const resetSquare = async () => { currentPage.value = 1 totalUsers.value = 0 pageCache.value = new Map() pageCacheVersion.value++ await fetchPage(1) ensurePages(1) } return { visibleCabins, currentPage, scaledCoords, cabinRenderDefs, fetchPage, ensurePages, initCabin, updateCurrentUserNickname, resetSquare, wrapPage, } }