通知中心游标分页

This commit is contained in:
biss committed 2026-08-23 09:21:04 +08:00
1 parent 172577f668
commit f5db2f6e77
4 files changed
+160 -64

No files matched your search

@@ -213,7 +213,7 @@ public sealed class NotificationsController(
Guid? administrativeClassId,
Guid? teachingTaskId,
string? keyword,
int page = 1,
string? cursor = null,
int pageSize = 30,
CancellationToken cancellationToken = default)
{
@@ -241,17 +241,22 @@ public sealed class NotificationsController(
if (filter.Role is not null && !SystemRoles.All.Contains(filter.Role))
return ValidationProblem("所选身份类型无效。");
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 100);
if (!CursorToken.TryDecode<RecipientCursor>(cursor, out var after))
return BadRequest("收件人游标无效。");
var source = ApplyRecipientFilter(
ScopedRecipientQuery(scope, resolvedCollegeId),
filter);
var total = await source.CountAsync(cancellationToken);
if (after is not null)
{
source = source.Where(x => x.DisplayName.CompareTo(after.DisplayName) > 0 ||
x.DisplayName == after.DisplayName && x.Id.CompareTo(after.Id) > 0);
}
var users = await source
.OrderBy(x => x.DisplayName)
.ThenBy(x => x.UserName)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ThenBy(x => x.Id)
.Take(pageSize + 1)
.Select(user => new
{
user.Id,
@@ -276,6 +281,8 @@ public sealed class NotificationsController(
.FirstOrDefault()
})
.ToListAsync(cancellationToken);
var hasMore = users.Count > pageSize;
users = users.Take(pageSize).ToList();
var userIds = users.Select(x => x.Id).ToArray();
var roleRows = await db.UserRoles.AsNoTracking()
.Where(x => userIds.Contains(x.UserId))
@@ -295,6 +302,7 @@ public sealed class NotificationsController(
group => group.Key,
group => group.Select(x => x.RoleName).Distinct().ToArray());
var last = users.LastOrDefault();
return Ok(new
{
Items = users.Select(user => new
@@ -308,11 +316,16 @@ public sealed class NotificationsController(
Roles = rolesByUser.GetValueOrDefault(user.Id, [])
}),
Total = total,
Page = page,
HasMore = hasMore,
NextCursor = hasMore && last is not null
? CursorToken.Encode(new RecipientCursor(last.DisplayName, last.Id))
: null,
PageSize = pageSize
});
}
private sealed record RecipientCursor(string DisplayName, Guid Id);
[HttpPost("send")]
[Authorize(Roles = Senders)]
public async Task<ActionResult> Send(
@@ -485,20 +498,24 @@ public sealed class NotificationsController(
[HttpGet("sent")]
[Authorize(Roles = Senders)]
public async Task<ActionResult> GetSent(
int page = 1,
string? cursor = null,
int pageSize = 20,
CancellationToken cancellationToken = default)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 1, 100);
if (!CursorToken.TryDecode<NotificationCursor>(cursor, out var after))
return BadRequest("已发送消息游标无效。");
var userId = currentUserDataScope.Current.UserId;
var source = db.MessageDispatches.AsNoTracking()
.Where(x => x.SenderUserId == userId);
var total = await source.CountAsync(cancellationToken);
var items = await source
.OrderByDescending(x => x.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
if (after is not null)
{
source = source.Where(x => x.CreatedAt < after.CreatedAt ||
x.CreatedAt == after.CreatedAt && x.Id.CompareTo(after.Id) < 0);
}
var rows = await source
.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
.Take(pageSize + 1)
.Select(x => new
{
x.Id,
@@ -511,7 +528,18 @@ public sealed class NotificationsController(
x.CreatedAt
})
.ToListAsync(cancellationToken);
return Ok(new { Items = items, Total = total });
var hasMore = rows.Count > pageSize;
var items = rows.Take(pageSize).ToArray();
var last = items.LastOrDefault();
return Ok(new
{
Items = items,
HasMore = hasMore,
NextCursor = hasMore && last is not null
? CursorToken.Encode(new NotificationCursor(last.CreatedAt, last.Id))
: null,
PageSize = pageSize
});
}
[HttpPost("{id:guid}/read")]
@@ -138,15 +138,17 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
[HttpGet("records")]
[Authorize(Roles = Managers + "," + SystemRoles.Counselor)]
public async Task<ActionResult<PagedResult<WarningRecordListItem>>> GetRecords(
public async Task<ActionResult<CursorPagedResult<WarningRecordListItem>>> GetRecords(
Guid? academicTermId,
WarningType? type,
int page = 1,
string? cursor = null,
int pageSize = 20,
CancellationToken ct = default)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
if (pageSize is < 1 or > 100)
return ValidationProblem("每批条数应在 1 至 100 之间。");
if (!CursorToken.TryDecode<WarningCursor>(cursor, out var after))
return BadRequest("预警记录游标无效。");
var q = db.WarningRecords.AsNoTracking().AsQueryable();
if (scope.Current.Scope == DataScope.College || scope.Current.IsInRole(SystemRoles.Counselor))
@@ -160,12 +162,15 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
if (academicTermId.HasValue) q = q.Where(x => x.AcademicTermId == academicTermId);
if (type.HasValue) q = q.Where(x => x.Type == type);
var total = await q.CountAsync(ct);
var items = await q
if (after is not null)
{
q = q.Where(x => x.CreatedAt < after.CreatedAt ||
x.CreatedAt == after.CreatedAt && x.Id.CompareTo(after.Id) < 0);
}
var rows = await q
.OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Take(pageSize + 1)
.Select(x => new WarningRecordListItem(
x.Id,
x.StudentId,
@@ -180,9 +185,20 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
x.AcknowledgeComment,
x.CreatedAt))
.ToListAsync(ct);
return Ok(new PagedResult<WarningRecordListItem>(items, total, page, pageSize));
var hasMore = rows.Count > pageSize;
var items = rows.Take(pageSize).ToArray();
var last = items.LastOrDefault();
return Ok(new CursorPagedResult<WarningRecordListItem>(
items,
hasMore && last is not null
? CursorToken.Encode(new WarningCursor(last.CreatedAt, last.Id))
: null,
hasMore,
pageSize));
}
private sealed record WarningCursor(DateTime CreatedAt, Guid Id);
// ═══════════ Student ═══════════
[HttpGet("my-warnings")]
[Authorize(Roles = SystemRoles.Student)]
+63 -24
View File
@@ -28,7 +28,6 @@ const activeTab = ref<'inbox' | 'compose' | 'sent'>('inbox')
const notifications = ref<any[]>([])
const sentMessages = ref<any[]>([])
const unreadCount = ref(0)
const sentTotal = ref(0)
const loading = ref(false)
const markingAllRead = ref(false)
const sending = ref(false)
@@ -39,7 +38,13 @@ const inboxCursors = ref<string[]>([])
const nextInboxCursor = ref<string>()
const inboxHasMore = ref(false)
const sentPage = ref(1)
const sentCursors = ref<string[]>([])
const nextSentCursor = ref<string>()
const sentHasMore = ref(false)
const recipientPage = ref(1)
const recipientCursors = ref<string[]>([])
const nextRecipientCursor = ref<string>()
const recipientHasMore = ref(false)
const pageSize = 20
const recipientPageSize = 30
const unreadOnly = ref(false)
@@ -169,14 +174,19 @@ async function previousInboxPage() {
}
async function loadSent(reset = false) {
if (reset) sentPage.value = 1
if (reset) {
sentPage.value = 1
sentCursors.value = []
nextSentCursor.value = undefined
}
loading.value = true
try {
const { data } = await http.get('/notifications/sent', {
params: { page: sentPage.value, pageSize },
params: { cursor: sentCursors.value.at(-1), pageSize },
})
sentMessages.value = data.items
sentTotal.value = data.total
sentHasMore.value = data.hasMore
nextSentCursor.value = data.nextCursor
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
@@ -184,6 +194,20 @@ async function loadSent(reset = false) {
}
}
async function nextSentPage() {
if (!nextSentCursor.value || loading.value) return
sentCursors.value.push(nextSentCursor.value)
sentPage.value++
await loadSent()
}
async function previousSentPage() {
if (!sentCursors.value.length || loading.value) return
sentCursors.value.pop()
sentPage.value--
await loadSent()
}
async function loadComposer() {
if (!canSend.value || composer.value) return
composerLoading.value = true
@@ -206,14 +230,18 @@ function recipientParams() {
administrativeClassId: recipientFilters.administrativeClassId || undefined,
teachingTaskId: recipientFilters.teachingTaskId || undefined,
keyword: recipientFilters.keyword.trim() || undefined,
page: recipientPage.value,
cursor: recipientCursors.value.at(-1),
pageSize: recipientPageSize,
}
}
async function loadRecipients(reset = false) {
if (!isAdministratorComposer.value) return
if (reset) recipientPage.value = 1
if (reset) {
recipientPage.value = 1
recipientCursors.value = []
nextRecipientCursor.value = undefined
}
recipientLoading.value = true
try {
const { data } = await http.get('/notifications/recipients', {
@@ -221,6 +249,8 @@ async function loadRecipients(reset = false) {
})
recipientItems.value = data.items
recipientTotal.value = data.total
recipientHasMore.value = data.hasMore
nextRecipientCursor.value = data.nextCursor
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
@@ -228,6 +258,20 @@ async function loadRecipients(reset = false) {
}
}
async function nextRecipientPage() {
if (!nextRecipientCursor.value || recipientLoading.value) return
recipientCursors.value.push(nextRecipientCursor.value)
recipientPage.value++
await loadRecipients()
}
async function previousRecipientPage() {
if (!recipientCursors.value.length || recipientLoading.value) return
recipientCursors.value.pop()
recipientPage.value--
await loadRecipients()
}
async function changeRecipientMode(value: 'Scope' | 'Filtered' | 'Selected') {
recipientMode.value = value
if (value !== 'Scope') await loadRecipients(true)
@@ -685,16 +729,11 @@ onMounted(() => load())
description="没有符合条件的可用账号"
/>
</div>
<el-pagination
v-if="recipientTotal > recipientPageSize"
v-model:current-page="recipientPage"
small
background
:page-size="recipientPageSize"
:total="recipientTotal"
layout="prev, pager, next"
@current-change="loadRecipients()"
/>
<div v-if="recipientCursors.length || recipientHasMore" class="cursor-pager">
<el-button size="small" :disabled="!recipientCursors.length || recipientLoading" @click="previousRecipientPage">上一批</el-button>
<span>第 {{ recipientPage }} 批</span>
<el-button size="small" type="primary" :disabled="!recipientHasMore || recipientLoading" @click="nextRecipientPage">下一批</el-button>
</div>
</div>
<div
@@ -828,14 +867,14 @@ onMounted(() => load())
<el-empty v-if="!loading && !sentMessages.length" description="还没有发送记录" />
</section>
<div v-if="sentTotal > pageSize" class="message-pager">
<el-pagination
v-model:current-page="sentPage"
:page-size="pageSize"
:total="sentTotal"
layout="prev, pager, next, total"
@current-change="loadSent()"
/>
<div v-if="sentCursors.length || sentHasMore" class="message-pager">
<el-button :disabled="!sentCursors.length || loading" @click="previousSentPage">
上一批
</el-button>
<span>第 {{ sentPage }} 批</span>
<el-button type="primary" :disabled="!sentHasMore || loading" @click="nextSentPage">
下一批
</el-button>
</div>
</template>
Loaded 3 of 4 files, more files were not shown because too many files have changed in this diff. Show more