production runtime and mock removal
This commit is contained in:
1 parent
2ef3da7af5
commit
6480e503eb
55 files changed
+581
-540
No files matched your search
@@ -52,6 +52,7 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
|
||||
}
|
||||
return map;
|
||||
}, [jobs]);
|
||||
const outputAssetIds = useMemo(() => new Set(assets.map((asset) => asset.id)), [assets]);
|
||||
|
||||
const visibleAssets = pageItems(assets, assetPage, ASSET_PAGE_SIZE);
|
||||
const visibleJobs = pageItems(jobs, jobPage, TASK_PAGE_SIZE);
|
||||
@@ -262,10 +263,10 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
|
||||
<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, durationNow)} / 输入 {job.inputAssetIds.length || job.inputUrls.length} 个,输出 {job.outputAssetIds.length} 个 / {billingLabel(job)}</p>
|
||||
<p className="muted compact-hint">{durationLabel(job, durationNow)} / 输入 {job.inputAssetIds.length || job.inputUrls.length} 个,输出 {resolvedOutputCount(job, outputAssetIds)} 个 / {billingLabel(job)}</p>
|
||||
</div>
|
||||
<div className="toolbar job-actions">
|
||||
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
|
||||
<span className={`status ${taskStatusClass(job, outputAssetIds)}`}>{statusLabel(job.status, hasResolvedOutput(job, outputAssetIds))}</span>
|
||||
<button className="button mini-button" type="button" aria-expanded={expandedJobId === job.id} onClick={() => setExpandedJobId(expandedJobId === job.id ? null : job.id)}>
|
||||
<Info size={15} />
|
||||
{expandedJobId === job.id ? "收起" : "详情"}
|
||||
@@ -278,7 +279,7 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{expandedJobId === job.id ? <JobDetails job={job} now={durationNow} /> : null}
|
||||
{expandedJobId === job.id ? <JobDetails job={job} now={durationNow} outputAssetIds={outputAssetIds} /> : null}
|
||||
</article>
|
||||
))}
|
||||
{!jobs.length ? <div className="callout">暂无生成任务。提交图片或视频生成后会保留任务历史。</div> : null}
|
||||
@@ -314,13 +315,13 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
|
||||
);
|
||||
}
|
||||
|
||||
function JobDetails({ job, now }: { job: GenerationJob; now: number }) {
|
||||
function JobDetails({ job, now, outputAssetIds }: { job: GenerationJob; now: number; outputAssetIds: Set<string> }) {
|
||||
return (
|
||||
<div className="job-detail-panel">
|
||||
<dl className="job-detail-grid">
|
||||
<div>
|
||||
<dt>状态</dt>
|
||||
<dd><span className={`status ${job.status}`}>{statusLabel(job.status)}</span></dd>
|
||||
<dd><span className={`status ${taskStatusClass(job, outputAssetIds)}`}>{statusLabel(job.status, hasResolvedOutput(job, outputAssetIds))}</span></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>耗时</dt>
|
||||
@@ -352,7 +353,7 @@ function JobDetails({ job, now }: { job: GenerationJob; now: number }) {
|
||||
</div>
|
||||
<div>
|
||||
<dt>输入/输出</dt>
|
||||
<dd>{job.inputAssetIds.length || job.inputUrls.length} / {job.outputAssetIds.length}</dd>
|
||||
<dd>{job.inputAssetIds.length || job.inputUrls.length} / {resolvedOutputCount(job, outputAssetIds)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>扣费</dt>
|
||||
@@ -456,10 +457,22 @@ function sourceLabel(source: Asset["source"]) {
|
||||
return "外部";
|
||||
}
|
||||
|
||||
function statusLabel(status: GenerationJob["status"]) {
|
||||
function hasResolvedOutput(job: GenerationJob, outputAssetIds: Set<string>) {
|
||||
return job.outputAssetIds.some((assetId) => outputAssetIds.has(assetId));
|
||||
}
|
||||
|
||||
function resolvedOutputCount(job: GenerationJob, outputAssetIds: Set<string>) {
|
||||
return job.outputAssetIds.filter((assetId) => outputAssetIds.has(assetId)).length;
|
||||
}
|
||||
|
||||
function taskStatusClass(job: GenerationJob, outputAssetIds: Set<string>) {
|
||||
return job.status === "succeeded" && !hasResolvedOutput(job, outputAssetIds) ? "syncing" : job.status;
|
||||
}
|
||||
|
||||
function statusLabel(status: GenerationJob["status"], hasOutput = true) {
|
||||
if (status === "queued") return "排队中";
|
||||
if (status === "running") return "生成中";
|
||||
if (status === "succeeded") return "已完成";
|
||||
if (status === "succeeded") return hasOutput ? "已完成" : "结果同步中";
|
||||
if (status === "failed") return "失败";
|
||||
if (status === "expired") return "已过期";
|
||||
if (status === "cancelled") return "已取消";
|
||||
@@ -485,7 +498,7 @@ function capabilityLabel(capability?: GenerationJob["capability"]) {
|
||||
}
|
||||
|
||||
function billingLabel(job: GenerationJob) {
|
||||
if (!job.billing) return job.provider === "mock" ? "免计费" : "未计费";
|
||||
if (!job.billing) return job.provider === "mock" ? "历史任务(未计费)" : "未计费";
|
||||
if (job.billing.quotaExempt) return `超管费用 ${formatBillingAmount(job.billing.amountFen)}(不计额度)`;
|
||||
if (job.billing.status === "refunded") return `已退款 ${formatBillingAmount(job.billing.amountFen)}`;
|
||||
if (job.billing.status === "pending") return `预计 ${formatBillingAmount(job.billing.amountFen)}`;
|
||||
|
||||
@@ -150,6 +150,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [billingQuote, setBillingQuote] = useState<BillingQuote | null>(null);
|
||||
const [billingQuoteLoading, setBillingQuoteLoading] = useState(false);
|
||||
const [billingQuoteError, setBillingQuoteError] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
@@ -307,10 +308,12 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
const timer = window.setTimeout(async () => {
|
||||
if (!prompt.trim() || missingMaterialPlaceholders.length) {
|
||||
if (active) setBillingQuote(null);
|
||||
if (active) setBillingQuoteError(null);
|
||||
if (active) setBillingQuoteLoading(false);
|
||||
return;
|
||||
}
|
||||
setBillingQuoteLoading(true);
|
||||
setBillingQuoteError(null);
|
||||
try {
|
||||
const response = await fetch("/api/billing/quote", {
|
||||
method: "POST",
|
||||
@@ -319,11 +322,18 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (active) {
|
||||
setBillingQuote(response.ok && payload.quote ? payload.quote as BillingQuote : null);
|
||||
if (response.ok && payload.quote) {
|
||||
setBillingQuote(payload.quote as BillingQuote);
|
||||
setBillingQuoteError(null);
|
||||
} else {
|
||||
setBillingQuote(null);
|
||||
setBillingQuoteError(typeof payload.error === "string" ? payload.error : "计费估算暂不可用,请检查服务配置");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (active) {
|
||||
setBillingQuote(null);
|
||||
setBillingQuoteError("计费估算暂不可用,请检查后端服务配置");
|
||||
}
|
||||
} finally {
|
||||
if (active) setBillingQuoteLoading(false);
|
||||
@@ -1167,7 +1177,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<BillingEstimate quote={billingQuote} loading={billingQuoteLoading} />
|
||||
<BillingEstimate quote={billingQuote} loading={billingQuoteLoading} error={billingQuoteError} />
|
||||
</div>
|
||||
</div>
|
||||
{templateEditorOpen ? (
|
||||
@@ -1425,7 +1435,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
<div className="create-task-body">
|
||||
<h3 title={taskName(job)}>{taskName(job)}</h3>
|
||||
<div className="create-task-meta">
|
||||
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
|
||||
<span className={`status ${taskStatusClass(job, taskAssetById)}`}>{statusLabel(job.status, Boolean(firstOutputAsset(job, taskAssetById)))}</span>
|
||||
<span>{durationLabel(job, durationNow)}</span>
|
||||
<span className="billing-task-cost">{billingLabel(job)}</span>
|
||||
</div>
|
||||
@@ -1477,10 +1487,11 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
function renderTaskThumbnail(job: GenerationJob, assetById: Map<string, Asset>) {
|
||||
const asset = job.outputAssetIds.map((assetId) => assetById.get(assetId)).find((item): item is Asset => Boolean(item));
|
||||
if (!asset) {
|
||||
const awaitingOutput = job.status === "succeeded";
|
||||
return (
|
||||
<div className={clsx("create-task-thumb create-task-thumb-placeholder", isPendingTask(job) && "generating")} aria-hidden="true">
|
||||
<div className={clsx("create-task-thumb create-task-thumb-placeholder", (isPendingTask(job) || awaitingOutput) && "generating")} aria-hidden="true">
|
||||
{isPendingTask(job) ? <Loader2 className="spin" size={18} /> : <ImageIcon size={18} />}
|
||||
<span>{isPendingTask(job) ? "生成中" : "无结果"}</span>
|
||||
<span>{isPendingTask(job) ? "生成中" : awaitingOutput ? "结果同步中" : "无结果"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1532,7 +1543,7 @@ function TaskDetailModal({
|
||||
<span className="task-detail-kicker">任务详情</span>
|
||||
<h2 id="task-detail-title" title={taskName(job)}>{taskName(job)}</h2>
|
||||
<div className="task-detail-subline">
|
||||
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
|
||||
<span className={`status ${taskStatusClass(job, assets)}`}>{statusLabel(job.status, outputAssets.length > 0)}</span>
|
||||
<span>{capabilityLabel(job.capability)}</span>
|
||||
<span>{durationLabel(job, now)}</span>
|
||||
</div>
|
||||
@@ -1666,20 +1677,23 @@ function firstOutputAsset(job: GenerationJob, assets: Map<string, Asset>) {
|
||||
|
||||
function BillingEstimate({
|
||||
quote,
|
||||
loading
|
||||
loading,
|
||||
error
|
||||
}: {
|
||||
quote: BillingQuote | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}) {
|
||||
return (
|
||||
<aside className={clsx("billing-estimate", loading && "is-loading")} aria-live="polite" aria-label={quote?.quotaExempt ? "本次预计费用(超级管理员不计额度)" : "本次预计消耗额度"}>
|
||||
<aside className={clsx("billing-estimate", loading && "is-loading", error && "has-error")} aria-live="polite" aria-label={quote?.quotaExempt ? "本次预计费用(超级管理员不计额度)" : "本次预计消耗额度"}>
|
||||
<div className="billing-estimate-head">
|
||||
<span className="billing-estimate-icon" aria-hidden="true"><CircleDollarSign size={16} /></span>
|
||||
<span>{quote?.quotaExempt ? "本次预计费用(不计额度)" : "本次预计消耗额度"}</span>
|
||||
</div>
|
||||
<strong className="billing-estimate-amount">
|
||||
{loading ? "…" : quote ? formatBillingAmount(quote.amountFen) : "—"}
|
||||
{loading ? "…" : error ? "暂不可用" : quote ? formatBillingAmount(quote.amountFen) : "—"}
|
||||
</strong>
|
||||
{error ? <span className="billing-estimate-error">{error}</span> : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1767,7 +1781,7 @@ function taskEngineLabel(value: string) {
|
||||
if (value === "evolink") return "Image2";
|
||||
if (value === "bailian") return "阿里云百炼";
|
||||
if (value === "seedance") return "Seedance";
|
||||
if (value === "mock") return "系统生成";
|
||||
if (value === "mock") return "历史任务";
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1805,10 +1819,14 @@ function taskName(job: GenerationJob) {
|
||||
return text ? text : capabilityLabel(job.capability);
|
||||
}
|
||||
|
||||
function statusLabel(status: GenerationJob["status"]) {
|
||||
function taskStatusClass(job: GenerationJob, assets: Map<string, Asset>) {
|
||||
return job.status === "succeeded" && !firstOutputAsset(job, assets) ? "syncing" : job.status;
|
||||
}
|
||||
|
||||
function statusLabel(status: GenerationJob["status"], hasOutput = true) {
|
||||
if (status === "queued") return "排队中";
|
||||
if (status === "running") return "生成中";
|
||||
if (status === "succeeded") return "已完成";
|
||||
if (status === "succeeded") return hasOutput ? "已完成" : "结果同步中";
|
||||
if (status === "failed") return "失败";
|
||||
if (status === "expired") return "已过期";
|
||||
if (status === "cancelled") return "已取消";
|
||||
@@ -1844,7 +1862,7 @@ function formatDateTime(value: string) {
|
||||
}
|
||||
|
||||
function billingLabel(job: GenerationJob) {
|
||||
if (!job.billing) return job.provider === "mock" ? "免计费" : "未计费";
|
||||
if (!job.billing) return job.provider === "mock" ? "历史任务(未计费)" : "未计费";
|
||||
if (job.billing.quotaExempt) return `超管费用 ${formatBillingAmount(job.billing.amountFen)}(不计额度)`;
|
||||
if (job.billing.status === "refunded") return `已退款 ${formatBillingAmount(job.billing.amountFen)}`;
|
||||
if (job.billing.status === "pending") return `预计 ${formatBillingAmount(job.billing.amountFen)}`;
|
||||
|
||||
Reference in new issue
Block a user