feat(front-end): 初始化STP前端工程并提交页面框架

引入前端项目基础结构、页面骨架和UI组件,建立可运行的开发与构建配置,方便后续按模块迭代。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-22 14:25:22 +08:00
parent f6ff2e3625
commit 7f0f79420c
107 changed files with 18103 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import {
ArrowLeft,
Lock,
Unlock,
ShieldCheck,
Link2,
CheckCircle2,
Loader2,
Play,
Clock,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
import { PageHeader } from "../components/shared/common";
import { bidders } from "../data/mock";
interface LogItem {
time: string;
title: string;
desc: string;
icon: any;
color: string;
}
const logSeq: LogItem[] = [
{ time: "10:00:00", title: "系统自动开标", desc: "开标时间到达,系统自动启动开标程序", icon: Play, color: "#2563eb" },
{ time: "10:00:03", title: "投标截止确认", desc: "确认共收到 3 家有效投标文件", icon: CheckCircle2, color: "#0ea5e9" },
{ time: "10:00:08", title: "文件自动解密", desc: "使用投标人 CA 证书自动解密投标文件", icon: Unlock, color: "#14b8a6" },
{ time: "10:00:15", title: "投标文件哈希上链", desc: "各投标文件哈希值已写入区块链存证,不可篡改", icon: Link2, color: "#8b5cf6" },
{ time: "10:00:20", title: "开标完成", desc: "所有投标文件解密成功,开标结束", icon: ShieldCheck, color: "#16a34a" },
];
export function BidOpeningHall() {
const navigate = useNavigate();
const [started, setStarted] = useState(false);
const [shown, setShown] = useState(0);
useEffect(() => {
if (!started) return;
if (shown >= logSeq.length) return;
const t = setTimeout(() => setShown((s) => s + 1), 900);
return () => clearTimeout(t);
}, [started, shown]);
const done = shown >= logSeq.length;
return (
<div>
<div className="mb-1 flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft className="size-5" />
</Button>
<PageHeader title="开标大厅" description="DCS 控制系统升级采购 · 自动化开标与链上存证" />
</div>
{/* 顶部状态条 */}
<Card className="mb-6 flex-row items-center justify-between bg-gradient-to-r from-[#0f1e3d] to-[#1e3a8a] p-6 text-white">
<div className="flex items-center gap-4">
<div className="flex size-14 items-center justify-center rounded-2xl bg-white/10">
{done ? <ShieldCheck className="size-7" /> : <Clock className="size-7" />}
</div>
<div>
<div className="text-sm text-white/70"></div>
<div className="text-xl font-semibold">
{!started ? "等待开标" : done ? "开标已完成" : "开标进行中…"}
</div>
</div>
</div>
<div className="flex items-center gap-8">
<div className="text-center">
<div className="text-2xl font-semibold">3</div>
<div className="text-xs text-white/60"></div>
</div>
<div className="text-center">
<div className="text-2xl font-semibold">{started ? Math.min(shown, 3) : 0}</div>
<div className="text-xs text-white/60"></div>
</div>
{!started && (
<Button className="bg-white text-primary hover:bg-white/90" onClick={() => setStarted(true)}>
<Play className="size-4" />
</Button>
)}
</div>
</Card>
<div className="grid grid-cols-5 gap-6">
{/* 实时日志时间轴 */}
<Card className="col-span-3 gap-0 p-6">
<h3 className="mb-5 text-foreground"></h3>
{!started ? (
<div className="flex h-64 flex-col items-center justify-center gap-2 text-muted-foreground">
<Clock className="size-10 text-muted-foreground/40" />
<p className="text-sm"></p>
</div>
) : (
<div className="space-y-0">
{logSeq.slice(0, shown).map((l, i) => {
const Icon = l.icon;
return (
<div key={i} className="flex gap-4">
<div className="flex flex-col items-center">
<div
className="flex size-9 shrink-0 items-center justify-center rounded-full text-white"
style={{ backgroundColor: l.color }}
>
<Icon className="size-4" />
</div>
{i < shown - 1 && <div className="my-1 w-0.5 flex-1 bg-border" />}
</div>
<div className="pb-5">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{l.title}</span>
<span className="font-mono text-xs text-muted-foreground">{l.time}</span>
</div>
<p className="mt-0.5 text-sm text-muted-foreground">{l.desc}</p>
</div>
</div>
);
})}
{!done && (
<div className="flex items-center gap-2 pl-1 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
</div>
)}
</div>
)}
</Card>
{/* 解密投标人列表 */}
<Card className="col-span-2 gap-0 p-6">
<h3 className="mb-4 text-foreground"></h3>
<div className="space-y-3">
{bidders.map((b, i) => {
const revealed = started && shown >= 3;
return (
<div key={b.id} className="rounded-xl border bg-card p-4">
<div className="flex items-center justify-between">
<span className="font-medium text-foreground">{revealed ? b.realName : b.anon}</span>
{revealed ? (
<span className="inline-flex items-center gap-1 text-xs text-emerald-600">
<Unlock className="size-3.5" />
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<Lock className="size-3.5" />
</span>
)}
</div>
<div className="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
<Link2 className="size-3.5 text-violet-500" />
<span className="font-mono">{revealed ? b.hash : "····················"}</span>
</div>
</div>
);
})}
</div>
{done && (
<Button className="mt-5 w-full" onClick={() => navigate("/app/projects/P2026001/evaluation")}>
线
</Button>
)}
</Card>
</div>
</div>
);
}