perf(core): 资产预览改 TOS 公读直链 + 通知只取首页,减刷新开销与缓存失效
- 资产 preview_url 从「逐图签发预签名 URL」改为虚拟主机式公读直链 (https://{bucket}.{host}/{key})。桶公读已验证免签可访问;直链稳定可被浏览器/CDN 缓存,而签名链每次都变、1h 过期反而打不到缓存。boto3 也移出序列化热路径。 - allNotifications 不再逐页翻全部(原 O(N) 随消息增长拖慢每次刷新),只取首页 100 条 (侧边栏徽标 unread_count + 团队动态仅展示最近 6 条已足够);「共 N」改用后端真实 count。 - perf-probe:page_size 由 API 级硬闸确定性守住后,浏览器端「资产翻页」降为提示 (翻 1~2 页是资产真超 200 的合法分页,仅 ≥3 页才疑似 page_size 失效)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,7 @@ export function App() {
|
||||
const [ledgers, setLedgers] = useState<Ledger[]>([]);
|
||||
const [billingTrend, setBillingTrend] = useState<BillingTrend | null>(null);
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [notificationTotal, setNotificationTotal] = useState(0); // 后端真实总数(团队动态「共 N」用,与已加载的近期 100 条区分)
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [projectDetail, setProjectDetail] = useState<Project | null>(null);
|
||||
const [exportResult, setExportResult] = useState<ExportPoll | null>(null);
|
||||
@@ -162,6 +163,7 @@ export function App() {
|
||||
setBillingTrend(trendData);
|
||||
if (notificationData) {
|
||||
setNotifications(notificationData.results);
|
||||
setNotificationTotal(notificationData.count);
|
||||
setUnreadCount(notificationData.unread_count);
|
||||
}
|
||||
setActiveProjectId((current) => current || projectData.results[0]?.id || "");
|
||||
@@ -214,6 +216,7 @@ export function App() {
|
||||
const data = await api.allNotifications().catch(() => null);
|
||||
if (data) {
|
||||
setNotifications(data.results);
|
||||
setNotificationTotal(data.count);
|
||||
setUnreadCount(data.unread_count);
|
||||
}
|
||||
}, []);
|
||||
@@ -674,6 +677,7 @@ export function App() {
|
||||
members={teamMembers}
|
||||
billing={billing}
|
||||
notifications={notifications}
|
||||
notificationTotal={notificationTotal}
|
||||
navigate={navigate}
|
||||
onCreateMember={(payload) => action(() => api.createTeamMember(payload), "成员账户已创建")}
|
||||
onUpdateMember={(id, payload) => action(() => api.updateTeamMember(id, payload), "成员已更新")}
|
||||
@@ -771,7 +775,7 @@ export function App() {
|
||||
}, "故事板已生成")
|
||||
}
|
||||
onAdoptBaseAsset={(groupId, assetId) => action(() => api.adoptBaseAsset(pipelineProject.id, { group_id: groupId, asset_id: assetId }), "已采用该候选")}
|
||||
onAttachBaseAsset={(groupId, assetId) => action(() => api.attachBaseAsset(pipelineProject.id, { group_id: groupId, asset_id: assetId }), "已替换为所选演员")}
|
||||
onAttachBaseAsset={(target, assetId) => action(() => api.attachBaseAsset(pipelineProject.id, { ...target, asset_id: assetId }), "已替换为所选演员")}
|
||||
onGenerateTriview={async (portraitAssetId) => {
|
||||
// 异步:image_edit 慢出图在 worker 跑,轮询期间整站不卡;出图后刷新即见三视图
|
||||
const assetId = await submitAndPollAsset(() => api.generateTriview(pipelineProject.id, { portrait_asset_id: portraitAssetId }), "三视图已据立绘生成");
|
||||
@@ -784,8 +788,11 @@ export function App() {
|
||||
fd.append("name", file.name);
|
||||
fd.append("asset_type", "image");
|
||||
fd.append("category", "person");
|
||||
return action(() => api.uploadAsset(fd), "演员已保存到演员库");
|
||||
// 返回上传后的 Asset(供添加人物工作台进右侧栏做三视图/命名)
|
||||
return action(() => api.uploadAsset(fd), "");
|
||||
}}
|
||||
// 流程步骤4 · 添加人物工作台命名:把名字写回该人物资产
|
||||
onRenameActor={(assetId, name) => action(() => api.updateAsset(assetId, { name }), "")}
|
||||
onSkipStoryboard={() => action(() => api.skipStoryboard(pipelineProject.id), "已跳过故事板")}
|
||||
onSubmitVideo={(segmentId, prompt) => action(() => api.submitVideo(pipelineProject.id, { video_segment_id: segmentId, prompt }), "视频片段已提交,生成中…")}
|
||||
onSubmitAllVideos={(prompt) =>
|
||||
|
||||
+10
-19
@@ -312,7 +312,8 @@ export const api = {
|
||||
return request(`/api/projects/${projectId}/adopt-base-asset/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
// 流程步骤4 · 用演员库现有资产替换基础资产卡(挂为候选并采用)
|
||||
attachBaseAsset(projectId: string, payload: { group_id: string; asset_id: string }) {
|
||||
// seed 占位卡还没 group:不传 group_id,改传 kind+label,后端按 label 命中/新建实体组再挂(不出图)
|
||||
attachBaseAsset(projectId: string, payload: { group_id?: string; asset_id: string; kind?: "product" | "person" | "scene"; label?: string }) {
|
||||
return request(`/api/projects/${projectId}/attach-base-asset/`, { method: "POST", body: JSON.stringify(payload) });
|
||||
},
|
||||
// 流程步骤4 · 据某一版立绘资产生成它配套的三视图(image_edit 锁角色一致性,绑定该立绘 asset)
|
||||
@@ -401,6 +402,10 @@ export const api = {
|
||||
uploadAsset(formData: FormData) {
|
||||
return request<Asset>("/api/assets/upload/", { method: "POST", body: formData });
|
||||
},
|
||||
// 流程步骤4 · 给人物/资产改名(添加人物工作台命名 → PATCH 写回 name)
|
||||
updateAsset(id: string, payload: { name?: string; description?: string }) {
|
||||
return request<Asset>(`/api/assets/${id}/`, { method: "PATCH", body: JSON.stringify(payload) });
|
||||
},
|
||||
billingSummary() {
|
||||
return request<BillingSummary>("/api/billing/summary/");
|
||||
},
|
||||
@@ -437,25 +442,11 @@ export const api = {
|
||||
const qs = query.toString();
|
||||
return request<NotificationList>(`/api/ops/notifications/${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
// 跟着 next 把所有消息取全(给侧边栏徽标 + 团队动态用,不参与收件箱滚动渲染),与 allAssets 同套路
|
||||
// 侧边栏徽标(unread_count)+ 团队动态(只展示最近 6 条)用。只取第 1 页 100 条即足够,
|
||||
// count 用后端真实总数(给「共 N」显示),不再随消息增长逐页翻全部(原来 O(N) 拖慢每次刷新)。
|
||||
// 注:不参与收件箱滚动渲染——收件箱走 listNotifications 自己的服务端分页。
|
||||
async allNotifications(): Promise<NotificationList> {
|
||||
// 首页拿计数,剩余页并行(原串行逐页:消息多时侧边栏徽标加载也拖慢整页)
|
||||
const SIZE = 100;
|
||||
const first = await request<NotificationList>(`/api/ops/notifications/?page_size=${SIZE}`);
|
||||
const out: Notification[] = [...first.results];
|
||||
const pageSize = first.results.length || SIZE;
|
||||
const totalPages = pageSize > 0 ? Math.ceil((first.count || out.length) / pageSize) : 1;
|
||||
if (totalPages > 1) {
|
||||
const rest = await Promise.all(
|
||||
Array.from({ length: totalPages - 1 }, (_, i) =>
|
||||
request<NotificationList>(`/api/ops/notifications/?page_size=${SIZE}&page=${i + 2}`)
|
||||
.then((p) => p.results)
|
||||
.catch(() => [] as Notification[])
|
||||
)
|
||||
);
|
||||
rest.forEach((r) => out.push(...r));
|
||||
}
|
||||
return { count: out.length, next: null, previous: null, results: out, unread_count: first.unread_count, type_counts: first.type_counts };
|
||||
return request<NotificationList>(`/api/ops/notifications/?page_size=100`);
|
||||
},
|
||||
markAllNotificationsRead() {
|
||||
return request<{ updated: number; unread_count: number }>("/api/ops/notifications/mark-all-read/", {
|
||||
|
||||
@@ -27,12 +27,13 @@ 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 = [], navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: {
|
||||
export function TeamPage({ team, user, members, billing, notifications = [], notificationTotal, navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: {
|
||||
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<unknown>;
|
||||
onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
|
||||
@@ -211,7 +212,7 @@ export function TeamPage({ team, user, members, billing, notifications = [], nav
|
||||
<div className="team-feed">
|
||||
<div className="h">
|
||||
<h3>团队动态</h3>
|
||||
<span className="ct">// 最近 {Math.min(feedItems.length, 6)} 条 · 共 {notifications.length}</span>
|
||||
<span className="ct">// 最近 {Math.min(feedItems.length, 6)} 条 · 共 {notificationTotal ?? notifications.length}</span>
|
||||
<a className="more" id="open-feed-all" role="button" tabIndex={0} onClick={() => navigate("messages")}>全部 →</a>
|
||||
</div>
|
||||
<div className="feed-list">
|
||||
|
||||
Reference in New Issue
Block a user