feat(core): notification inbox infinite scroll + command palette fix (+ pending WIP)

消息中心:全量渲染 → 真·后端分页滚动加载
- backend(ops/views): NotificationPagination(10/页,page_size 可覆盖)+
  响应回 type_counts(按收件人绝对计数,不受分页/搜索影响)
- frontend(messages): 自管分页,滚到底加载下一批;tab/搜索走服务端并重置到第1页;
  代号作废在途旧请求防切换卡空白;乐观标已读;「已加载 X / Y」分母用当前筛选总数
- api/App/types: listNotifications 支持 page/page_size/search;allNotifications 携带 type_counts

命令面板(侧边栏搜索):修复点开后 UI 错位
- app-shell: 遮罩 className 漏了基类 shell-command-bg(只有 .show)致无定位塌到左下;
  补回基类 + header 类名对齐 .shell-command-h
- messages-page.css: 工作台收进视口高度,收件箱在面板内滚动

本次提交一并带入此前若干未提交 WIP(account/ai-tools/library/pipeline/products/settings +
accounts/ai/assets/billing/projects 后端),按用户要求整体推 dev。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-10 09:37:41 +08:00
co-authored by Claude Opus 4.8
parent aa4bdeac83
commit 3fac38c5ef
29 changed files with 724 additions and 150 deletions
+34 -3
View File
@@ -231,14 +231,26 @@ export const api = {
assets() {
return request<Paginated<Asset>>("/api/assets/");
},
// 跟随 DRF 分页 next 取全部资产 —— 商品图/AI 素材/资产库都靠 asset.id 在这份列表里查 preview_url,
// 只取第 1 页(20 条)会让第 20 条之后的资产解析不到图、渲染成空占位。
async allAssets(): Promise<Asset[]> {
const out: Asset[] = [];
let path = "/api/assets/";
for (let guard = 0; guard < 50 && path; guard += 1) {
const page: Paginated<Asset> = await request<Paginated<Asset>>(path);
out.push(...page.results);
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
}
return out;
},
uploadAsset(formData: FormData) {
return request<Asset>("/api/assets/upload/", { method: "POST", body: formData });
},
billingSummary() {
return request<BillingSummary>("/api/billing/summary/");
},
ledgers() {
return request<Ledger[]>("/api/billing/ledgers/");
ledgers(page = 1, pageSize = 10) {
return request<{ count: number; page: number; page_size: number; results: Ledger[] }>(`/api/billing/ledgers/?page=${page}&page_size=${pageSize}`);
},
billingTrend(range?: "day" | "week" | "month") {
return request<BillingTrend>(`/api/billing/trend/${range ? `?range=${range}` : ""}`);
@@ -255,13 +267,32 @@ export const api = {
recharge(payload: { amount: number | string; bonus?: number | string; channel?: string }) {
return request<RechargeResult>("/api/billing/recharge/", { method: "POST", body: JSON.stringify(payload) });
},
listNotifications(params?: { type?: string; unread?: boolean }) {
// 收件箱按页拉取 —— 滚动加载逐页向后端要(tab/搜索 走服务端,计数随响应回来)
listNotifications(params?: { type?: string; unread?: boolean; search?: string; page?: number; pageSize?: number }) {
const query = new URLSearchParams();
if (params?.type && params.type !== "all") query.set("type", params.type);
if (params?.unread) query.set("unread", "1");
if (params?.search) query.set("search", params.search);
if (params?.page) query.set("page", String(params.page));
if (params?.pageSize) query.set("page_size", String(params.pageSize));
const qs = query.toString();
return request<NotificationList>(`/api/ops/notifications/${qs ? `?${qs}` : ""}`);
},
// 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路
async allNotifications(): Promise<NotificationList> {
const out: Notification[] = [];
let path = "/api/ops/notifications/?page_size=100";
let unreadCount = 0;
let typeCounts: NotificationList["type_counts"];
for (let guard = 0; guard < 100 && path; guard += 1) {
const page = await request<NotificationList>(path);
out.push(...page.results);
unreadCount = page.unread_count;
typeCounts = page.type_counts ?? typeCounts;
path = page.next ? new URL(page.next).pathname + new URL(page.next).search : "";
}
return { count: out.length, next: null, previous: null, results: out, unread_count: unreadCount, type_counts: typeCounts };
},
markAllNotificationsRead() {
return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", {
method: "POST"