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

View File

@@ -0,0 +1,116 @@
import type { HTMLAttributes } from "react";
import type {
OrderStatus,
ReviewDecision,
StepStatus,
WorkflowStepName,
} from "@/lib/types/backend";
import {
ORDER_STATUS_META,
REVIEW_DECISION_META,
STEP_STATUS_META,
WORKFLOW_STEP_META,
type StatusMeta,
type StatusTone,
} from "@/lib/types/status";
type StatusBadgeVariant =
| "order"
| "reviewDecision"
| "stepStatus"
| "workflowStep";
type StatusBadgeBaseProps = HTMLAttributes<HTMLSpanElement>;
type OrderStatusBadgeProps = StatusBadgeBaseProps & {
status: OrderStatus;
variant?: "order";
};
type ReviewDecisionBadgeProps = StatusBadgeBaseProps & {
status: ReviewDecision;
variant: "reviewDecision";
};
type StepStatusBadgeProps = StatusBadgeBaseProps & {
status: StepStatus;
variant: "stepStatus";
};
type WorkflowStepBadgeProps = StatusBadgeBaseProps & {
status: WorkflowStepName | null;
variant: "workflowStep";
};
export type StatusBadgeProps =
| OrderStatusBadgeProps
| ReviewDecisionBadgeProps
| StepStatusBadgeProps
| WorkflowStepBadgeProps;
const TONE_STYLES: Record<StatusTone, string> = {
neutral:
"border-[rgba(76,69,60,0.14)] bg-[rgba(110,98,84,0.08)] text-[var(--ink-muted)]",
info: "border-[rgba(57,86,95,0.16)] bg-[rgba(57,86,95,0.1)] text-[#2e4d56]",
warning:
"border-[rgba(145,104,46,0.18)] bg-[rgba(202,164,97,0.14)] text-[#7a5323]",
success:
"border-[rgba(88,106,56,0.18)] bg-[rgba(110,127,82,0.14)] text-[#50633b]",
danger:
"border-[rgba(140,74,67,0.18)] bg-[rgba(140,74,67,0.12)] text-[#7f3f38]",
};
function joinClasses(...values: Array<string | false | null | undefined>) {
return values.filter(Boolean).join(" ");
}
function getMetaFromRecord<T extends string>(
record: Record<T, StatusMeta>,
status: string,
variant: StatusBadgeVariant,
): StatusMeta {
const meta = record[status as T];
if (!meta) {
throw new Error(`Invalid status "${status}" for variant "${variant}".`);
}
return meta;
}
function resolveStatusMeta(props: StatusBadgeProps): StatusMeta {
switch (props.variant) {
case "reviewDecision":
return getMetaFromRecord(REVIEW_DECISION_META, props.status, props.variant);
case "stepStatus":
return getMetaFromRecord(STEP_STATUS_META, props.status, props.variant);
case "workflowStep":
if (props.status === null) {
return { label: "未开始", tone: "neutral" };
}
return getMetaFromRecord(WORKFLOW_STEP_META, props.status, props.variant);
case "order":
case undefined:
return getMetaFromRecord(ORDER_STATUS_META, props.status, "order");
}
}
export function StatusBadge({ className, ...props }: StatusBadgeProps) {
const meta = resolveStatusMeta(props);
return (
<span
data-tone={meta.tone}
className={joinClasses(
"inline-flex items-center rounded-full border px-2.5 py-1 font-[var(--font-mono)] text-[11px] uppercase tracking-[0.16em]",
TONE_STYLES[meta.tone],
className,
)}
{...props}
>
{meta.label}
</span>
);
}