feat:星榜新增翻页功能
This commit is contained in:
parent
c90dcf3d36
commit
37fbcb42c6
@ -26,7 +26,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 区域 B:分类标签 -->
|
<!-- 区域 B:分类标签 -->
|
||||||
<view
|
<!-- <view
|
||||||
ref="categoryRef"
|
ref="categoryRef"
|
||||||
id="category-section"
|
id="category-section"
|
||||||
class="category-section"
|
class="category-section"
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<text class="category-text">{{ category.label }}</text>
|
<text class="category-text">{{ category.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view> -->
|
||||||
<!-- fixed 时占位,避免下方内容跳变 -->
|
<!-- fixed 时占位,避免下方内容跳变 -->
|
||||||
<view
|
<view
|
||||||
v-if="isFixed && categoryHeight > 0"
|
v-if="isFixed && categoryHeight > 0"
|
||||||
|
|||||||
@ -30,7 +30,14 @@
|
|||||||
scroll-y
|
scroll-y
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
:bounce="false"
|
:bounce="false"
|
||||||
|
:lower-threshold="80"
|
||||||
|
:scroll-into-view="scrollIntoView"
|
||||||
|
:scroll-with-animation="false"
|
||||||
|
@scrolltolower="handleScrollToLower"
|
||||||
>
|
>
|
||||||
|
<!-- 顶部哨兵:切换 tab 时通过 scroll-into-view 把列表强制滚回顶部 -->
|
||||||
|
<view id="grid-top" class="scroll-anchor"></view>
|
||||||
|
|
||||||
<!-- 骨架屏 -->
|
<!-- 骨架屏 -->
|
||||||
<view v-if="loading" class="grid-skeleton">
|
<view v-if="loading" class="grid-skeleton">
|
||||||
<view v-for="i in 11" :key="i" class="skeleton-card">
|
<view v-for="i in 11" :key="i" class="skeleton-card">
|
||||||
@ -120,13 +127,33 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 分页底部状态:加载中 / 没有更多了 / 暂无数据 -->
|
||||||
|
<view
|
||||||
|
v-if="loadingMore"
|
||||||
|
class="load-more-tip load-more-tip-loading"
|
||||||
|
>
|
||||||
|
<text class="load-more-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else-if="!hasMore && items.length > 0"
|
||||||
|
class="load-more-tip"
|
||||||
|
>
|
||||||
|
<text class="load-more-text">— 没有更多了 —</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else-if="!loading && items.length === 0"
|
||||||
|
class="load-more-tip"
|
||||||
|
>
|
||||||
|
<text class="load-more-text">暂无数据</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import { getHotRankingApi } from "@/utils/api.js";
|
import { getHotRankingApi } from "@/utils/api.js";
|
||||||
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
|
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
|
||||||
@ -149,8 +176,24 @@ const loading = ref(false);
|
|||||||
const likingMap = ref({});
|
const likingMap = ref({});
|
||||||
const activeTabKey = ref("");
|
const activeTabKey = ref("");
|
||||||
|
|
||||||
|
// ===== 分页状态 =====
|
||||||
|
// currentPage : 已加载到的页码(从 1 开始)
|
||||||
|
// hasMore : 是否还有下一页(接口返回数量 < PAGE_SIZE 或累计已 >= total 时置 false)
|
||||||
|
// loadingMore : 是否正在加载下一页(防止 scrolltolower 重复触发)
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const hasMore = ref(true);
|
||||||
|
const loadingMore = ref(false);
|
||||||
|
|
||||||
|
// scroll-into-view 目标元素 id —— 切换 tab 时设为 'grid-top' 把 scroll-view 滚回顶部
|
||||||
|
// 通过先置空 → nextTick 设回目标 id 来强制触发(即使上一次已经是同一个 id 也能生效)
|
||||||
|
const scrollIntoView = ref("");
|
||||||
|
|
||||||
|
// 每页数量
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
// Tab 配置(直接写死在组件内)
|
// Tab 配置(直接写死在组件内)
|
||||||
// 新增 tab 在这里 push 一项即可:{ key, label, icon, iconWidth, iconHeight, fetch }
|
// 新增 tab 在这里 push 一项即可:{ key, label, icon, iconWidth, iconHeight, fetch }
|
||||||
|
// fetch 接受 page 参数,由组件内部分页逻辑统一传入。
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
key: "hot",
|
key: "hot",
|
||||||
@ -158,7 +201,7 @@ const tabs = [
|
|||||||
icon: "/static/square/galaxy/dianzanbang.png",
|
icon: "/static/square/galaxy/dianzanbang.png",
|
||||||
iconWidth: 64,
|
iconWidth: 64,
|
||||||
iconHeight: 72,
|
iconHeight: 72,
|
||||||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "new",
|
key: "new",
|
||||||
@ -166,7 +209,7 @@ const tabs = [
|
|||||||
icon: "/static/square/galaxy/huoyuebang.png",
|
icon: "/static/square/galaxy/huoyuebang.png",
|
||||||
iconWidth: 64,
|
iconWidth: 64,
|
||||||
iconHeight: 72,
|
iconHeight: 72,
|
||||||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "trending",
|
key: "trending",
|
||||||
@ -174,7 +217,7 @@ const tabs = [
|
|||||||
icon: "/static/square/galaxy/baoguangbang.png",
|
icon: "/static/square/galaxy/baoguangbang.png",
|
||||||
iconWidth: 64,
|
iconWidth: 64,
|
||||||
iconHeight: 72,
|
iconHeight: 72,
|
||||||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "tongcheng",
|
key: "tongcheng",
|
||||||
@ -182,7 +225,7 @@ const tabs = [
|
|||||||
icon: "/static/square/galaxy/tongchengbang.png",
|
icon: "/static/square/galaxy/tongchengbang.png",
|
||||||
iconWidth: 64,
|
iconWidth: 64,
|
||||||
iconHeight: 72,
|
iconHeight: 72,
|
||||||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -226,9 +269,9 @@ const handleTabClick = (e) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// activeTab 变化时重新加载数据
|
// activeTab 变化时重新加载数据(重置分页)
|
||||||
watch(activeTab, () => {
|
watch(activeTab, () => {
|
||||||
loadData();
|
resetAndLoad();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 格式化数量
|
// 格式化数量
|
||||||
@ -265,20 +308,41 @@ const onAssetLiked = ({ asset_id, data }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 重置分页并重新加载第 1 页
|
||||||
|
// 切换 tab 时调用:清空 items、把 currentPage 拉回 1、hasMore 设回 true、滚动到顶。
|
||||||
|
const resetAndLoad = () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
hasMore.value = true;
|
||||||
|
loadingMore.value = false;
|
||||||
|
items.value = [];
|
||||||
|
// 强制滚回顶部:先清空再设回 id,避免上一次值就是 'grid-top' 时不触发
|
||||||
|
scrollIntoView.value = "";
|
||||||
|
nextTick(() => {
|
||||||
|
scrollIntoView.value = "grid-top";
|
||||||
|
});
|
||||||
|
loadData({ append: false });
|
||||||
|
};
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
// append=false:首页加载,覆盖 items;append=true:分页追加,拼接到 items 末尾。
|
||||||
|
const loadData = async ({ append = false } = {}) => {
|
||||||
const tab = activeTab.value;
|
const tab = activeTab.value;
|
||||||
if (!tab || typeof tab.fetch !== "function") {
|
if (!tab || typeof tab.fetch !== "function") {
|
||||||
console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab);
|
console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab);
|
||||||
items.value = [];
|
if (!append) items.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
// 首页用 loading 整屏骨架;分页用 loadingMore 底部小指示器
|
||||||
|
if (append) {
|
||||||
|
loadingMore.value = true;
|
||||||
|
} else {
|
||||||
|
loading.value = true;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await tab.fetch();
|
const res = await tab.fetch(currentPage.value);
|
||||||
if (res && res.code === 200 && res.data?.items) {
|
if (res && res.code === 200 && res.data?.items) {
|
||||||
// 逐个把 cover_url 转换成真实可访问 URL(OSS 预签名前端实现)
|
// 逐个把 cover_url 转换成真实可访问 URL(OSS 预签名前端实现)
|
||||||
items.value = await Promise.all(
|
const newItems = await Promise.all(
|
||||||
res.data.items.map(async (item) => {
|
res.data.items.map(async (item) => {
|
||||||
return await resolveItemUrls({
|
return await resolveItemUrls({
|
||||||
...item,
|
...item,
|
||||||
@ -286,17 +350,42 @@ const loadData = async () => {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
|
if (append) {
|
||||||
|
items.value = [...items.value, ...newItems];
|
||||||
|
} else {
|
||||||
|
items.value = newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判定是否还有下一页:
|
||||||
|
// 1) 接口返回 total 时,按累计长度比对;
|
||||||
|
// 2) 兜底:本次返回不足一页认为到底。
|
||||||
|
const total = Number(res.data.total ?? 0);
|
||||||
|
if (total > 0) {
|
||||||
|
hasMore.value = items.value.length < total;
|
||||||
|
} else {
|
||||||
|
hasMore.value = newItems.length >= PAGE_SIZE;
|
||||||
|
}
|
||||||
|
} else if (!append) {
|
||||||
items.value = [];
|
items.value = [];
|
||||||
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
|
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
|
||||||
items.value = [];
|
if (!append) items.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
loadingMore.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 滚动到底部触发加载下一页
|
||||||
|
const handleScrollToLower = () => {
|
||||||
|
if (loading.value || loadingMore.value || !hasMore.value) return;
|
||||||
|
currentPage.value += 1;
|
||||||
|
loadData({ append: true });
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
uni.$on("assetLiked", onAssetLiked);
|
uni.$on("assetLiked", onAssetLiked);
|
||||||
loadData();
|
loadData();
|
||||||
@ -754,4 +843,45 @@ onUnmounted(() => {
|
|||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分页底部加载提示 */
|
||||||
|
.load-more-tip {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 0 8rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动哨兵:仅作为 scroll-into-view 的锚点,不占据视觉空间 */
|
||||||
|
.scroll-anchor {
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #fffabd;
|
||||||
|
text-shadow: -1px 1px 4px #ce0909d6;
|
||||||
|
opacity: 0.85;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-tip-loading .load-more-text {
|
||||||
|
animation: loadMorePulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadMorePulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -360,6 +360,7 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
/* 100vh - header(208rpx) - banner(360rpx) - tabs(88rpx) = 可用 body 高度 */
|
/* 100vh - header(208rpx) - banner(360rpx) - tabs(88rpx) = 可用 body 高度 */
|
||||||
height: calc(100vh - 208rpx - 360rpx - 88rpx);
|
height: calc(100vh - 208rpx - 360rpx - 88rpx);
|
||||||
|
top: -8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 蒙层 */
|
/* 蒙层 */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user