Files
STP/front-end/src/app/pages/LoginPage.tsx
T
vame 41fc6e86dd feat(frontend): 对齐万华门户风格并更新全量截图文档
- 新增门户导航子页面(交易信息、政策法规、操作指南、常见问题、采购平台)并统一门户壳层样式
- 按万华站点风格重构首页与登录页(导航、轮播、公告、登录区)
- 优化小滨助手:首页/登录页接入、可拖拽吸边、圆形图标、圆形外框与切换动画优化
- 重新生成全量页面截图并更新《滨化智慧招标平台-前端界面截图汇总.docx》

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:35:20 +08:00

318 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useNavigate, Link } from "react-router";
import { Hexagon, User, Lock, Smartphone, ShieldCheck, Eye, EyeOff } from "lucide-react";
import { toast } from "sonner";
import { Button } from "../components/ui/button";
import { cn } from "../components/ui/utils";
import { XiaobinAssistant } from "../components/XiaobinAssistant";
type Mode = "employee" | "vendor";
type Tab = "account" | "mobile";
type LoginRole = "purchaser" | "supplier" | "expert" | "other";
const ROLE_BUTTONS: { id: LoginRole; label: string; en: string }[] = [
{ id: "purchaser", label: "采购人", en: "Purchaser" },
{ id: "supplier", label: "供应商", en: "Supplier" },
{ id: "expert", label: "专家", en: "Expert" },
{ id: "other", label: "其他", en: "Other" },
];
export function LoginPage() {
const navigate = useNavigate();
const [mode, setMode] = useState<Mode>("vendor");
const [tab, setTab] = useState<Tab>("account");
const [loginRole, setLoginRole] = useState<LoginRole>("supplier");
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");
}
function selectRole(role: LoginRole) {
setLoginRole(role);
// 与现有登录逻辑联动:供应商走外部账号,其他角色默认走内部员工
if (role === "supplier") {
setMode("vendor");
setTab("account");
} else {
setMode("employee");
setTab("account");
}
}
return (
<div className="min-h-screen bg-[#f3f5f8]">
<header className="border-b bg-[#0f4ea6] text-white">
<div className="mx-auto flex h-14 max-w-[1200px] items-center justify-between px-6">
<Link to="/portal" className="flex items-center gap-2">
<Hexagon className="size-5" />
<span className="text-base font-semibold"></span>
</Link>
<div className="text-xs text-white/80"> STP </div>
</div>
</header>
<main className="mx-auto grid max-w-[1200px] grid-cols-12 gap-4 px-6 py-8">
<section className="col-span-12 border bg-white p-5 lg:col-span-4">
<h2 className="text-lg font-semibold"></h2>
<p className="mt-1 text-xs text-muted-foreground">
<Link to="/register" className="ml-1 text-primary hover:underline"></Link>
</p>
{/* 角色登录按钮:对齐万华站点风格 */}
<div className="mt-4 grid grid-cols-2 gap-2">
{ROLE_BUTTONS.map((r) => (
<button
key={r.id}
type="button"
onClick={() => selectRole(r.id)}
className={cn(
"border px-3 py-3 text-center transition-colors",
loginRole === r.id
? "border-primary bg-[#eef5ff] text-primary"
: "border-[#d9dee7] bg-white text-[#374151] hover:border-primary/60 hover:bg-[#f8fbff]",
)}
>
<div className="text-sm font-medium leading-none">{r.label}</div>
<div className="mt-1 text-[11px] text-[#6b7280]">{r.en}</div>
</button>
))}
</div>
<div className="mt-4 flex border bg-[#f8fafc] 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 py-2 text-sm",
mode === m.id ? "bg-primary font-medium text-white" : "text-muted-foreground",
)}
>
{m.label}
</button>
))}
</div>
<p className="mt-1 text-[11px] text-muted-foreground">
{ROLE_BUTTONS.find((x) => x.id === loginRole)?.label}
</p>
<div className="mt-3 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 px-4 py-2 text-sm",
tab === t.id ? "font-medium text-primary" : "text-muted-foreground",
)}
>
{t.label}
{tab === t.id && <span className="absolute inset-x-1 bottom-0 h-0.5 bg-primary" />}
</button>
))}
</div>
<form className="mt-4 space-y-3" 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"
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">
</button>
</div>
<Button type="submit" className="h-10 w-full text-base">
</Button>
</form>
<div className="mt-4 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>
<div className="mt-4 border bg-[#fffbea] px-3 py-2 text-xs text-[#92400e]">
SRM 使 SRM
</div>
<div className="mt-4 text-center">
<Link to="/portal" className="text-xs text-muted-foreground hover:text-primary">
</Link>
</div>
</section>
<section className="col-span-12 space-y-4 lg:col-span-8">
<div className="border bg-white">
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold"></div>
<ul className="space-y-1 p-4 text-sm">
{[
["[公开招标] 扣电测试柜框架", "2026-07-13"],
["[公开招标] 2026年橡胶密封件框架招标O型圈", "2026-07-13"],
["[公开招标] 万华莱州100MW分散式风电项目EPC工程总承包[变更公告]", "2026-07-13"],
["[公开招标] 万华旋塞阀框架包一 金属旋塞阀、包二 衬氟旋塞阀", "2026-07-10"],
["[公开招标] 改性TPU熟化料仓", "2026-07-09"],
].map(([title, date]) => (
<li key={title} className="flex items-center gap-3 border-b border-dashed py-1.5">
<span className="line-clamp-1 flex-1 hover:text-primary">{title}</span>
<span className="text-xs text-muted-foreground">{date}</span>
</li>
))}
</ul>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="border bg-white">
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold"></div>
<div className="space-y-1 p-4 text-sm text-muted-foreground">
<p></p>
<p></p>
<p></p>
</div>
</div>
<div className="border bg-white">
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold"></div>
<div className="space-y-1 p-4 text-sm text-muted-foreground">
<p></p>
<p></p>
<p>使</p>
</div>
</div>
</div>
</section>
</main>
<XiaobinAssistant pageType="login" />
</div>
);
}