Files
STP/front-end/src/app/components/shared/common.tsx
T
vame 787de5aa88 feat(front-end): 依据《详细设计》文档对前端 UI 进行全面升级
- 设计系统:品牌主色统一为品红渐变 #c2185b→#e91e63,新增品牌令牌与呼吸/入场动画
- 登录页重建:内部员工/外部账号切换、账号/手机验证码 Tab、SRM 存量提示
- 注册页重建为多步向导:企业信息→联系人→AI 审核,OCR 状态、密码复杂度校验与强度条
- 小滨 AI 助手重建:品牌渐变悬浮球+呼吸光环,角色感知主动提醒(紧急/AI完成/待办/风险/建议)
- 新增公开门户首页 /portal:Banner 轮播 + 三列公告区 + 免责声明
- 各角色主题色对齐文档:供应商=红、评标专家=蓝、招标专员=绿

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:46:04 +08:00

191 lines
6.0 KiB
TypeScript

import { Sparkles, Check } from "lucide-react";
import { cn } from "../ui/utils";
import { Badge } from "../ui/badge";
import type { ProjectStatus } from "../../data/mock";
/** AI 生成 / AI 能力标签 */
export function AiBadge({ label = "AI 生成", className }: { label?: string; className?: string }) {
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-md bg-brand-gradient px-2 py-0.5 text-xs font-medium text-white",
className,
)}
>
<Sparkles className="size-3" />
{label}
</span>
);
}
/** 页面标题区 */
export function PageHeader({
title,
description,
actions,
}: {
title: string;
description?: string;
actions?: React.ReactNode;
}) {
return (
<div className="mb-6 flex items-start justify-between gap-4">
<div>
<h1 className="text-foreground">{title}</h1>
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
</div>
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
</div>
);
}
const statusColor: Record<string, string> = {
: "bg-slate-100 text-slate-700",
: "bg-amber-100 text-amber-700",
: "bg-sky-100 text-sky-700",
: "bg-indigo-100 text-indigo-700",
线: "bg-blue-100 text-blue-700",
: "bg-violet-100 text-violet-700",
: "bg-teal-100 text-teal-700",
: "bg-emerald-100 text-emerald-700",
};
export function StatusBadge({ status }: { status: ProjectStatus | string }) {
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
statusColor[status] ?? "bg-slate-100 text-slate-700",
)}
>
{status}
</span>
);
}
export function RiskBadge({ risk }: { risk: string }) {
const map: Record<string, string> = {
: "bg-emerald-50 text-emerald-700 border-emerald-200",
: "bg-amber-50 text-amber-700 border-amber-200",
: "bg-red-50 text-red-700 border-red-200",
};
return (
<Badge variant="outline" className={cn("border", map[risk])}>
{risk}
</Badge>
);
}
/** AI 审核结论标签 */
export function AiResultTag({ result }: { result: string }) {
const ok = result === "资质有效";
const map: Record<string, string> = {
: "bg-emerald-50 text-emerald-700 border-emerald-200",
: "bg-amber-50 text-amber-700 border-amber-200",
: "bg-red-50 text-red-700 border-red-200",
};
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium",
map[result] ?? "bg-slate-50 text-slate-700 border-slate-200",
)}
>
<Sparkles className="size-3" />
{result}
</span>
);
}
/** 横向步骤条 */
export function Steps({
steps,
current,
}: {
steps: string[];
current: number;
}) {
return (
<div className="flex items-center">
{steps.map((s, i) => {
const done = i < current;
const active = i === current;
return (
<div key={s} className="flex flex-1 items-center last:flex-none">
<div className="flex items-center gap-2">
<div
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors",
done && "border-primary bg-primary text-primary-foreground",
active && "border-primary bg-primary/10 text-primary",
!done && !active && "border-border bg-card text-muted-foreground",
)}
>
{done ? <Check className="size-4" /> : i + 1}
</div>
<span
className={cn(
"whitespace-nowrap text-sm",
active ? "font-medium text-foreground" : "text-muted-foreground",
)}
>
{s}
</span>
</div>
{i < steps.length - 1 && (
<div className={cn("mx-3 h-0.5 flex-1 rounded", done ? "bg-primary" : "bg-border")} />
)}
</div>
);
})}
</div>
);
}
export interface ApprovalNode {
name: string;
role: string;
status: "completed" | "current" | "pending";
time?: string;
}
/** 审批流程跟踪条 */
export function ApprovalTracker({ nodes }: { nodes: ApprovalNode[] }) {
return (
<div className="flex flex-col gap-0">
{nodes.map((n, i) => (
<div key={n.name} className="flex gap-3">
<div className="flex flex-col items-center">
<div
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-full text-xs",
n.status === "completed" && "bg-emerald-500 text-white",
n.status === "current" && "bg-primary text-white ring-4 ring-primary/15",
n.status === "pending" && "bg-muted text-muted-foreground",
)}
>
{n.status === "completed" ? <Check className="size-4" /> : i + 1}
</div>
{i < nodes.length - 1 && (
<div className={cn("my-1 w-0.5 flex-1", n.status === "completed" ? "bg-emerald-500" : "bg-border")} />
)}
</div>
<div className="pb-5">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">{n.role}</span>
{n.status === "current" && (
<span className="rounded bg-primary/10 px-1.5 py-0.5 text-xs text-primary"></span>
)}
{n.status === "completed" && (
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-600"></span>
)}
</div>
<p className="text-xs text-muted-foreground">{n.name}</p>
{n.time && <p className="mt-0.5 text-xs text-muted-foreground/70">{n.time}</p>}
</div>
</div>
))}
</div>
);
}