feat: bootstrap auto virtual tryon admin frontend

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
commit 98c6b741d6
119 changed files with 19046 additions and 0 deletions

121
src/lib/adapters/orders.ts Normal file
View File

@@ -0,0 +1,121 @@
import type {
AssetDto,
AssetType,
OrderDetailResponseDto,
OrderListItemDto,
WorkflowStepName,
} from "@/lib/types/backend";
import { getOrderStatusMeta, getWorkflowStepMeta } from "@/lib/types/status";
import {
businessEmptyState,
READY_STATE,
type AssetViewModel,
type OrderDetailVM,
type OrderSummaryVM,
} from "@/lib/types/view-models";
const ASSET_TYPE_LABELS: Record<AssetType, string> = {
prepared_model: "模型准备图",
tryon: "试穿图",
scene: "场景图",
texture: "纹理图",
face: "面部图",
fusion: "融合图",
qc_candidate: "质检候选图",
manual_revision: "人工修订稿",
final: "最终图",
};
function isMockUri(uri: string): boolean {
return uri.startsWith("mock://");
}
function getAssetLabel(assetType: AssetType, stepName: WorkflowStepName | null) {
if (stepName) {
return `${getWorkflowStepMeta(stepName).label}产物`;
}
return ASSET_TYPE_LABELS[assetType];
}
export function adaptAsset(asset: AssetDto): AssetViewModel {
const stepMeta = getWorkflowStepMeta(asset.step_name);
return {
id: asset.id,
orderId: asset.order_id,
type: asset.asset_type,
stepName: asset.step_name,
parentAssetId: asset.parent_asset_id,
rootAssetId: asset.root_asset_id,
versionNo: asset.version_no,
isCurrentVersion: asset.is_current_version,
stepLabel: stepMeta.label,
label: getAssetLabel(asset.asset_type, asset.step_name),
uri: asset.uri,
metadata: asset.metadata_json,
createdAt: asset.created_at,
isMock: isMockUri(asset.uri),
};
}
export function adaptOrderSummary(
order: Pick<
OrderDetailResponseDto | OrderListItemDto,
"order_id" | "workflow_id" | "status" | "current_step" | "updated_at"
>,
): OrderSummaryVM {
return {
orderId: order.order_id,
workflowId: order.workflow_id,
status: order.status,
statusMeta: getOrderStatusMeta(order.status),
currentStep: order.current_step,
currentStepLabel: getWorkflowStepMeta(order.current_step).label,
updatedAt: order.updated_at,
};
}
export function adaptOrderDetail(
order: OrderDetailResponseDto,
assets: AssetDto[] = [],
): OrderDetailVM {
const finalAsset = order.final_asset ? adaptAsset(order.final_asset) : null;
const assetItems = assets.map(adaptAsset);
const hasMockAssets = [finalAsset, ...assetItems].some(
(asset) => asset?.isMock,
);
return {
orderId: order.order_id,
workflowId: order.workflow_id,
customerLevel: order.customer_level,
serviceMode: order.service_mode,
status: order.status,
statusMeta: getOrderStatusMeta(order.status),
currentStep: order.current_step,
currentStepLabel: getWorkflowStepMeta(order.current_step).label,
modelId: order.model_id,
poseId: order.pose_id,
garmentAssetId: order.garment_asset_id,
sceneRefAssetId: order.scene_ref_asset_id,
currentRevisionAssetId: order.current_revision_asset_id,
currentRevisionVersion: order.current_revision_version,
latestRevisionAssetId: order.latest_revision_asset_id,
latestRevisionVersion: order.latest_revision_version,
revisionCount: order.revision_count,
reviewTaskStatus: order.review_task_status,
pendingManualConfirm: order.pending_manual_confirm,
createdAt: order.created_at,
updatedAt: order.updated_at,
finalAsset,
finalAssetState: finalAsset
? READY_STATE
: businessEmptyState("最终图暂未生成", "当前订单还没有可展示的最终结果。"),
assets: assetItems,
assetGalleryState: assetItems.length
? READY_STATE
: businessEmptyState("暂无资产", "当前订单还没有生成可查看的资产列表。"),
hasMockAssets,
};
}

