249 lines
7.2 KiB
JavaScript
249 lines
7.2 KiB
JavaScript
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,
|
||
}
|
||
}
|