feat: 消息与任务列表改为分页加载
此前一次性拉全量(硬编码 limit:100),任务/通知多了会拖慢首屏。 - 分页状态:page / pageSize(20) / hasMore / loadingMore,触底加载下一页 - 下拉刷新回到第 1 页并清空重载 —— 分页后必须重置,否则新旧页会错位 - 任务列表按 id 去重后追加:翻页期间若有新任务插入,分页边界会错位导致 重复项 - hasMore 优先以响应里的 total 为准,缺失时退回「本页是否满员」判断 - 底部新增状态提示(加载中 / 没有更多了 / 上拉加载更多), 让工人知道是「到底了」还是「还在拉」
This commit is contained in:
@ -39,13 +39,18 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="card-arrow">›</text>
|
<text class="card-arrow">›</text>
|
||||||
</view>
|
</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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
|
||||||
import { getNotifications, markNotificationRead } from "../../api/notification";
|
import { getNotifications, markNotificationRead } from "../../api/notification";
|
||||||
|
|
||||||
const TYPE_CONFIG = {
|
const TYPE_CONFIG = {
|
||||||
@ -58,11 +63,17 @@ const notifications = ref([]);
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
|
|
||||||
|
// 📄 分页状态:此前用默认 limit=20 但翻不动页,第 21 条以后的消息永远看不到
|
||||||
|
const pageSize = 20;
|
||||||
|
let page = 1;
|
||||||
|
const hasMore = ref(true);
|
||||||
|
const loadingMore = ref(false);
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!currentUser) loadUser();
|
if (!currentUser) loadUser();
|
||||||
fetchNotifications();
|
fetchNotifications("initial");
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,17 +87,69 @@ function loadUser() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNotifications() {
|
// 🚀 下拉刷新:界面提示了「请下拉刷新重试」,就必须把功能做实。
|
||||||
const userId = currentUser?.username || currentUser?.id || "";
|
// 分页后下拉必须重置 page 并清空列表 —— 否则新的第 1 页会和旧的第 2、3 页
|
||||||
if (!userId) { loading.value = false; return; }
|
// 混在一起,出现重复项与排序错乱。
|
||||||
loading.value = true;
|
onPullDownRefresh(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getNotifications(userId);
|
loadUser();
|
||||||
notifications.value = res.notifications || [];
|
await fetchNotifications("refresh");
|
||||||
} catch {
|
|
||||||
notifications.value = [];
|
|
||||||
} finally {
|
} 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-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; }
|
.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>
|
</style>
|
||||||
|
|||||||
@ -32,6 +32,11 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="card-time">创建: {{ formatTime(task.created_at) }}</view>
|
<view class="card-time">创建: {{ formatTime(task.created_at) }}</view>
|
||||||
</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>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -54,6 +59,11 @@ export default {
|
|||||||
tasks: [],
|
tasks: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
|
// 📄 分页状态:此前硬编码 limit:100 一次性拉全量,任务多了会拖慢首屏
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
hasMore: true,
|
||||||
|
loadingMore: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -64,7 +74,21 @@ export default {
|
|||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
this.loadCurrentUser();
|
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: {
|
methods: {
|
||||||
statusLabel(s) {
|
statusLabel(s) {
|
||||||
@ -95,20 +119,51 @@ export default {
|
|||||||
if (u) this.currentUser = typeof u === "string" ? JSON.parse(u) : u;
|
if (u) this.currentUser = typeof u === "string" ? JSON.parse(u) : u;
|
||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
async fetchTasks() {
|
// 排序:主干任务在前,协助分支在后;同组内按创建时间升序
|
||||||
this.loading = true;
|
compareTask(a, b) {
|
||||||
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 aIsMain = !a.parent_task_id || a.task_type !== 'SPAWN';
|
||||||
const bIsMain = !b.parent_task_id || b.task_type !== 'SPAWN';
|
const bIsMain = !b.parent_task_id || b.task_type !== 'SPAWN';
|
||||||
if (aIsMain && !bIsMain) return -1;
|
if (aIsMain && !bIsMain) return -1;
|
||||||
if (!aIsMain && bIsMain) return 1;
|
if (!aIsMain && bIsMain) return 1;
|
||||||
return new Date(a.created_at) - new Date(b.created_at);
|
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,
|
||||||
|
skip: (target - 1) * this.pageSize,
|
||||||
|
limit: this.pageSize,
|
||||||
});
|
});
|
||||||
} catch { this.tasks = []; }
|
const batch = res.tasks || [];
|
||||||
finally { this.loading = false; }
|
// 按 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) {
|
goDetail(task) {
|
||||||
const sn = task.product_sn || task.product_id;
|
const sn = task.product_sn || task.product_id;
|
||||||
@ -155,4 +210,7 @@ export default {
|
|||||||
.s-gray { background: #f3f4f6; color: #6b7280; }
|
.s-gray { background: #f3f4f6; color: #6b7280; }
|
||||||
.card-meta { font-size: 12px; color: #6b7280; display: flex; gap: 12px; }
|
.card-meta { font-size: 12px; color: #6b7280; display: flex; gap: 12px; }
|
||||||
.card-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
.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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user