feat: add admin accounts and image templates

This commit is contained in:
inman
2026-07-03 11:25:25 +08:00
parent d98e58adfa
commit c3ea9f0eb1
61 changed files with 7016 additions and 199 deletions

View File

@@ -10,16 +10,22 @@ type AssetView = "assets" | "tasks";
const ASSET_PAGE_SIZE = 8;
const TASK_PAGE_SIZE = 8;
export function AssetManager() {
type AssetManagerProps = {
initialView?: AssetView;
initialTaskId?: string;
};
export function AssetManager({ initialView = "assets", initialTaskId }: AssetManagerProps = {}) {
const [assets, setAssets] = useState<Asset[]>([]);
const [jobs, setJobs] = useState<GenerationJob[]>([]);
const [view, setView] = useState<AssetView>("assets");
const [view, setView] = useState<AssetView>(initialView === "tasks" ? "tasks" : "assets");
const [loading, setLoading] = useState(false);
const [syncingJobId, setSyncingJobId] = useState<string | null>(null);
const [deletingAssetId, setDeletingAssetId] = useState<string | null>(null);
const [deletingJobId, setDeletingJobId] = useState<string | null>(null);
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
const [expandedJobId, setExpandedJobId] = useState<string | null>(null);
const [expandedJobId, setExpandedJobId] = useState<string | null>(initialTaskId || null);
const [durationNow, setDurationNow] = useState(() => Date.now());
const [assetPage, setAssetPage] = useState(1);
const [jobPage, setJobPage] = useState(1);
const [error, setError] = useState<string | null>(null);
@@ -33,6 +39,11 @@ export function AssetManager() {
refresh().catch(() => undefined);
}, []);
useEffect(() => {
if (initialView === "tasks" || initialTaskId) setView("tasks");
if (initialTaskId) setExpandedJobId(initialTaskId);
}, [initialTaskId, initialView]);
const jobByOutputAssetId = useMemo(() => {
const map = new Map<string, GenerationJob>();
for (const job of jobs) {
@@ -43,6 +54,7 @@ export function AssetManager() {
const visibleAssets = pageItems(assets, assetPage, ASSET_PAGE_SIZE);
const visibleJobs = pageItems(jobs, jobPage, TASK_PAGE_SIZE);
const hasLiveJobs = useMemo(() => jobs.some((job) => !isTerminalStatus(job.status)), [jobs]);
useEffect(() => {
setAssetPage((page) => clampPage(page, assets.length, ASSET_PAGE_SIZE));
@@ -57,6 +69,12 @@ export function AssetManager() {
if (view === "tasks") setJobPage((page) => clampPage(page, jobs.length, TASK_PAGE_SIZE));
}, [view, assets.length, jobs.length]);
useEffect(() => {
if (!expandedJobId) return;
const index = jobs.findIndex((job) => job.id === expandedJobId);
if (index >= 0) setJobPage(Math.floor(index / TASK_PAGE_SIZE) + 1);
}, [expandedJobId, jobs]);
useEffect(() => {
return runScopedMotion(managerRef, (scope) => revealChildren(scope));
}, []);
@@ -69,6 +87,15 @@ export function AssetManager() {
pulseFeedback(feedbackRef.current);
}, [error]);
useEffect(() => {
if (!hasLiveJobs) return undefined;
setDurationNow(Date.now());
const timer = window.setInterval(() => {
setDurationNow(Date.now());
}, 1000);
return () => window.clearInterval(timer);
}, [hasLiveJobs]);
useEffect(() => {
if (!previewAsset) return undefined;
modalEnter(previewDialogRef.current);
@@ -234,7 +261,7 @@ export function AssetManager() {
<div className="job-summary">
<h3>{job.prompt?.slice(0, 90) || capabilityLabel(job.capability)}</h3>
<p className="muted">{capabilityLabel(job.capability)} / {job.reqKey}</p>
<p className="muted compact-hint">{durationLabel(job)} / {job.inputAssetIds.length || job.inputUrls.length} {job.outputAssetIds.length} </p>
<p className="muted compact-hint">{durationLabel(job, durationNow)} / {job.inputAssetIds.length || job.inputUrls.length} {job.outputAssetIds.length} </p>
</div>
<div className="toolbar job-actions">
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
@@ -250,7 +277,7 @@ export function AssetManager() {
</button>
</div>
</div>
{expandedJobId === job.id ? <JobDetails job={job} /> : null}
{expandedJobId === job.id ? <JobDetails job={job} now={durationNow} /> : null}
</article>
))}
{!jobs.length ? <div className="callout"></div> : null}
@@ -286,7 +313,7 @@ export function AssetManager() {
);
}
function JobDetails({ job }: { job: GenerationJob }) {
function JobDetails({ job, now }: { job: GenerationJob; now: number }) {
return (
<div className="job-detail-panel">
<dl className="job-detail-grid">
@@ -296,7 +323,7 @@ function JobDetails({ job }: { job: GenerationJob }) {
</div>
<div>
<dt></dt>
<dd>{durationValue(job)}</dd>
<dd>{durationValue(job, now)}</dd>
</div>
<div>
<dt></dt>
@@ -435,6 +462,10 @@ function statusLabel(status: GenerationJob["status"]) {
return status;
}
function isTerminalStatus(status: GenerationJob["status"]) {
return status === "succeeded" || status === "failed" || status === "expired" || status === "cancelled";
}
function providerLabel(provider: GenerationJob["provider"]) {
if (provider === "volcengine-visual") return "火山视觉";
if (provider === "evolink") return "EvoLink";
@@ -473,15 +504,15 @@ function formatDateTime(value: string) {
}).format(new Date(value));
}
function durationLabel(job: GenerationJob) {
const terminal = ["succeeded", "failed", "expired", "cancelled"].includes(job.status);
return `${terminal ? "耗时" : "已等待"} ${durationValue(job)}`;
function durationLabel(job: GenerationJob, now: number) {
const terminal = isTerminalStatus(job.status);
return `${terminal ? "耗时" : "已耗时"} ${durationValue(job, now)}`;
}
function durationValue(job: GenerationJob) {
function durationValue(job: GenerationJob, now: number) {
const start = new Date(job.createdAt).getTime();
const terminal = ["succeeded", "failed", "expired", "cancelled"].includes(job.status);
const end = terminal ? new Date(job.updatedAt).getTime() : Date.now();
const terminal = isTerminalStatus(job.status);
const end = terminal ? new Date(job.updatedAt).getTime() : now;
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return "--";
return formatDuration(end - start);
}