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

157 lines
7.3 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, useParams } from "react-router";
import { ArrowLeft, Sparkles, Send, Eye, Check, Globe, Database, Boxes } from "lucide-react";
import { toast } from "sonner";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
import { AiBadge } from "../components/shared/common";
import { sourcingRecords, projects } from "../data/mock";
type SourceKey = "all" | "internet" | "srm" | "pool";
const SOURCES: { key: SourceKey; label: string; icon: any; desc: string }[] = [
{ key: "all", label: "全部来源", icon: Sparkles, desc: "AI 综合多渠道寻源" },
{ key: "internet", label: "互联网寻源", icon: Globe, desc: "公共资源交易平台等" },
{ key: "srm", label: "SRM 库寻源", icon: Database, desc: "集团合格供方库" },
{ key: "pool", label: "临时供应商池", icon: Boxes, desc: "STP 平台临时池" },
];
// 依据记录派生来源(示例):按序分配到三类来源
function sourceOf(index: number): Exclude<SourceKey, "all"> {
return (["internet", "srm", "pool"] as const)[index % 3];
}
const SOURCE_LABEL: Record<Exclude<SourceKey, "all">, string> = {
internet: "互联网",
srm: "SRM 库",
pool: "临时池",
};
export function SmartSourcing() {
const navigate = useNavigate();
const { id } = useParams();
const project = projects.find((p) => p.id === id) ?? projects[0];
const [tab, setTab] = useState<SourceKey>("all");
const records = sourcingRecords.map((r, i) => ({ ...r, source: sourceOf(i) }));
const filtered = tab === "all" ? records : records.filter((r) => r.source === tab);
const viewed = filtered.filter((r) => r.viewed).length;
const registered = filtered.filter((r) => r.registered).length;
return (
<div>
<div className="mb-5 flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft className="size-5" />
</Button>
<div className="flex-1">
<div className="flex items-center gap-2">
<h1 className="text-foreground">AI </h1>
<AiBadge label="AI 主动寻源" />
</div>
{/* 关联项目 */}
<p className="mt-0.5 text-sm text-muted-foreground">
<span className="font-medium text-foreground">{project.name}</span> · {project.code}
</p>
</div>
</div>
{/* 三类寻源来源 */}
<div className="mb-6 grid grid-cols-4 gap-4">
{SOURCES.map((s) => {
const Icon = s.icon;
const active = tab === s.key;
const count = s.key === "all" ? records.length : records.filter((r) => r.source === s.key).length;
return (
<button
key={s.key}
onClick={() => setTab(s.key)}
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
active ? "border-primary bg-secondary/60 shadow-sm" : "bg-card hover:border-primary/50"
}`}
>
<div className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${active ? "bg-primary text-white" : "bg-primary/8 text-primary"}`}>
<Icon className="size-5" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">{s.label}</span>
<span className="rounded-full bg-primary/10 px-1.5 text-xs text-primary">{count}</span>
</div>
<p className="mt-0.5 truncate text-xs text-muted-foreground">{s.desc}</p>
</div>
</button>
);
})}
</div>
<div className="mb-6 grid grid-cols-3 gap-4">
{[
{ l: "AI 外发邀请", v: filtered.length, c: "var(--primary)" },
{ l: "已查看回执", v: viewed, c: "#f59e0b" },
{ l: "已报名", v: registered, c: "#14b8a6" },
].map((s) => (
<Card key={s.l} className="gap-0 p-5">
<div className="text-sm text-muted-foreground">{s.l}</div>
<div className="mt-2 text-2xl font-semibold" style={{ color: s.c }}>{s.v}</div>
</Card>
))}
</div>
<Card className="gap-0 p-0">
<div className="flex items-center gap-2 border-b px-6 py-4">
<Sparkles className="size-5 text-primary" />
<h3 className="text-foreground"></h3>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
{SOURCES.find((s) => s.key === tab)?.label}
</span>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/40 text-left text-muted-foreground">
<th className="px-6 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
</tr>
</thead>
<tbody className="divide-y">
{filtered.map((r) => (
<tr key={r.id} className={`hover:bg-muted/20 ${!r.viewed ? "bg-amber-50/30" : ""}`}>
<td className="px-6 py-3 font-medium text-foreground">{r.supplier}</td>
<td className="px-4 py-3">
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs text-primary">{SOURCE_LABEL[r.source]}</span>
</td>
<td className="px-4 py-3 text-muted-foreground">{r.channel}</td>
<td className="px-4 py-3 text-muted-foreground">{r.sentAt}</td>
<td className="px-4 py-3">
{r.viewed ? (
<span className="inline-flex items-center gap-1 text-emerald-600"><Eye className="size-4" /> </span>
) : (
<span className="inline-flex items-center gap-1 text-amber-600"><Send className="size-4" /> </span>
)}
</td>
<td className="px-4 py-3">
{r.registered ? <span className="text-emerald-600"></span> : <span className="text-muted-foreground"></span>}
</td>
<td className="px-4 py-3">
<Button size="sm" variant="outline" className="h-7" onClick={() => toast.success("已确认回执")}>
<Check className="size-3.5" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</Card>
<div className="mt-4 flex items-start gap-2 rounded-lg border border-primary/20 bg-secondary/40 p-3 text-sm text-muted-foreground">
<Sparkles className="mt-0.5 size-4 shrink-0 text-primary" />
SRM STP
</div>
</div>
);
}