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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user