Files
STP/front-end/src/app/pages/BidOpeningHall.tsx
T
vame d81ac936e7 feat(front-end): 依据内部评审意见对前端 UI 进行二次升级优化
- 主题色统一改回科技蓝(呼应滨化 LOGO)
- 合并"采购需求人员"、移除审批人角色(审批在 OA)
- 发起委托归属采购需求人员,招标专员不含该功能
- 删除合并招标/定标审批/合同生成/合同管理/供应商入库/审批中心/流程配置页面
- 小滨助手默认展开可收起、各角色主动提醒、问答直接给出按钮/链接
- 待办中心去掉"事找人"前缀
- 新增后台智能体任务监控组件(招标专员/管理员)
- 发起委托增加导入采购计划/直接填写/选择模板(含自动)
- 项目列表:排序/高级搜索/委托人列/提交审批/列显隐
- 项目详情:修正流程步骤、委托人信息、方案与文件合一、提交审批
- 长短名单去分支与推荐部门列、寻源关联项目并支持三来源
- 开标去自动开标、专家管理去抽取与回避、供应商管理去新增与入库
2026-07-10 23:35:54 +08:00

169 lines
6.9 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, 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: "#1565c0" },
{ 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-primary" />
<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>
);
}