feat: 修改瀑布流布局
This commit is contained in:
parent
e545f41c3b
commit
da6c0a5ef2
@ -88,8 +88,9 @@ type InspirationFlowItem struct {
|
|||||||
Name string
|
Name string
|
||||||
CoverURL string
|
CoverURL string
|
||||||
LikeCount int32
|
LikeCount int32
|
||||||
|
Level string // 藏品等级: N, R, SR, SSR, UR
|
||||||
OwnerNickname string
|
OwnerNickname string
|
||||||
Span int32 // 卡片大小: 0-30→1, 31-100→2, 101-200→3, 200+→4
|
Span int32 // 卡片大小: N→1, R→2, SR→3, SSR→4, UR→5
|
||||||
MaterialType string // 素材类型: hot(人气王者), potential(潜力之星), new(新鲜上架)
|
MaterialType string // 素材类型: hot(人气王者), potential(潜力之星), new(新鲜上架)
|
||||||
CreatedAt int64 // 创建时间(用于判断是否为潜力之星)
|
CreatedAt int64 // 创建时间(用于判断是否为潜力之星)
|
||||||
}
|
}
|
||||||
@ -545,8 +546,9 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
|
|||||||
var err error
|
var err error
|
||||||
if materialType == "" || materialType == "all" || materialType == "random" {
|
if materialType == "" || materialType == "all" || materialType == "random" {
|
||||||
err = baseQuery.
|
err = baseQuery.
|
||||||
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
|
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, COALESCE(alr.current_level, 'N') as level, fp.nickname as owner_nickname, a.material_type, a.created_at`).
|
||||||
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
|
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
|
||||||
|
Joins("LEFT JOIN asset_level_records alr ON alr.asset_id = a.id").
|
||||||
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
|
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
|
||||||
Where("a.status = 1 AND a.is_active = true").
|
Where("a.status = 1 AND a.is_active = true").
|
||||||
Order("RANDOM()").
|
Order("RANDOM()").
|
||||||
@ -556,7 +558,8 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
|
|||||||
} else {
|
} else {
|
||||||
// baseQuery 已经包含了 assets JOIN,不需要重复添加
|
// baseQuery 已经包含了 assets JOIN,不需要重复添加
|
||||||
err = baseQuery.
|
err = baseQuery.
|
||||||
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
|
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, COALESCE(alr.current_level, 'N') as level, fp.nickname as owner_nickname, a.material_type, a.created_at`).
|
||||||
|
Joins("LEFT JOIN asset_level_records alr ON alr.asset_id = a.id").
|
||||||
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
|
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
|
||||||
Where("a.status = 1 AND a.is_active = true").
|
Where("a.status = 1 AND a.is_active = true").
|
||||||
Order("RANDOM()").
|
Order("RANDOM()").
|
||||||
@ -571,7 +574,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
|
|||||||
|
|
||||||
// Read-repair: 检查并补充 material_type(多类型共存,不降级)
|
// Read-repair: 检查并补充 material_type(多类型共存,不降级)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item.Span = calcSpanByLikes(item.LikeCount)
|
item.Span = calcSpanByLevel(item.Level)
|
||||||
correctTypes := calcMaterialTypes(item.LikeCount, item.CreatedAt)
|
correctTypes := calcMaterialTypes(item.LikeCount, item.CreatedAt)
|
||||||
|
|
||||||
// 如果缺少任何类型,追加(不覆盖原有类型)
|
// 如果缺少任何类型,追加(不覆盖原有类型)
|
||||||
@ -640,17 +643,23 @@ func calcMaterialTypes(likes int32, createdAt int64) string {
|
|||||||
return strings.Join(types, ",")
|
return strings.Join(types, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcSpanByLikes 根据点赞数计算卡片大小
|
// calcSpanByLevel 根据藏品等级计算卡片大小
|
||||||
// 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4
|
// N → span 1, R → span 2, SR → span 3, SSR → span 4, UR → span 5
|
||||||
func calcSpanByLikes(likes int32) int32 {
|
func calcSpanByLevel(level string) int32 {
|
||||||
if likes <= 30 {
|
switch level {
|
||||||
|
case "N":
|
||||||
return 1
|
return 1
|
||||||
} else if likes <= 100 {
|
case "R":
|
||||||
return 2
|
return 2
|
||||||
} else if likes <= 200 {
|
case "SR":
|
||||||
return 3
|
return 3
|
||||||
|
case "SSR":
|
||||||
|
return 4
|
||||||
|
case "UR":
|
||||||
|
return 5
|
||||||
|
default:
|
||||||
|
return 1 // 默认返回1
|
||||||
}
|
}
|
||||||
return 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSlotOwnerUserID 获取展位所有者的用户ID
|
// GetSlotOwnerUserID 获取展位所有者的用户ID
|
||||||
|
|||||||
@ -4,30 +4,32 @@
|
|||||||
@touchend="onTouchEnd" @touchcancel="onTouchEnd">
|
@touchend="onTouchEnd" @touchcancel="onTouchEnd">
|
||||||
<view ref="waterfallInnerRef" :class="{ 'waterfall-inner': true, 'ios-css-animate': !iosScrollPaused }"
|
<view ref="waterfallInnerRef" :class="{ 'waterfall-inner': true, 'ios-css-animate': !iosScrollPaused }"
|
||||||
:style="innerStyle">
|
:style="innerStyle">
|
||||||
<view v-for="card in cards" :key="card.id" class="wf-card" :style="cardStyle(card)"
|
<block v-for="card in cards" :key="card.id">
|
||||||
@click="handleCardClick(card)">
|
<view v-if="card && !card._blank" class="wf-card" :style="cardStyle(card)"
|
||||||
<!-- 渐变边框光效 -->
|
@click="handleCardClick(card)">
|
||||||
<view class="wf-card-border" :style="borderStyle(card)" />
|
<!-- 渐变边框光效 -->
|
||||||
|
<view class="wf-card-border" :style="borderStyle(card)" />
|
||||||
|
|
||||||
<!-- 封面图 -->
|
<!-- 封面图 -->
|
||||||
<image v-if="card.coverUrl" class="wf-card-img" :src="card.coverUrl" mode="aspectFill"
|
<image v-if="card.coverUrl" class="wf-card-img" :src="card.coverUrl" mode="aspectFill"
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
|
|
||||||
<!-- 光波动画层 - 外层 -->
|
<!-- 光波动画层 - 外层 -->
|
||||||
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
<!-- 光波动画层 - 内层 -->
|
<!-- 光波动画层 - 内层 -->
|
||||||
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
|
|
||||||
<!-- 底部点赞数 -->
|
<!-- 底部点赞数 -->
|
||||||
<view class="wf-card-footer">
|
<view class="wf-card-footer">
|
||||||
<image class="wf-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
|
<image class="wf-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
|
||||||
<view class="wf-likes-wrap">
|
<view class="wf-likes-wrap">
|
||||||
<text class="wf-likes">{{ formatLikes(card.likes) }}</text>
|
<text class="wf-likes">{{ formatLikes(card.likes) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
@ -104,6 +106,14 @@ const startIOSAutoScroll = () => {
|
|||||||
const elapsed = (Date.now() % duration)
|
const elapsed = (Date.now() % duration)
|
||||||
const pos = (elapsed / duration) * scrollDist
|
const pos = (elapsed / duration) * scrollDist
|
||||||
emit('scroll', pos)
|
emit('scroll', pos)
|
||||||
|
|
||||||
|
// iOS 预加载检查(CSS 动画不触发 onScroll,所以在这里检查)
|
||||||
|
const remainingScroll = scrollDist - pos - props.screenWidth
|
||||||
|
if (remainingScroll < Math.max(scrollDist / 2, props.screenWidth)) {
|
||||||
|
if (!isLoadingMore && !appendFailed && !isInitialLoading) {
|
||||||
|
scheduleAppend()
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 16)
|
}, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,10 +172,10 @@ const startAutoScroll = () => {
|
|||||||
}
|
}
|
||||||
scrollLeft.value = autoScrollPos
|
scrollLeft.value = autoScrollPos
|
||||||
|
|
||||||
// 预加载
|
// 预加载(不依赖 useMockData,真实 API 也能追加)
|
||||||
const remainingScroll = totalWidth.value - autoScrollPos - props.screenWidth
|
const remainingScroll = totalWidth.value - autoScrollPos - props.screenWidth
|
||||||
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
|
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
|
||||||
if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) {
|
if (!isLoadingMore && !appendFailed && !isInitialLoading) {
|
||||||
appendMore()
|
appendMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,174 +330,231 @@ const innerStyle = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ========== 布局引擎 ==========
|
// ========== 布局引擎 ==========
|
||||||
// 核心思路:按列放置,每列的卡片 span 之和 = ROWS,保证零空缺
|
// 核心思路:4×4 网格装箱,按块放置(每块16格),优先竖形状,放不下换横,最后填空白
|
||||||
// 同一列内所有卡片宽度统一 = 整列宽(span=ROWS 对应的宽),高度各自按 span 计算
|
// span 对应面积:
|
||||||
// 支持两种模式:
|
// span 1: 1×1 = 1格
|
||||||
// 1. 后端传 span:直接按 span 分组成列
|
// span 2: 优先 1×2(竖) 或 2×1(横) = 2格
|
||||||
// 2. 后端不传 span:使用分类特定的 span 阈值计算
|
// span 3: 2×2 = 4格
|
||||||
|
// span 4: 优先 2×3(竖) 或 3×2(横) = 6格
|
||||||
|
// span 5: 3×3 = 9格
|
||||||
|
// 注意:所有图片都是 4:3 比例
|
||||||
class WaterfallLayout {
|
class WaterfallLayout {
|
||||||
constructor(containerH, category = 'random', gap = GAP) {
|
constructor(containerW, containerH, gap = GAP) {
|
||||||
|
this.containerW = containerW
|
||||||
this.containerH = containerH
|
this.containerH = containerH
|
||||||
this.category = category
|
this.category = 'hot' // 默认分类
|
||||||
this.gap = gap
|
this.gap = gap
|
||||||
this.rowH = Math.floor((containerH - gap * (ROWS - 1)) / ROWS)
|
this.COLS = 4
|
||||||
// 列宽固定 = rowH × 9/16,严格竖长 9:16,span 只影响高度
|
this.ROWS = 4
|
||||||
this.colW = Math.round(this.rowH * 4 / 3)
|
this.BLOCK_SIZE = this.COLS * this.ROWS // 16格
|
||||||
this.curX = 0
|
this.IMG_RATIO = 4 / 3 // 图片宽高比 4:3
|
||||||
// 跨批次追踪连续 N 级卡片数量,凑满 5 个后在第 6 个 N 卡前插入空白格
|
|
||||||
this.consecutiveNCount = 0
|
// 计算每格尺寸(预留 gap)- 基于 4:3 图片比例计算
|
||||||
|
// cellW 是基准宽度,cellH 根据图片比例计算以保持 4:3
|
||||||
|
const totalGapW = gap * (this.COLS - 1)
|
||||||
|
const totalGapH = gap * (this.ROWS - 1)
|
||||||
|
this.cellW = Math.floor((containerW - totalGapW) / this.COLS)
|
||||||
|
this.cellH = Math.floor(this.cellW * 3 / 4) // 保持 4:3 比例
|
||||||
|
|
||||||
|
// 实际行高可能不足以填满容器,调整
|
||||||
|
const usedH = this.cellH * this.ROWS + totalGapH
|
||||||
|
if (usedH > containerH) {
|
||||||
|
// 高度超出,压缩 cellH
|
||||||
|
this.cellH = Math.floor((containerH - totalGapH) / this.ROWS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isNLevel(item) {
|
// span → 形状列表(优先竖的)
|
||||||
const span = item.span != null ? item.span : this._span(item.likes || 0)
|
// 竖 = 高 > 宽(占列多),横 = 宽 > 高(占行多)
|
||||||
return span === 1
|
_getShapes(span) {
|
||||||
|
switch (span) {
|
||||||
|
case 1: return [{ w: 1, h: 1, vertical: false }]
|
||||||
|
case 2: return [
|
||||||
|
{ w: 1, h: 2, vertical: true }, // 竖:窄长
|
||||||
|
{ w: 2, h: 1, vertical: false } // 横:扁宽
|
||||||
|
]
|
||||||
|
case 3: return [{ w: 2, h: 2, vertical: false }]
|
||||||
|
case 4: return [
|
||||||
|
{ w: 2, h: 3, vertical: true }, // 竖:高>宽
|
||||||
|
{ w: 3, h: 2, vertical: false } // 横:宽>高
|
||||||
|
]
|
||||||
|
case 5: return [{ w: 3, h: 3, vertical: false }]
|
||||||
|
default: return [{ w: 1, h: 1, vertical: false }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cardSize(span) {
|
// 点赞数 → span
|
||||||
const h = Math.round((span * this.rowH + (span - 1) * this.gap) * SCALE)
|
_calcSpan(likes) {
|
||||||
const w = Math.round(h * 4 / 3)
|
|
||||||
return { w, h }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点赞数 → span(使用分类特定的阈值)
|
|
||||||
_span(likes) {
|
|
||||||
return calcSpan(this.category, likes)
|
return calcSpan(this.category, likes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将用户列表按点赞数决定 span,分组成列,每列 span 之和 = ROWS
|
// 创建空网格
|
||||||
_groupIntoColumns(users) {
|
_createGrid() {
|
||||||
// 复制一份避免修改原数组
|
return Array(this.ROWS).fill(null).map(() => Array(this.COLS).fill(0))
|
||||||
const remaining = users.map((u, idx) => ({
|
}
|
||||||
...u,
|
|
||||||
originalIndex: idx,
|
|
||||||
span: u.span != null ? u.span : this._span(u.likes || 0)
|
|
||||||
}))
|
|
||||||
|
|
||||||
const columns = []
|
// 检查是否能放入
|
||||||
let colIndex = 0
|
_canPlace(grid, row, col, w, h) {
|
||||||
|
if (row + h > this.ROWS || col + w > this.COLS) return false
|
||||||
|
for (let r = row; r < row + h; r++) {
|
||||||
|
for (let c = col; c < col + w; c++) {
|
||||||
|
if (grid[r][c] !== 0) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
while (remaining.length > 0) {
|
// 标记占用
|
||||||
const col = []
|
_markGrid(grid, row, col, w, h) {
|
||||||
let sum = 0
|
for (let r = row; r < row + h; r++) {
|
||||||
const colStart = users.length - remaining.length
|
for (let c = col; c < col + w; c++) {
|
||||||
|
grid[r][c] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试放入能填满当前列的卡片
|
// 找空白位置(列优先:先左右,再上下)
|
||||||
let i = 0
|
_findSpace(grid, w, h) {
|
||||||
while (i < remaining.length) {
|
for (let c = 0; c <= this.COLS - w; c++) {
|
||||||
const rawSpan = remaining[i].span
|
for (let r = 0; r <= this.ROWS - h; r++) {
|
||||||
const isN = remaining[i]._blank ? false : this._isNLevel(remaining[i])
|
if (this._canPlace(grid, r, c, w, h)) {
|
||||||
|
return { row: r, col: c }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// N 级连续 5 张后,第 6 个 N 卡前先插入 1-2 个空白格
|
// 填空白格(列优先:先左右,再上下)
|
||||||
if (isN && this.consecutiveNCount >= 5) {
|
_fillBlanks(grid, placed, baseX, baseY) {
|
||||||
const blankCount = Math.random() < 0.5 ? 1 : 2
|
for (let c = 0; c < this.COLS; c++) {
|
||||||
for (let b = 0; b < blankCount; b++) {
|
for (let r = 0; r < this.ROWS; r++) {
|
||||||
if (sum + 1 <= ROWS) {
|
if (grid[r][c] === 0) {
|
||||||
remaining.splice(i, 0, { id: idCounter++, span: 1, _blank: true, likes: 0 })
|
placed.push({
|
||||||
|
id: idCounter++,
|
||||||
|
left: baseX + c * (this.cellW + this.gap),
|
||||||
|
top: baseY + r * (this.cellH + this.gap),
|
||||||
|
w: this.cellW,
|
||||||
|
h: this.cellH,
|
||||||
|
_blank: true,
|
||||||
|
radius: 8,
|
||||||
|
})
|
||||||
|
grid[r][c] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放置一块(4×4 = 16格)
|
||||||
|
_placeBlock(items, blockIndex) {
|
||||||
|
const grid = this._createGrid()
|
||||||
|
const placed = []
|
||||||
|
const baseX = blockIndex * (this.COLS * (this.cellW + this.gap) + this.gap)
|
||||||
|
const baseY = 0
|
||||||
|
|
||||||
|
let consecutiveSpan1 = 0
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const span = item.span != null ? item.span : this._calcSpan(item.likes || 0)
|
||||||
|
|
||||||
|
// span1 连续放置 5 个后,先留 1-2 个空白格再继续
|
||||||
|
if (span === 1 && consecutiveSpan1 >= 5) {
|
||||||
|
const blanks = Math.random() < 0.5 ? 1 : 2
|
||||||
|
for (let b = 0; b < blanks; b++) {
|
||||||
|
const pos = this._findSpace(grid, 1, 1)
|
||||||
|
if (pos) {
|
||||||
|
this._markGrid(grid, pos.row, pos.col, 1, 1)
|
||||||
|
placed.push({
|
||||||
|
id: idCounter++,
|
||||||
|
left: baseX + pos.col * (this.cellW + this.gap),
|
||||||
|
top: baseY + pos.row * (this.cellH + this.gap),
|
||||||
|
w: this.cellW,
|
||||||
|
h: this.cellH,
|
||||||
|
_blank: true,
|
||||||
|
radius: 8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consecutiveSpan1 = 0 // 重置计数
|
||||||
|
}
|
||||||
|
|
||||||
|
const shape = this._getShapes(span)[0] // 只用第一个形状
|
||||||
|
let success = false
|
||||||
|
|
||||||
|
// 优先第一个形状,找不到就全网格搜索能放的位置
|
||||||
|
if (shape) {
|
||||||
|
const pos = this._findSpace(grid, shape.w, shape.h)
|
||||||
|
if (pos) {
|
||||||
|
this._markGrid(grid, pos.row, pos.col, shape.w, shape.h)
|
||||||
|
const left = baseX + pos.col * (this.cellW + this.gap)
|
||||||
|
const top = baseY + pos.row * (this.cellH + this.gap)
|
||||||
|
const w = shape.w * this.cellW + (shape.w - 1) * this.gap
|
||||||
|
const h = shape.h * this.cellH + (shape.h - 1) * this.gap
|
||||||
|
placed.push({ ...item, left, top, w, h, radius: 8 })
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个形状放不下,全网格搜索其他可用的位置
|
||||||
|
if (!success) {
|
||||||
|
for (let c = 0; c <= this.COLS - shape.w && !success; c++) {
|
||||||
|
for (let r = 0; r <= this.ROWS - shape.h && !success; r++) {
|
||||||
|
if (grid[r][c] === 0) {
|
||||||
|
this._markGrid(grid, r, c, shape.w, shape.h)
|
||||||
|
const left = baseX + c * (this.cellW + this.gap)
|
||||||
|
const top = baseY + r * (this.cellH + this.gap)
|
||||||
|
const w = shape.w * this.cellW + (shape.w - 1) * this.gap
|
||||||
|
const h = shape.h * this.cellH + (shape.h - 1) * this.gap
|
||||||
|
placed.push({ ...item, left, top, w, h, radius: 8 })
|
||||||
|
success = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.consecutiveNCount = 0
|
|
||||||
// 空白格仅占位,不断 i,直接 break 让外层开新列处理剩余卡片
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果当前卡片加入会导致超出 ROWS,尝试下一个
|
|
||||||
if (sum + rawSpan > ROWS) {
|
|
||||||
i++
|
|
||||||
// 已检查完所有卡片仍无法放入 → 当前列已满但未凑满 ROWS,强开新列
|
|
||||||
if (i >= remaining.length && col.length > 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 可以放入,从 remaining 中移除并加入 col
|
|
||||||
const item = remaining.splice(i, 1)[0]
|
|
||||||
col.push(item)
|
|
||||||
sum += rawSpan
|
|
||||||
|
|
||||||
// 非 N 卡重置连续计数;N 卡累加计数
|
|
||||||
if (!item._blank) {
|
|
||||||
if (this._isNLevel(item)) {
|
|
||||||
this.consecutiveNCount++
|
|
||||||
} else {
|
|
||||||
this.consecutiveNCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经达到 ROWS,结束这列
|
|
||||||
if (sum >= ROWS) break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果放不下任何卡片(剩余卡片都太大),强制放入最小的
|
|
||||||
if (col.length === 0 && remaining.length > 0) {
|
|
||||||
const item = remaining.shift()
|
|
||||||
col.push(item)
|
|
||||||
sum = item.span
|
|
||||||
if (!item._blank) {
|
|
||||||
if (this._isNLevel(item)) {
|
|
||||||
this.consecutiveNCount++
|
|
||||||
} else {
|
|
||||||
this.consecutiveNCount = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
colIndex++
|
if (!success) {
|
||||||
|
console.warn('[WaterfallLayout] 无法放置卡片 span=' + span)
|
||||||
// 凑不满则补 _pad
|
|
||||||
while (sum < ROWS) {
|
|
||||||
col.push({ id: idCounter++, span: 1, _pad: true, likes: 0 })
|
|
||||||
sum++
|
|
||||||
}
|
}
|
||||||
columns.push(col)
|
|
||||||
}
|
|
||||||
return columns
|
|
||||||
}
|
|
||||||
|
|
||||||
_placeColumn(colUsers) {
|
if (span === 1 && success) {
|
||||||
const result = []
|
consecutiveSpan1++
|
||||||
let curY = 0
|
|
||||||
const colX = this.curX
|
|
||||||
const maxSpan = Math.max(...colUsers.map(u => u.span || 1))
|
|
||||||
const colW = this._cardSize(maxSpan).w
|
|
||||||
|
|
||||||
for (const u of colUsers) {
|
|
||||||
const span = u.span || 1
|
|
||||||
const { w, h } = this._cardSize(span)
|
|
||||||
if (!u._pad && !u._blank) {
|
|
||||||
result.push({ ...u, left: colX, top: curY, w, h, radius: 8 })
|
|
||||||
}
|
}
|
||||||
curY += h + this.gap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.curX += colW + this.gap
|
// 填空白
|
||||||
return result
|
this._fillBlanks(grid, placed, baseX, baseY)
|
||||||
|
return placed
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算全部卡片布局(重置状态)
|
// 计算全部卡片布局
|
||||||
compute(users) {
|
compute(users) {
|
||||||
this.curX = 0
|
this.curX = 0
|
||||||
this.consecutiveNCount = 0
|
|
||||||
const columns = this._groupIntoColumns(users)
|
|
||||||
const result = []
|
const result = []
|
||||||
for (const col of columns) {
|
|
||||||
result.push(...this._placeColumn(col))
|
// 每 16 张卡片为一块
|
||||||
|
for (let i = 0; i < users.length; i += this.BLOCK_SIZE) {
|
||||||
|
const block = users.slice(i, i + this.BLOCK_SIZE)
|
||||||
|
const blockResult = this._placeBlock(block, Math.floor(i / this.BLOCK_SIZE))
|
||||||
|
result.push(...blockResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.totalWidth = (Math.ceil(users.length / this.BLOCK_SIZE)) *
|
||||||
|
(this.COLS * (this.cellW + this.gap) + this.gap)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 追加更多卡片(保持当前 X 继续)
|
// 追加更多卡片(追加到下一块)
|
||||||
addCards(users) {
|
addCards(users) {
|
||||||
const columns = this._groupIntoColumns(users)
|
|
||||||
const result = []
|
const result = []
|
||||||
for (const col of columns) {
|
const blockIndex = Math.floor(allUsers.value.length / this.BLOCK_SIZE)
|
||||||
result.push(...this._placeColumn(col))
|
const blockResult = this._placeBlock(users, blockIndex)
|
||||||
}
|
result.push(...blockResult)
|
||||||
|
this.totalWidth = (blockIndex + 1) * (this.COLS * (this.cellW + this.gap) + this.gap)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalWidth() {
|
getTotalWidth() {
|
||||||
return this.curX
|
return this.totalWidth || 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -747,6 +814,7 @@ const appendMore = async () => {
|
|||||||
const placed = layout.addCards(withData)
|
const placed = layout.addCards(withData)
|
||||||
|
|
||||||
cards.value = [...cards.value, ...placed]
|
cards.value = [...cards.value, ...placed]
|
||||||
|
allUsers.value = [...allUsers.value, ...withData]
|
||||||
totalWidth.value = layout.getTotalWidth()
|
totalWidth.value = layout.getTotalWidth()
|
||||||
|
|
||||||
// totalWidth 变化后重启 iOS CSS 动画,确保新宽度生效
|
// totalWidth 变化后重启 iOS CSS 动画,确保新宽度生效
|
||||||
@ -819,7 +887,7 @@ onMounted(() => {
|
|||||||
const sysInfo = uni.getSystemInfoSync()
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
isIOS = sysInfo.platform === 'ios'
|
isIOS = sysInfo.platform === 'ios'
|
||||||
const containerH = props.screenHeight - props.bannerBottom
|
const containerH = props.screenHeight - props.bannerBottom
|
||||||
layout = new WaterfallLayout(containerH, props.category)
|
layout = new WaterfallLayout(props.screenWidth, containerH, GAP)
|
||||||
loadUsers().then(() => {
|
loadUsers().then(() => {
|
||||||
isInitialLoading = false
|
isInitialLoading = false
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@ -69,38 +69,48 @@ const NICKNAMES = [
|
|||||||
|
|
||||||
// ========== 分类 span 阈值配置 ==========
|
// ========== 分类 span 阈值配置 ==========
|
||||||
// 每个分类有不同的 span 计算规则
|
// 每个分类有不同的 span 计算规则
|
||||||
|
// span 对应网格中的面积:
|
||||||
|
// span 1: 1×1 = 1格(正方形)
|
||||||
|
// span 2: 1×2/2×1 = 2格(竖长/横长)
|
||||||
|
// span 3: 2×2 = 4格(正方形)
|
||||||
|
// span 4: 2×3/3×2 = 6格(竖长/横长)
|
||||||
|
// span 5: 3×3 = 9格(正方形)
|
||||||
export const SPAN_CONFIG = {
|
export const SPAN_CONFIG = {
|
||||||
// 统一 span 阈值
|
// 统一 span 阈值
|
||||||
hot: {
|
hot: {
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ max: 30, span: 1 }, // 0-30 → span 1
|
{ max: 30, span: 1 }, // 0-30 → span 1 (1格)
|
||||||
{ max: 100, span: 2 }, // 31-100 → span 2
|
{ max: 100, span: 2 }, // 31-100 → span 2 (2格)
|
||||||
{ max: 200, span: 3 }, // 101-200 → span 3
|
{ max: 200, span: 3 }, // 101-200 → span 3 (4格)
|
||||||
{ max: Infinity, span: 4 }, // 200+ → span 4
|
{ max: 400, span: 4 }, // 201-400 → span 4 (6格)
|
||||||
|
{ max: Infinity, span: 5 }, // 401+ → span 5 (9格)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
new: {
|
new: {
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ max: 30, span: 1 }, // 0-30 → span 1
|
{ max: 30, span: 1 },
|
||||||
{ max: 100, span: 2 }, // 31-100 → span 2
|
{ max: 100, span: 2 },
|
||||||
{ max: 200, span: 3 }, // 101-200 → span 3
|
{ max: 200, span: 3 },
|
||||||
{ max: Infinity, span: 4 }, // 200+ → span 4
|
{ max: 400, span: 4 },
|
||||||
|
{ max: Infinity, span: 5 },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
potential: {
|
potential: {
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ max: 30, span: 1 }, // 0-30 → span 1
|
{ max: 30, span: 1 },
|
||||||
{ max: 100, span: 2 }, // 31-100 → span 2
|
{ max: 100, span: 2 },
|
||||||
{ max: 200, span: 3 }, // 101-200 → span 3
|
{ max: 200, span: 3 },
|
||||||
{ max: Infinity, span: 4 }, // 200+ → span 4
|
{ max: 400, span: 4 },
|
||||||
|
{ max: Infinity, span: 5 },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
random: {
|
random: {
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ max: 30, span: 1 }, // 0-30 → span 1
|
{ max: 30, span: 1 },
|
||||||
{ max: 100, span: 2 }, // 31-100 → span 2
|
{ max: 100, span: 2 },
|
||||||
{ max: 200, span: 3 }, // 101-200 → span 3
|
{ max: 200, span: 3 },
|
||||||
{ max: Infinity, span: 4 }, // 200+ → span 4
|
{ max: 400, span: 4 },
|
||||||
|
{ max: Infinity, span: 5 },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user