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
+51 -10
View File
@@ -22,6 +22,7 @@ import {
X
} from "lucide-react";
import type { AITask, Asset, ModelConfig, Product } from "../types";
import { MediaLightbox } from "../components/overlays";
import type { Page } from "./route-config";
import { statusPill } from "./stage-config";
import "../ai-tools-page.css";
@@ -46,7 +47,14 @@ const STATUS_LABEL: Record<string, string> = {
running: "生成中",
queued: "排队中",
polling: "生成中",
needs_review: "待确认"
needs_review: "待确认",
// 后端 AITask.Status 全量中文化(原先缺这些会直接透出英文)
created: "待处理",
reserved: "排队中",
submitted: "已提交",
postprocessing: "处理中",
compensating: "回滚中",
cancelled: "已取消"
};
function statusText(status: string) {
@@ -71,7 +79,18 @@ async function downloadImage(url: string, filename: string) {
}
}
export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page) => void; aiTasks: AITask[] }) {
export function AssetFactoryPage({ navigate, aiTasks, assets = [] }: { navigate: (page: Page) => void; aiTasks: AITask[]; assets?: Asset[] }) {
// 任务 → 生成结果图:按 asset.origin_task 关联,取首张有预览 URL 的图片文件(脚本/视频任务无图则留占位)
const taskImage = useMemo(() => {
const map: Record<string, string> = {};
for (const asset of assets) {
const taskId = asset.origin_task;
if (!taskId || map[taskId]) continue;
const file = asset.files?.find((f) => f.preview_url && (f.content_type?.startsWith("image") ?? true));
if (file?.preview_url) map[taskId] = file.preview_url;
}
return map;
}, [assets]);
const cards = [
{
page: "modelPhoto" as Page,
@@ -112,8 +131,11 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
const [query, setQuery] = useState("");
const [timeFilter, setTimeFilter] = useState<"all" | "1" | "7" | "30">("all");
const [typeFilter, setTypeFilter] = useState("");
const [view, setView] = useState<"grid" | "list">("list");
const [view, setView] = useState<"grid" | "list">("grid");
const [openChip, setOpenChip] = useState<"" | "time" | "type">("");
// 任务中心分页:每次加载 12 条,「加载更多」递增;筛选/搜索变化时重置
const TASKS_PER_LOAD = 12;
const [shown, setShown] = useState(TASKS_PER_LOAD);
useEffect(() => {
if (!openChip) return;
const close = (event: MouseEvent) => { if (!(event.target as HTMLElement).closest(".chip-wrap")) setOpenChip(""); };
@@ -141,6 +163,10 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
}
return true;
});
// 筛选条件变化时回到第一屏(12 条)
useEffect(() => { setShown(TASKS_PER_LOAD); }, [filter, query, timeFilter, typeFilter]);
const paged = visible.slice(0, shown);
const hasMore = visible.length > paged.length;
return (
<div className="asset-factory">
@@ -246,7 +272,7 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
</div>
<div className="result-meta">
// 显示 {visible.length} / {aiTasks.length} 个任务
// 显示 {paged.length} / {visible.length} 个任务
</div>
{aiTasks.length === 0 ? (
@@ -261,12 +287,13 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
</div>
) : view === "grid" ? (
<div className="history-grid">
{visible.map((task) => {
{paged.map((task) => {
const pill = statusPill(task.status);
const typeLabel = TASK_TYPE_LABEL[task.task_type] || task.task_type;
const img = taskImage[task.id];
return (
<article className="task-card history-card" key={task.id}>
<div className="placeholder"><span className="ph-frame">{task.id.slice(0, 4)}</span></div>
<div className={`placeholder${img ? " has-img" : ""}`}>{img ? <img src={img} alt={typeLabel} /> : <span className="ph-frame">{task.id.slice(0, 4)}</span>}</div>
<div className="history-body">
<div className="history-name">{typeLabel}</div>
<div className="history-type">// {task.task_type}</div>
@@ -292,15 +319,16 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
</tr>
</thead>
<tbody>
{visible.map((task) => {
{paged.map((task) => {
const pill = statusPill(task.status);
const typeLabel = TASK_TYPE_LABEL[task.task_type] || task.task_type;
const img = taskImage[task.id];
return (
<tr key={task.id}>
<td>
<div className="task-name-cell">
<div className="placeholder task-thumb">
<span className="ph-frame">{task.id.slice(0, 4)}</span>
<div className={`placeholder task-thumb${img ? " has-img" : ""}`}>
{img ? <img src={img} alt={typeLabel} /> : <span className="ph-frame">{task.id.slice(0, 4)}</span>}
</div>
<div>
<div className="task-name">{typeLabel}</div>
@@ -337,6 +365,14 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
</table>
</div>
)}
{hasMore && (
<div className="task-load-more">
<button className="btn" type="button" onClick={() => setShown((n) => n + TASKS_PER_LOAD)}>
加载更多 <span className="lm-rest">还有 {visible.length - paged.length} 个</span>
</button>
</div>
)}
</div>
);
}
@@ -451,6 +487,8 @@ export function ImageWorkbenchPage({
const [results, setResults] = useState<Asset[] | null>(null);
const [refImage, setRefImage] = useState<{ name: string; url: string } | null>(null);
const refInputRef = useRef<HTMLInputElement | null>(null);
// 生成结果图片放大预览
const [preview, setPreview] = useState<{ src: string; name: string } | null>(null);
// 模特/平台 工作台头部:搜索 + 时间排序 + 模特筛选(对左侧网格真实生效)
const [gridQuery, setGridQuery] = useState("");
const [gridSort, setGridSort] = useState<"recent" | "name">("recent");
@@ -502,6 +540,8 @@ export function ImageWorkbenchPage({
function renderResultGrid() {
const cols = (results?.length ?? candidateCount) >= 4 ? 4 : 2;
return (
<>
<MediaLightbox open={!!preview} src={preview?.src || ""} kind="image" name={preview?.name} close={() => setPreview(null)} />
<div
className="gen-images"
style={{ "--cols": cols, "--ratio": ratioVar } as React.CSSProperties}
@@ -512,7 +552,7 @@ export function ImageWorkbenchPage({
).map(({ key, index, url }) => (
<div className={`gen-image ${generating && !url ? "gen" : ""}`} key={key}>
{url ? (
<img className="gen-image-img" src={url} alt={`${meta.title} #${index + 1}`} loading="lazy" />
<img className="gen-image-img" src={url} alt={`${meta.title} #${index + 1}`} loading="lazy" title="点击放大" style={{ cursor: "zoom-in" }} onClick={() => setPreview({ src: url, name: `${meta.title} #${index + 1}` })} />
) : (
<div className="placeholder">
<span className="ph-frame">
@@ -538,6 +578,7 @@ export function ImageWorkbenchPage({
</div>
))}
</div>
</>
);
}