feat: 消息与任务列表改为分页加载
此前一次性拉全量(硬编码 limit:100),任务/通知多了会拖慢首屏。 - 分页状态:page / pageSize(20) / hasMore / loadingMore,触底加载下一页 - 下拉刷新回到第 1 页并清空重载 —— 分页后必须重置,否则新旧页会错位 - 任务列表按 id 去重后追加:翻页期间若有新任务插入,分页边界会错位导致 重复项 - hasMore 优先以响应里的 total 为准,缺失时退回「本页是否满员」判断 - 底部新增状态提示(加载中 / 没有更多了 / 上拉加载更多), 让工人知道是「到底了」还是「还在拉」
This commit is contained in:
@ -39,13 +39,18 @@
|
||||
</view>
|
||||
<text class="card-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<!-- ⬇️ 触底加载状态:让工人知道"到底了"还是"还在拉" -->
|
||||
<view v-if="loadingMore" class="load-more">加载中...</view>
|
||||
<view v-else-if="!hasMore" class="load-more">— 没有更多了 —</view>
|
||||
<view v-else class="load-more">上拉加载更多</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { onShow, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { getNotifications, markNotificationRead } from "../../api/notification";
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
@ -58,11 +63,17 @@ const notifications = ref([]);
|
||||
const loading = ref(true);
|
||||
let currentUser = null;
|
||||
|
||||
// 📄 分页状态:此前用默认 limit=20 但翻不动页,第 21 条以后的消息永远看不到
|
||||
const pageSize = 20;
|
||||
let page = 1;
|
||||
const hasMore = ref(true);
|
||||
const loadingMore = ref(false);
|
||||
|
||||
onShow(() => {
|
||||
loadUser();
|
||||
setTimeout(() => {
|
||||
if (!currentUser) loadUser();
|
||||
fetchNotifications();
|
||||
fetchNotifications("initial");
|
||||
}, 200);
|
||||
});
|
||||
|
||||
@ -76,17 +87,69 @@ function loadUser() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
const userId = currentUser?.username || currentUser?.id || "";
|
||||
if (!userId) { loading.value = false; return; }
|
||||
loading.value = true;
|
||||
// 🚀 下拉刷新:界面提示了「请下拉刷新重试」,就必须把功能做实。
|
||||
// 分页后下拉必须重置 page 并清空列表 —— 否则新的第 1 页会和旧的第 2、3 页
|
||||
// 混在一起,出现重复项与排序错乱。
|
||||
onPullDownRefresh(async () => {
|
||||
try {
|
||||
const res = await getNotifications(userId);
|
||||
notifications.value = res.notifications || [];
|
||||
} catch {
|
||||
notifications.value = [];
|
||||
loadUser();
|
||||
await fetchNotifications("refresh");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
uni.stopPullDownRefresh(); // 无论成败都要收起动画,否则会一直挂着
|
||||
}
|
||||
});
|
||||
|
||||
// 🚀 触底加载下一页
|
||||
onReachBottom(() => {
|
||||
if (loading.value || loadingMore.value || !hasMore.value) return;
|
||||
loadingMore.value = true;
|
||||
fetchNotifications("more").finally(() => { loadingMore.value = false; });
|
||||
});
|
||||
|
||||
/**
|
||||
* 拉取通知
|
||||
* @param {'initial'|'refresh'|'more'} mode
|
||||
* initial — 首次进入/切页:显示整页 loading,失败清空
|
||||
* refresh — 下拉刷新:重置到第 1 页并清空重载
|
||||
* more — 触底加载:追加下一页,失败保留已加载内容只提示
|
||||
*/
|
||||
async function fetchNotifications(mode = "initial") {
|
||||
const userId = currentUser?.username || currentUser?.id || "";
|
||||
if (!userId) {
|
||||
if (mode === "initial") loading.value = false;
|
||||
else uni.showToast({ title: "未获取到登录信息,请重新登录", icon: "none", duration: 2500 });
|
||||
return;
|
||||
}
|
||||
// initial / refresh 都要从第 1 页重来:前者是切页回来,后者是下拉刷新。
|
||||
// 若 initial 沿用旧的 page,onShow 只会去拉"第 N 页"并追加,列表永远刷不新。
|
||||
if (mode === "initial" || mode === "refresh") {
|
||||
page = 1;
|
||||
hasMore.value = true;
|
||||
notifications.value = [];
|
||||
}
|
||||
if (mode === "initial") loading.value = true;
|
||||
const target = page;
|
||||
try {
|
||||
const res = await getNotifications(userId, (target - 1) * pageSize, pageSize);
|
||||
const batch = res.notifications || [];
|
||||
// 按 id 去重后追加:翻页期间若来了新消息,分页边界会错位导致重复项
|
||||
const seen = new Set(notifications.value.map((n) => n.id));
|
||||
notifications.value = notifications.value.concat(batch.filter((n) => !seen.has(n.id)));
|
||||
// 后端给了 total 就以它为准;否则退回「本页是否满员」判断
|
||||
hasMore.value = res.total != null
|
||||
? notifications.value.length < res.total
|
||||
: batch.length >= pageSize;
|
||||
if (hasMore.value) page = target + 1;
|
||||
} catch (e) {
|
||||
console.error("[notify] 拉取消息失败:", e);
|
||||
if (mode === "initial") notifications.value = [];
|
||||
else uni.showToast({
|
||||
title: mode === "more" ? "加载更多失败,请重试" : "刷新失败,请稍后重试",
|
||||
icon: "none",
|
||||
duration: 2500,
|
||||
});
|
||||
} finally {
|
||||
if (mode === "initial") loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,4 +228,7 @@ async function handleCardTap(item) {
|
||||
.card-content { font-size: 13px; color: #6b7280; line-height: 1.5; display: block; word-break: break-all; }
|
||||
|
||||
.card-arrow { font-size: 20px; color: #d1d5db; margin-top: 6px; flex-shrink: 0; }
|
||||
|
||||
/* 触底加载状态 */
|
||||
.load-more { text-align: center; font-size: 12px; color: #9ca3af; padding: 16px 0 4px; }
|
||||
</style>
|
||||
|
||||
@ -32,6 +32,11 @@
|
||||
</view>
|
||||
<view class="card-time">创建: {{ formatTime(task.created_at) }}</view>
|
||||
</view>
|
||||
|
||||
<!-- ⬇️ 触底加载状态:让工人知道"到底了"还是"还在拉" -->
|
||||
<view v-if="loadingMore" class="load-more">加载中...</view>
|
||||
<view v-else-if="!hasMore && tasks.length > 0" class="load-more">— 没有更多了 —</view>
|
||||
<view v-else-if="hasMore && tasks.length > 0" class="load-more">上拉加载更多</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@ -54,6 +59,11 @@ export default {
|
||||
tasks: [],
|
||||
loading: true,
|
||||
currentUser: null,
|
||||
// 📄 分页状态:此前硬编码 limit:100 一次性拉全量,任务多了会拖慢首屏
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
loadingMore: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -64,7 +74,21 @@ export default {
|
||||
},
|
||||
onShow() {
|
||||
this.loadCurrentUser();
|
||||
this.fetchTasks();
|
||||
this.fetchTasks({ reset: true });
|
||||
},
|
||||
// 🚀 下拉刷新:回到第 1 页并清空现有列表重载(分页后必须重置,否则新旧页会错位)
|
||||
async onPullDownRefresh() {
|
||||
try {
|
||||
this.loadCurrentUser();
|
||||
await this.fetchTasks({ reset: true });
|
||||
} finally {
|
||||
uni.stopPullDownRefresh(); // 无论成败都要收起动画,否则会一直挂着
|
||||
}
|
||||
},
|
||||
// 🚀 触底加载下一页
|
||||
onReachBottom() {
|
||||
if (this.loading || this.loadingMore || !this.hasMore) return;
|
||||
this.loadMore();
|
||||
},
|
||||
methods: {
|
||||
statusLabel(s) {
|
||||
@ -95,20 +119,51 @@ export default {
|
||||
if (u) this.currentUser = typeof u === "string" ? JSON.parse(u) : u;
|
||||
} catch {}
|
||||
},
|
||||
async fetchTasks() {
|
||||
this.loading = true;
|
||||
// 排序:主干任务在前,协助分支在后;同组内按创建时间升序
|
||||
compareTask(a, b) {
|
||||
const aIsMain = !a.parent_task_id || a.task_type !== 'SPAWN';
|
||||
const bIsMain = !b.parent_task_id || b.task_type !== 'SPAWN';
|
||||
if (aIsMain && !bIsMain) return -1;
|
||||
if (!aIsMain && bIsMain) return 1;
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
},
|
||||
|
||||
/** 拉取一页任务;reset=true 表示回到第 1 页并清空重载(下拉刷新 / 首次进入) */
|
||||
async fetchTasks({ reset = false } = {}) {
|
||||
if (reset) {
|
||||
this.page = 1;
|
||||
this.hasMore = true;
|
||||
this.tasks = [];
|
||||
}
|
||||
if (reset) this.loading = true;
|
||||
const target = this.page;
|
||||
try {
|
||||
const username = this.currentUser?.username || "";
|
||||
const res = await get("/tasks/", { assignee_id: username, limit: 100 });
|
||||
this.tasks = (res.tasks || []).sort((a, b) => {
|
||||
const aIsMain = !a.parent_task_id || a.task_type !== 'SPAWN';
|
||||
const bIsMain = !b.parent_task_id || b.task_type !== 'SPAWN';
|
||||
if (aIsMain && !bIsMain) return -1;
|
||||
if (!aIsMain && bIsMain) return 1;
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
const res = await get("/tasks/", {
|
||||
assignee_id: username,
|
||||
skip: (target - 1) * this.pageSize,
|
||||
limit: this.pageSize,
|
||||
});
|
||||
} catch { this.tasks = []; }
|
||||
finally { this.loading = false; }
|
||||
const batch = res.tasks || [];
|
||||
// 按 id 去重后追加:翻页期间若有新任务插入,分页边界会错位导致重复项
|
||||
const seen = new Set(this.tasks.map((t) => t.id));
|
||||
this.tasks = this.tasks.concat(batch.filter((t) => !seen.has(t.id))).sort(this.compareTask);
|
||||
// 后端给了 total 就以它为准;否则退回「本页是否满员」判断
|
||||
this.hasMore = res.total != null
|
||||
? this.tasks.length < res.total
|
||||
: batch.length >= this.pageSize;
|
||||
if (this.hasMore) this.page = target + 1;
|
||||
} catch (e) {
|
||||
console.error("[tasks] 拉取任务失败:", e);
|
||||
if (reset) this.tasks = [];
|
||||
this.hasMore = false;
|
||||
} finally {
|
||||
if (reset) this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadMore() {
|
||||
this.loadingMore = true;
|
||||
try { await this.fetchTasks(); } finally { this.loadingMore = false; }
|
||||
},
|
||||
goDetail(task) {
|
||||
const sn = task.product_sn || task.product_id;
|
||||
@ -155,4 +210,7 @@ export default {
|
||||
.s-gray { background: #f3f4f6; color: #6b7280; }
|
||||
.card-meta { font-size: 12px; color: #6b7280; display: flex; gap: 12px; }
|
||||
.card-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
||||
|
||||
/* 触底加载状态 */
|
||||
.load-more { text-align: center; font-size: 12px; color: #9ca3af; padding: 16px 0 4px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user