feat(media): 为媒体素材库添加分页加载功能
- 新增 PaginatedResult 泛型类型统一分页返回数据结构 - 重构后端媒体素材列表接口,支持分页参数并返回完整分页信息 - 前端媒体素材页面添加分页控件,适配分页加载逻辑 - 上传素材后自动重置页码至第一页并重新加载列表 - 更新接口文档和用户文档,补充分页API使用规范 - 新增对应测试用例覆盖分页接口与前端调用逻辑
This commit is contained in:
1 parent
68d2b88c04
commit
c381eb8b38
9 files changed
+132
-14
No files matched your search
@@ -37,7 +37,7 @@ describe("operations client", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("supports filtered leads, media listing and status contracts", async () => {
|
||||
it("supports filtered leads, paginated media listing and status contracts", async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(jsonResponse(200, { items: [] }))
|
||||
.mockResolvedValueOnce(jsonResponse(200, { id: "lead-1", status: "contacted" }))
|
||||
@@ -46,12 +46,12 @@ describe("operations client", () => {
|
||||
|
||||
await getLeads({ leadType: "vehicle", status: "new", keyword: "贵州", take: 50 });
|
||||
await updateLeadStatus("lead-1", "contacted");
|
||||
await getMediaAssets();
|
||||
await getMediaAssets({ pageNum: 2, pageSize: 20 });
|
||||
|
||||
expect(fetchMock.mock.calls.map(([url, options]) => [url, options?.method])).toEqual([
|
||||
["/api/admin/leads?leadType=vehicle&status=new&keyword=%E8%B4%B5%E5%B7%9E&take=50", undefined],
|
||||
["/api/admin/leads/lead-1/status", "PATCH"],
|
||||
["/api/admin/media-assets", undefined],
|
||||
["/api/admin/media-assets?pageNum=2&pageSize=20", undefined],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
LeadStatus,
|
||||
LeadType,
|
||||
MediaAsset,
|
||||
PaginatedResult,
|
||||
SiteConfig,
|
||||
SiteConfigItem,
|
||||
SiteItemPatch,
|
||||
@@ -242,7 +243,13 @@ export const updateLeadStatus = (leadId: string, status: LeadStatus) =>
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
export const getMediaAssets = () => request<{ items: MediaAsset[] }>("/api/admin/media-assets");
|
||||
export const getMediaAssets = (params: { pageNum?: number; pageSize?: number } = {}) => {
|
||||
const query = new URLSearchParams({
|
||||
pageNum: String(params.pageNum ?? 1),
|
||||
pageSize: String(params.pageSize ?? 20),
|
||||
});
|
||||
return request<PaginatedResult<MediaAsset>>(`/api/admin/media-assets?${query.toString()}`);
|
||||
};
|
||||
export const getSystemResource = (resource: string) => request<{ items: unknown[] }>(`/api/admin/system/${resource}`);
|
||||
export const getSystemUsers = () => request<{ items: SystemUserItem[] }>("/api/admin/system/users");
|
||||
export const getSystemRoles = () => request<{ items: SystemRoleItem[] }>("/api/admin/system/roles");
|
||||
|
||||
@@ -12,13 +12,18 @@ const uploading = ref(false);
|
||||
const error = ref("");
|
||||
const uploadError = ref("");
|
||||
const group = ref("general");
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
assets.value = (await getMediaAssets()).items;
|
||||
const result = await getMediaAssets({ pageNum: pageNum.value, pageSize: pageSize.value });
|
||||
assets.value = result.items;
|
||||
total.value = result.total;
|
||||
} catch (loadError) {
|
||||
error.value = loadError instanceof Error ? loadError.message : "媒体素材加载失败";
|
||||
} finally {
|
||||
@@ -42,7 +47,8 @@ async function upload(event: Event) {
|
||||
if (validationError) throw new Error(validationError);
|
||||
uploaded.push(await uploadMediaAsset(file, group.value.trim() || "general"));
|
||||
}
|
||||
assets.value = [...uploaded, ...assets.value];
|
||||
pageNum.value = 1;
|
||||
await load();
|
||||
ElMessage.success(`已上传 ${uploaded.length} 个素材`);
|
||||
} catch (uploadFailure) {
|
||||
uploadError.value = uploadFailure instanceof Error ? uploadFailure.message : "素材上传失败,请重试";
|
||||
@@ -51,16 +57,54 @@ async function upload(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(value: number) {
|
||||
pageNum.value = value;
|
||||
void load();
|
||||
}
|
||||
|
||||
function changePageSize(value: number) {
|
||||
pageSize.value = value;
|
||||
pageNum.value = 1;
|
||||
void load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<div class="page-intro site-config-intro"><div><p class="eyebrow">LIBRARY / MEDIA</p><h2>媒体素材</h2><p>统一查看已上传图片,并为站点、玩法详情和管家资料提供可复用 URL。</p></div><div class="media-toolbar"><el-input v-model="group" aria-label="素材分组" placeholder="素材分组" /><el-button v-permission="'admin:media:read'" type="primary" :loading="uploading" @click="openPicker">上传素材</el-button></div></div>
|
||||
<input ref="input" class="media-file-input" type="file" multiple accept="image/jpeg,image/png,image/webp,image/gif" :disabled="uploading" @change="upload" />
|
||||
<el-alert v-if="error" :title="error" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="load">重试</el-button></template></el-alert>
|
||||
<div class="page-intro site-config-intro">
|
||||
<div>
|
||||
<p class="eyebrow">LIBRARY / MEDIA</p>
|
||||
<h2>媒体素材</h2>
|
||||
<p>统一查看已上传图片,并为站点、玩法详情和管家资料提供可复用 URL。</p>
|
||||
</div>
|
||||
<div class="media-toolbar"><el-input v-model="group" aria-label="素材分组" placeholder="素材分组" /><el-button
|
||||
v-permission="'admin:media:read'" type="primary" :loading="uploading" @click="openPicker">上传素材</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="input" class="media-file-input" type="file" multiple accept="image/jpeg,image/png,image/webp,image/gif"
|
||||
:disabled="uploading" @change="upload" />
|
||||
<el-alert v-if="error" :title="error" type="error" show-icon :closable="false"><template #default><el-button link
|
||||
type="primary" @click="load">重试</el-button></template></el-alert>
|
||||
<el-alert v-if="uploadError" :title="uploadError" type="error" show-icon :closable="false" />
|
||||
<el-skeleton v-if="loading" :rows="8" animated />
|
||||
<el-card v-else shadow="never" class="panel-card"><el-empty v-if="!assets.length" description="暂无媒体素材"><el-button v-permission="'admin:media:read'" type="primary" :loading="uploading" @click="openPicker">上传第一张图片</el-button></el-empty><div v-else class="media-grid"><article v-for="asset in assets" :key="asset.id" class="media-card"><el-image :src="resolveAdminMediaUrl(asset.url)" fit="cover" :preview-src-list="[resolveAdminMediaUrl(asset.url) ?? asset.url]" /><div class="media-card-content"><strong>{{ asset.name || "未命名素材" }}</strong><span>{{ asset.group || "general" }} · {{ formatMediaSize(asset.sizeBytes) }}</span><small>{{ asset.url }}</small></div></article></div></el-card>
|
||||
<el-card v-else shadow="never" class="panel-card">
|
||||
<el-empty v-if="!assets.length" description="暂无媒体素材"><el-button v-permission="'admin:media:read'" type="primary"
|
||||
:loading="uploading" @click="openPicker">上传第一张图片</el-button></el-empty>
|
||||
<template v-else>
|
||||
<div class="media-grid">
|
||||
<article v-for="asset in assets" :key="asset.id" class="media-card"><el-image
|
||||
:src="resolveAdminMediaUrl(asset.url)" fit="cover"
|
||||
:preview-src-list="[resolveAdminMediaUrl(asset.url) ?? asset.url]" />
|
||||
<div class="media-card-content"><strong>{{ asset.name || "未命名素材" }}</strong><span>{{ asset.group ||
|
||||
"general" }} · {{ formatMediaSize(asset.sizeBytes) }}</span><small>{{ asset.url }}</small></div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="media-pagination"><el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
|
||||
:page-sizes="[12, 20, 40]" :total="total" background layout="total, sizes, prev, pager, next"
|
||||
@current-change="changePage" @size-change="changePageSize" /></div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -403,6 +403,11 @@ input {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.media-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.media-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
|
||||
@@ -225,6 +225,13 @@ export type MediaAsset = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type PaginatedResult<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
export type ConciergeAdvisor = {
|
||||
id: string;
|
||||
avatar?: string;
|
||||
|
||||
Reference in new issue
Block a user