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:
+15
@@ -0,0 +1,15 @@
|
||||
# 依赖
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
**/dist/
|
||||
.vite/
|
||||
**/.vite/
|
||||
|
||||
# 系统 / 编辑器
|
||||
.DS_Store
|
||||
*.log
|
||||
.idea/
|
||||
.vscode/
|
||||
Generated
-16
@@ -5667,22 +5667,6 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RoleProvider } from "./role-context";
|
||||
import { AppLayout } from "./components/layout/AppLayout";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { RegisterPage } from "./pages/RegisterPage";
|
||||
import { PortalHome } from "./pages/PortalHome";
|
||||
import { Workbench } from "./pages/Workbench";
|
||||
import { Delegation } from "./pages/Delegation";
|
||||
import { ProjectList } from "./pages/ProjectList";
|
||||
@@ -39,6 +40,7 @@ export default function App() {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginPage />} />
|
||||
<Route path="/portal" element={<PortalHome />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/app" element={<AppLayout />}>
|
||||
<Route index element={<Workbench />} />
|
||||
|
||||
@@ -1,20 +1,63 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Sparkles, X, Send, Bot, Lock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { X, Send, Minus, Lock, ArrowRight } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "./ui/utils";
|
||||
import { useRole } from "../role-context";
|
||||
import type { RoleId } from "../roles";
|
||||
|
||||
interface Msg {
|
||||
role: "bot" | "user";
|
||||
text: string;
|
||||
}
|
||||
|
||||
const quickReplies = [
|
||||
"如何发起招标委托?",
|
||||
"邀请招标和公开招标有什么区别?",
|
||||
"在线评标怎么打分?",
|
||||
"供应商如何入库?",
|
||||
];
|
||||
type ProactiveType = "urgent" | "ai" | "todo" | "risk" | "suggestion";
|
||||
|
||||
interface Proactive {
|
||||
type: ProactiveType;
|
||||
title: string;
|
||||
content: string;
|
||||
to?: string;
|
||||
actionLabel?: string;
|
||||
}
|
||||
|
||||
const TYPE_STYLE: Record<ProactiveType, { bg: string; border: string; title: string; badge: string }> = {
|
||||
urgent: { bg: "bg-[#fff3e0]", border: "border-[#ffcc80]", title: "text-[#e65100]", badge: "🔥" },
|
||||
ai: { bg: "bg-[#e8f5e9]", border: "border-[#a5d6a7]", title: "text-[#2e7d32]", badge: "✅" },
|
||||
todo: { bg: "bg-[#e3f2fd]", border: "border-[#90caf9]", title: "text-[#0d47a1]", badge: "📋" },
|
||||
risk: { bg: "bg-[#ffebee]", border: "border-[#ef9a9a]", title: "text-[#c62828]", badge: "⚠️" },
|
||||
suggestion: { bg: "bg-[#f5f5f5]", border: "border-[#e0e0e0]", title: "text-[#455a64]", badge: "💡" },
|
||||
};
|
||||
|
||||
// 各角色的主动提醒(最多 3~4 条,按优先级)
|
||||
const PROACTIVE: Partial<Record<RoleId, Proactive[]>> = {
|
||||
specialist: [
|
||||
{ type: "urgent", title: "BID-2026-005 今日需开标", content: "“2026年度防腐保温工程框架”已到开标时间,请立即组织评标", to: "/app/projects", actionLabel: "去处理" },
|
||||
{ type: "ai", title: "AI 已完成:寻源报告生成", content: "“年产5万吨聚丙烯项目”已自动寻源,匹配到 12 家潜在供应商", to: "/app/projects", actionLabel: "查看" },
|
||||
{ type: "todo", title: "您有 3 个委托单待审核", content: "来自技术部、采购部的委托申请已提交,请及时审核确认", to: "/app/delegation", actionLabel: "去审核" },
|
||||
],
|
||||
supplier: [
|
||||
{ type: "urgent", title: "报名即将截止", content: "“年产5万吨聚丙烯项目”报名将于 7月15日截止,请尽快完成报名", to: "/app/bidding", actionLabel: "去报名" },
|
||||
{ type: "todo", title: "2 笔保证金待缴纳", content: "有 2 个项目需缴纳投标保证金,逾期将影响投标资格", to: "/app/bidding", actionLabel: "去缴纳" },
|
||||
{ type: "ai", title: "开标结果已公布", content: "“数字化交付平台”项目已完成开标,点击查看评标结果", to: "/app/bidding", actionLabel: "查看" },
|
||||
],
|
||||
expert: [
|
||||
{ type: "urgent", title: "您收到催化剂采购项目的评标邀请", content: "请查看详情,接受邀请或申请回避(剩余不足 24 小时)", to: "/app/expert-tasks", actionLabel: "去查看" },
|
||||
{ type: "todo", title: "《评标纪律承诺书》待签署", content: "进入评审前需先签署当前项目的评标纪律承诺书", to: "/app/expert-tasks", actionLabel: "去签署" },
|
||||
{ type: "suggestion", title: "3 个评审项目待完成", content: "建议优先处理即将截止的评审任务,避免影响评审资格", to: "/app/expert-tasks", actionLabel: "查看" },
|
||||
],
|
||||
approver: [
|
||||
{ type: "todo", title: "待审批事项 2 项", content: "有 2 个定标 / 立项审批等待您处理", to: "/app/approvals", actionLabel: "去审批" },
|
||||
],
|
||||
committee: [
|
||||
{ type: "risk", title: "异常决策待处理", content: "检测到 1 个项目触发异常上报,需领导小组决策", to: "/app/exceptions", actionLabel: "去处理" },
|
||||
],
|
||||
auditor: [
|
||||
{ type: "risk", title: "围串标风险提示", content: "AI 检测到 1 组投标文件相似度偏高,建议核查", to: "/app/collusion", actionLabel: "去核查" },
|
||||
],
|
||||
};
|
||||
|
||||
const quickReplies = ["如何发起招标委托?", "公开招标和邀请招标的区别?", "在线评标怎么打分?", "供应商如何入库?"];
|
||||
|
||||
const knowledge: Record<string, string> = {
|
||||
委托:
|
||||
@@ -26,17 +69,20 @@ const knowledge: Record<string, string> = {
|
||||
入库:
|
||||
"在「供应商管理」页点击「发起入库流程」,或在短名单管理中对供应商点击「发起入库」,系统将自动同步工商风险信息并进入审批。",
|
||||
合并:
|
||||
"您好,当系统中存在同类型未完成立项的委托时,系统会自动推送合并建议。您可以在立项阶段的「合并招标建议」卡片中查看并确认合并。需要我为您演示具体步骤吗?",
|
||||
"当系统中存在同类型未完成立项的委托时,系统会自动推送合并建议。您可以在立项阶段的「合并招标建议」卡片中查看并确认合并。",
|
||||
上传:
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,只有您本人可查看和修改,投标截止前还可撤回重传。",
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,投标截止前还可撤回重传。",
|
||||
};
|
||||
|
||||
const dataKeywords = ["报价", "投标价", "多少钱", "金额", "中标", "得分", "分数", "名单", "谁"];
|
||||
|
||||
export function XiaobinAssistant() {
|
||||
const { sensitive } = useRole();
|
||||
const { sensitive, role, roleId } = useRole();
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [proactive, setProactive] = useState<Proactive[]>(PROACTIVE[roleId] ?? []);
|
||||
const [msgs, setMsgs] = useState<Msg[]>([
|
||||
{
|
||||
role: "bot",
|
||||
@@ -45,14 +91,21 @@ export function XiaobinAssistant() {
|
||||
]);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 切换角色时刷新主动提醒
|
||||
useEffect(() => {
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, open]);
|
||||
setProactive(PROACTIVE[roleId] ?? []);
|
||||
}, [roleId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showChat) endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, showChat]);
|
||||
|
||||
const gradient = `linear-gradient(135deg, ${role.bannerFrom}, ${role.bannerTo})`;
|
||||
const unread = proactive.length;
|
||||
|
||||
function answer(q: string) {
|
||||
// 流程敏感期:拒答数据类问题
|
||||
if (sensitive && dataKeywords.some((k) => q.includes(k))) {
|
||||
return "当前为教学模式(流程敏感期),数据类问题暂无法回答。暂未到查看时间,请等待系统通知。我可以继续为您讲解系统操作哦~";
|
||||
return "当前为流程敏感期(如开标前),数据类问题暂无法回答。请等待系统通知。我可以继续为您讲解系统操作哦~";
|
||||
}
|
||||
const key = Object.keys(knowledge).find((k) => q.includes(k));
|
||||
return key
|
||||
@@ -63,80 +116,141 @@ export function XiaobinAssistant() {
|
||||
function send(text: string) {
|
||||
const q = text.trim();
|
||||
if (!q) return;
|
||||
setShowChat(true);
|
||||
setMsgs((m) => [...m, { role: "user", text: q }]);
|
||||
setInput("");
|
||||
setTimeout(() => {
|
||||
setMsgs((m) => [...m, { role: "bot", text: answer(q) }]);
|
||||
}, 500);
|
||||
setTimeout(() => setMsgs((m) => [...m, { role: "bot", text: answer(q) }]), 500);
|
||||
}
|
||||
|
||||
function handleAction(p: Proactive) {
|
||||
if (p.to) navigate(p.to);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 悬浮球 + 呼吸光环 */}
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full bg-gradient-to-r from-violet-500 to-blue-600 px-4 py-3 text-white shadow-lg shadow-blue-500/30 transition-transform hover:scale-105"
|
||||
className="group fixed bottom-8 right-8 z-50 flex size-[68px] items-center justify-center rounded-full text-white shadow-xl transition-transform hover:scale-105"
|
||||
style={{ backgroundImage: gradient }}
|
||||
aria-label="打开小滨 AI 助手"
|
||||
>
|
||||
<Sparkles className="size-5" />
|
||||
<span className="text-sm font-medium">小滨助手</span>
|
||||
<span className="pointer-events-none absolute inset-0 rounded-full animate-breathe" style={{ backgroundImage: gradient }} />
|
||||
<span className="relative flex flex-col items-center leading-none">
|
||||
<span className="text-sm font-bold">小滨</span>
|
||||
<span className="mt-0.5 text-[9px] text-white/90">AI 助手</span>
|
||||
</span>
|
||||
{unread > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-[#f44336] text-[10px] font-bold text-white">
|
||||
{unread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex h-[540px] w-[380px] flex-col overflow-hidden rounded-2xl border bg-card shadow-2xl">
|
||||
<div className="flex items-center justify-between bg-gradient-to-r from-violet-500 to-blue-600 px-4 py-3 text-white">
|
||||
<div className="fixed bottom-8 right-8 z-50 flex h-[520px] w-[360px] flex-col overflow-hidden rounded-2xl border bg-card shadow-2xl">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 text-white" style={{ backgroundImage: gradient }}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-white/20">
|
||||
<Bot className="size-5" />
|
||||
</div>
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-white/20 text-sm font-bold">滨</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">智能助手 · 小滨</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/80">
|
||||
<span className="size-1.5 rounded-full bg-emerald-300" /> 在线 · 操作教学 & 业务咨询
|
||||
<div className="text-sm font-semibold">小滨 AI 助手</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/85">
|
||||
<span className="size-1.5 rounded-full bg-emerald-300" /> 在线 · 已为您分析今日待办
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setOpen(false)} className="rounded-md p-1 hover:bg-white/15">
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setOpen(false)} className="rounded-md p-1 hover:bg-white/15" title="最小化">
|
||||
<Minus className="size-4" />
|
||||
</button>
|
||||
<button onClick={() => setOpen(false)} className="rounded-md p-1 hover:bg-white/15" title="关闭">
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sensitive && (
|
||||
<div className="flex items-center gap-1.5 border-b bg-amber-50 px-4 py-2 text-xs text-amber-700">
|
||||
<Lock className="size-3.5" /> 当前为教学模式,数据类问题暂无法回答
|
||||
<Lock className="size-3.5" /> 当前为流程敏感期,数据类问题暂无法回答
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 space-y-3 overflow-y-auto bg-muted/30 p-4">
|
||||
{msgs.map((m, i) => (
|
||||
<div key={i} className={cn("flex", m.role === "user" ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[80%] rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
|
||||
m.role === "user"
|
||||
? "rounded-br-sm bg-primary text-primary-foreground"
|
||||
: "rounded-bl-sm border bg-card text-card-foreground",
|
||||
)}
|
||||
>
|
||||
{m.text}
|
||||
{!showChat ? (
|
||||
<>
|
||||
{proactive.length === 0 && (
|
||||
<div className="rounded-xl border bg-card p-4 text-center text-sm text-muted-foreground">
|
||||
🎉 暂无待办,继续保持!
|
||||
</div>
|
||||
)}
|
||||
{proactive.map((p, i) => {
|
||||
const st = TYPE_STYLE[p.type];
|
||||
return (
|
||||
<div key={i} className={cn("animate-slide-up rounded-xl border p-3", st.bg, st.border)}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className={cn("text-[13px] font-bold", st.title)}>
|
||||
{st.badge} {p.title}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setProactive((ps) => ps.filter((_, idx) => idx !== i))}
|
||||
className="text-muted-foreground/60 hover:text-muted-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-[#4a5568]">{p.content}</p>
|
||||
{p.to && (
|
||||
<button
|
||||
onClick={() => handleAction(p)}
|
||||
className={cn("mt-2 inline-flex items-center gap-0.5 text-xs font-bold underline", st.title)}
|
||||
>
|
||||
{p.actionLabel ?? "去处理"} <ArrowRight className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="pt-1">
|
||||
<p className="mb-2 text-xs text-muted-foreground">猜你想问:</p>
|
||||
<div className="space-y-2">
|
||||
{quickReplies.map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => send(q)}
|
||||
className="block w-full rounded-lg border bg-card px-3 py-2 text-left text-xs text-foreground transition-colors hover:border-primary hover:text-primary"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{msgs.length <= 1 && (
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="text-xs text-muted-foreground">猜你想问:</p>
|
||||
{quickReplies.map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => send(q)}
|
||||
className="block w-full rounded-lg border bg-card px-3 py-2 text-left text-xs text-foreground transition-colors hover:border-primary hover:text-primary"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => setShowChat(false)} className="text-xs text-primary hover:underline">
|
||||
← 返回待办提醒
|
||||
</button>
|
||||
{msgs.map((m, i) => (
|
||||
<div key={i} className={cn("flex", m.role === "user" ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[80%] rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
|
||||
m.role === "user"
|
||||
? "rounded-br-sm bg-primary text-primary-foreground"
|
||||
: "rounded-bl-sm border bg-card text-card-foreground",
|
||||
)}
|
||||
>
|
||||
{m.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div ref={endRef} />
|
||||
</>
|
||||
)}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-t bg-card p-3">
|
||||
@@ -145,9 +259,9 @@ export function XiaobinAssistant() {
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send(input)}
|
||||
placeholder="输入您的问题…"
|
||||
className="h-9 flex-1 rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
className="h-9 flex-1 rounded-full border bg-input-background px-3.5 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Button size="icon" onClick={() => send(input)} className="size-9 shrink-0">
|
||||
<Button size="icon" onClick={() => send(input)} className="size-9 shrink-0 rounded-full" style={{ backgroundImage: gradient }}>
|
||||
<Send className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function AiBadge({ label = "AI 生成", className }: { label?: string; cl
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md bg-gradient-to-r from-violet-500 to-blue-500 px-2 py-0.5 text-xs font-medium text-white",
|
||||
"inline-flex items-center gap-1 rounded-md bg-brand-gradient px-2 py-0.5 text-xs font-medium text-white",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -96,7 +96,7 @@ function ApprovalDetail({ item, onBack }: { item: ApprovalItem; onBack: () => vo
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="col-span-2 space-y-6">
|
||||
{/* AI 审批摘要 */}
|
||||
<Card className="gap-0 border-violet-200 bg-gradient-to-br from-violet-50 to-blue-50 p-6">
|
||||
<Card className="gap-0 border-primary/20 bg-secondary/50 p-6">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Sparkles className="size-4 text-violet-500" />
|
||||
<h3 className="text-foreground">AI 审批摘要</h3>
|
||||
|
||||
@@ -165,7 +165,7 @@ export function Delegation() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 bg-gradient-to-br from-violet-50 to-blue-50 p-6">
|
||||
<Card className="gap-0 border-primary/20 bg-secondary/50 p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4 text-violet-500" />
|
||||
<h4 className="text-foreground">AI 解析提示</h4>
|
||||
|
||||
@@ -106,7 +106,7 @@ function Detail({ item, onBack }: { item: Exception; onBack: () => void }) {
|
||||
<Card className="gap-0 p-6">
|
||||
<h3 className="mb-2 text-foreground">问题描述</h3>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{item.desc}</p>
|
||||
<div className="mt-4 rounded-lg border border-violet-200 bg-gradient-to-br from-violet-50 to-blue-50 p-4">
|
||||
<div className="mt-4 rounded-lg border border-primary/20 bg-secondary/50 p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-4 text-violet-500" /> AI 分析摘要
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function MergeBidding() {
|
||||
<PageHeader title="合并招标" description="AI 检测同类型未完成立项的委托,给出合并建议与效益估算" />
|
||||
|
||||
{/* 提示横幅 */}
|
||||
<div className="mb-6 flex items-center gap-3 rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-blue-50 p-4">
|
||||
<div className="mb-6 flex items-center gap-3 rounded-xl border border-primary/20 bg-secondary/50 p-4">
|
||||
<Sparkles className="size-5 text-violet-500" />
|
||||
<p className="text-sm text-foreground">
|
||||
系统检测到 <span className="font-semibold">{mergeCandidates.length}</span> 个可合并的招标委托(采购品类均为「通用设备」),合并后可降低成本、缩短周期。
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export function ProjectDetail() {
|
||||
{ t: "推荐评标办法", d: "综合评估法:技术 60 分 / 商务 20 分 / 价格 20 分,技术权重较高。" },
|
||||
{ t: "推荐供应商短名单", d: "基于合格供方库与历史业绩,推荐浙江中控、和利时、西门子等 3 家。" },
|
||||
].map((c) => (
|
||||
<div key={c.t} className="rounded-xl border bg-gradient-to-br from-violet-50/60 to-blue-50/60 p-4">
|
||||
<div key={c.t} className="rounded-xl border border-primary/15 bg-secondary/40 p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-3.5 text-violet-500" /> {c.t}
|
||||
</div>
|
||||
|
||||
@@ -1,81 +1,591 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate, Link } from "react-router";
|
||||
import { Hexagon, ArrowLeft, CheckCircle2 } from "lucide-react";
|
||||
import {
|
||||
Hexagon,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
UploadCloud,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
XCircle,
|
||||
Building2,
|
||||
UserRound,
|
||||
ShieldCheck,
|
||||
X,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { cn } from "../components/ui/utils";
|
||||
|
||||
const fields = [
|
||||
{ label: "企业全称", placeholder: "请输入营业执照上的企业全称", col: 2 },
|
||||
{ label: "统一社会信用代码", placeholder: "18 位信用代码" },
|
||||
{ label: "企业类型", placeholder: "如:有限责任公司" },
|
||||
{ label: "法定代表人", placeholder: "请输入法人姓名" },
|
||||
{ label: "注册资本(万元)", placeholder: "请输入注册资本" },
|
||||
{ label: "联系人", placeholder: "请输入联系人姓名" },
|
||||
{ label: "联系电话", placeholder: "请输入手机号码" },
|
||||
{ label: "电子邮箱", placeholder: "用于接收平台通知" },
|
||||
{ label: "经营类别", placeholder: "如:自动化控制 / 环保工程" },
|
||||
{ label: "企业地址", placeholder: "请输入详细注册地址", col: 2 },
|
||||
type Registrant = "supplier" | "expert";
|
||||
type AuditState = "idle" | "auditing" | "success" | "rejected";
|
||||
|
||||
const SUPPLIER_STEPS = ["企业信息", "联系人", "提交 / 自动审核"];
|
||||
const EXPERT_STEPS = ["基本信息", "专业资质 / 收款", "提交审核"];
|
||||
|
||||
interface PwdRule {
|
||||
label: string;
|
||||
test: (v: string) => boolean;
|
||||
}
|
||||
|
||||
const PWD_RULES: PwdRule[] = [
|
||||
{ label: "长度 8~20 位", test: (v) => v.length >= 8 && v.length <= 20 },
|
||||
{ label: "包含大写字母(A-Z)与小写字母(a-z)", test: (v) => /[A-Z]/.test(v) && /[a-z]/.test(v) },
|
||||
{ label: "包含数字(0-9)与特殊字符(!@#$%^&*)", test: (v) => /\d/.test(v) && /[!@#$%^&*()_+\-=]/.test(v) },
|
||||
{ label: "禁止连续/重复字符(如 12345678、aaaa)", test: (v) => !/(.)\1{3,}/.test(v) && !/0123|1234|2345|3456|4567|5678|6789/.test(v) },
|
||||
{ label: "非常见弱口令(password、admin123 等)", test: (v) => !["password", "admin123", "12345678", "qwerty"].includes(v.toLowerCase()) },
|
||||
];
|
||||
|
||||
function Field({
|
||||
label,
|
||||
placeholder,
|
||||
required,
|
||||
hint,
|
||||
className,
|
||||
type = "text",
|
||||
readOnly,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
className?: string;
|
||||
type?: string;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
{label} {required && <span className="text-[#e91e63]">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type={type}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
"h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary",
|
||||
readOnly && "bg-muted text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepBar({ steps, current }: { steps: string[]; current: number }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-full border bg-white px-6 py-4 shadow-sm">
|
||||
{steps.map((s, i) => (
|
||||
<div key={s} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-9 items-center justify-center rounded-full text-sm font-bold transition-colors",
|
||||
i <= current ? "bg-brand-gradient text-white" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{i < current ? <CheckCircle2 className="size-5" /> : i + 1}
|
||||
</div>
|
||||
<span className={cn("mt-2 text-xs", i <= current ? "font-medium text-primary" : "text-muted-foreground")}>{s}</span>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div className={cn("mx-3 mb-6 h-0.5 w-16 rounded-full sm:w-24", i < current ? "bg-brand-gradient" : "bg-border")} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Notice({ items }: { items: string[] }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#ffcc80] bg-[#fff3e0] p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold text-[#e65100]">
|
||||
<Info className="size-4" /> 注册须知
|
||||
</div>
|
||||
<ol className="mt-2 space-y-1.5 text-xs leading-relaxed text-[#bf360c]">
|
||||
{items.map((t, i) => (
|
||||
<li key={i}>
|
||||
{i + 1}. {t}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
const [registrant, setRegistrant] = useState<Registrant>("supplier");
|
||||
const [step, setStep] = useState(0);
|
||||
const [ocr, setOcr] = useState<"none" | "scanning" | "done">("none");
|
||||
const [audit, setAudit] = useState<AuditState>("idle");
|
||||
const [products, setProducts] = useState<string[]>(["工业阀门", "管道配件"]);
|
||||
const [productInput, setProductInput] = useState("");
|
||||
const [pwd, setPwd] = useState("");
|
||||
const [confirmPwd, setConfirmPwd] = useState("");
|
||||
const [showPwd, setShowPwd] = useState(false);
|
||||
const [smsCountdown, setSmsCountdown] = useState(0);
|
||||
|
||||
const steps = registrant === "supplier" ? SUPPLIER_STEPS : EXPERT_STEPS;
|
||||
|
||||
const passedRules = useMemo(() => PWD_RULES.map((r) => r.test(pwd)), [pwd]);
|
||||
const passedCount = passedRules.filter(Boolean).length;
|
||||
const strength: 0 | 1 | 2 = passedCount >= 5 && pwd.length >= 12 ? 2 : passedCount >= 4 ? 1 : 0;
|
||||
|
||||
function switchRegistrant(r: Registrant) {
|
||||
setRegistrant(r);
|
||||
setStep(0);
|
||||
setAudit("idle");
|
||||
setOcr("none");
|
||||
}
|
||||
|
||||
function triggerOcr() {
|
||||
setOcr("scanning");
|
||||
setTimeout(() => {
|
||||
setOcr("done");
|
||||
toast.success("营业执照识别完成,已自动回填企业信息");
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function addProduct() {
|
||||
const v = productInput.trim();
|
||||
if (v && products.length < 20 && !products.includes(v)) {
|
||||
setProducts((p) => [...p, v]);
|
||||
}
|
||||
setProductInput("");
|
||||
}
|
||||
|
||||
function sendSms() {
|
||||
toast.success("验证码已发送");
|
||||
setSmsCountdown(60);
|
||||
const t = setInterval(() => {
|
||||
setSmsCountdown((c) => {
|
||||
if (c <= 1) {
|
||||
clearInterval(t);
|
||||
return 0;
|
||||
}
|
||||
return c - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function submitAudit() {
|
||||
setStep(steps.length - 1);
|
||||
setAudit("auditing");
|
||||
setTimeout(() => {
|
||||
setAudit("success");
|
||||
}, 3800);
|
||||
}
|
||||
|
||||
const isLastFormStep = step === steps.length - 2;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<div className="min-h-screen bg-[#f5f7fa]">
|
||||
<header className="flex h-16 items-center justify-between border-b bg-card px-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hexagon className="size-6 text-primary" />
|
||||
<span className="font-semibold">滨化集团智慧招标平台 · 供应商注册</span>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-8 items-center justify-center rounded-lg bg-brand-gradient">
|
||||
<Hexagon className="size-5 text-white" />
|
||||
</div>
|
||||
<span className="font-semibold">滨化集团智慧招标平台 · 新用户注册</span>
|
||||
</div>
|
||||
<Link to="/" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary">
|
||||
<ArrowLeft className="size-4" /> 返回登录
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto max-w-3xl px-6 py-10">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-foreground">供应商入驻注册</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
完善企业信息提交后,平台将自动同步工商风险信息并进入资质审核,预计 1-3 个工作日完成。
|
||||
</p>
|
||||
<div className="mx-auto max-w-5xl px-6 py-8">
|
||||
{/* 注册对象选择 */}
|
||||
<div className="mb-6 flex justify-center gap-3">
|
||||
{(
|
||||
[
|
||||
{ id: "supplier" as Registrant, label: "供应商注册", icon: Building2 },
|
||||
{ id: "expert" as Registrant, label: "外部专家注册", icon: UserRound },
|
||||
]
|
||||
).map((r) => {
|
||||
const Icon = r.icon;
|
||||
return (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => switchRegistrant(r.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full border px-5 py-2.5 text-sm font-medium transition-all",
|
||||
registrant === r.id
|
||||
? "border-transparent bg-brand-gradient text-white shadow-sm"
|
||||
: "border-border bg-white text-muted-foreground hover:border-primary hover:text-primary",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" /> {r.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-8">
|
||||
<h3 className="mb-5 text-foreground">企业基本信息</h3>
|
||||
<form
|
||||
className="grid grid-cols-2 gap-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.success("注册申请已提交,平台正在进行资质审核");
|
||||
setTimeout(() => navigate("/"), 1200);
|
||||
}}
|
||||
>
|
||||
{fields.map((f) => (
|
||||
<div key={f.label} className={f.col === 2 ? "col-span-2" : ""}>
|
||||
<label className="mb-1.5 block text-sm">{f.label}</label>
|
||||
<input
|
||||
className="h-10 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
placeholder={f.placeholder}
|
||||
<div className="mb-6 flex items-center justify-between gap-4">
|
||||
<StepBar steps={steps} current={step} />
|
||||
<div className="hidden shrink-0 items-center gap-1.5 rounded-full border border-[#4caf50] bg-[#e8f5e9] px-3 py-1.5 text-xs text-[#2e7d32] lg:flex">
|
||||
<ShieldCheck className="size-3.5" /> AI 即时审核 · 秒级通过
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ============ 供应商注册 ============ */}
|
||||
{registrant === "supplier" && (
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_300px]">
|
||||
<div className="rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">供应商注册</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">请填写企业真实信息,上传营业执照,系统将自动识别并回填</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
|
||||
{/* 营业执照上传 */}
|
||||
<div
|
||||
onClick={triggerOcr}
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary bg-[#fafbfc] py-6 transition-colors hover:bg-secondary/40"
|
||||
>
|
||||
<UploadCloud className="mb-2 size-7 text-primary" />
|
||||
<div className="text-sm font-medium text-primary">上传营业执照(JPG/PNG/PDF,≤10MB)</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">支持拖拽或点击上传,AI 将自动识别企业信息并填充表单</div>
|
||||
</div>
|
||||
|
||||
{ocr === "scanning" && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg bg-[#fff8e1] px-3 py-2 text-sm text-[#e65100]">
|
||||
<Loader2 className="size-4 animate-spin" /> 正在识别营业执照...
|
||||
</div>
|
||||
)}
|
||||
{ocr === "done" && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-[#a5d6a7] bg-[#e8f5e9] px-3 py-2 text-sm text-[#2e7d32]">
|
||||
<CheckCircle2 className="size-4" /> 已识别:营业执照信息提取成功(统一信用代码:91370100XXXXXXXXXX)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<Field label="企业全称" required placeholder="系统自动识别或手动输入" className="col-span-2" />
|
||||
<Field label="统一社会信用代码" required placeholder="18 位代码" />
|
||||
<Field label="注册资本(万元)" required placeholder="请输入数字" />
|
||||
<Field label="法定代表人" required placeholder="请输入法定代表人姓名" />
|
||||
<Field label="行业分类" required placeholder="请选择或搜索行业(GB/T 4754)" />
|
||||
<Field label="注册地址" required placeholder="请填写详细地址" className="col-span-2" />
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
经营范围 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="系统自动识别或手动输入,不超过 500 字"
|
||||
className="w-full resize-none rounded-lg border bg-input-background px-3 py-2 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
主要产品 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={productInput}
|
||||
onChange={(e) => setProductInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addProduct())}
|
||||
placeholder="输入产品名称,回车添加"
|
||||
className="h-11 flex-1 rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<Button type="button" variant="outline" className="h-11" onClick={addProduct}>
|
||||
+ 添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{products.map((p) => (
|
||||
<span
|
||||
key={p}
|
||||
className="flex items-center gap-1 rounded-full border border-primary bg-secondary px-3 py-1 text-xs text-primary"
|
||||
>
|
||||
{p}
|
||||
<button onClick={() => setProducts((ps) => ps.filter((x) => x !== p))}>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Notice
|
||||
items={[
|
||||
"请确保信息与营业执照一致",
|
||||
"统一社会信用代码将作为唯一标识",
|
||||
"提交后由 AI 即时自动审核",
|
||||
"审核通过后可参与投标报名",
|
||||
"已有 SRM 账号请直接登录",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm">营业执照 / 资质文件</label>
|
||||
<div className="flex h-28 cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30 text-sm text-muted-foreground hover:border-primary">
|
||||
<CheckCircle2 className="mb-2 size-6 text-muted-foreground/60" />
|
||||
点击上传或拖拽文件至此(支持 PDF / JPG / PNG)
|
||||
{step === 1 && (
|
||||
<div className="mx-auto max-w-2xl rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">联系人信息</h3>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="space-y-4">
|
||||
<Field label="联系人姓名" required placeholder="请输入联系人姓名" hint="必填,最大 64 字符" />
|
||||
<Field label="联系电话(手机号)" required placeholder="请输入 11 位手机号" hint="必填,格式校验,全局唯一" />
|
||||
<Field label="电子邮箱" required placeholder="请输入常用邮箱" hint="必填,格式校验,全局唯一" />
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
短信验证码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
placeholder="点击发送验证码"
|
||||
className="h-11 flex-1 rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={sendSms}
|
||||
disabled={smsCountdown > 0}
|
||||
className="h-11 w-32 shrink-0 rounded-lg bg-brand-gradient text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{smsCountdown > 0 ? `${smsCountdown}s 后重发` : "发送验证码"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">用于验证手机号真实性(注册必验)</p>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
输入密码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPwd ? "text" : "password"}
|
||||
value={pwd}
|
||||
onChange={(e) => setPwd(e.target.value)}
|
||||
placeholder="请设置登录密码"
|
||||
className="h-11 w-full rounded-lg border bg-input-background px-3 pr-10 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPwd((s) => !s)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showPwd ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
确认密码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type={showPwd ? "text" : "password"}
|
||||
value={confirmPwd}
|
||||
onChange={(e) => setConfirmPwd(e.target.value)}
|
||||
placeholder="请再次输入密码"
|
||||
className={cn(
|
||||
"h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary",
|
||||
confirmPwd && confirmPwd !== pwd && "border-destructive",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 密码复杂度要求 */}
|
||||
<div className="rounded-xl border bg-[#f8fafc] p-4">
|
||||
<div className="text-sm font-medium text-[#374151]">密码复杂度要求</div>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{PWD_RULES.map((r, i) => (
|
||||
<li key={r.label} className="flex items-center gap-2 text-[13px]">
|
||||
<span className={passedRules[i] ? "text-[#10b981]" : "text-[#ef4444]"}>{passedRules[i] ? "✔" : "✘"}</span>
|
||||
<span className="text-[#374151]">{r.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* 强度条 */}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground">密码强度</span>
|
||||
<div className="h-2 w-40 overflow-hidden rounded-full bg-[#e5e7eb]">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all",
|
||||
strength === 0 ? "w-1/3 bg-[#ef4444]" : strength === 1 ? "w-2/3 bg-[#f59e0b]" : "w-full bg-[#10b981]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
strength === 0 ? "text-[#ef4444]" : strength === 1 ? "text-[#f59e0b]" : "text-[#10b981]",
|
||||
)}
|
||||
>
|
||||
{strength === 0 ? "弱" : strength === 1 ? "中" : "强"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-span-2 mt-2 flex items-center gap-3">
|
||||
<Button type="submit" className="h-11 flex-1">
|
||||
提交注册申请
|
||||
{/* ============ 外部专家注册 ============ */}
|
||||
{registrant === "expert" && (
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_300px]">
|
||||
<div className="rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">外部专家注册</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">请完整填写以下信息,所有数据将受到严格加密保护</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<Field label="真实姓名" required placeholder="请输入中文姓名" />
|
||||
<Field label="性别" required placeholder="请选择" />
|
||||
<Field label="身份证号码" required placeholder="请输入 18 位身份证号" hint="🔒 将使用 AES-256 加密存储" className="col-span-2" />
|
||||
<Field label="出生日期" placeholder="由身份证自动识别" readOnly />
|
||||
<Field label="年龄" placeholder="自动计算" readOnly />
|
||||
<Field label="手机号码" required placeholder="请输入手机号" />
|
||||
<Field label="电子邮箱" required placeholder="请输入常用邮箱" />
|
||||
<Field label="工作单位" placeholder="请输入单位全称" />
|
||||
<Field label="现从事专业" required placeholder="如:化工工艺、设备管理" />
|
||||
<Field label="技术职称" required placeholder="高级 / 副高级 / 中级" />
|
||||
<Field label="职称专业" placeholder="如:化学工程" />
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
职称 / 资质证书扫描件 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary bg-[#fafbfc] py-5 hover:bg-secondary/40">
|
||||
<UploadCloud className="mb-1.5 size-6 text-primary" />
|
||||
<div className="text-sm text-primary">点击上传或拖拽文件(支持 PDF/JPG/PNG,≤20MB)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Notice
|
||||
items={[
|
||||
"所有信息须与身份证及职称证书一致",
|
||||
"集团公司将对信息进行真实性核验",
|
||||
"入库专家须遵守《评委管理规定》",
|
||||
"至少具备中级及以上职称方可入库",
|
||||
"个人信息变更需及时在线更新",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="mx-auto max-w-2xl rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">专业资质与收款账户</h3>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">擅长领域标签(用于智能匹配)</label>
|
||||
<input
|
||||
placeholder="如:石油化工、反应工程、分离技术,逗号分隔"
|
||||
className="h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Field label="开户银行" placeholder="如:中国银行北京分行" />
|
||||
<Field label="银行账号" placeholder="请输入收款账号" hint="🔒 将使用 AES-256 加密存储" />
|
||||
<label className="col-span-2 mt-2 flex items-start gap-2 rounded-lg border border-[#ffcc80] bg-[#fff3e0] p-3 text-xs text-[#bf360c]">
|
||||
<input type="checkbox" className="mt-0.5 size-4 accent-[var(--primary)]" />
|
||||
我已阅读并同意签署《评标纪律承诺书》,承诺客观公正评审,遵守评委管理规定。
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ============ 提交 / 自动审核态(两种注册对象共用最后一步) ============ */}
|
||||
{step === steps.length - 1 && (
|
||||
<div className="mx-auto max-w-lg">
|
||||
{audit === "auditing" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<Loader2 className="mx-auto size-14 animate-spin text-primary" />
|
||||
<div className="mt-5 text-lg font-bold text-foreground">
|
||||
{registrant === "supplier" ? "AI 正在审核企业信息..." : "已提交,AI 正在初审..."}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">预计 3~8 秒,请勿关闭页面</div>
|
||||
<div className="mt-3 text-[13px] text-muted-foreground/80">✓ 工商信息核验 ✓ 风险扫描 ✓ 唯一性检查</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audit === "success" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-[#e8f5e9]">
|
||||
<CheckCircle2 className="size-14 text-[#4caf50]" />
|
||||
</div>
|
||||
<div className="mt-5 text-2xl font-bold text-foreground">🎉 注册成功!</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{registrant === "supplier" ? "您的供应商账号已激活,正在自动登录..." : "您的专家账号已创建,请留意激活通知短信/邮件。"}
|
||||
</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="space-y-1.5 text-sm text-muted-foreground">
|
||||
<div>
|
||||
{registrant === "supplier" ? "供应商编号" : "专家编号"}:
|
||||
<span className="font-semibold text-foreground">
|
||||
{registrant === "supplier" ? "SUP_20260709_0158" : "EXP_20260709_002"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
登录账号:<span className="font-semibold text-foreground">138****8001</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 rounded-lg border border-[#ffcc80] bg-[#fff3e0] px-4 py-2.5 text-xs text-[#e65100]">
|
||||
📌 温馨提示:参与具体项目投标时,需按招标公告要求提交资质审核材料
|
||||
</div>
|
||||
<Button className="mt-6 h-11 w-full bg-brand-gradient hover:opacity-90" onClick={() => navigate("/app")}>
|
||||
进入工作台 <ArrowRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audit === "rejected" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-[#ffcdd2]">
|
||||
<XCircle className="size-14 text-[#c62828]" />
|
||||
</div>
|
||||
<div className="mt-5 text-2xl font-bold text-[#c62828]">审核未通过</div>
|
||||
<div className="mt-5 rounded-lg border bg-[#fafafa] p-4 text-left text-[13px] text-[#c62828]">
|
||||
<div>• 统一社会信用代码与工商登记信息不匹配</div>
|
||||
<div>• 企业状态为“注销”,不符合注册条件</div>
|
||||
<div className="mt-2 text-[#666]">💡 请核对信息后重新提交,如有疑问请联系客服</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-6 h-11 w-full bg-brand-gradient hover:opacity-90"
|
||||
onClick={() => {
|
||||
setAudit("idle");
|
||||
setStep(0);
|
||||
}}
|
||||
>
|
||||
修改后重新提交
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部导航按钮 */}
|
||||
{audit === "idle" && (
|
||||
<div className="mx-auto mt-6 flex max-w-5xl items-center justify-end gap-3">
|
||||
{step > 0 && (
|
||||
<Button variant="outline" className="h-11 px-6" onClick={() => setStep((s) => s - 1)}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="button" variant="outline" className="h-11" onClick={() => navigate("/")}>
|
||||
取消
|
||||
)}
|
||||
{isLastFormStep ? (
|
||||
<Button className="h-11 bg-brand-gradient px-8 hover:opacity-90" onClick={submitAudit}>
|
||||
提交注册
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<Button className="h-11 bg-brand-gradient px-8 hover:opacity-90" onClick={() => setStep((s) => s + 1)}>
|
||||
下一步 <ArrowRight className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -122,9 +122,9 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
|
||||
if (extra === "specialist") {
|
||||
return (
|
||||
<Card className="gap-0 border-violet-200 bg-gradient-to-br from-violet-50 to-blue-50 p-5">
|
||||
<Card className="gap-0 border-primary/20 bg-secondary/50 p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<GitMerge className="size-4 text-violet-500" />
|
||||
<GitMerge className="size-4 text-primary" />
|
||||
<h4 className="text-foreground">合并招标提醒</h4>
|
||||
<AiBadge label="AI" />
|
||||
</div>
|
||||
@@ -135,7 +135,7 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
{mergeCandidates.map((m) => (
|
||||
<div key={m.id} className="flex items-center justify-between rounded-lg border bg-card px-3 py-2 text-sm">
|
||||
<span className="line-clamp-1 flex-1 text-foreground">{m.name}</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-violet-600">重叠 {m.overlap}%</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-primary">重叠 {m.overlap}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
return (
|
||||
<Card className="gap-0 p-5">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Sparkles className="size-4 text-violet-500" />
|
||||
<Sparkles className="size-4 text-primary" />
|
||||
<h4 className="text-foreground">AI 项目推荐</h4>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
@@ -164,7 +164,7 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="line-clamp-1 flex-1 text-sm text-foreground">{p.name}</span>
|
||||
<span className="ml-2 shrink-0 rounded bg-violet-50 px-1.5 py-0.5 text-xs text-violet-600">
|
||||
<span className="ml-2 shrink-0 rounded bg-secondary px-1.5 py-0.5 text-xs text-primary">
|
||||
匹配 {p.match}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -48,9 +48,9 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "招标专员",
|
||||
user: "张伟",
|
||||
dept: "招标采购中心",
|
||||
color: "#2563eb",
|
||||
bannerFrom: "#0f1e3d",
|
||||
bannerTo: "#2563eb",
|
||||
color: "#00695c",
|
||||
bannerFrom: "#00695c",
|
||||
bannerTo: "#00897b",
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/projects", label: "项目管理", icon: "FolderKanban" },
|
||||
@@ -82,9 +82,9 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "评标专家",
|
||||
user: "郑国华",
|
||||
dept: "外部专家 · 化工工艺",
|
||||
color: "#f59e0b",
|
||||
bannerFrom: "#7c2d12",
|
||||
bannerTo: "#f59e0b",
|
||||
color: "#1565c0",
|
||||
bannerFrom: "#0d47a1",
|
||||
bannerTo: "#1976d2",
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/expert-tasks", label: "评标任务", icon: "Gavel" },
|
||||
@@ -121,9 +121,9 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "供应商",
|
||||
user: "浙江中控技术股份有限公司",
|
||||
dept: "投标用户",
|
||||
color: "#8b5cf6",
|
||||
bannerFrom: "#4c1d95",
|
||||
bannerTo: "#8b5cf6",
|
||||
color: "#c2185b",
|
||||
bannerFrom: "#c2185b",
|
||||
bannerTo: "#e91e63",
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/bidding", label: "投标中心", icon: "PackageOpen" },
|
||||
|
||||
@@ -2,43 +2,46 @@
|
||||
|
||||
:root {
|
||||
--font-size: 14px;
|
||||
--background: #f8fafc;
|
||||
--foreground: #1e293b;
|
||||
--background: #f5f7fa;
|
||||
--foreground: #1a2332;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1e293b;
|
||||
--card-foreground: #1a2332;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1e293b;
|
||||
--primary: #2563eb;
|
||||
--popover-foreground: #1a2332;
|
||||
--primary: #c2185b;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eff6ff;
|
||||
--secondary-foreground: #1d4ed8;
|
||||
--secondary: #fce4ec;
|
||||
--secondary-foreground: #ad1457;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #eff6ff;
|
||||
--accent-foreground: #1d4ed8;
|
||||
--accent: #fce4ec;
|
||||
--accent-foreground: #ad1457;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--input: transparent;
|
||||
--input-background: #f8fafc;
|
||||
--input-background: #f7f8fc;
|
||||
--switch-background: #cbd5e1;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-normal: 400;
|
||||
--ring: #2563eb;
|
||||
--chart-1: #2563eb;
|
||||
--chart-2: #0ea5e9;
|
||||
--chart-3: #14b8a6;
|
||||
--ring: #c2185b;
|
||||
--chart-1: #c2185b;
|
||||
--chart-2: #1565c0;
|
||||
--chart-3: #00897b;
|
||||
--chart-4: #f59e0b;
|
||||
--chart-5: #8b5cf6;
|
||||
--radius: 0.625rem;
|
||||
--sidebar: #0f1e3d;
|
||||
--sidebar-foreground: #cbd5e1;
|
||||
--sidebar-primary: #2563eb;
|
||||
--chart-5: #9c27b0;
|
||||
--radius: 0.75rem;
|
||||
/* 品牌渐变(全平台统一): #c2185b → #e91e63 */
|
||||
--brand-from: #c2185b;
|
||||
--brand-to: #e91e63;
|
||||
--sidebar: #2d0a19;
|
||||
--sidebar-foreground: #f4d9e4;
|
||||
--sidebar-primary: #e91e63;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #1e3a8a;
|
||||
--sidebar-accent: #6d1338;
|
||||
--sidebar-accent-foreground: #ffffff;
|
||||
--sidebar-border: #1e293b;
|
||||
--sidebar-ring: #2563eb;
|
||||
--sidebar-border: #4a0e26;
|
||||
--sidebar-ring: #e91e63;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -117,6 +120,8 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-brand-from: var(--brand-from);
|
||||
--color-brand-to: var(--brand-to);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -179,3 +184,61 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 平台品牌工具类与动效(依据《详细设计》统一视觉语言) ===== */
|
||||
@layer utilities {
|
||||
/* 品牌渐变背景: #c2185b → #e91e63 */
|
||||
.bg-brand-gradient {
|
||||
background-image: linear-gradient(135deg, var(--brand-from), var(--brand-to));
|
||||
}
|
||||
.text-brand-gradient {
|
||||
background-image: linear-gradient(135deg, var(--brand-from), var(--brand-to));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* AI 助手呼吸光环 */
|
||||
@keyframes brand-breathe {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.55;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.55);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.animate-breathe {
|
||||
animation: brand-breathe 2s ease-out infinite;
|
||||
}
|
||||
|
||||
/* 列表/卡片入场(自下而上淡入) */
|
||||
@keyframes brand-slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: brand-slide-up 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* 待办数字变化呼吸提醒 */
|
||||
@keyframes brand-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
.animate-pulse-soft {
|
||||
animation: brand-pulse 1s ease-in-out 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user