Compare commits
13 Commits
4d42d9a6d0
...
e29a718b2f
| Author | SHA1 | Date | |
|---|---|---|---|
| e29a718b2f | |||
| c8501c9895 | |||
| 3370f0507d | |||
| 7588498452 | |||
| aff114afdd | |||
| 7222598961 | |||
| c05953b84b | |||
| 091a3eb5c9 | |||
| ef46ec832e | |||
| cc5565ffaf | |||
| 95d8c75500 | |||
| 72ea3f4305 | |||
| d5d311c2b2 |
1104
docs/superpowers/specs/2026-05-22-sms-register-design.md
Normal file
1104
docs/superpowers/specs/2026-05-22-sms-register-design.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,52 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<scroll-view
|
<scroll-view class="waterfall-scroll" :style="scrollStyle" scroll-x :show-scrollbar="false"
|
||||||
class="waterfall-scroll"
|
:scroll-left="isIOS ? undefined : scrollLeft" :bounce="false" @scroll="onScroll" @touchstart="onTouchStart"
|
||||||
:style="scrollStyle"
|
@touchend="onTouchEnd" @touchcancel="onTouchEnd">
|
||||||
scroll-x
|
<view ref="waterfallInnerRef" :class="{ 'waterfall-inner': true, 'ios-css-animate': !iosScrollPaused }"
|
||||||
:show-scrollbar="false"
|
:style="innerStyle">
|
||||||
:scroll-left="isIOS ? undefined : scrollLeft"
|
<view v-for="card in cards" :key="card.id" class="wf-card" :style="cardStyle(card)"
|
||||||
:bounce="false"
|
@click="handleCardClick(card)">
|
||||||
@scroll="onScroll"
|
|
||||||
@touchstart="onTouchStart"
|
|
||||||
@touchend="onTouchEnd"
|
|
||||||
@touchcancel="onTouchEnd"
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
ref="waterfallInnerRef"
|
|
||||||
:class="{'waterfall-inner': true, 'ios-css-animate': !iosScrollPaused}"
|
|
||||||
:style="innerStyle"
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
v-for="card in cards"
|
|
||||||
:key="card.id"
|
|
||||||
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
|
<image v-if="card.coverUrl" class="wf-card-img" :src="card.coverUrl" mode="aspectFill"
|
||||||
v-if="card.coverUrl"
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
class="wf-card-img"
|
|
||||||
:src="card.coverUrl"
|
|
||||||
mode="aspectFill"
|
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 光波动画层 - 外层 -->
|
<!-- 光波动画层 - 外层 -->
|
||||||
<view
|
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||||
class="wf-like-wave wf-like-wave-outer"
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
:class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
|
||||||
/>
|
|
||||||
<!-- 光波动画层 - 内层 -->
|
<!-- 光波动画层 - 内层 -->
|
||||||
<view
|
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||||
class="wf-like-wave wf-like-wave-inner"
|
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }" />
|
||||||
:class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
|
||||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 底部点赞数 -->
|
<!-- 底部点赞数 -->
|
||||||
<view class="wf-card-footer">
|
<view class="wf-card-footer">
|
||||||
@ -184,6 +156,10 @@ const startAutoScroll = () => {
|
|||||||
scrollUpdateTimer = setInterval(() => {
|
scrollUpdateTimer = setInterval(() => {
|
||||||
if (!isComponentMounted || userInteracting || isLoadingMore) return
|
if (!isComponentMounted || userInteracting || isLoadingMore) return
|
||||||
autoScrollPos += AUTO_SCROLL_SPEED_ANDROID
|
autoScrollPos += AUTO_SCROLL_SPEED_ANDROID
|
||||||
|
// 滚到头时重置到 0,实现无缝循环
|
||||||
|
if (autoScrollPos >= totalWidth.value) {
|
||||||
|
autoScrollPos = 0
|
||||||
|
}
|
||||||
scrollLeft.value = autoScrollPos
|
scrollLeft.value = autoScrollPos
|
||||||
|
|
||||||
// 预加载
|
// 预加载
|
||||||
@ -358,6 +334,13 @@ class WaterfallLayout {
|
|||||||
// 列宽固定 = rowH × 9/16,严格竖长 9:16,span 只影响高度
|
// 列宽固定 = rowH × 9/16,严格竖长 9:16,span 只影响高度
|
||||||
this.colW = Math.round(this.rowH * 4 / 3)
|
this.colW = Math.round(this.rowH * 4 / 3)
|
||||||
this.curX = 0
|
this.curX = 0
|
||||||
|
// 跨批次追踪连续 N 级卡片数量,凑满 5 个后在第 6 个 N 卡前插入空白格
|
||||||
|
this.consecutiveNCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_isNLevel(item) {
|
||||||
|
const span = item.span != null ? item.span : this._span(item.likes || 0)
|
||||||
|
return span === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
_cardSize(span) {
|
_cardSize(span) {
|
||||||
@ -392,10 +375,28 @@ class WaterfallLayout {
|
|||||||
let i = 0
|
let i = 0
|
||||||
while (i < remaining.length) {
|
while (i < remaining.length) {
|
||||||
const rawSpan = remaining[i].span
|
const rawSpan = remaining[i].span
|
||||||
|
const isN = remaining[i]._blank ? false : this._isNLevel(remaining[i])
|
||||||
|
|
||||||
|
// N 级连续 5 张后,第 6 个 N 卡前先插入 1-2 个空白格
|
||||||
|
if (isN && this.consecutiveNCount >= 5) {
|
||||||
|
const blankCount = Math.random() < 0.5 ? 1 : 2
|
||||||
|
for (let b = 0; b < blankCount; b++) {
|
||||||
|
if (sum + 1 <= ROWS) {
|
||||||
|
remaining.splice(i, 0, { id: idCounter++, span: 1, _blank: true, likes: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.consecutiveNCount = 0
|
||||||
|
// 空白格仅占位,不断 i,直接 break 让外层开新列处理剩余卡片
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// 如果当前卡片加入会导致超出 ROWS,尝试下一个
|
// 如果当前卡片加入会导致超出 ROWS,尝试下一个
|
||||||
if (sum + rawSpan > ROWS) {
|
if (sum + rawSpan > ROWS) {
|
||||||
i++
|
i++
|
||||||
|
// 已检查完所有卡片仍无法放入 → 当前列已满但未凑满 ROWS,强开新列
|
||||||
|
if (i >= remaining.length && col.length > 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,6 +405,15 @@ class WaterfallLayout {
|
|||||||
col.push(item)
|
col.push(item)
|
||||||
sum += rawSpan
|
sum += rawSpan
|
||||||
|
|
||||||
|
// 非 N 卡重置连续计数;N 卡累加计数
|
||||||
|
if (!item._blank) {
|
||||||
|
if (this._isNLevel(item)) {
|
||||||
|
this.consecutiveNCount++
|
||||||
|
} else {
|
||||||
|
this.consecutiveNCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经达到 ROWS,结束这列
|
// 如果已经达到 ROWS,结束这列
|
||||||
if (sum >= ROWS) break
|
if (sum >= ROWS) break
|
||||||
}
|
}
|
||||||
@ -413,6 +423,13 @@ class WaterfallLayout {
|
|||||||
const item = remaining.shift()
|
const item = remaining.shift()
|
||||||
col.push(item)
|
col.push(item)
|
||||||
sum = item.span
|
sum = item.span
|
||||||
|
if (!item._blank) {
|
||||||
|
if (this._isNLevel(item)) {
|
||||||
|
this.consecutiveNCount++
|
||||||
|
} else {
|
||||||
|
this.consecutiveNCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
colIndex++
|
colIndex++
|
||||||
@ -437,7 +454,7 @@ class WaterfallLayout {
|
|||||||
for (const u of colUsers) {
|
for (const u of colUsers) {
|
||||||
const span = u.span || 1
|
const span = u.span || 1
|
||||||
const { w, h } = this._cardSize(span)
|
const { w, h } = this._cardSize(span)
|
||||||
if (!u._pad) {
|
if (!u._pad && !u._blank) {
|
||||||
result.push({ ...u, left: colX, top: curY, w, h, radius: 8 })
|
result.push({ ...u, left: colX, top: curY, w, h, radius: 8 })
|
||||||
}
|
}
|
||||||
curY += h + this.gap
|
curY += h + this.gap
|
||||||
@ -450,6 +467,7 @@ class WaterfallLayout {
|
|||||||
// 计算全部卡片布局(重置状态)
|
// 计算全部卡片布局(重置状态)
|
||||||
compute(users) {
|
compute(users) {
|
||||||
this.curX = 0
|
this.curX = 0
|
||||||
|
this.consecutiveNCount = 0
|
||||||
const columns = this._groupIntoColumns(users)
|
const columns = this._groupIntoColumns(users)
|
||||||
const result = []
|
const result = []
|
||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
@ -508,9 +526,9 @@ const cardStyle = (card) => ({
|
|||||||
|
|
||||||
const borderStyle = (card) => ({
|
const borderStyle = (card) => ({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: `-${BORDER_W}px`,
|
|
||||||
borderRadius: (card.radius + BORDER_W) + 'px',
|
borderRadius: (card.radius + BORDER_W) + 'px',
|
||||||
background: BORDER_COLORS[Math.abs(card.id) % BORDER_COLORS.length],
|
background: BORDER_COLORS[Math.abs(card.id) % BORDER_COLORS.length],
|
||||||
|
inset: `-${BORDER_W}px`,
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
})
|
})
|
||||||
@ -731,6 +749,12 @@ const appendMore = async () => {
|
|||||||
cards.value = [...cards.value, ...placed]
|
cards.value = [...cards.value, ...placed]
|
||||||
totalWidth.value = layout.getTotalWidth()
|
totalWidth.value = layout.getTotalWidth()
|
||||||
|
|
||||||
|
// totalWidth 变化后重启 iOS CSS 动画,确保新宽度生效
|
||||||
|
if (isIOS && !iosScrollPaused.value) {
|
||||||
|
stopIOSAutoScroll()
|
||||||
|
startIOSAutoScroll()
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 没有可追加的数据,标记失败停止
|
// 没有可追加的数据,标记失败停止
|
||||||
appendFailed = true
|
appendFailed = true
|
||||||
@ -999,8 +1023,11 @@ const loadUsersAndStartScroll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes iosAutoScroll {
|
@keyframes iosAutoScroll {
|
||||||
|
|
||||||
/* from { transform: translateX(0); } */
|
/* from { transform: translateX(0); } */
|
||||||
to { transform: translateX(var(--scroll-dist)); }
|
to {
|
||||||
|
transform: translateX(var(--scroll-dist));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wf-card {
|
.wf-card {
|
||||||
@ -1010,6 +1037,7 @@ const loadUsersAndStartScroll = () => {
|
|||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
pointer-events: visible;
|
pointer-events: visible;
|
||||||
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wf-card:active {
|
.wf-card:active {
|
||||||
@ -1019,6 +1047,7 @@ const loadUsersAndStartScroll = () => {
|
|||||||
.wf-card-border {
|
.wf-card-border {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
will-change: opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wf-card-img {
|
.wf-card-img {
|
||||||
@ -1048,8 +1077,7 @@ const loadUsersAndStartScroll = () => {
|
|||||||
background: linear-gradient(to bottom right,
|
background: linear-gradient(to bottom right,
|
||||||
#F0E4B1 0%,
|
#F0E4B1 0%,
|
||||||
#F08399 50%,
|
#F08399 50%,
|
||||||
#B94E73 100%
|
#B94E73 100%);
|
||||||
);
|
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
padding: 2rpx 10rpx;
|
padding: 2rpx 10rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1099,10 +1127,10 @@ const loadUsersAndStartScroll = () => {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user