feat(front-end): 依据《详细设计》文档对前端 UI 进行全面升级

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-09 23:46:04 +08:00
parent f7a49ffb78
commit 787de5aa88
16 changed files with 1301 additions and 244 deletions
+224
View File
@@ -0,0 +1,224 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router";
import { Hexagon, ArrowRight, Sparkles, Bot } from "lucide-react";
import { cn } from "../components/ui/utils";
const NAV = ["首页", "招标公告", "定标公告", "企业新闻"];
const BANNERS = [
{
title: "AI 赋能的智慧招标",
subtitle: "全流程线上闭环,主动智能寻源,自动风险评估",
tags: ["智能招标委托", "供应商 AI 寻源", "自动化评标辅助"],
},
{
title: "一次注册,双平台通用",
subtitle: "自动同步 SRM,建立统一供应商身份,秒级 AI 审核即时生效",
tags: ["营业执照 OCR", "工商实时验真", "SRM 无感登录"],
},
{
title: "阳光招采,全程存证",
subtitle: "投标文件加密上链,评审操作哈希存证,围串标智能预警",
tags: ["链上存证", "盲评脱敏", "AI 雷同检测"],
},
];
const TENDERS = [
{ title: "年产5万吨聚丙烯项目设备采购", meta: "2026-07-01 货物", status: "报名中", color: "#2e7d32", dot: "#4caf50" },
{ title: "数字化交付平台开发服务", meta: "2026-06-29 服务", status: "即将截止", color: "#e65100", dot: "#ff9800" },
{ title: "2026年度防腐保温工程框架", meta: "2026-06-28 工程", status: "已截止", color: "#9e9e9e", dot: "#bdbdbd" },
];
const AWARDS = [
{ title: "催化剂采购项目(包1", meta: "2026-07-01 中标方:滨化催化剂有限公司" },
{ title: "厂区安防监控升级项目", meta: "2026-06-30 中标方:海康威视" },
{ title: "办公楼装修工程", meta: "2026-06-28 中标方:滨州建工集团" },
];
const NEWS = [
{ title: "滨化集团获评“省级智能工厂”", meta: "2026-07-01 集团动态" },
{ title: "2026 年供应商大会圆满举办", meta: "2026-06-25 合作伙伴" },
{ title: "智慧招标平台正式上线运营", meta: "2026-06-01 平台公告" },
];
function AnnouncementCard({
title,
tag,
headerColor,
items,
moreBg,
moreText,
}: {
title: string;
tag: string;
headerColor: string;
items: { title: string; meta: string; status?: string; color?: string; dot?: string }[];
moreBg: string;
moreText: string;
}) {
return (
<div className="flex flex-col overflow-hidden rounded-2xl bg-white shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
<div className="px-5 py-3 text-white" style={{ backgroundColor: headerColor }}>
<div className="text-base font-bold">{title}</div>
<div className="text-[11px] text-white/80">{tag}</div>
</div>
<div className="flex-1 space-y-2 p-4">
{items.map((it) => (
<div key={it.title} className="cursor-pointer rounded-lg bg-[#fafafa] px-3 py-2.5 transition-colors hover:bg-[#f0f2f7]">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-sm text-[#1a2332]">{it.title}</span>
{it.status && (
<span className="flex shrink-0 items-center gap-1 text-xs" style={{ color: it.color }}>
<span className="size-1.5 rounded-full" style={{ backgroundColor: it.dot }} />
{it.status}
</span>
)}
</div>
<div className="mt-1 text-xs text-[#8895aa]">{it.meta}</div>
</div>
))}
</div>
<div className="px-4 pb-4">
<button
className="w-full rounded-full py-1.5 text-xs font-bold transition-opacity hover:opacity-80"
style={{ backgroundColor: moreBg, color: moreText }}
>
</button>
</div>
</div>
);
}
export function PortalHome() {
const navigate = useNavigate();
const [banner, setBanner] = useState(0);
const [activeNav, setActiveNav] = useState(0);
useEffect(() => {
const t = setInterval(() => setBanner((b) => (b + 1) % BANNERS.length), 5000);
return () => clearInterval(t);
}, []);
const b = BANNERS[banner];
return (
<div className="min-h-screen bg-[#f5f7fa]">
{/* 顶部导航 */}
<header className="sticky top-0 z-30 bg-brand-gradient shadow-sm">
<div className="mx-auto flex h-[68px] max-w-[1200px] items-center px-8">
<Link to="/portal" className="flex items-baseline gap-2 text-white">
<Hexagon className="size-6 self-center" />
<span className="text-xl font-bold"></span>
<span className="hidden text-xs text-white/70 sm:inline">Smart Tendering Platform</span>
</Link>
<nav className="ml-12 hidden gap-1 md:flex">
{NAV.map((n, i) => (
<button
key={n}
onClick={() => setActiveNav(i)}
className={cn(
"group relative px-4 py-2 text-sm text-white/80 transition-colors hover:text-white",
activeNav === i && "font-bold text-white",
)}
>
{n}
<span
className={cn(
"absolute inset-x-4 -bottom-0.5 h-0.5 rounded-full bg-white transition-all",
activeNav === i ? "opacity-100" : "opacity-0 group-hover:opacity-60",
)}
/>
</button>
))}
</nav>
<div className="ml-auto flex items-center gap-3">
<button
onClick={() => navigate("/")}
className="rounded-lg border border-white/60 px-4 py-1.5 text-sm text-white transition-colors hover:bg-white/15"
>
</button>
<button
onClick={() => navigate("/register")}
className="rounded-lg bg-white px-4 py-1.5 text-sm font-bold text-primary transition-transform hover:-translate-y-0.5"
>
</button>
</div>
</div>
</header>
<main className="mx-auto max-w-[1200px] px-8 pb-16">
{/* Banner 轮播 */}
<section className="relative mt-6 overflow-hidden rounded-2xl bg-gradient-to-br from-[#fce4ec] to-[#f3e5f5] shadow-[0_6px_24px_rgba(0,0,0,0.08)]">
<div className="flex min-h-[280px] flex-col justify-center px-12 py-10">
<div className="mb-3 inline-flex w-fit items-center gap-1.5 rounded-full bg-white/70 px-3 py-1 text-xs text-primary">
<Sparkles className="size-3.5" /> AI · 线
</div>
<h1 key={banner} className="animate-slide-up text-primary" style={{ fontSize: 34, fontWeight: 700 }}>
{b.title}
</h1>
<p className="mt-3 text-lg text-[#4a5568]">{b.subtitle}</p>
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-sm text-[#8895aa]">
{b.tags.map((t) => (
<span key={t}> {t}</span>
))}
</div>
<button
onClick={() => navigate("/register")}
className="mt-6 inline-flex w-fit items-center gap-1.5 rounded-lg bg-brand-gradient px-6 py-2.5 text-sm font-bold text-white shadow-md transition-transform hover:scale-105"
>
<ArrowRight className="size-4" />
</button>
</div>
{/* 指示器 */}
<div className="absolute bottom-6 right-12 flex gap-2">
{BANNERS.map((_, i) => (
<button
key={i}
aria-label={`切换到第 ${i + 1}`}
onClick={() => setBanner(i)}
className={cn("h-2 rounded-full transition-all", i === banner ? "w-6 bg-primary" : "w-2 bg-[#d1d5db]")}
/>
))}
</div>
</section>
{/* 公告区 */}
<section className="mt-8">
<h2 className="mb-4 flex items-center gap-2 text-[#1a2332]" style={{ fontSize: 20, fontWeight: 700 }}>
📢
</h2>
<div className="grid gap-5 md:grid-cols-3">
<AnnouncementCard title="招标公告" tag="最新发布" headerColor="#c2185b" items={TENDERS} moreBg="#fce4ec" moreText="#c2185b" />
<AnnouncementCard title="定标公告" tag="最新定标" headerColor="#1565c0" items={AWARDS} moreBg="#e3f2fd" moreText="#1565c0" />
<AnnouncementCard title="企业新闻" tag="集团动态" headerColor="#e65100" items={NEWS} moreBg="#fff3e0" moreText="#e65100" />
</div>
</section>
{/* 免责声明 */}
<section className="mt-8 rounded-2xl bg-white p-6 text-center shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
<div className="text-sm font-semibold text-[#4a5568]"></div>
<p className="mx-auto mt-2 max-w-3xl text-xs leading-relaxed text-[#8895aa]">
</p>
<p className="mt-2 text-xs text-[#8895aa]"> © 2026 ICP备XXXXXXXX号</p>
</section>
</main>
{/* AI 助手浮标 */}
<button
onClick={() => navigate("/")}
className="group fixed bottom-8 right-8 z-50 flex size-[68px] items-center justify-center rounded-full bg-brand-gradient text-white shadow-xl transition-transform hover:scale-105"
aria-label="小滨 AI 助手"
>
<span className="pointer-events-none absolute inset-0 rounded-full bg-brand-gradient animate-breathe" />
<span className="relative flex flex-col items-center leading-none">
<Bot className="size-5" />
<span className="mt-0.5 text-[9px] text-white/90"></span>
</span>
</button>
</div>
);
}