Compare commits

..

2 Commits

Author SHA1 Message Date
168aecfd5c feat(admin): 公共管理列表添加分页
后端 listForAdmin 支持 page/pageSize 分页查询,控制器接收分页参数并校验边界,前端 Contents 页面添加 el-pagination 组件并同步分页状态。

🤖 Generated with [Qoder][https://qoder.com]
2026-06-12 09:31:38 +08:00
5a15159ff3 fix(admin): 修复公共内容展示时段与编辑表单时区不一致
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 09:24:25 +08:00
3 changed files with 76 additions and 19 deletions

View File

@@ -5,6 +5,10 @@ import type { TableInstance } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
import {
normalizeStartTimeForApi,
normalizeStartTimeForPicker,
} from './match-form';
const { t, localeTag } = useAdminLocale();
@@ -126,6 +130,9 @@ const filterStatus = ref<ContentStatus | ''>('');
const loading = ref(false);
const saving = ref(false);
const items = ref<ContentItem[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const tableRef = ref<TableInstance>();
const selectedRows = ref<ContentItem[]>([]);
@@ -202,9 +209,12 @@ async function load() {
params: {
type: activeType.value,
status: filterStatus.value || undefined,
page: page.value,
pageSize: pageSize.value,
},
});
items.value = data.data ?? [];
items.value = data.data.items ?? [];
total.value = data.data.total ?? 0;
selectedRows.value = [];
tableRef.value?.clearSelection();
} catch (e: unknown) {
@@ -215,7 +225,19 @@ async function load() {
}
}
function onPageChange(p: number) {
page.value = p;
load();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
load();
}
watch([activeType, filterStatus], () => {
page.value = 1;
selectedRows.value = [];
tableRef.value?.clearSelection();
void load();
@@ -286,7 +308,7 @@ function batchDelete() {
function resetForm() {
form.value = {
sortOrder: items.value.length + 1,
sortOrder: total.value + 1,
status: 'DRAFT',
linkType: '',
linkTarget: '',
@@ -311,8 +333,8 @@ function openEdit(row: ContentItem) {
status: row.status,
linkType: (row.linkType as '' | 'ROUTE' | 'URL') || '',
linkTarget: row.linkTarget ?? '',
startTime: row.startTime ? row.startTime.slice(0, 19) : '',
endTime: row.endTime ? row.endTime.slice(0, 19) : '',
startTime: normalizeStartTimeForPicker(row.startTime ?? undefined),
endTime: normalizeStartTimeForPicker(row.endTime ?? undefined),
translations: LOCALES.map((locale) => {
const tr = byLocale.get(locale);
return {
@@ -340,8 +362,8 @@ function buildPayload() {
linkType: isBanner.value && form.value.linkType ? form.value.linkType : null,
linkTarget:
isBanner.value && form.value.linkType ? form.value.linkTarget.trim() : null,
startTime: form.value.startTime || null,
endTime: form.value.endTime || null,
startTime: form.value.startTime ? normalizeStartTimeForApi(form.value.startTime) : null,
endTime: form.value.endTime ? normalizeStartTimeForApi(form.value.endTime) : null,
translations: form.value.translations.map((tr) => ({
locale: tr.locale,
title: tr.title.trim() || undefined,
@@ -558,6 +580,18 @@ void load();
</el-table-column>
</el-table>
</div>
<div class="pager">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
background
@current-change="onPageChange"
@size-change="onSizeChange"
/>
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close>

View File

@@ -2249,9 +2249,13 @@ export class AdminController {
async listContents(
@Query('type') type?: string,
@Query('status') status?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
const items = await this.content.listForAdmin(type, status);
return jsonResponse(items);
const p = Math.max(1, page ? parseInt(page, 10) || 1 : 1);
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 20), 100);
const result = await this.content.listForAdmin(type, status, p, size);
return jsonResponse(result);
}
@Get('contents/:id')

View File

@@ -255,7 +255,12 @@ export class ContentService {
});
}
async listForAdmin(contentType?: string, status?: string) {
async listForAdmin(
contentType?: string,
status?: string,
page = 1,
pageSize = 20,
) {
const typeWhere =
contentType === ANNOUNCEMENT_ADMIN_TYPE
? { contentType: { in: [...ANNOUNCEMENT_TYPES] } }
@@ -263,15 +268,28 @@ export class ContentService {
? { contentType }
: {};
const items = await this.prisma.content.findMany({
where: {
...typeWhere,
...(status ? { status } : {}),
},
include: { translations: true },
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
});
return items.map((item) => this.mapAdminItem(item));
const where = {
...typeWhere,
...(status ? { status } : {}),
};
const [items, total] = await Promise.all([
this.prisma.content.findMany({
where,
include: { translations: true },
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.content.count({ where }),
]);
return {
items: items.map((item) => this.mapAdminItem(item)),
total,
page,
pageSize,
};
}
async getForAdmin(id: bigint) {
@@ -442,6 +460,7 @@ export class ContentService {
/** @deprecated use listForAdmin */
async listAll(contentType?: string) {
return this.listForAdmin(contentType);
const result = await this.listForAdmin(contentType);
return result.items;
}
}