topfans/frontend/pages/square/composables/useCabin.js
2026-04-13 01:02:57 +08:00

249 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}