d81ac936e7
- 主题色统一改回科技蓝(呼应滨化 LOGO) - 合并"采购需求人员"、移除审批人角色(审批在 OA) - 发起委托归属采购需求人员,招标专员不含该功能 - 删除合并招标/定标审批/合同生成/合同管理/供应商入库/审批中心/流程配置页面 - 小滨助手默认展开可收起、各角色主动提醒、问答直接给出按钮/链接 - 待办中心去掉"事找人"前缀 - 新增后台智能体任务监控组件(招标专员/管理员) - 发起委托增加导入采购计划/直接填写/选择模板(含自动) - 项目列表:排序/高级搜索/委托人列/提交审批/列显隐 - 项目详情:修正流程步骤、委托人信息、方案与文件合一、提交审批 - 长短名单去分支与推荐部门列、寻源关联项目并支持三来源 - 开标去自动开标、专家管理去抽取与回避、供应商管理去新增与入库
169 lines
6.9 KiB
TypeScript
169 lines
6.9 KiB
TypeScript
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>
|
||
);
|
||
}
|