From f1bc5933ef53fb69582ef0d1b17fdb7caa3f6f1c Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Wed, 17 Jun 2026 18:58:53 +0800 Subject: [PATCH] =?UTF-8?q?perf(core):=20=E9=9D=9E=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E6=80=81=E4=B9=9F=E6=94=B9=E6=8C=89=E9=A1=B5?= =?UTF-8?q?=E6=87=92=E5=8A=A0=E8=BD=BD,bootstrap=20=E4=BB=8E=20~10=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=98=A6=E5=88=B0=20~6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启动只保留「每页 shell 都要」的全局数据:商品/项目(侧栏+导航)、余额(顶栏)、 模型配置(多生成页+pipeline 共用)、未读数(侧栏徽标,page_size=1 轻量)。其余移到各页按需: - 账户页:流水(本就服务端分页)+ 趋势 + 团队成员 本页自取,充值后刷新。 - 团队页:成员 + 团队动态 本页自取,增删成员/充值后刷新。 - 生图工厂:任务历史 + 任务缩略图本页自取。 - 通知:bootstrap 只取 page_size=1 拿未读数(notificationsBadge),列表由消息中心/团队页各自拉。 - 全局默认分页改 DefaultPagination(支持 ?page_size 覆盖,默认 20 上限 200): 根治 projects/products 等列表端点忽略 page_size、数据过 20 即被迫翻页(同 assets 旧病)。 闸:9 页功能正常、无瀑布、API 检查全过;首屏接口数 5~8(原 8~13)。perf 预算下调到新基线。 Co-Authored-By: Claude Opus 4.8 --- core/backend/airshelf/settings/base.py | 2 +- core/backend/apps/common/pagination.py | 9 ++++++ core/frontend/src/App.tsx | 45 ++++++-------------------- core/frontend/src/api.ts | 4 +++ core/frontend/src/routes/account.tsx | 25 ++++++++++---- core/frontend/src/routes/ai-tools.tsx | 7 ++-- core/frontend/src/routes/team.tsx | 24 +++++++++++--- core/qa/function-audit/perf-probe.mjs | 15 +++++---- 8 files changed, 74 insertions(+), 57 deletions(-) create mode 100644 core/backend/apps/common/pagination.py diff --git a/core/backend/airshelf/settings/base.py b/core/backend/airshelf/settings/base.py index 7e549e2..d1c2813 100644 --- a/core/backend/airshelf/settings/base.py +++ b/core/backend/airshelf/settings/base.py @@ -133,7 +133,7 @@ REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "DEFAULT_PAGINATION_CLASS": "apps.common.pagination.DefaultPagination", "DEFAULT_FILTER_BACKENDS": [ "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", diff --git a/core/backend/apps/common/pagination.py b/core/backend/apps/common/pagination.py new file mode 100644 index 0000000..400b692 --- /dev/null +++ b/core/backend/apps/common/pagination.py @@ -0,0 +1,9 @@ +from rest_framework.pagination import PageNumberPagination + + +class DefaultPagination(PageNumberPagination): + """全局默认分页:默认 20 条/页,允许前端用 ?page_size= 覆盖(上限 200)。 + 各列表端点据此可按需懒加载/取大页,不再因默认类忽略 page_size 而被迫并行翻页。""" + + page_size_query_param = "page_size" + max_page_size = 200 diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 99f9570..22a3a31 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -102,14 +102,8 @@ export function App() { const [team, setTeam] = useState(null); const [products, setProducts] = useState([]); const [projects, setProjects] = useState([]); - const [teamMembers, setTeamMembers] = useState([]); const [modelConfigs, setModelConfigs] = useState([]); - const [aiTasks, setAiTasks] = useState([]); const [billing, setBilling] = useState(null); - const [ledgers, setLedgers] = useState([]); - const [billingTrend, setBillingTrend] = useState(null); - const [notifications, setNotifications] = useState([]); - const [notificationTotal, setNotificationTotal] = useState(0); // 后端真实总数(团队动态「共 N」用,与已加载的近期 100 条区分) const [unreadCount, setUnreadCount] = useState(0); const [projectDetail, setProjectDetail] = useState(null); const [exportResult, setExportResult] = useState(null); @@ -138,32 +132,22 @@ export function App() { ); const loadData = useCallback(async () => { - // 资产不再全局取全:各页按需服务端分页懒加载(library/生图/商品详情/dashboard 自取)。 - const [productData, projectData, billingData, ledgerData, trendData, memberData, modelData, taskData, notificationData] = + // bootstrap 只拉「每页 shell 都要」的全局数据:商品/项目(侧栏+导航+active 解析)、余额(顶栏)、 + // 模型配置(多生成页+pipeline 共用)、未读数(侧栏徽标,page_size=1 轻量)。 + // 资产/流水/趋势/成员/任务/通知列表均改各页按需懒加载,不再全局取全。 + const [productData, projectData, billingData, modelData, badgeData] = await Promise.all([ api.products(), api.projects(), api.billingSummary().catch(() => null), - api.ledgers(1, 10).catch(() => ({ count: 0, page: 1, page_size: 10, results: [] as Ledger[] })), - api.billingTrend().catch(() => null), - api.teamMembers().catch(() => []), api.modelConfigs().catch(() => null), - api.aiTasks().catch(() => null), - api.allNotifications().catch(() => null) + api.notificationsBadge().catch(() => null) ]); setProducts(productData.results); setProjects(projectData.results); - setTeamMembers(memberData); setModelConfigs(modelData?.results || []); - setAiTasks(taskData?.results || []); if (billingData) setBilling(billingData); - setLedgers(ledgerData.results); - setBillingTrend(trendData); - if (notificationData) { - setNotifications(notificationData.results); - setNotificationTotal(notificationData.count); - setUnreadCount(notificationData.unread_count); - } + if (badgeData) setUnreadCount(badgeData.unread_count); setActiveProjectId((current) => current || projectData.results[0]?.id || ""); setActiveProductId((current) => current || productData.results[0]?.id || ""); }, []); @@ -210,13 +194,10 @@ export function App() { setSessions(await api.loginSessions().catch(() => [])); } + // 只刷新侧边栏未读徽标(通知列表由消息中心/团队页各自拉);标记已读后调用 const reloadNotifications = useCallback(async () => { - const data = await api.allNotifications().catch(() => null); - if (data) { - setNotifications(data.results); - setNotificationTotal(data.count); - setUnreadCount(data.unread_count); - } + const data = await api.notificationsBadge().catch(() => null); + if (data) setUnreadCount(data.unread_count); }, []); // Boot: validate token, hydrate identity + data. @@ -659,10 +640,7 @@ export function App() { return ( action(() => api.recharge({ amount, bonus }), "充值成功")} /> ); @@ -671,10 +649,7 @@ export function App() { action(() => api.createTeamMember(payload), "成员账户已创建")} onUpdateMember={(id, payload) => action(() => api.updateTeamMember(id, payload), "成员已更新")} @@ -693,7 +668,7 @@ export function App() { /> ); case "assetFactory": - return ; + return ; case "imageOptimize": return navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} />; case "modelPhoto": diff --git a/core/frontend/src/api.ts b/core/frontend/src/api.ts index 99f9d9c..040a363 100644 --- a/core/frontend/src/api.ts +++ b/core/frontend/src/api.ts @@ -472,6 +472,10 @@ export const api = { async allNotifications(): Promise { return request(`/api/ops/notifications/?page_size=100`); }, + // 侧边栏未读徽标:只要 unread_count,取 1 条即可(不为徽标拉 100 条) + notificationsBadge() { + return request(`/api/ops/notifications/?page_size=1`); + }, markAllNotificationsRead() { return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", { method: "POST" diff --git a/core/frontend/src/routes/account.tsx b/core/frontend/src/routes/account.tsx index 4929733..5fd8d0a 100644 --- a/core/frontend/src/routes/account.tsx +++ b/core/frontend/src/routes/account.tsx @@ -45,23 +45,32 @@ const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_s { k: "脚本 LLM", color: "var(--black-alpha-32)", bucket: "script" } ]; -export function AccountPage({ billing, ledgers, trend, projects, teamMembers, onRecharge }: { +export function AccountPage({ billing, projects, onRecharge }: { billing: BillingSummary | null; - ledgers: Ledger[]; - trend: BillingTrend | null; projects: Project[]; - teamMembers: TeamMember[]; onRecharge: (amount: number, bonus: number) => void | Promise; }) { const [tab, setTab] = useState("overview"); const [recharge, setRecharge] = useState(500); const [customAmt, setCustomAmt] = useState(""); + // 账户页数据本页自取(不再走全局 bootstrap):充值后 bump 刷新流水/趋势 + const [reloadFlag, setReloadFlag] = useState(0); + + // 趋势(day,首屏)+ 团队成员(成员/限额 tab)本页懒加载 + const [trend, setTrend] = useState(null); + const [teamMembers, setTeamMembers] = useState([]); + useEffect(() => { + let alive = true; + api.billingTrend().then((d) => { if (alive) setTrend(d); }).catch(() => {}); + api.teamMembers().then((m) => { if (alive) setTeamMembers(m); }).catch(() => {}); + return () => { alive = false; }; + }, [reloadFlag]); // 账单流水分页:服务端分页(总数随流水增长,不再写死 100),每页 10 条 const BILLS_PER_PAGE = 10; const [billPage, setBillPage] = useState(1); - const [ledgerRows, setLedgerRows] = useState(ledgers); - const [ledgerCount, setLedgerCount] = useState(ledgers.length); + const [ledgerRows, setLedgerRows] = useState([]); + const [ledgerCount, setLedgerCount] = useState(0); useEffect(() => { let alive = true; api.ledgers(billPage, BILLS_PER_PAGE).then((data) => { @@ -70,7 +79,7 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on setLedgerCount(data.count); }).catch(() => {}); return () => { alive = false; }; - }, [billPage]); + }, [billPage, reloadFlag]); const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE)); const safeBillPage = Math.min(billPage, billTotalPages); @@ -82,6 +91,8 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on if (effectiveAmount <= 0) return; await onRecharge(effectiveAmount, effectiveBonus); setCustomAmt(""); + setBillPage(1); + setReloadFlag((n) => n + 1); // 充值后刷新流水/趋势(余额由全局 action→loadData 刷新) } const balance = Number(billing?.account.balance || 0); diff --git a/core/frontend/src/routes/ai-tools.tsx b/core/frontend/src/routes/ai-tools.tsx index 24d2f16..44e03af 100644 --- a/core/frontend/src/routes/ai-tools.tsx +++ b/core/frontend/src/routes/ai-tools.tsx @@ -81,12 +81,13 @@ async function downloadImage(url: string, filename: string) { } } -export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page) => void; aiTasks: AITask[] }) { - // 任务 → 生成结果图:按 asset.origin_task 关联取缩略图。服务端懒加载最近的 AI 生成图(不再吃全局 assets 全量), - // 覆盖近 200 张生成图的任务卡缩略图;更早的任务卡留占位。 +export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void }) { + // 任务历史 + 任务→结果图:都在本页懒加载(不再吃全局 bootstrap 的 aiTasks/assets 全量)。 + const [aiTasks, setAiTasks] = useState([]); const [assets, setAssets] = useState([]); useEffect(() => { let alive = true; + api.aiTasks().then((res) => { if (alive) setAiTasks(res.results || []); }).catch(() => {}); api.assetsPage({ asset_type: "image", source: "ai_generated", pageSize: 200 }) .then((res) => { if (alive) setAssets(res.results); }) .catch(() => { if (alive) setAssets([]); }); diff --git a/core/frontend/src/routes/team.tsx b/core/frontend/src/routes/team.tsx index c80f646..b2d5a7f 100644 --- a/core/frontend/src/routes/team.tsx +++ b/core/frontend/src/routes/team.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { CircleDollarSign, KeyRound, UserPlus } from "lucide-react"; +import { api } from "../api"; import type { BillingSummary, Notification, Team, TeamMember, User } from "../types"; import type { Page } from "./route-config"; import { money } from "./stage-config"; @@ -27,13 +28,10 @@ const PERM_ROWS: Array<{ cap: string; cells: [string, string, string]; last?: bo { cap: "创建项目 / 用 AI 流程", cells: ["✓", "✓", "✓"], last: true } ]; -export function TeamPage({ team, user, members, billing, notifications = [], notificationTotal, navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: { +export function TeamPage({ team, user, billing, navigate, onCreateMember: onCreateMemberRaw, onUpdateMember: onUpdateMemberRaw, onRemoveMember: onRemoveMemberRaw, onResetPassword, onRecharge: onRechargeRaw }: { team: Team; user: User; - members: TeamMember[]; billing: BillingSummary | null; - notifications?: Notification[]; - notificationTotal?: number; // 后端真实总数(只加载了近期 100 条,「共 N」用这个而非数组长度) navigate: (page: Page) => void; onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; monthly_credit_limit?: number }) => void | Promise; onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise; @@ -44,6 +42,24 @@ export function TeamPage({ team, user, members, billing, notifications = [], not const [modal, setModal] = useState<"" | "invite" | "limit" | "recharge">(""); const [search, setSearch] = useState(""); + // 团队成员 + 团队动态本页懒加载(不再走全局 bootstrap);成员/充值操作后 bump 刷新 + const [reloadFlag, setReloadFlag] = useState(0); + const [members, setMembers] = useState([]); + const [notifications, setNotifications] = useState([]); + const [notificationTotal, setNotificationTotal] = useState(0); + useEffect(() => { + let alive = true; + api.teamMembers().then((m) => { if (alive) setMembers(m); }).catch(() => {}); + api.allNotifications().then((n) => { if (alive) { setNotifications(n.results); setNotificationTotal(n.count); } }).catch(() => {}); + return () => { alive = false; }; + }, [reloadFlag]); + const reloadTeam = () => setReloadFlag((n) => n + 1); + // 包一层:操作完成后刷新本页成员列表(余额由全局 action→loadData 刷新);透传返回值(调用处据此判成功) + const onCreateMember = async (p: Parameters[0]) => { const r = await onCreateMemberRaw(p); reloadTeam(); return r; }; + const onUpdateMember = async (id: string, p: Parameters[1]) => { const r = await onUpdateMemberRaw(id, p); reloadTeam(); return r; }; + const onRemoveMember = async (id: string) => { const r = await onRemoveMemberRaw(id); reloadTeam(); return r; }; + const onRecharge = async (amount: number, bonus: number) => { const r = await onRechargeRaw(amount, bonus); reloadTeam(); return r; }; + // 创建账户表单 const [cuUser, setCuUser] = useState(""); const [cuPass, setCuPass] = useState(""); diff --git a/core/qa/function-audit/perf-probe.mjs b/core/qa/function-audit/perf-probe.mjs index 763963c..e7b1455 100644 --- a/core/qa/function-audit/perf-probe.mjs +++ b/core/qa/function-audit/perf-probe.mjs @@ -50,16 +50,17 @@ const baselinePath = path.resolve(here, "perf-baseline.json"); // 注:App.tsx 启动会并行拉 ~10 个全局接口(products/projects/assets/billing/...)。 // 这些是「全局首屏」开销,记在 dashboard 头上。其余页若在已加载全局态后还重复拉同样数据 = 架构问题。 // 阈值是「当前现状 + 少量余量」起步;优化推进后人工调低,逼着请求数下降。 +// 资产+非资产全局态改按页懒加载后的新基线(shell 全局 ~6 个 + 各页自取少量)+ 小余量,作回归护栏。 const REQUEST_BUDGET = { - dashboard: 14, + dashboard: 7, products: 6, projects: 6, - library: 8, - account: 8, - team: 4, - messages: 4, - productDetail: 8, - pipeline: 10, + library: 9, + account: 9, + team: 7, + messages: 6, + productDetail: 7, + pipeline: 8, }; async function login() {