View File

@@ -0,0 +1,69 @@
import type {
PendingReviewResponseDto,
SubmitReviewResponseDto,
} from "@/lib/types/backend";
import {
getOrderStatusMeta,
getReviewDecisionMeta,
getWorkflowStepMeta,
} from "@/lib/types/status";
import {
businessEmptyState,
READY_STATE,
type ReviewQueueItemVM,
type ReviewQueueVM,
type ReviewSubmissionVM,
} from "@/lib/types/view-models";
function adaptPendingReviewItem(
review: PendingReviewResponseDto,
): ReviewQueueItemVM {
return {
reviewTaskId: review.review_task_id,
orderId: review.order_id,
workflowId: review.workflow_id,
workflowType: null,
currentStep: review.current_step,
currentStepLabel: getWorkflowStepMeta(review.current_step).label,
createdAt: review.created_at,
status: "waiting_review",
statusMeta: getOrderStatusMeta("waiting_review"),
reviewTaskStatus: review.review_task_status,
latestRevisionAssetId: review.latest_revision_asset_id,
currentRevisionAssetId: review.current_revision_asset_id,
latestRevisionVersion: review.latest_revision_version,
revisionCount: review.revision_count,
pendingManualConfirm: review.pending_manual_confirm,
hasMockAssets: false,
failureCount: 0,
};
}
export function adaptPendingReviews(
reviews: PendingReviewResponseDto[],
): ReviewQueueVM {
const items = reviews.map(adaptPendingReviewItem);
return {
items,
state: items.length
? READY_STATE
: businessEmptyState(
"暂无待审核订单",
"当前没有等待人工处理的审核任务。",
),
};
}
export function adaptReviewSubmission(
submission: SubmitReviewResponseDto,
): ReviewSubmissionVM {
return {
orderId: submission.order_id,
workflowId: submission.workflow_id,
revisionAssetId: submission.revision_asset_id,
decision: submission.decision,
decisionMeta: getReviewDecisionMeta(submission.decision),
status: submission.status,
};
}

View File

@@ -0,0 +1,44 @@
import type {
RegisterRevisionResponseDto,
RevisionChainResponseDto,
} from "@/lib/types/backend";
import type {
RevisionChainVM,
RevisionRegistrationVM,
} from "@/lib/types/view-models";
export function adaptRevisionRegistration(
payload: RegisterRevisionResponseDto,
): RevisionRegistrationVM {
return {
orderId: payload.order_id,
workflowId: payload.workflow_id,
assetId: payload.asset_id,
parentAssetId: payload.parent_asset_id,
rootAssetId: payload.root_asset_id,
versionNo: payload.version_no,
reviewTaskStatus: payload.review_task_status,
latestRevisionAssetId: payload.latest_revision_asset_id,
revisionCount: payload.revision_count,
};
}
export function adaptRevisionChain(
payload: RevisionChainResponseDto,
): RevisionChainVM {
return {
orderId: payload.order_id,
latestRevisionAssetId: payload.latest_revision_asset_id,
revisionCount: payload.revision_count,
items: payload.items.map((item) => ({
assetId: item.asset_id,
orderId: item.order_id,
parentAssetId: item.parent_asset_id,
rootAssetId: item.root_asset_id,
versionNo: item.version_no,
isCurrentVersion: item.is_current_version,
uri: item.uri,
createdAt: item.created_at,
})),
};
}

View File

