185 lines
6.4 KiB
Markdown
185 lines
6.4 KiB
Markdown
# Square Tab 滚动行为差异化设计
|
||
|
||
**日期**: 2026-06-11
|
||
**状态**: 已通过 spec 评审
|
||
**作者**: Claude (brainstorming session)
|
||
|
||
## 背景
|
||
|
||
`square.vue` 页面通过 `activeContentTab` 切换 星河 / 星榜 / 广场 三个内容。
|
||
当前所有 tab 共用同一个外层 `<scroll-view class="content-wrapper" scroll-y>`,
|
||
导致三个 tab 的滚动行为完全一致:都是页面整体滚动。
|
||
|
||
需求:三个 tab 的滚动行为需要差异化:
|
||
|
||
| Tab | 期望行为 |
|
||
| --- | --- |
|
||
| 星河 (`xinghe`) | 完全禁用滚动(外层 + 内部都不滚) |
|
||
| 星榜 (`xingbang`) | 页面不滚动,只有 `HotCategoryBlock` 内部 grid 区域可滚 |
|
||
| 广场 (`guangchang`) | 保持当前行为(外层 scroll-view 整体滚动) |
|
||
|
||
## 目标
|
||
|
||
1. 不重构页面骨架
|
||
2. 不修改 `StarGalaxy` 任何样式(保留 `top: -128rpx`、`min-height: 1440rpx` 等视觉设计)
|
||
3. 仅通过外层 `scroll-view` 的 `scroll-y` 动态绑定 + `HotCategoryBlock` 内部增加独立 scroll 容器达成
|
||
4. 切换 tab 时重置外层 scrollTop,避免残留滚动位置
|
||
|
||
## 设计
|
||
|
||
### 1. 模板改动 (`frontend/pages/square/square.vue`)
|
||
|
||
唯一改动:把 `<scroll-view>` 上的 `scroll-y` 从静态改为动态绑定。
|
||
|
||
```vue
|
||
<scroll-view
|
||
class="content-wrapper"
|
||
:scroll-y="activeContentTab === 'guangchang'"
|
||
:show-scrollbar="false"
|
||
:bounce="false"
|
||
@scroll="onScroll"
|
||
@scrolltolower="handleScrollToLower"
|
||
>
|
||
<!-- banner + ContentTabs + 三个 tab 内容(结构完全不变) -->
|
||
<view class="banner-section">...</view>
|
||
<ContentTabs ... />
|
||
<view v-if="activeContentTab === 'xinghe'" class="star-galaxy-wrapper">
|
||
<StarGalaxy @cardClick="handleCardClick" />
|
||
</view>
|
||
<view v-else-if="activeContentTab === 'xingbang'" class="hot-category-wrapper">
|
||
<HotCategoryBlock @cardClick="handleCardClick" />
|
||
</view>
|
||
<CreationGrid
|
||
v-if="activeContentTab === 'guangchang'"
|
||
@cardClick="handleCardClick"
|
||
ref="creationGridRef"
|
||
/>
|
||
</scroll-view>
|
||
```
|
||
|
||
**样式改动**:给 `.hot-category-wrapper` 固定高度,让 `HotCategoryBlock` 的内部 scroll 有边界:
|
||
|
||
```scss
|
||
.hot-category-wrapper {
|
||
position: relative;
|
||
/* 100vh - header(208rpx) - banner(360rpx) - tabs(88rpx) = 可用 body 高度 */
|
||
height: calc(100vh - 208rpx - 360rpx - 88rpx);
|
||
}
|
||
```
|
||
|
||
### 2. HotCategoryBlock 内部改造 (`frontend/pages/square/components/HotCategoryBlock.vue`)
|
||
|
||
把 grid 区域(loading 骨架 / items-grid)包到一个新的内部 `<scroll-view>` 中。
|
||
|
||
**模板改动**:
|
||
|
||
```vue
|
||
<template>
|
||
<view class="hot-category-block">
|
||
<!-- Tab 栏(固定不滚)-->
|
||
<view class="ranking-tabs">
|
||
<!-- ... existing code ... -->
|
||
</view>
|
||
|
||
<!-- 网格区域:内部 scroll -->
|
||
<scroll-view
|
||
class="content-scroll"
|
||
scroll-y
|
||
:show-scrollbar="false"
|
||
:bounce="false"
|
||
>
|
||
<view v-if="loading" class="grid-skeleton">...</view>
|
||
<view v-else class="items-grid">...</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
**样式改动**:
|
||
|
||
```scss
|
||
.hot-category-block {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-scroll {
|
||
flex: 1;
|
||
min-height: 0; /* flex 子项需要 min-height: 0 才能正确伸缩 */
|
||
overflow: hidden;
|
||
}
|
||
```
|
||
|
||
### 3. 切换 tab 时重置 scrollTop
|
||
|
||
在 `square.vue` 的 `<script setup>` 中加 watch:
|
||
|
||
```javascript
|
||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||
|
||
// ... 现有代码 ...
|
||
|
||
watch(activeContentTab, () => {
|
||
// 切 tab 时把外层 scroll-view 滚回顶部
|
||
// 星河/星榜 时 scroll-y=false,但 scrollTop 残留可能导致显示异常
|
||
uni.pageScrollTo({ scrollTop: 0, duration: 0 });
|
||
});
|
||
```
|
||
|
||
> 备注:`uni.pageScrollTo` 是页面级 API,对 scroll-view 的 scrollTop 重置,
|
||
> 实际实现可改用 `ref` 绑定 `scroll-top` 属性并显式置 0。视具体平台支持调整。
|
||
> 如果运行时发现 `uni.pageScrollTo` 不生效,可改为:
|
||
> ```javascript
|
||
> const scrollTop = ref(0);
|
||
> watch(activeContentTab, () => { scrollTop.value = 0; });
|
||
> // <scroll-view :scroll-top="scrollTop" ...>
|
||
> ```
|
||
|
||
## 数据流 & 副作用
|
||
|
||
| Tab | 外层 scroll-y | onScroll 触发? | CreationGrid 分类标签 fixed |
|
||
| --- | --- | --- | --- |
|
||
| 星河 | `false` | 否 | 不相关(CreationGrid 不渲染) |
|
||
| 星榜 | `false` | 否 | 不相关 |
|
||
| 广场 | `true` | 是 | 正常 |
|
||
|
||
- `onScroll` / `handleScrollToLower` 内部已用 `creationGridRef.value?.updateScroll?.()` 可选链,
|
||
在 `creationGridRef` 为 `null` 时安全无副作用,**无需修改**
|
||
- `onShow` 中 `activeContentTab.value = 'xinghe'` 重置行为保留
|
||
- `Header` / `BottomNav` / `RankingModal` / `GuideOverlay` 都不受影响
|
||
|
||
## 错误处理
|
||
|
||
| 场景 | 处理 |
|
||
| --- | --- |
|
||
| 切到 星河/星榜 时 scrollTop 残留 | watch 重置 |
|
||
| `HotCategoryBlock` 内部 scroll-view 在加载更多时 | 当前 `HotCategoryBlock` 内部没有 `loadMore`(数据固定 11 条),不需要处理;如未来需要,可参考 `CreationGrid` 的 `scrolltolower` 模式 |
|
||
| `top: -128rpx` 在 scroll-y=false 时的视觉 | 保留现状,不调整;StarGalaxy 顶部 mask 透明渐变已设计为"上面有 banner 透出" |
|
||
|
||
## 测试要点
|
||
|
||
手动验证(无单元测试覆盖此 UI 行为):
|
||
|
||
1. **星河**:进入 星河 tab → 尝试上下/左右滑动 → 页面无任何滚动;StarGalaxy 完整可见部分铺满
|
||
2. **星榜**:进入 星榜 tab → 尝试滑动 banner / 顶部 → 顶部不滚;滑动 grid 卡片区域 → grid 区域独立滚
|
||
3. **广场**:进入 广场 tab → 整体页面可滚(与改造前完全一致)
|
||
4. **切换**:从 广场 滚动到中段 → 切到 星河 → 切回 广场 → 页面从顶部开始
|
||
5. **横幅/分类 fixed**:`CreationGrid` 的"分类标签切 fixed"在 广场 tab 仍正常
|
||
6. **双击点赞/单击跳转**:三个 tab 都正常
|
||
|
||
## 涉及文件
|
||
|
||
| 文件 | 改动 |
|
||
| --- | --- |
|
||
| `frontend/pages/square/square.vue` | scroll-view 加 `:scroll-y` 动态绑定;加 watch 重置 scrollTop;`.hot-category-wrapper` 加 `height: calc(...)` |
|
||
| `frontend/pages/square/components/HotCategoryBlock.vue` | grid 区域包到内部 scroll-view;加 `.content-scroll` 样式;改 `.hot-category-block` 为 flex 布局 |
|
||
|
||
## 不涉及
|
||
|
||
- `StarGalaxy` 任何文件(设计要求保留原状)
|
||
- `CreationGrid` 任何文件(行为不变)
|
||
- `BannerCarousel` / `ContentTabs` / `Header` / `BottomNav` / `RankingModal` / `GuideOverlay` 任何文件
|
||
- 后端 API、store、composables
|