787de5aa88
- 设计系统:品牌主色统一为品红渐变 #c2185b→#e91e63,新增品牌令牌与呼吸/入场动画 - 登录页重建:内部员工/外部账号切换、账号/手机验证码 Tab、SRM 存量提示 - 注册页重建为多步向导:企业信息→联系人→AI 审核,OCR 状态、密码复杂度校验与强度条 - 小滨 AI 助手重建:品牌渐变悬浮球+呼吸光环,角色感知主动提醒(紧急/AI完成/待办/风险/建议) - 新增公开门户首页 /portal:Banner 轮播 + 三列公告区 + 免责声明 - 各角色主题色对齐文档:供应商=红、评标专家=蓝、招标专员=绿 Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|