@@ -0,0 +1,154 @@
import type {
JsonObject,
JsonValue,
WorkflowListItemDto,
WorkflowStatusResponseDto,
} from "@/lib/types/backend";
import {
getOrderStatusMeta,
getStepStatusMeta,
getWorkflowStepMeta,
} from "@/lib/types/status";
import {
businessEmptyState,
READY_STATE,
type WorkflowDetailVM,
type WorkflowLookupItemVM,
type WorkflowStepVM,
} from "@/lib/types/view-models";
type WorkflowAssetUriField =
| "asset_uri"
| "candidate_uri"
| "preview_uri"
| "result_uri"
| "source_uri";
const WORKFLOW_ASSET_URI_FIELDS = new Set<WorkflowAssetUriField>([
"asset_uri",
"candidate_uri",
"preview_uri",
"result_uri",
"source_uri",
]);
function collectKnownAssetUris(
value: JsonValue | undefined,
results: string[] = [],
): string[] {
if (!value || typeof value !== "object") {
return results;
}
if (Array.isArray(value)) {
for (const item of value) {
collectKnownAssetUris(item, results);
}
return results;
}
for (const [key, nestedValue] of Object.entries(value)) {
if (
WORKFLOW_ASSET_URI_FIELDS.has(key as WorkflowAssetUriField) &&
typeof nestedValue === "string" &&
nestedValue.startsWith("mock://")
) {
results.push(nestedValue);
continue;
}
collectKnownAssetUris(nestedValue, results);
}
return results;
}
function uniqueMockUris(...payloads: Array<JsonObject | null>): string[] {
return [...new Set(payloads.flatMap((payload) => collectKnownAssetUris(payload)))];
}
function adaptWorkflowStep(
currentStep: WorkflowStatusResponseDto["current_step"],
step: WorkflowStatusResponseDto["steps"][number],
): WorkflowStepVM {
const stepMeta = getWorkflowStepMeta(step.step_name);
const mockAssetUris = uniqueMockUris(step.input_json, step.output_json);
return {
id: step.id,
workflowRunId: step.workflow_run_id,
name: step.step_name,
label: stepMeta.label,
status: step.step_status,
statusMeta: getStepStatusMeta(step.step_status),
input: step.input_json,
output: step.output_json,
errorMessage: step.error_message,
startedAt: step.started_at,
endedAt: step.ended_at,
containsMockAssets: mockAssetUris.length > 0,
mockAssetUris,
isCurrent: currentStep === step.step_name,
isFailed: step.step_status === "failed",
};
}
export function adaptWorkflowLookupItem(
workflow: Pick<
WorkflowStatusResponseDto | WorkflowListItemDto,
| "order_id"
| "workflow_id"
| "workflow_type"
| "workflow_status"
| "current_step"
| "updated_at"
>,
): WorkflowLookupItemVM {
return {
orderId: workflow.order_id,
workflowId: workflow.workflow_id,
workflowType: workflow.workflow_type,
status: workflow.workflow_status,
statusMeta: getOrderStatusMeta(workflow.workflow_status),
currentStep: workflow.current_step,
currentStepLabel: getWorkflowStepMeta(workflow.current_step).label,
updatedAt: workflow.updated_at,
};
}
export function adaptWorkflowDetail(
workflow: WorkflowStatusResponseDto,
): WorkflowDetailVM {
const steps = workflow.steps.map((step) =>
adaptWorkflowStep(workflow.current_step, step),
);
return {
orderId: workflow.order_id,
workflowId: workflow.workflow_id,
workflowType: workflow.workflow_type,
status: workflow.workflow_status,
statusMeta: getOrderStatusMeta(workflow.workflow_status),
currentStep: workflow.current_step,
currentStepLabel: getWorkflowStepMeta(workflow.current_step).label,
currentRevisionAssetId: workflow.current_revision_asset_id,
currentRevisionVersion: workflow.current_revision_version,
latestRevisionAssetId: workflow.latest_revision_asset_id,
latestRevisionVersion: workflow.latest_revision_version,
revisionCount: workflow.revision_count,
reviewTaskStatus: workflow.review_task_status,
pendingManualConfirm: workflow.pending_manual_confirm,
createdAt: workflow.created_at,
updatedAt: workflow.updated_at,
steps,
stepTimelineState: steps.length
? READY_STATE
: businessEmptyState(
"暂无流程记录",
"当前工作流还没有可展示的步骤执行记录。",
),
failureCount: steps.filter((step) => step.isFailed).length,
hasMockAssets: steps.some((step) => step.containsMockAssets),
};
}