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
+218 -73
View File
@@ -1,119 +1,246 @@
import { useState } from "react";
import { useNavigate, Link } from "react-router";
import { Hexagon, User, Lock, ShieldCheck, Sparkles, ArrowRight } from "lucide-react";
import { Hexagon, User, Lock, Smartphone, ShieldCheck, ArrowRight, Eye, EyeOff, Info } from "lucide-react";
import { toast } from "sonner";
import { Button } from "../components/ui/button";
import { cn } from "../components/ui/utils";
type Mode = "employee" | "vendor";
type Tab = "account" | "mobile";
export function LoginPage() {
const navigate = useNavigate();
const [mode, setMode] = useState<Mode>("vendor");
const [tab, setTab] = useState<Tab>("account");
const [account, setAccount] = useState("zhangwei");
const [pwd, setPwd] = useState("123456");
const [showPwd, setShowPwd] = useState(false);
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const [remember, setRemember] = useState(true);
const [countdown, setCountdown] = useState(0);
function sendCode() {
if (!/^1[3-9]\d{9}$/.test(phone)) {
toast.error("请输入正确的手机号");
return;
}
toast.success("验证码已发送,请注意查收");
setCountdown(60);
const timer = setInterval(() => {
setCountdown((c) => {
if (c <= 1) {
clearInterval(timer);
return 0;
}
return c - 1;
});
}, 1000);
}
function submit(e: React.FormEvent) {
e.preventDefault();
if (mode === "employee") {
toast.info("正在跳转至集团 IAM 统一身份认证…");
setTimeout(() => navigate("/app"), 800);
return;
}
navigate("/app");
}
return (
<div className="flex h-screen w-full overflow-hidden bg-slate-50">
<div className="flex h-screen w-full overflow-hidden bg-[#f5f7fa]">
{/* 左侧品牌区 */}
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-gradient-to-br from-[#0f1e3d] via-[#13306b] to-[#2563eb] p-12 text-white lg:flex">
<div className="flex items-center gap-3">
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-brand-gradient p-12 text-white lg:flex">
<Link to="/portal" className="flex items-center gap-3">
<div className="flex size-11 items-center justify-center rounded-xl bg-white/15 backdrop-blur">
<Hexagon className="size-6" />
</div>
<div>
<div className="text-lg font-semibold"></div>
<div className="text-sm text-white/60">Binhua Smart Tendering Platform</div>
<div className="text-sm text-white/70">Smart Tendering Platform</div>
</div>
</div>
</Link>
<div className="relative z-10">
<div className="mb-4 inline-flex items-center gap-2 rounded-full bg-white/10 px-3 py-1 text-sm backdrop-blur">
<Sparkles className="size-4" /> AI · 线
</div>
<h1 className="text-white" style={{ fontSize: 40, lineHeight: 1.2, fontWeight: 700 }}>
AI
<h1 className="text-white" style={{ fontSize: 44, lineHeight: 1.2, fontWeight: 700, letterSpacing: 2 }}>
</h1>
<p className="mt-4 max-w-md text-white/70">
<p className="mt-5 max-w-md text-white/80">
AI · 线 ·
</p>
<p className="mt-2 max-w-md text-sm text-white/60">
· · ·
</p>
<div className="mt-8 grid grid-cols-3 gap-4">
{[
{ k: "全流程", v: "9 大阶段闭环" },
{ k: "AI 预填", v: "委托效率 +60%" },
{ k: "AI 寻源", v: "委托效率 +60%" },
{ k: "链上存证", v: "投标哈希上链" },
].map((s) => (
<div key={s.k} className="rounded-xl bg-white/10 p-4 backdrop-blur">
<div className="text-lg font-semibold">{s.k}</div>
<div className="mt-1 text-xs text-white/60">{s.v}</div>
<div className="mt-1 text-xs text-white/70">{s.v}</div>
</div>
))}
</div>
</div>
<div className="text-xs text-white/40">© 2026 · </div>
<div className="text-xs text-white/50">© 2026 V1.0</div>
{/* 装饰 */}
<div className="pointer-events-none absolute -right-20 top-1/4 size-72 rounded-full bg-blue-400/20 blur-3xl" />
<div className="pointer-events-none absolute -bottom-10 right-1/3 size-60 rounded-full bg-violet-400/20 blur-3xl" />
<div className="pointer-events-none absolute -right-24 top-1/4 size-80 rounded-full bg-white/10 blur-3xl" />
<div className="pointer-events-none absolute -bottom-16 right-1/3 size-60 rounded-full bg-white/10 blur-3xl" />
<div className="pointer-events-none absolute left-1/2 top-1/2 size-72 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10" />
<div className="pointer-events-none absolute left-1/2 top-1/2 size-52 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10" />
</div>
{/* 右侧表单区 */}
<div className="flex flex-1 items-center justify-center p-8">
<div className="w-full max-w-sm">
<div className="mb-8 lg:hidden">
<div className="w-full max-w-[400px]">
<div className="mb-6 lg:hidden">
<div className="flex items-center gap-2">
<Hexagon className="size-7 text-primary" />
<span className="font-semibold"></span>
</div>
</div>
<h2 className="text-foreground"></h2>
<p className="mt-1 text-sm text-muted-foreground"></p>
<div className="text-center">
<h2 className="text-foreground" style={{ fontSize: 24 }}></h2>
<p className="mt-1 text-sm text-muted-foreground"></p>
</div>
<form
className="mt-8 space-y-4"
onSubmit={(e) => {
e.preventDefault();
navigate("/app");
}}
>
<div>
<label className="mb-1.5 block text-sm"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
value={account}
onChange={(e) => setAccount(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
placeholder="请输入账号"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
type="password"
value={pwd}
onChange={(e) => setPwd(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
placeholder="请输入密码"
/>
</div>
</div>
<div className="flex gap-3">
<div className="relative flex-1">
<ShieldCheck className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
placeholder="验证码"
/>
</div>
<div className="flex h-11 w-28 items-center justify-center rounded-lg bg-gradient-to-r from-blue-100 to-violet-100 font-mono tracking-widest text-primary select-none">
8 K 2 7
</div>
</div>
{/* 角色切换开关: 内部员工 / 外部账号 */}
<div className="mt-6 flex rounded-full bg-muted p-1">
{(
[
{ id: "employee" as Mode, label: "内部员工" },
{ id: "vendor" as Mode, label: "外部账号" },
]
).map((m) => (
<button
key={m.id}
type="button"
onClick={() => setMode(m.id)}
className={cn(
"flex-1 rounded-full py-2 text-sm font-medium transition-all",
mode === m.id ? "bg-brand-gradient text-white shadow-sm" : "text-muted-foreground hover:text-foreground",
)}
>
{m.label}
</button>
))}
</div>
<p className="mt-2 text-center text-xs text-primary">
{mode === "employee" ? "当前:内部员工模式(走 IAM 集团统一认证)" : "当前:外部账号模式(走平台账号 / SRM 认证)"}
</p>
{/* Tab: 账号登录 / 手机验证码 */}
<div className="mt-5 flex border-b border-border">
{(
[
{ id: "account" as Tab, label: "账号登录" },
{ id: "mobile" as Tab, label: "手机验证码" },
]
).map((t) => (
<button
key={t.id}
type="button"
onClick={() => setTab(t.id)}
className={cn(
"relative -mb-px px-4 pb-2.5 text-sm transition-colors",
tab === t.id ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground",
)}
>
{t.label}
{tab === t.id && <span className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-brand-gradient" />}
</button>
))}
</div>
<form className="mt-5 space-y-4" onSubmit={submit}>
{tab === "account" ? (
<>
<div>
<label className="mb-1.5 block text-sm text-[#4a5568]"> / </label>
<div className="relative">
<User className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
value={account}
onChange={(e) => setAccount(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
placeholder="请输入您的账号"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm text-[#4a5568]"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
type={showPwd ? "text" : "password"}
value={pwd}
onChange={(e) => setPwd(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-10 text-sm outline-none transition-colors focus:border-primary"
placeholder="请输入您的密码"
/>
<button
type="button"
onClick={() => setShowPwd((s) => !s)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPwd ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</button>
</div>
</div>
</>
) : (
<>
<div>
<label className="mb-1.5 block text-sm text-[#4a5568]"></label>
<div className="relative">
<Smartphone className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
placeholder="请输入11位手机号"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm text-[#4a5568]"></label>
<div className="flex gap-3">
<div className="relative flex-1">
<ShieldCheck className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
value={code}
onChange={(e) => setCode(e.target.value)}
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
placeholder="6 位验证码"
/>
</div>
<button
type="button"
onClick={sendCode}
disabled={countdown > 0}
className="h-11 w-28 shrink-0 rounded-lg border border-primary text-sm font-medium text-primary transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:border-border disabled:text-muted-foreground"
>
{countdown > 0 ? `${countdown}s 后重发` : "发送验证码"}
</button>
</div>
</div>
</>
)}
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 text-muted-foreground">
<input type="checkbox" className="size-4 rounded border-border accent-[var(--primary)]" defaultChecked />
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="size-4 rounded border-border accent-[var(--primary)]"
/>
</label>
<button type="button" className="text-primary hover:underline">
@@ -121,15 +248,33 @@ export function LoginPage() {
</button>
</div>
<Button type="submit" className="h-11 w-full">
<ArrowRight className="size-4" />
<Button type="submit" className="h-11 w-full bg-brand-gradient text-base hover:opacity-90">
<ArrowRight className="size-4" />
</Button>
</form>
<div className="mt-6 text-center text-sm text-muted-foreground">
<Link to="/register" className="ml-1 font-medium text-primary hover:underline">
<div className="mt-5 text-center text-sm text-muted-foreground">
{mode === "employee" ? (
<span className="ml-1 text-muted-foreground/70"> HR </span>
) : (
<Link to="/register" className="ml-1 font-medium text-primary hover:underline">
</Link>
)}
</div>
{/* SRM 温馨提示 */}
{mode === "vendor" && (
<div className="mt-5 flex items-start gap-2 rounded-lg border border-[#ffcc80] bg-[#fff3e0] px-3 py-2.5 text-xs text-[#e65100]">
<Info className="mt-0.5 size-4 shrink-0" />
<span> SRM 使 SRM </span>
</div>
)}
<div className="mt-6 text-center">
<Link to="/portal" className="text-xs text-muted-foreground hover:text-primary">
</Link>
</div>
</div>