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