feat: add admin accounts and image templates
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user