feat(front-end): 依据内部评审意见对前端 UI 进行二次升级优化
- 主题色统一改回科技蓝(呼应滨化 LOGO) - 合并"采购需求人员"、移除审批人角色(审批在 OA) - 发起委托归属采购需求人员,招标专员不含该功能 - 删除合并招标/定标审批/合同生成/合同管理/供应商入库/审批中心/流程配置页面 - 小滨助手默认展开可收起、各角色主动提醒、问答直接给出按钮/链接 - 待办中心去掉"事找人"前缀 - 新增后台智能体任务监控组件(招标专员/管理员) - 发起委托增加导入采购计划/直接填写/选择模板(含自动) - 项目列表:排序/高级搜索/委托人列/提交审批/列显隐 - 项目详情:修正流程步骤、委托人信息、方案与文件合一、提交审批 - 长短名单去分支与推荐部门列、寻源关联项目并支持三来源 - 开标去自动开标、专家管理去抽取与回避、供应商管理去新增与入库
This commit is contained in:
@@ -12,15 +12,11 @@ import { ProjectDetail } from "./pages/ProjectDetail";
|
||||
import { SupplierShortlist } from "./pages/SupplierShortlist";
|
||||
import { BidOpeningHall } from "./pages/BidOpeningHall";
|
||||
import { EvaluationRoom } from "./pages/EvaluationRoom";
|
||||
import { AwardApproval } from "./pages/AwardApproval";
|
||||
import { ContractGeneration } from "./pages/ContractGeneration";
|
||||
import { SupplierManagement } from "./pages/SupplierManagement";
|
||||
import { ExpertManagement } from "./pages/ExpertManagement";
|
||||
import { ReportCenter } from "./pages/ReportCenter";
|
||||
import { Placeholder } from "./pages/Placeholder";
|
||||
import { ApprovalCenter } from "./pages/ApprovalCenter";
|
||||
import { BiddingCenter } from "./pages/BiddingCenter";
|
||||
import { MergeBidding } from "./pages/MergeBidding";
|
||||
import { SmartSourcing } from "./pages/SmartSourcing";
|
||||
import { DepositManagement } from "./pages/DepositManagement";
|
||||
import { ExceptionDecision } from "./pages/ExceptionDecision";
|
||||
@@ -28,10 +24,7 @@ import { ExpertTasks } from "./pages/ExpertTasks";
|
||||
import { AuditLog } from "./pages/AuditLog";
|
||||
import { CollusionAnalysis } from "./pages/CollusionAnalysis";
|
||||
import { EvidenceVerification } from "./pages/EvidenceVerification";
|
||||
import { ContractManagement } from "./pages/ContractManagement";
|
||||
import { SupplierInbound } from "./pages/SupplierInbound";
|
||||
import { UserManagement } from "./pages/admin/UserManagement";
|
||||
import { ProcessConfig } from "./pages/admin/ProcessConfig";
|
||||
import { KeyManagement } from "./pages/admin/KeyManagement";
|
||||
|
||||
export default function App() {
|
||||
@@ -51,20 +44,11 @@ export default function App() {
|
||||
<Route path="projects/:id/sourcing" element={<SmartSourcing />} />
|
||||
<Route path="projects/:id/opening" element={<BidOpeningHall />} />
|
||||
<Route path="projects/:id/evaluation" element={<EvaluationRoom />} />
|
||||
<Route path="projects/:id/award" element={<AwardApproval />} />
|
||||
<Route path="projects/:id/contract" element={<ContractGeneration />} />
|
||||
{/* 招标专员 */}
|
||||
<Route path="merge" element={<MergeBidding />} />
|
||||
<Route path="deposits" element={<DepositManagement />} />
|
||||
{/* 资源管理 */}
|
||||
<Route path="suppliers" element={<SupplierManagement />} />
|
||||
<Route path="experts" element={<ExpertManagement />} />
|
||||
<Route path="reports" element={<ReportCenter />} />
|
||||
{/* 采购专员 */}
|
||||
<Route path="contracts" element={<ContractManagement />} />
|
||||
<Route path="inbound" element={<SupplierInbound />} />
|
||||
{/* 审批人 */}
|
||||
<Route path="approvals" element={<ApprovalCenter />} />
|
||||
{/* 领导小组 */}
|
||||
<Route path="exceptions" element={<ExceptionDecision />} />
|
||||
{/* 评标专家 */}
|
||||
@@ -77,7 +61,6 @@ export default function App() {
|
||||
<Route path="evidence" element={<EvidenceVerification />} />
|
||||
{/* 管理员 */}
|
||||
<Route path="admin/users" element={<UserManagement />} />
|
||||
<Route path="admin/process" element={<ProcessConfig />} />
|
||||
<Route path="admin/keys" element={<KeyManagement />} />
|
||||
<Route path="settings" element={<Placeholder title="系统设置" />} />
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Bot, XCircle } from "lucide-react";
|
||||
import { Card } from "./ui/card";
|
||||
import { AiBadge } from "./shared/common";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type AgentStatus = "queued" | "running" | "done" | "failed";
|
||||
|
||||
interface AgentTask {
|
||||
icon: string;
|
||||
name: string;
|
||||
project: string;
|
||||
status: AgentStatus;
|
||||
progress: number;
|
||||
result: string;
|
||||
remain: string;
|
||||
}
|
||||
|
||||
// 示例数据(评审说明书第 6 节)
|
||||
const TASKS: AgentTask[] = [
|
||||
{ icon: "🔍", name: "AI 智慧寻源", project: "年产5万吨聚丙烯项目", status: "running", progress: 70, result: "已匹配:8 家供应商", remain: "预计剩余 2 分钟" },
|
||||
{ icon: "🔬", name: "AI 雷同检测", project: "DCS 控制系统升级采购", status: "running", progress: 30, result: "已扫描:3 / 10 份", remain: "预计剩余 5 分钟" },
|
||||
{ icon: "📊", name: "AI 价格分析", project: "环氧丙烷装置大修工程", status: "done", progress: 100, result: "发现 1 份报价偏离 >20%", remain: "09:35 完成" },
|
||||
{ icon: "👨🏫", name: "AI 专家推荐", project: "污水处理提标改造工程", status: "queued", progress: 0, result: "排队中…", remain: "—" },
|
||||
{ icon: "📄", name: "AI 评标报告生成", project: "实验室耗材集采", status: "failed", progress: 45, result: "模板解析异常,请重试", remain: "已终止" },
|
||||
];
|
||||
|
||||
const STATUS: Record<AgentStatus, { bar: string; barBg: string; label: string; chip: string; dot: string }> = {
|
||||
queued: { bar: "bg-[#9e9e9e]", barBg: "bg-[#eeeeee]", label: "⏸ 待执行", chip: "bg-slate-100 text-slate-500", dot: "#9e9e9e" },
|
||||
running: { bar: "bg-gradient-to-r from-[#fb8c00] to-[#ffb74d]", barBg: "bg-[#fff3e0]", label: "⏳ 执行中", chip: "bg-amber-50 text-amber-600", dot: "#ff9800" },
|
||||
done: { bar: "bg-[#4caf50]", barBg: "bg-[#e8f5e9]", label: "✅ 已完成", chip: "bg-emerald-50 text-emerald-600", dot: "#4caf50" },
|
||||
failed: { bar: "bg-[#f44336]", barBg: "bg-[#ffebee]", label: "❌ 执行失败", chip: "bg-red-50 text-red-600", dot: "#f44336" },
|
||||
};
|
||||
|
||||
export function AgentTaskMonitor() {
|
||||
const running = TASKS.filter((t) => t.status === "running").length;
|
||||
|
||||
return (
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="size-[18px] text-primary" />
|
||||
<h3 className="text-foreground">后台智能体任务</h3>
|
||||
<AiBadge label="AI Agent" />
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">{running} 个执行中</span>
|
||||
</div>
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-500" /> 每 3 秒实时刷新
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y">
|
||||
{TASKS.map((t, i) => {
|
||||
const st = STATUS[t.status];
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-4 px-6 py-3.5">
|
||||
{/* 状态色条 */}
|
||||
<span className="h-11 w-1.5 shrink-0 rounded-full" style={{ backgroundColor: st.dot }} />
|
||||
{/* 图标 */}
|
||||
<div
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full text-base"
|
||||
style={{ backgroundColor: `${st.dot}1a` }}
|
||||
>
|
||||
{t.icon}
|
||||
</div>
|
||||
{/* 任务信息 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground">{t.name}</span>
|
||||
<span className={`rounded px-1.5 py-0.5 text-xs ${st.chip}`}>{st.label}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{t.project} · {t.result}
|
||||
</div>
|
||||
</div>
|
||||
{/* 进度条 */}
|
||||
<div className="hidden w-40 shrink-0 md:block">
|
||||
<div className={`h-2 w-full overflow-hidden rounded-full ${st.barBg}`} title={`${t.progress}%`}>
|
||||
<div className={`h-full rounded-full transition-all ${st.bar}`} style={{ width: `${t.progress}%` }} />
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>{t.progress}%</span>
|
||||
<span>{t.remain}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 操作 */}
|
||||
<div className="w-20 shrink-0 text-right">
|
||||
{t.status === "running" && (
|
||||
<button
|
||||
onClick={() => toast("已发送取消请求,正在终止任务…")}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<XCircle className="size-3.5" /> 取消
|
||||
</button>
|
||||
)}
|
||||
{t.status === "done" && (
|
||||
<button onClick={() => toast.success("正在打开报告…")} className="text-xs font-medium text-primary hover:underline">
|
||||
查看报告
|
||||
</button>
|
||||
)}
|
||||
{t.status === "failed" && (
|
||||
<button onClick={() => toast("正在重试任务…")} className="text-xs font-medium text-primary hover:underline">
|
||||
重试
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { X, Send, Minus, Lock, ArrowRight } from "lucide-react";
|
||||
import { X, Send, Minus, Lock, ArrowRight, Sparkles } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "./ui/utils";
|
||||
import { useRole } from "../role-context";
|
||||
import type { RoleId } from "../roles";
|
||||
|
||||
interface MsgAction {
|
||||
label: string;
|
||||
to: string;
|
||||
}
|
||||
interface Msg {
|
||||
role: "bot" | "user";
|
||||
text: string;
|
||||
actions?: MsgAction[];
|
||||
}
|
||||
|
||||
type ProactiveType = "urgent" | "ai" | "todo" | "risk" | "suggestion";
|
||||
@@ -23,8 +28,8 @@ interface Proactive {
|
||||
|
||||
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: "📋" },
|
||||
ai: { bg: "bg-[#e3f2fd]", border: "border-[#90caf9]", title: "text-[#0d47a1]", badge: "✅" },
|
||||
todo: { bg: "bg-[#e8f0fe]", border: "border-[#a3c2f0]", title: "text-[#1565c0]", badge: "📋" },
|
||||
risk: { bg: "bg-[#ffebee]", border: "border-[#ef9a9a]", title: "text-[#c62828]", badge: "⚠️" },
|
||||
suggestion: { bg: "bg-[#f5f5f5]", border: "border-[#e0e0e0]", title: "text-[#455a64]", badge: "💡" },
|
||||
};
|
||||
@@ -32,61 +37,112 @@ const TYPE_STYLE: Record<ProactiveType, { bg: string; border: string; title: str
|
||||
// 各角色的主动提醒(最多 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: "去审核" },
|
||||
{ type: "urgent", title: "BID-2026-005 今日需开标", content: "「2026年度防腐保温工程框架」已到开标时间,请立即组织评标。", to: "/app/projects", actionLabel: "去处理" },
|
||||
{ type: "ai", title: "AI 智慧寻源已完成", content: "「年产5万吨聚丙烯项目」已自动寻源,匹配到 12 家潜在供应商,请确认寻源结果。", to: "/app/projects/P2026005/sourcing", actionLabel: "查看结果" },
|
||||
{ type: "todo", title: "您有 3 个委托单待受理", content: "来自采购需求人员的委托申请已提交,请及时受理并立项。", to: "/app/projects", 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: "查看" },
|
||||
requester: [
|
||||
{ type: "ai", title: "阀门采购项目已定标", content: "您的「氯碱车间耐腐蚀阀门采购」已完成定标,下一步请进入 SRM 生成采购合同。", to: "/app/projects", actionLabel: "查看项目" },
|
||||
{ type: "todo", title: "2 个 AI 预填委托单待确认", content: "AI 已根据采购计划为您预填委托单,确认无误后即可提交立项。", to: "/app/delegation", 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: "查看" },
|
||||
{ type: "urgent", title: "保温材料招标项目评标邀请", content: "您有一个「保温材料招标项目」的评标邀请,可以选择接受或拒绝,点击查看详情。", to: "/app/expert-tasks", actionLabel: "查看详情" },
|
||||
{ type: "todo", title: "《评标纪律承诺书》待签署", content: "进入评审前需先签署当前项目的评标纪律承诺书。", to: "/app/expert-tasks", actionLabel: "去签署" },
|
||||
],
|
||||
approver: [
|
||||
{ type: "todo", title: "待审批事项 2 项", content: "有 2 个定标 / 立项审批等待您处理", to: "/app/approvals", actionLabel: "去审批" },
|
||||
supplier: [
|
||||
{ type: "ai", title: "聚丙烯项目报名已通过", content: "您在「年产5万吨聚丙烯项目」中的报名已通过,请查看招标文件;如确认投标,可缴纳投标保证金。", to: "/app/bidding", actionLabel: "查看招标文件" },
|
||||
{ type: "urgent", title: "报名即将截止", content: "「污水处理提标改造工程」报名将于 7月15日截止,请尽快完成报名。", to: "/app/bidding", actionLabel: "去报名" },
|
||||
],
|
||||
committee: [
|
||||
{ type: "risk", title: "异常决策待处理", content: "检测到 1 个项目触发异常上报,需领导小组决策", to: "/app/exceptions", actionLabel: "去处理" },
|
||||
{ type: "risk", title: "异常决策待处理", content: "检测到 1 个项目触发异常上报(有效投标人不足 3 家),需领导小组决策。", to: "/app/exceptions", actionLabel: "去处理" },
|
||||
],
|
||||
auditor: [
|
||||
{ type: "risk", title: "围串标风险提示", content: "AI 检测到 1 组投标文件相似度偏高,建议核查", to: "/app/collusion", actionLabel: "去核查" },
|
||||
{ type: "risk", title: "围串标风险提示", content: "AI 检测到 1 组投标文件相似度偏高,建议核查。", to: "/app/collusion", actionLabel: "去核查" },
|
||||
],
|
||||
};
|
||||
|
||||
const quickReplies = ["如何发起招标委托?", "公开招标和邀请招标的区别?", "在线评标怎么打分?", "供应商如何入库?"];
|
||||
const quickReplies = [
|
||||
"如何发起招标委托?",
|
||||
"帮我统计本年度项目预算总和",
|
||||
"在线评标怎么打分?",
|
||||
"公开招标和邀请招标的区别?",
|
||||
];
|
||||
|
||||
const knowledge: Record<string, string> = {
|
||||
委托:
|
||||
"发起招标委托很简单:① 点击工作台或项目管理页的「发起委托」按钮;② 在「填写需求」步骤用自然语言描述需求,点击「AI 智能解析」即可自动生成《招标委托表》;③ 确认预填单后提交审批即可。需要我带您过去吗?",
|
||||
区别:
|
||||
"公开招标面向所有符合条件的供应商发布寻源公告,参与方需报名审核;邀请招标则由您从长名单中筛选短名单进行定向邀请。两者在本平台均支持全流程线上闭环。",
|
||||
评标:
|
||||
"进入「在线评标室」后:左栏选择匿名投标人,中栏查看脱敏标书,右栏按技术分/商务分/价格分录入分数并填写评分说明,系统会同步展示 AI 雷同检测与价格合理性分析,最后点击「提交评分」。",
|
||||
入库:
|
||||
"在「供应商管理」页点击「发起入库流程」,或在短名单管理中对供应商点击「发起入库」,系统将自动同步工商风险信息并进入审批。",
|
||||
合并:
|
||||
"当系统中存在同类型未完成立项的委托时,系统会自动推送合并建议。您可以在立项阶段的「合并招标建议」卡片中查看并确认合并。",
|
||||
上传:
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,投标截止前还可撤回重传。",
|
||||
};
|
||||
interface Knowledge {
|
||||
text: string;
|
||||
actions?: MsgAction[];
|
||||
}
|
||||
|
||||
const knowledge: { key: string; ans: Knowledge }[] = [
|
||||
{
|
||||
key: "委托",
|
||||
ans: {
|
||||
text:
|
||||
"发起招标委托很简单:①点击工作台或项目管理页的「发起委托」按钮;②在「填写需求」步骤用自然语言描述需求,点击「AI 智能解析」即可自动生成《招标委托书》;③确认预填单后提交审批即可。",
|
||||
actions: [{ label: "现在去发起委托", to: "/app/delegation" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "统计",
|
||||
ans: {
|
||||
text: "据统计,本年度一共有 13 个项目,预算总和为 12,348 万元。",
|
||||
actions: [{ label: "查看项目列表", to: "/app/projects" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "预算",
|
||||
ans: {
|
||||
text: "据统计,本年度一共有 13 个项目,预算总和为 12,348 万元。",
|
||||
actions: [{ label: "查看项目列表", to: "/app/projects" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "区别",
|
||||
ans: {
|
||||
text:
|
||||
"公开招标面向所有符合条件的供应商发布寻源公告,参与方需报名审核;邀请招标则由您从长名单中筛选短名单进行定向邀请。招标方式在需求委托阶段即已确定,两者均支持全流程线上闭环。",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "评标",
|
||||
ans: {
|
||||
text:
|
||||
"进入「在线评标室」后:左栏选择匿名投标人,中栏查看脱敏标书,右栏按技术分/商务分/价格分录入分数并填写评分说明,系统会同步展示 AI 雷同检测与价格合理性分析,最后点击「提交评分」。",
|
||||
actions: [{ label: "进入在线评标室", to: "/app/projects/P2026001/evaluation" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "入库",
|
||||
ans: {
|
||||
text:
|
||||
"供应商在「供应商管理」中提交入库申请,AI 会自动核验工商与风险信息;审核通过后系统将自动入库,无需单独操作。",
|
||||
actions: [{ label: "前往供应商管理", to: "/app/suppliers" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "上传",
|
||||
ans: {
|
||||
text:
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,投标截止前还可撤回重传。",
|
||||
actions: [{ label: "进入投标中心", to: "/app/bidding" }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dataKeywords = ["报价", "投标价", "多少钱", "金额", "中标", "得分", "分数", "名单", "谁"];
|
||||
|
||||
export function XiaobinAssistant() {
|
||||
const { sensitive, role, roleId } = useRole();
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
// 默认展开:用户进入工作台即可看到小滨的主动提醒,可随时收起
|
||||
const [open, setOpen] = useState(true);
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [proactive, setProactive] = useState<Proactive[]>(PROACTIVE[roleId] ?? []);
|
||||
const [msgs, setMsgs] = useState<Msg[]>([
|
||||
{
|
||||
role: "bot",
|
||||
text: "您好,我是智能助手「小滨」👋 我可以为您解答系统操作与业务咨询。注意:我不参与任何业务决策哦。请问有什么可以帮您?",
|
||||
text: "您好,我是智能助手「小滨」👋 我可以为您解答系统操作与业务咨询。注意:我不参与任何业务决策哦。请问有什么可以帮您?",
|
||||
},
|
||||
]);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
@@ -103,14 +159,19 @@ export function XiaobinAssistant() {
|
||||
const gradient = `linear-gradient(135deg, ${role.bannerFrom}, ${role.bannerTo})`;
|
||||
const unread = proactive.length;
|
||||
|
||||
function answer(q: string) {
|
||||
function answer(q: string): Knowledge {
|
||||
if (sensitive && dataKeywords.some((k) => q.includes(k))) {
|
||||
return "当前为流程敏感期(如开标前),数据类问题暂无法回答。请等待系统通知。我可以继续为您讲解系统操作哦~";
|
||||
return {
|
||||
text: "当前为流程敏感期(如开标前),数据类问题暂无法回答。请等待系统通知。我可以继续为您讲解系统操作哦~",
|
||||
};
|
||||
}
|
||||
const key = Object.keys(knowledge).find((k) => q.includes(k));
|
||||
return key
|
||||
? knowledge[key]
|
||||
: "这是一个很好的问题。作为操作助手,我建议您查阅对应模块的帮助说明,或描述更具体的操作场景,我会尽力为您讲解系统使用方法。(温馨提示:业务决策需由有权限的人员完成)";
|
||||
const hit = knowledge.find((k) => q.includes(k.key));
|
||||
return (
|
||||
hit?.ans ?? {
|
||||
text:
|
||||
"这是一个很好的问题。作为操作助手,我建议您查阅对应模块的帮助说明,或描述更具体的操作场景,我会尽力为您讲解系统使用方法。(温馨提示:业务决策需由有权限的人员完成)",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function send(text: string) {
|
||||
@@ -119,17 +180,19 @@ export function XiaobinAssistant() {
|
||||
setShowChat(true);
|
||||
setMsgs((m) => [...m, { role: "user", text: q }]);
|
||||
setInput("");
|
||||
setTimeout(() => setMsgs((m) => [...m, { role: "bot", text: answer(q) }]), 500);
|
||||
setTimeout(() => {
|
||||
const a = answer(q);
|
||||
setMsgs((m) => [...m, { role: "bot", text: a.text, actions: a.actions }]);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handleAction(p: Proactive) {
|
||||
if (p.to) navigate(p.to);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 悬浮球 + 呼吸光环 */}
|
||||
{/* 悬浮球 + 呼吸光环(收起状态) */}
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
@@ -163,14 +226,9 @@ export function XiaobinAssistant() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<button onClick={() => setOpen(false)} className="rounded-md p-1 hover:bg-white/15" title="收起">
|
||||
<Minus className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sensitive && (
|
||||
@@ -182,6 +240,19 @@ export function XiaobinAssistant() {
|
||||
<div className="flex-1 space-y-3 overflow-y-auto bg-muted/30 p-4">
|
||||
{!showChat ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
📢 为您整理的今日提醒({proactive.length})
|
||||
</span>
|
||||
{proactive.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowChat(true)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
去提问 →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{proactive.length === 0 && (
|
||||
<div className="rounded-xl border bg-card p-4 text-center text-sm text-muted-foreground">
|
||||
🎉 暂无待办,继续保持!
|
||||
@@ -206,7 +277,7 @@ export function XiaobinAssistant() {
|
||||
{p.to && (
|
||||
<button
|
||||
onClick={() => handleAction(p)}
|
||||
className={cn("mt-2 inline-flex items-center gap-0.5 text-xs font-bold underline", st.title)}
|
||||
className={cn("mt-2 inline-flex items-center gap-0.5 rounded-md bg-white/70 px-2 py-1 text-xs font-bold", st.title)}
|
||||
>
|
||||
{p.actionLabel ?? "去处理"} <ArrowRight className="size-3" />
|
||||
</button>
|
||||
@@ -238,13 +309,26 @@ export function XiaobinAssistant() {
|
||||
<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",
|
||||
"max-w-[85%] 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>{m.text}</div>
|
||||
{m.actions && m.actions.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{m.actions.map((a) => (
|
||||
<button
|
||||
key={a.to}
|
||||
onClick={() => navigate(a.to)}
|
||||
className="inline-flex items-center gap-1 rounded-lg bg-primary px-2.5 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
<Sparkles className="size-3.5" /> {a.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
Eye,
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
FilePlus2,
|
||||
Bot,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ICONS: Record<string, any> = {
|
||||
@@ -52,4 +54,6 @@ export const ICONS: Record<string, any> = {
|
||||
Eye,
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
FilePlus2,
|
||||
Bot,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,9 @@ export interface WbConfig {
|
||||
stats: WbStat[];
|
||||
groups: WbGroup[];
|
||||
/** 角色专属附加组件标识 */
|
||||
extra?: "specialist" | "supplier" | "admin" | "auditor" | "committee" | "expert" | "approver";
|
||||
extra?: "specialist" | "supplier" | "admin" | "auditor" | "committee" | "expert" | "requester";
|
||||
/** 是否展示后台智能体任务监控模块 */
|
||||
agentMonitor?: boolean;
|
||||
primaryAction?: { label: string; link: string };
|
||||
}
|
||||
|
||||
@@ -32,16 +34,19 @@ export const WORKBENCH: Record<RoleId, WbConfig> = {
|
||||
stats: [
|
||||
{ label: "进行中项目", value: "4" },
|
||||
{ label: "已定标", value: "7" },
|
||||
{ label: "保证金在途(万元)", value: "42" },
|
||||
{ label: "被驳回", value: "1", alert: true },
|
||||
],
|
||||
extra: "requester",
|
||||
primaryAction: { label: "✨ AI 发起新委托", link: "/app/delegation" },
|
||||
groups: [
|
||||
{
|
||||
stage: "我的待办",
|
||||
todos: [
|
||||
{ title: "待确认的 AI 预填委托单", count: 2, project: "氯碱车间耐腐蚀阀门采购", link: "/app/delegation", ai: true, priority: "高" },
|
||||
{ title: "待参与的技术标评审", count: 1, project: "DCS 控制系统升级采购", link: "/app/projects/P2026001/evaluation", priority: "中" },
|
||||
{ title: "待补充需求资料", count: 1, project: "污水处理提标改造工程", link: "/app/projects/P2026004", priority: "低" },
|
||||
{ title: "待确认中标结果", count: 1, project: "环氧丙烷装置大修工程", link: "/app/projects/P2026003", priority: "高" },
|
||||
{ title: "待缴纳 / 退还保证金", count: 3, link: "/app/deposits", priority: "中" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -55,13 +60,13 @@ export const WORKBENCH: Record<RoleId, WbConfig> = {
|
||||
{ label: "超时预警", value: "2", alert: true },
|
||||
],
|
||||
extra: "specialist",
|
||||
primaryAction: { label: "+ 新建招标项目", link: "/app/delegation" },
|
||||
agentMonitor: true,
|
||||
primaryAction: { label: "+ 新建招标项目", link: "/app/projects" },
|
||||
groups: [
|
||||
{
|
||||
stage: "需求阶段",
|
||||
todos: [
|
||||
{ title: "待确认委托单", count: 3, link: "/app/delegation", ai: true, priority: "高" },
|
||||
{ title: "待合并招标建议", count: 1, link: "/app/merge", ai: true, priority: "中" },
|
||||
{ title: "待受理委托单", count: 3, link: "/app/projects", ai: true, priority: "高" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -74,35 +79,15 @@ export const WORKBENCH: Record<RoleId, WbConfig> = {
|
||||
{
|
||||
stage: "评标阶段",
|
||||
todos: [
|
||||
{ title: "待确认专家抽取", count: 1, link: "/app/experts", priority: "高" },
|
||||
{ title: "待组织专家抽取", count: 1, link: "/app/projects/P2026001", priority: "高" },
|
||||
{ title: "待处理评标异常", count: 1, link: "/app/projects/P2026001/evaluation", priority: "高" },
|
||||
],
|
||||
},
|
||||
{
|
||||
stage: "定标阶段",
|
||||
todos: [
|
||||
{ title: "待生成评标报告", count: 1, link: "/app/projects/P2026003/award", ai: true, priority: "中" },
|
||||
{ title: "待发起定标审批", count: 1, link: "/app/projects/P2026003/award", priority: "高" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
purchaser: {
|
||||
greeting: "中标结果确认、合同生成与保证金管理一站式处理。",
|
||||
stats: [
|
||||
{ label: "待签约合同", value: "3" },
|
||||
{ label: "本月应入库供应商", value: "5" },
|
||||
{ label: "保证金应收(万元)", value: "126" },
|
||||
],
|
||||
primaryAction: { label: "✨ AI 生成合同", link: "/app/contracts" },
|
||||
groups: [
|
||||
{
|
||||
stage: "我的待办",
|
||||
todos: [
|
||||
{ title: "待确认中标结果", count: 1, project: "环氧丙烷装置大修工程", link: "/app/projects/P2026003/award", priority: "高" },
|
||||
{ title: "待生成合同", count: 2, link: "/app/contracts", ai: true, priority: "高" },
|
||||
{ title: "待收取履约保证金", count: 3, link: "/app/deposits", priority: "中" },
|
||||
{ title: "待发起供应商入库", count: 5, link: "/app/inbound", priority: "中" },
|
||||
{ title: "待生成评标报告", count: 1, link: "/app/projects/P2026003", ai: true, priority: "中" },
|
||||
{ title: "待提交定标结果", count: 1, link: "/app/projects/P2026003", priority: "高" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -145,26 +130,6 @@ export const WORKBENCH: Record<RoleId, WbConfig> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
approver: {
|
||||
greeting: "AI 已为每项审批提炼关键信息,助您高效决策。",
|
||||
stats: [
|
||||
{ label: "待审批总数", value: "12" },
|
||||
{ label: "平均审批耗时", value: "2.4h" },
|
||||
{ label: "本月已审批", value: "38" },
|
||||
],
|
||||
extra: "approver",
|
||||
groups: [
|
||||
{
|
||||
stage: "按类型分组",
|
||||
todos: [
|
||||
{ title: "待审批委托", count: 4, link: "/app/approvals", priority: "中" },
|
||||
{ title: "待审批立项", count: 3, link: "/app/approvals", priority: "中" },
|
||||
{ title: "待审批公告", count: 2, link: "/app/approvals", priority: "高" },
|
||||
{ title: "待审批定标", count: 3, link: "/app/approvals", priority: "高" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
supplier: {
|
||||
greeting: "AI 已为您推荐高匹配度项目,请关注报名与投标截止时间。",
|
||||
stats: [
|
||||
@@ -193,13 +158,14 @@ export const WORKBENCH: Record<RoleId, WbConfig> = {
|
||||
{ label: "待处理告警", value: "2", alert: true },
|
||||
],
|
||||
extra: "admin",
|
||||
agentMonitor: true,
|
||||
groups: [
|
||||
{
|
||||
stage: "待处理事项",
|
||||
todos: [
|
||||
{ title: "用户权限申请", count: 3, link: "/app/admin/users", priority: "中" },
|
||||
{ title: "流程配置变更", count: 1, link: "/app/admin/process", priority: "中" },
|
||||
{ title: "密钥漂移待执行", count: 1, link: "/app/admin/keys", priority: "高" },
|
||||
{ title: "系统告警待处理", count: 2, link: "/app/audit-logs", priority: "高" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Sparkles, ClipboardCheck, ArrowLeft, ThumbsUp, RotateCcw, Forward, FileText, Paperclip } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { PageHeader, ApprovalTracker } from "../components/shared/common";
|
||||
import { approvals, type ApprovalItem } from "../data/mock";
|
||||
|
||||
export function ApprovalCenter() {
|
||||
const [filter, setFilter] = useState("全部");
|
||||
const [active, setActive] = useState<ApprovalItem | null>(null);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
|
||||
const types = ["全部", "委托审批", "立项审批", "公告审批", "定标审批"];
|
||||
const list = approvals.filter((a) => filter === "全部" || a.type === filter);
|
||||
|
||||
if (active) return <ApprovalDetail item={active} onBack={() => setActive(null)} />;
|
||||
|
||||
function toggle(id: string) {
|
||||
setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="待审批"
|
||||
description="AI 已为每项审批提炼关键信息,支持批量审批同类型简单事项"
|
||||
actions={
|
||||
selected.length > 0 ? (
|
||||
<Button onClick={() => { toast.success(`已批量通过 ${selected.length} 项审批`); setSelected([]); }}>
|
||||
<ThumbsUp className="size-4" /> 批量通过({selected.length})
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs value={filter} onValueChange={setFilter} className="mb-4">
|
||||
<TabsList>
|
||||
{types.map((t) => (
|
||||
<TabsTrigger key={t} value={t}>
|
||||
{t}
|
||||
{t !== "全部" && (
|
||||
<span className="ml-1 rounded-full bg-muted px-1.5 text-xs">
|
||||
{approvals.filter((a) => a.type === t).length}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-3">
|
||||
{list.map((a) => (
|
||||
<Card key={a.id} className="flex-row items-center gap-4 p-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 accent-[var(--primary)]"
|
||||
checked={selected.includes(a.id)}
|
||||
onChange={() => toggle(a.id)}
|
||||
/>
|
||||
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600">
|
||||
<ClipboardCheck className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-emerald-50 px-2 py-0.5 text-xs text-emerald-600">{a.type}</span>
|
||||
{a.urgent && <span className="rounded bg-red-50 px-2 py-0.5 text-xs text-red-600">超时预警</span>}
|
||||
</div>
|
||||
<div className="mt-1 font-medium text-foreground">{a.project}</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
申请人 {a.applicant} · 金额 {a.amount.toLocaleString()} 万元 · {a.time}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setActive(a)}>查看详情并审批</Button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalDetail({ item, onBack }: { item: ApprovalItem; onBack: () => void }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-foreground">{item.type}</h1>
|
||||
<p className="text-sm text-muted-foreground">{item.project}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="col-span-2 space-y-6">
|
||||
{/* AI 审批摘要 */}
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{item.summary}</p>
|
||||
<div className="mt-4 grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ l: "申请金额", v: `${item.amount.toLocaleString()} 万元` },
|
||||
{ l: "申请人", v: item.applicant },
|
||||
{ l: "提交时间", v: item.time },
|
||||
].map((x) => (
|
||||
<div key={x.l} className="rounded-lg bg-white/70 p-3">
|
||||
<div className="text-xs text-muted-foreground">{x.l}</div>
|
||||
<div className="mt-0.5 font-medium text-foreground">{x.v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 申请单据(只读) */}
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">申请单据(只读)</h3>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
{[
|
||||
["项目名称", item.project],
|
||||
["审批类型", item.type],
|
||||
["预算/金额", `${item.amount.toLocaleString()} 万元`],
|
||||
["申请部门", "招标采购中心"],
|
||||
["事项说明", item.summary],
|
||||
].map(([l, v]) => (
|
||||
<div key={l} className="flex gap-4 border-b pb-3 last:border-0">
|
||||
<span className="w-24 shrink-0 text-muted-foreground">{l}</span>
|
||||
<span className="text-foreground">{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2 rounded-lg border bg-muted/30 px-3 py-2 text-sm">
|
||||
<Paperclip className="size-4 text-muted-foreground" />
|
||||
<span className="flex-1 text-foreground">{item.project}-申请附件.pdf</span>
|
||||
<Button size="sm" variant="outline" className="h-7">在线预览</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 审批操作 */}
|
||||
<Card className="gap-0 p-6">
|
||||
<h3 className="text-foreground">审批操作</h3>
|
||||
<label className="mb-1.5 mt-3 block text-sm">审批意见</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="请填写审批意见(驳回时必填)…"
|
||||
className="w-full resize-none rounded-lg border bg-input-background p-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<div className="mt-4 flex gap-3">
|
||||
<Button className="flex-1 bg-emerald-600 hover:bg-emerald-700" onClick={() => { toast.success("已审批通过"); onBack(); }}>
|
||||
<ThumbsUp className="size-4" /> 通过
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1 text-destructive" onClick={() => { toast.error("已驳回"); onBack(); }}>
|
||||
<RotateCcw className="size-4" /> 驳回
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast("已转签给下一审批人")}>
|
||||
<Forward className="size-4" /> 转签
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="gap-0 p-6">
|
||||
<h4 className="mb-4 text-foreground">审批流进度</h4>
|
||||
<ApprovalTracker
|
||||
nodes={[
|
||||
{ role: "经办人提交", name: item.applicant, status: "completed", time: item.time },
|
||||
{ role: "部门负责人", name: "已通过", status: "completed" },
|
||||
{ role: "您(当前节点)", name: "审批中", status: "current" },
|
||||
{ role: "审批完成", name: "流程归档", status: "pending" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { ArrowLeft, Sparkles, Trophy, ThumbsUp, RotateCcw, FileText } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { AiBadge, ApprovalTracker } from "../components/shared/common";
|
||||
|
||||
const ranking = [
|
||||
{ rank: 1, name: "供应商 A(浙江中控技术)", tech: 56, biz: 18, price: 28, total: 102, reason: "技术方案最优,冗余架构与本地化服务承诺突出,综合得分第一。" },
|
||||
{ rank: 2, name: "供应商 B(和利时科技)", tech: 52, biz: 17, price: 30, total: 99, reason: "报价最具竞争力,技术方案满足要求,略逊于第一名。" },
|
||||
{ rank: 3, name: "供应商 C(西门子中国)", tech: 54, biz: 16, price: 24, total: 94, reason: "品牌与性能优秀,但报价偏高,价格分较低。" },
|
||||
];
|
||||
|
||||
export function AwardApproval() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-foreground">定标审批</h1>
|
||||
<p className="text-sm text-muted-foreground">环氧丙烷装置大修工程 · 评标报告审批</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-3 gap-6">
|
||||
<div className="col-span-2 space-y-6">
|
||||
{/* 评标报告 */}
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">评标报告</h3>
|
||||
<AiBadge label="AI 汇总" />
|
||||
</div>
|
||||
|
||||
{/* AI 评标汇总表 */}
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/40 text-left text-muted-foreground">
|
||||
<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">
|
||||
{ranking.map((r) => (
|
||||
<tr key={r.rank} className={r.rank === 1 ? "bg-amber-50/50" : ""}>
|
||||
<td className="px-4 py-3">
|
||||
{r.rank === 1 ? (
|
||||
<span className="inline-flex items-center gap-1 font-medium text-amber-600">
|
||||
<Trophy className="size-4" /> 第 1 名
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">第 {r.rank} 名</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-foreground">{r.name}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{r.tech}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{r.biz}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{r.price}</td>
|
||||
<td className="px-4 py-3 font-semibold text-foreground">{r.total}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 中标候选人排名及理由 */}
|
||||
<div className="mt-5">
|
||||
<h4 className="mb-3 text-foreground">中标候选人排名及理由</h4>
|
||||
<div className="space-y-3">
|
||||
{ranking.map((r) => (
|
||||
<div key={r.rank} className="flex gap-3 rounded-lg border bg-card p-4">
|
||||
<div
|
||||
className={`flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold ${
|
||||
r.rank === 1 ? "bg-amber-100 text-amber-700" : "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{r.rank}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{r.name}</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Sparkles className="mr-1 inline size-3.5 text-violet-500" />
|
||||
{r.reason}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 审批决策 */}
|
||||
<Card className="gap-0 p-6">
|
||||
<h3 className="text-foreground">审批决策</h3>
|
||||
<div className="mt-3 rounded-lg bg-primary/5 p-4 text-sm">
|
||||
评标委员会推荐 <span className="font-semibold text-primary">供应商 A(浙江中控技术)</span> 为第一中标候选人,建议授予中标。
|
||||
</div>
|
||||
<label className="mb-1.5 mt-4 block text-sm">审批意见</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="请填写您的审批意见…"
|
||||
className="w-full resize-none rounded-lg border bg-input-background p-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<div className="mt-4 flex gap-3">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
toast.success("已同意推荐中标人,进入合同生成阶段");
|
||||
setTimeout(() => navigate("/app/projects/P2026003/contract"), 800);
|
||||
}}
|
||||
>
|
||||
<ThumbsUp className="size-4" /> 同意推荐中标人
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-destructive"
|
||||
onClick={() => toast.error("已驳回,项目将重新评标")}
|
||||
>
|
||||
<RotateCcw className="size-4" /> 驳回重评
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="gap-0 p-6">
|
||||
<h4 className="mb-4 text-foreground">审批流程跟踪</h4>
|
||||
<ApprovalTracker
|
||||
nodes={[
|
||||
{ role: "评标委员会", name: "完成评标并出具报告", status: "completed", time: "06-20 16:00" },
|
||||
{ role: "招标专员复核", name: "王强 · 已复核", status: "completed", time: "06-21 09:30" },
|
||||
{ role: "分管领导审批", name: "分管副总 · 审批中", status: "current" },
|
||||
{ role: "定标完成", name: "发出中标通知书", status: "pending" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ interface LogItem {
|
||||
}
|
||||
|
||||
const logSeq: LogItem[] = [
|
||||
{ time: "10:00:00", title: "系统自动开标", desc: "开标时间到达,系统自动启动开标程序", icon: Play, color: "#2563eb" },
|
||||
{ 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" },
|
||||
@@ -52,7 +52,7 @@ export function BidOpeningHall() {
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<PageHeader title="开标大厅" description="DCS 控制系统升级采购 · 自动化开标与链上存证" />
|
||||
<PageHeader title="开标大厅" description="DCS 控制系统升级采购 · 在线开标与链上存证" />
|
||||
</div>
|
||||
|
||||
{/* 顶部状态条 */}
|
||||
@@ -79,7 +79,7 @@ export function BidOpeningHall() {
|
||||
</div>
|
||||
{!started && (
|
||||
<Button className="bg-white text-primary hover:bg-white/90" onClick={() => setStarted(true)}>
|
||||
<Play className="size-4" /> 启动自动开标
|
||||
<Play className="size-4" /> 启动开标
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@ export function BidOpeningHall() {
|
||||
{!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>
|
||||
<p className="text-sm">点击右上角「启动开标」开始</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0">
|
||||
@@ -149,7 +149,7 @@ export function BidOpeningHall() {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Link2 className="size-3.5 text-violet-500" />
|
||||
<Link2 className="size-3.5 text-primary" />
|
||||
哈希存证:<span className="font-mono">{revealed ? b.hash : "····················"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { ArrowLeft, Sparkles, FileSignature, PenTool, Check, AlertCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { AiBadge, ApprovalTracker } from "../components/shared/common";
|
||||
|
||||
const diffs = [
|
||||
{ clause: "付款方式", standard: "验收合格后 30 日内支付 100%", current: "预付 10%,验收后 80%,质保期满付 10%", type: "modified" },
|
||||
{ clause: "质保期", standard: "12 个月", current: "36 个月", type: "modified" },
|
||||
{ clause: "违约金比例", standard: "合同总额 5%", current: "合同总额 5%", type: "same" },
|
||||
{ clause: "争议解决", standard: "提交滨州仲裁委员会仲裁", current: "提交滨州仲裁委员会仲裁", type: "same" },
|
||||
{ clause: "履约保证金", standard: "(无)", current: "合同总额 10%,履约完成后退还", type: "added" },
|
||||
];
|
||||
|
||||
export function ContractGeneration() {
|
||||
const navigate = useNavigate();
|
||||
const [signing, setSigning] = useState(false);
|
||||
const [signed, setSigned] = useState(false);
|
||||
|
||||
function sign() {
|
||||
setSigning(true);
|
||||
setTimeout(() => {
|
||||
setSigning(false);
|
||||
setSigned(true);
|
||||
toast.success("电子签章流程已发起,等待双方签署");
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-foreground">合同生成</h1>
|
||||
<p className="text-sm text-muted-foreground">环氧丙烷装置大修工程 · 中标人:浙江中控技术</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-3 gap-6">
|
||||
<Card className="col-span-2 gap-0 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileSignature className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">合同初稿 · 差异对比</h3>
|
||||
<AiBadge label="AI 拟稿" />
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">系统已自动填充,并高亮与标准模板的差异项</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex gap-3 text-xs">
|
||||
<span className="flex items-center gap-1"><span className="size-3 rounded bg-amber-100" /> 修改项</span>
|
||||
<span className="flex items-center gap-1"><span className="size-3 rounded bg-emerald-100" /> 新增项</span>
|
||||
<span className="flex items-center gap-1"><span className="size-3 rounded bg-muted" /> 一致</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/40 text-left text-muted-foreground">
|
||||
<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">
|
||||
{diffs.map((d) => (
|
||||
<tr
|
||||
key={d.clause}
|
||||
className={
|
||||
d.type === "modified"
|
||||
? "bg-amber-50/40"
|
||||
: d.type === "added"
|
||||
? "bg-emerald-50/40"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-foreground">{d.clause}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{d.standard}</td>
|
||||
<td className="px-4 py-3 text-foreground">{d.current}</td>
|
||||
<td className="px-4 py-3">
|
||||
{d.type === "modified" && <span className="text-amber-600">已修改</span>}
|
||||
{d.type === "added" && <span className="text-emerald-600">新增</span>}
|
||||
{d.type === "same" && <span className="text-muted-foreground">一致</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50/60 p-3 text-sm text-muted-foreground">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-500" />
|
||||
小滨提示:本合同存在 2 项条款修改、1 项新增条款,建议法务重点关注「履约保证金」与「质保期」条款。
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end gap-3">
|
||||
<Button variant="outline">下载合同</Button>
|
||||
{!signed ? (
|
||||
<Button onClick={sign} disabled={signing}>
|
||||
{signing ? <Sparkles className="size-4 animate-spin" /> : <PenTool className="size-4" />}
|
||||
{signing ? "发起中…" : "发起电子签章"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => { toast.success("合同已归档"); navigate("/app/projects"); }}>
|
||||
<Check className="size-4" /> 完成并归档
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 p-6">
|
||||
<h4 className="mb-4 text-foreground">电子签章进度</h4>
|
||||
<ApprovalTracker
|
||||
nodes={[
|
||||
{ role: "合同生成", name: "AI 自动拟稿完成", status: "completed", time: "06-22 10:00" },
|
||||
{ role: "法务审核", name: "法务合规 · 已通过", status: signed ? "completed" : "current", time: signed ? "06-22 11:20" : undefined },
|
||||
{ role: "采购方签章", name: "滨化集团 · 电子签章", status: signed ? "current" : "pending" },
|
||||
{ role: "供应商签章", name: "浙江中控技术", status: "pending" },
|
||||
{ role: "合同归档", name: "归档至合同管理库", status: "pending" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Sparkles, Search, FileSignature, PenTool } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { PageHeader } from "../components/shared/common";
|
||||
import { contracts } from "../data/mock";
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
草稿: "bg-slate-100 text-slate-600",
|
||||
审批中: "bg-amber-100 text-amber-700",
|
||||
待签署: "bg-blue-100 text-blue-700",
|
||||
已生效: "bg-emerald-100 text-emerald-700",
|
||||
已归档: "bg-teal-100 text-teal-700",
|
||||
};
|
||||
|
||||
export function ContractManagement() {
|
||||
const navigate = useNavigate();
|
||||
const [q, setQ] = useState("");
|
||||
const list = contracts.filter((c) => c.project.includes(q) || c.code.includes(q));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="合同管理"
|
||||
description="AI 自动生成合同初稿,差异项高亮标注,支持电子签章"
|
||||
actions={
|
||||
<Button onClick={() => navigate("/app/projects/P2026003/contract")}>
|
||||
<Sparkles className="size-4" /> AI 生成合同
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card className="mb-4 flex-row items-center gap-3 p-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索合同编号 / 项目名称"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 overflow-hidden p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40 text-left text-muted-foreground">
|
||||
<th className="px-5 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">
|
||||
{list.map((c) => (
|
||||
<tr key={c.id} className="hover:bg-muted/20">
|
||||
<td className="px-5 py-3 font-mono text-xs text-foreground">{c.code}</td>
|
||||
<td className="px-4 py-3 font-medium text-foreground">{c.project}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{c.supplier}</td>
|
||||
<td className="px-4 py-3 font-medium text-foreground">{c.amount.toLocaleString()}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs ${statusColor[c.status]}`}>{c.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{c.status === "草稿" && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1" onClick={() => navigate("/app/projects/P2026003/contract")}>
|
||||
<Sparkles className="size-3.5" /> 生成
|
||||
</Button>
|
||||
)}
|
||||
{c.status === "待签署" && (
|
||||
<Button size="sm" className="h-7 gap-1" onClick={() => toast.success("已发起电子签章")}>
|
||||
<PenTool className="size-3.5" /> 签章
|
||||
</Button>
|
||||
)}
|
||||
{(c.status === "已生效" || c.status === "已归档" || c.status === "审批中") && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1" onClick={() => toast("打开合同详情")}>
|
||||
<FileSignature className="size-3.5" /> 查看
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { ArrowLeft, Sparkles, Loader2, Check, ArrowRight, FileText } from "lucide-react";
|
||||
import { ArrowLeft, Sparkles, Loader2, Check, ArrowRight, FileText, FileDown, LayoutTemplate } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
@@ -102,6 +102,7 @@ export function Delegation() {
|
||||
);
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [parsed, setParsed] = useState(false);
|
||||
const [template, setTemplate] = useState("自动");
|
||||
|
||||
const form = parsed ? filledForm : emptyForm;
|
||||
|
||||
@@ -131,8 +132,66 @@ export function Delegation() {
|
||||
<Steps steps={STEPS} current={step} />
|
||||
</Card>
|
||||
|
||||
{/* 步骤一:填写需求 */}
|
||||
{/* 步骤一:填写需求 —— 多种发起方式 */}
|
||||
{step === 0 && (
|
||||
<>
|
||||
<Card className="mb-6 gap-0 p-5">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="text-foreground">选择发起方式</h3>
|
||||
<span className="text-xs text-muted-foreground">支持三种方式,任选其一</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setParsed(true);
|
||||
toast.success("已导入采购计划,AI 已据此预填委托单");
|
||||
}}
|
||||
className="group flex items-start gap-3 rounded-xl border bg-card p-4 text-left transition-all hover:border-primary hover:shadow-md"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary group-hover:bg-primary group-hover:text-white">
|
||||
<FileDown className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">导入采购计划</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">从年度采购计划一键带入</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className="group flex items-start gap-3 rounded-xl border bg-card p-4 text-left transition-all hover:border-primary hover:shadow-md"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary group-hover:bg-primary group-hover:text-white">
|
||||
<FileText className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">直接填写委托单</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">跳过 AI 解析,手动录入</p>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex items-start gap-3 rounded-xl border bg-card p-4">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary">
|
||||
<LayoutTemplate className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-foreground">选择模板</div>
|
||||
<select
|
||||
value={template}
|
||||
onChange={(e) => {
|
||||
setTemplate(e.target.value);
|
||||
toast.success(e.target.value === "自动" ? "已设为 AI 自动选择最合适的模板" : `已选择「${e.target.value}」模板`);
|
||||
}}
|
||||
className="mt-1 h-8 w-full rounded-md border bg-input-background px-2 text-xs outline-none focus:border-primary"
|
||||
>
|
||||
<option value="自动">🤖 自动(AI 智能选择)</option>
|
||||
<option value="软件采购">软件采购模板</option>
|
||||
<option value="工程施工">工程施工模板</option>
|
||||
<option value="货物采购">货物采购模板</option>
|
||||
<option value="服务采购">服务采购模板</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<Card className="col-span-2 gap-0 p-6">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
@@ -148,7 +207,7 @@ export function Delegation() {
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Sparkles className="mr-1 inline size-3.5 text-violet-500" />
|
||||
<Sparkles className="mr-1 inline size-3.5 text-primary" />
|
||||
小滨将自动识别项目名称、预算、招标方式、技术与资格要求等字段
|
||||
</p>
|
||||
<Button onClick={handleParse} disabled={parsing}>
|
||||
@@ -167,7 +226,7 @@ export function Delegation() {
|
||||
|
||||
<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" />
|
||||
<Sparkles className="size-4 text-primary" />
|
||||
<h4 className="text-foreground">AI 解析提示</h4>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2.5 text-sm text-muted-foreground">
|
||||
@@ -202,6 +261,7 @@ export function Delegation() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 步骤二:确认预填单 */}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Search, Filter, Dices, Loader2, Check, ShieldAlert, UserCog } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "../components/ui/dialog";
|
||||
import { Search, Filter } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -14,42 +7,25 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../components/ui/select";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { PageHeader } from "../components/shared/common";
|
||||
import { experts } from "../data/mock";
|
||||
|
||||
// 说明(评审意见):专家的随机抽取与回避校验属于"具体项目评标流程",
|
||||
// 不在专家总体管理页体现;本页仅做专家库信息维护与查询。
|
||||
export function ExpertManagement() {
|
||||
const [q, setQ] = useState("");
|
||||
const [cat, setCat] = useState("all");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [drawing, setDrawing] = useState(false);
|
||||
const [result, setResult] = useState<typeof experts | null>(null);
|
||||
|
||||
const list = experts.filter(
|
||||
(e) => (cat === "all" || e.category === cat) && e.name.includes(q),
|
||||
);
|
||||
|
||||
function draw() {
|
||||
setDrawing(true);
|
||||
setResult(null);
|
||||
setTimeout(() => {
|
||||
setDrawing(false);
|
||||
// 抽取未需回避的专家
|
||||
setResult(experts.filter((e) => !e.avoid).slice(0, 3));
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="专家管理"
|
||||
description="管理内外部评标专家,支持随机抽取与回避校验"
|
||||
actions={
|
||||
<Button onClick={() => { setOpen(true); setResult(null); }}>
|
||||
<Dices className="size-4" /> 抽取专家
|
||||
</Button>
|
||||
}
|
||||
description="维护内外部评标专家库信息(随机抽取与回避校验在具体项目评标流程中进行)"
|
||||
/>
|
||||
|
||||
<Card className="mb-4 flex-row items-center gap-3 p-4">
|
||||
@@ -88,7 +64,6 @@ export function ExpertManagement() {
|
||||
<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">
|
||||
@@ -102,109 +77,17 @@ export function ExpertManagement() {
|
||||
<td className="px-4 py-4">
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs ${
|
||||
e.type === "内部" ? "bg-blue-50 text-blue-600" : "bg-violet-50 text-violet-600"
|
||||
e.type === "内部" ? "bg-blue-50 text-blue-600" : "bg-sky-50 text-sky-600"
|
||||
}`}
|
||||
>
|
||||
{e.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
{e.avoid ? (
|
||||
<span className="inline-flex items-center gap-1 text-red-600">
|
||||
<ShieldAlert className="size-4" /> 需回避
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-emerald-600">可参与</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
|
||||
{/* 抽取弹窗 */}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Dices className="size-5 text-primary" /> 随机抽取评标专家
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{!result && !drawing && (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<label className="mb-1.5 block">抽取专业</label>
|
||||
<Select defaultValue="自动化控制">
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="自动化控制">自动化控制</SelectItem>
|
||||
<SelectItem value="化工工艺">化工工艺</SelectItem>
|
||||
<SelectItem value="环保工程">环保工程</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block">抽取人数</label>
|
||||
<Select defaultValue="3">
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3">3 人</SelectItem>
|
||||
<SelectItem value="5">5 人</SelectItem>
|
||||
<SelectItem value="7">7 人</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 rounded-lg bg-muted/40 p-3 text-xs text-muted-foreground">
|
||||
<ShieldAlert className="mt-0.5 size-4 shrink-0 text-amber-500" />
|
||||
系统将自动执行回避校验,排除与投标人存在关联或利益冲突的专家。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{drawing && (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-10">
|
||||
<Loader2 className="size-10 animate-spin text-primary" />
|
||||
<p className="text-sm text-muted-foreground">正在随机抽取并执行回避校验…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="space-y-3 py-2">
|
||||
<div className="flex items-center gap-2 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700">
|
||||
<Check className="size-4" /> 抽取完成,回避校验通过,已排除 1 名需回避专家
|
||||
</div>
|
||||
{result.map((e) => (
|
||||
<div key={e.id} className="flex items-center gap-3 rounded-lg border bg-card p-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<UserCog className="size-4" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{e.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{e.title} · {e.category} · {e.region}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-emerald-600">无需回避</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{!result ? (
|
||||
<Button onClick={draw} disabled={drawing}>
|
||||
<Dices className="size-4" /> {drawing ? "抽取中…" : "开始抽取"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => setOpen(false)}>确认专家组</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { GitMerge, Sparkles, Check, TrendingDown, Clock, Users, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { PageHeader, AiBadge } from "../components/shared/common";
|
||||
import { mergeCandidates } from "../data/mock";
|
||||
|
||||
export function MergeBidding() {
|
||||
const [picked, setPicked] = useState<string[]>(mergeCandidates.map((m) => m.id));
|
||||
const [merged, setMerged] = useState(false);
|
||||
|
||||
const chosen = mergeCandidates.filter((m) => picked.includes(m.id));
|
||||
const totalBudget = chosen.reduce((s, m) => s + m.budget, 0);
|
||||
|
||||
function toggle(id: string) {
|
||||
setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="合并招标" description="AI 检测同类型未完成立项的委托,给出合并建议与效益估算" />
|
||||
|
||||
{/* 提示横幅 */}
|
||||
<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> 个可合并的招标委托(采购品类均为「通用设备」),合并后可降低成本、缩短周期。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{merged ? (
|
||||
<Card className="flex flex-col items-center justify-center gap-4 py-16">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-100">
|
||||
<Check className="size-8 text-emerald-600" />
|
||||
</div>
|
||||
<h2 className="text-foreground">合并成功</h2>
|
||||
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||
已将 {chosen.length} 个委托合并为新招标项目「通用设备阀门管件集中采购」,原招标方案已自动更新,标注「合并自:
|
||||
{chosen.map((c) => c.name).join("、")}」。
|
||||
</p>
|
||||
<Button onClick={() => toast.success("已跳转至新项目")}>查看合并后项目</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* 可合并项目 */}
|
||||
<Card className="col-span-2 gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitMerge className="size-5 text-violet-500" />
|
||||
<h3 className="text-foreground">可合并委托列表</h3>
|
||||
<AiBadge label="AI 推荐" />
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">已选 {chosen.length} / {mergeCandidates.length}</span>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{mergeCandidates.map((m) => {
|
||||
const on = picked.includes(m.id);
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => toggle(m.id)}
|
||||
className="flex w-full items-center gap-4 px-6 py-4 text-left hover:bg-muted/20"
|
||||
>
|
||||
<div
|
||||
className={`flex size-5 shrink-0 items-center justify-center rounded border ${
|
||||
on ? "border-primary bg-primary text-white" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{on && <Check className="size-3.5" />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{m.name}</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
{m.dept} · 预算 {m.budget} 万元 · {m.category}
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-md bg-violet-50 px-2 py-1 text-xs text-violet-600">
|
||||
时间重叠度 {m.overlap}%
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 效益估算 */}
|
||||
<Card className="gap-0 p-6">
|
||||
<h4 className="mb-4 text-foreground">预期效益估算</h4>
|
||||
<div className="space-y-4">
|
||||
<Benefit icon={Users} color="#2563eb" label="预算汇总" value={`${totalBudget} 万元`} desc={`合并 ${chosen.length} 个委托`} />
|
||||
<Benefit icon={Clock} color="#14b8a6" label="时间节省估算" value="~15 天" desc="减少重复寻源与评标" />
|
||||
<Benefit icon={TrendingDown} color="#8b5cf6" label="供应商资源优化" value="↓ 30%" desc="集中议价提升规模效益" />
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Button className="w-full" disabled={chosen.length < 2} onClick={() => { setMerged(true); toast.success("委托已合并"); }}>
|
||||
<GitMerge className="size-4" /> 确认合并
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={() => toast("已暂不合并")}>
|
||||
<X className="size-4" /> 暂不合并
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Benefit({ icon: Icon, color, label, value, desc }: { icon: any; color: string; label: string; value: string; desc: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-card p-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${color}1a`, color }}>
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="font-semibold text-foreground">{value}</div>
|
||||
<div className="text-xs text-muted-foreground">{desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
FileText,
|
||||
Users,
|
||||
Gavel,
|
||||
Award,
|
||||
FileSignature,
|
||||
Building2,
|
||||
Eye,
|
||||
Send,
|
||||
@@ -18,30 +16,21 @@ import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../components/ui/tabs";
|
||||
import { StatusBadge, AiBadge, Steps } from "../components/shared/common";
|
||||
import { StatusBadge, Steps } from "../components/shared/common";
|
||||
import { projects } from "../data/mock";
|
||||
|
||||
const PHASES = ["需求委托", "立项审批", "招标文件", "投标开标", "在线评标", "定标审批", "合同归档"];
|
||||
// 正确的招标业务流程(评审意见修正)
|
||||
const PHASES = ["发起委托", "供应商寻源", "招标立项", "供应商投标", "开标评标", "定标", "归档"];
|
||||
|
||||
export function ProjectDetail() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const project = projects.find((p) => p.id === id) ?? projects[0];
|
||||
const phaseIdx = Math.max(0, PHASES.indexOf(project.status === "已完成" ? "合同归档" : project.status));
|
||||
const phaseIdx = project.status === "已完成" ? PHASES.length - 1 : 4;
|
||||
|
||||
const [genPlan, setGenPlan] = useState(false);
|
||||
const [planDone, setPlanDone] = useState(false);
|
||||
const [genFile, setGenFile] = useState(false);
|
||||
const [fileDone, setFileDone] = useState(false);
|
||||
|
||||
function runPlan() {
|
||||
setGenPlan(true);
|
||||
setTimeout(() => {
|
||||
setGenPlan(false);
|
||||
setPlanDone(true);
|
||||
toast.success("AI 已生成招标方案,请查看方案摘要");
|
||||
}, 1600);
|
||||
}
|
||||
function runFile() {
|
||||
setGenFile(true);
|
||||
setTimeout(() => {
|
||||
@@ -55,8 +44,8 @@ export function ProjectDetail() {
|
||||
{ label: "项目编号", value: project.code },
|
||||
{ label: "招标方式", value: project.type },
|
||||
{ label: "预算金额", value: `${project.budget.toLocaleString()} 万元` },
|
||||
{ label: "需求部门", value: project.dept },
|
||||
{ label: "负责人", value: project.owner },
|
||||
{ label: "委托单位", value: project.dept },
|
||||
{ label: "委托人", value: project.owner },
|
||||
{ label: "创建时间", value: project.createdAt },
|
||||
];
|
||||
|
||||
@@ -73,6 +62,9 @@ export function ProjectDetail() {
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{project.code}</p>
|
||||
</div>
|
||||
<Button onClick={() => toast.success("已提交至 OA 审批流,请在 OA 中查看审批进度")}>
|
||||
<Send className="size-4" /> 提交审批
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 流程总览 */}
|
||||
@@ -102,48 +94,12 @@ export function ProjectDetail() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="action" className="mt-5 space-y-6">
|
||||
{/* 招标方案生成 */}
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">招标方案</h3>
|
||||
<AiBadge label="AI 生成" />
|
||||
</div>
|
||||
{!planDone && (
|
||||
<Button onClick={runPlan} disabled={genPlan}>
|
||||
{genPlan ? <Loader2 className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
|
||||
{genPlan ? "AI 生成中…" : "生成招标方案"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{planDone ? (
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ t: "标段划分建议", d: "建议划分为 2 个标段:①控制系统硬件 ②系统集成与服务,便于专业化竞争。" },
|
||||
{ t: "推荐评标办法", d: "综合评估法:技术 60 分 / 商务 20 分 / 价格 20 分,技术权重较高。" },
|
||||
{ t: "推荐供应商短名单", d: "基于合格供方库与历史业绩,推荐浙江中控、和利时、西门子等 3 家。" },
|
||||
].map((c) => (
|
||||
<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>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{c.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
点击「生成招标方案」,小滨将基于委托单自动生成标段划分、评标办法与推荐短名单建议。
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 行动卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ActionCard
|
||||
icon={Sparkles}
|
||||
title="AI 智慧寻源"
|
||||
desc="AI 主动外发邀请与回执跟踪"
|
||||
desc="从互联网 / SRM 库 / 临时供应商池寻源"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/sourcing`)}
|
||||
/>
|
||||
<ActionCard
|
||||
@@ -155,7 +111,7 @@ export function ProjectDetail() {
|
||||
<ActionCard
|
||||
icon={Gavel}
|
||||
title="开标大厅"
|
||||
desc="自动开标、解密与哈希上链存证"
|
||||
desc="在线开标、解密与哈希上链存证"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/opening`)}
|
||||
/>
|
||||
<ActionCard
|
||||
@@ -164,18 +120,6 @@ export function ProjectDetail() {
|
||||
desc="专家脱敏评标与 AI 辅助分析"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/evaluation`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={Award}
|
||||
title="定标审批"
|
||||
desc="评标报告预览与定标决策"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/award`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={FileSignature}
|
||||
title="合同生成"
|
||||
desc="自动拟稿、差异对比与电子签章"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/contract`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={Building2}
|
||||
title="供应商库"
|
||||
@@ -184,18 +128,17 @@ export function ProjectDetail() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 招标文件 */}
|
||||
{/* 招标文件(方案与文件合一) */}
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">招标文件</h3>
|
||||
{fileDone && <AiBadge label="AI 生成" />}
|
||||
</div>
|
||||
{!fileDone ? (
|
||||
<Button onClick={runFile} disabled={genFile}>
|
||||
{genFile ? <Loader2 className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
|
||||
{genFile ? "AI 生成文件中…" : "AI 生成文件"}
|
||||
{genFile ? "AI 生成中…" : "AI 生成招标文件"}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
@@ -209,21 +152,37 @@ export function ProjectDetail() {
|
||||
)}
|
||||
</div>
|
||||
{fileDone ? (
|
||||
<div className="mt-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<FileText className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{project.name}-招标文件.docx</div>
|
||||
<div className="text-xs text-muted-foreground">AI 生成 · 共 42 页 · 支持在线编辑</div>
|
||||
</div>
|
||||
<Check className="size-5 text-emerald-500" />
|
||||
<>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ t: "标段划分建议", d: "建议划分为 2 个标段:①控制系统硬件 ②系统集成与服务,便于专业化竞争。" },
|
||||
{ t: "推荐评标办法", d: "综合评估法:技术 60 分 / 商务 20 分 / 价格 20 分,技术权重较高。" },
|
||||
{ t: "推荐供应商短名单", d: "基于合格供方库与历史业绩,推荐浙江中控、和利时、西门子等 3 家。" },
|
||||
].map((c) => (
|
||||
<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-primary" /> {c.t}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{c.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<FileText className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{project.name}-招标文件.docx</div>
|
||||
<div className="text-xs text-muted-foreground">AI 生成 · 共 42 页 · 支持在线编辑</div>
|
||||
</div>
|
||||
<Check className="size-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
点击「AI 生成文件」,系统将根据招标方案自动生成招标文件初稿,支持在线预览、编辑与一键发布公告。
|
||||
点击「AI 生成招标文件」,小滨将基于委托单自动生成标段划分、评标办法、推荐短名单与完整招标文件初稿,支持在线预览、编辑与一键发布公告。
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Plus, Search, Filter, ChevronRight } from "lucide-react";
|
||||
import { Search, Filter, ChevronRight, ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, Columns3, Send } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import {
|
||||
@@ -10,66 +11,184 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../components/ui/dropdown-menu";
|
||||
import { PageHeader, StatusBadge } from "../components/shared/common";
|
||||
import { projects } from "../data/mock";
|
||||
|
||||
type ColKey = "type" | "budget" | "owner" | "dept" | "status" | "progress";
|
||||
type SortKey = "name" | "budget" | "progress";
|
||||
|
||||
const COLUMNS: { key: ColKey; label: string; sortable?: SortKey }[] = [
|
||||
{ key: "type", label: "招标方式" },
|
||||
{ key: "budget", label: "预算(万元)", sortable: "budget" },
|
||||
{ key: "owner", label: "委托人" },
|
||||
{ key: "dept", label: "委托单位" },
|
||||
{ key: "status", label: "当前阶段" },
|
||||
{ key: "progress", label: "进度", sortable: "progress" },
|
||||
];
|
||||
|
||||
export function ProjectList() {
|
||||
const navigate = useNavigate();
|
||||
const [q, setQ] = useState("");
|
||||
const [type, setType] = useState("all");
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const [owner, setOwner] = useState("");
|
||||
const [dept, setDept] = useState("");
|
||||
const [minBudget, setMinBudget] = useState("");
|
||||
const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" } | null>(null);
|
||||
const [cols, setCols] = useState<Record<ColKey, boolean>>({
|
||||
type: true,
|
||||
budget: true,
|
||||
owner: true,
|
||||
dept: true,
|
||||
status: true,
|
||||
progress: true,
|
||||
});
|
||||
|
||||
const list = projects.filter(
|
||||
(p) =>
|
||||
(type === "all" || p.type === type) &&
|
||||
(p.name.includes(q) || p.code.includes(q)),
|
||||
);
|
||||
const list = useMemo(() => {
|
||||
let r = projects.filter(
|
||||
(p) =>
|
||||
(type === "all" || p.type === type) &&
|
||||
(p.name.includes(q) || p.code.includes(q)) &&
|
||||
(!owner || p.owner.includes(owner)) &&
|
||||
(!dept || p.dept.includes(dept)) &&
|
||||
(!minBudget || p.budget >= Number(minBudget)),
|
||||
);
|
||||
if (sort) {
|
||||
r = [...r].sort((a, b) => {
|
||||
let cmp = 0;
|
||||
if (sort.key === "name") cmp = a.name.localeCompare(b.name, "zh");
|
||||
else cmp = (a[sort.key] as number) - (b[sort.key] as number);
|
||||
return sort.dir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}, [q, type, owner, dept, minBudget, sort]);
|
||||
|
||||
function toggleSort(key: SortKey) {
|
||||
setSort((s) =>
|
||||
s?.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" },
|
||||
);
|
||||
}
|
||||
|
||||
function SortIcon({ k }: { k: SortKey }) {
|
||||
if (sort?.key !== k) return <ChevronsUpDown className="size-3.5 text-muted-foreground/50" />;
|
||||
return sort.dir === "asc" ? <ArrowUp className="size-3.5 text-primary" /> : <ArrowDown className="size-3.5 text-primary" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="项目管理"
|
||||
description="管理全部招标项目,覆盖从需求委托到合同归档的全生命周期"
|
||||
description="管理全部招标项目,覆盖从需求委托到归档的全生命周期"
|
||||
actions={
|
||||
<Button onClick={() => navigate("/app/delegation")}>
|
||||
<Plus className="size-4" /> 发起委托
|
||||
<Button onClick={() => toast.success("已提交至 OA 审批流,请在 OA 中查看审批进度")}>
|
||||
<Send className="size-4" /> 提交审批
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card className="mb-4 flex-row items-center gap-3 p-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索项目名称或编号"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Card className="mb-4 gap-3 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索项目名称或编号"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger className="w-40">
|
||||
<Filter className="size-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部招标方式</SelectItem>
|
||||
<SelectItem value="公开招标">公开招标</SelectItem>
|
||||
<SelectItem value="邀请招标">邀请招标</SelectItem>
|
||||
<SelectItem value="竞争性谈判">竞争性谈判</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant={advanced ? "default" : "outline"} onClick={() => setAdvanced((v) => !v)}>
|
||||
<SlidersHorizontal className="size-4" /> 高级搜索
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns3 className="size-4" /> 列设置
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuLabel>显示的列</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{COLUMNS.map((c) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={c.key}
|
||||
checked={cols[c.key]}
|
||||
onCheckedChange={(v) => setCols((s) => ({ ...s, [c.key]: !!v }))}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{c.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger className="w-40">
|
||||
<Filter className="size-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部招标方式</SelectItem>
|
||||
<SelectItem value="公开招标">公开招标</SelectItem>
|
||||
<SelectItem value="邀请招标">邀请招标</SelectItem>
|
||||
<SelectItem value="竞争性谈判">竞争性谈判</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{advanced && (
|
||||
<div className="grid grid-cols-4 gap-3 rounded-lg border bg-muted/20 p-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">委托人</label>
|
||||
<input value={owner} onChange={(e) => setOwner(e.target.value)} placeholder="按委托人筛选" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">委托单位</label>
|
||||
<input value={dept} onChange={(e) => setDept(e.target.value)} placeholder="按委托单位筛选" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">最低预算(万元)</label>
|
||||
<input value={minBudget} onChange={(e) => setMinBudget(e.target.value.replace(/\D/g, ""))} placeholder="如 500" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button variant="outline" className="h-9" onClick={() => { setOwner(""); setDept(""); setMinBudget(""); }}>
|
||||
重置条件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 overflow-hidden p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40 text-left text-muted-foreground">
|
||||
<th className="px-5 py-3 font-medium">项目名称 / 编号</th>
|
||||
<th className="px-5 py-3 font-medium">招标方式</th>
|
||||
<th className="px-5 py-3 font-medium">预算(万元)</th>
|
||||
<th className="px-5 py-3 font-medium">负责人</th>
|
||||
<th className="px-5 py-3 font-medium">当前阶段</th>
|
||||
<th className="px-5 py-3 font-medium">进度</th>
|
||||
<th className="px-5 py-3 font-medium">
|
||||
<button onClick={() => toggleSort("name")} className="flex items-center gap-1 hover:text-foreground">
|
||||
项目名称 / 编号 <SortIcon k="name" />
|
||||
</button>
|
||||
</th>
|
||||
{COLUMNS.map((c) =>
|
||||
cols[c.key] ? (
|
||||
<th key={c.key} className="px-5 py-3 font-medium">
|
||||
{c.sortable ? (
|
||||
<button onClick={() => toggleSort(c.sortable!)} className="flex items-center gap-1 hover:text-foreground">
|
||||
{c.label} <SortIcon k={c.sortable} />
|
||||
</button>
|
||||
) : (
|
||||
c.label
|
||||
)}
|
||||
</th>
|
||||
) : null,
|
||||
)}
|
||||
<th className="px-5 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -84,20 +203,25 @@ export function ProjectList() {
|
||||
<div className="font-medium text-foreground">{p.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{p.code}</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-muted-foreground">{p.type}</td>
|
||||
<td className="px-5 py-4 font-medium text-foreground">{p.budget.toLocaleString()}</td>
|
||||
<td className="px-5 py-4 text-muted-foreground">{p.owner}</td>
|
||||
<td className="px-5 py-4">
|
||||
<StatusBadge status={p.status} />
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary" style={{ width: `${p.progress}%` }} />
|
||||
{cols.type && <td className="px-5 py-4 text-muted-foreground">{p.type}</td>}
|
||||
{cols.budget && <td className="px-5 py-4 font-medium text-foreground">{p.budget.toLocaleString()}</td>}
|
||||
{cols.owner && <td className="px-5 py-4 text-muted-foreground">{p.owner}</td>}
|
||||
{cols.dept && <td className="px-5 py-4 text-muted-foreground">{p.dept}</td>}
|
||||
{cols.status && (
|
||||
<td className="px-5 py-4">
|
||||
<StatusBadge status={p.status} />
|
||||
</td>
|
||||
)}
|
||||
{cols.progress && (
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary" style={{ width: `${p.progress}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{p.progress}%</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{p.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-5 py-4 text-right">
|
||||
<ChevronRight className="size-4 text-muted-foreground" />
|
||||
</td>
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { ArrowLeft, Sparkles, Send, Eye, Check, Globe } from "lucide-react";
|
||||
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 } from "../data/mock";
|
||||
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 viewed = sourcingRecords.filter((r) => r.viewed).length;
|
||||
const registered = sourcingRecords.filter((r) => r.registered).length;
|
||||
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>
|
||||
@@ -17,15 +43,50 @@ export function SmartSourcing() {
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-foreground">AI 智慧寻源结果</h1>
|
||||
<AiBadge label="AI 主动寻源" />
|
||||
<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: sourcingRecords.length, c: "#2563eb" },
|
||||
{ l: "AI 外发邀请", v: filtered.length, c: "var(--primary)" },
|
||||
{ l: "已查看回执", v: viewed, c: "#f59e0b" },
|
||||
{ l: "已报名", v: registered, c: "#14b8a6" },
|
||||
].map((s) => (
|
||||
@@ -38,13 +99,17 @@ export function SmartSourcing() {
|
||||
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center gap-2 border-b px-6 py-4">
|
||||
<Globe className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">互联网平台主动外发邀请记录</h3>
|
||||
<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>
|
||||
@@ -53,9 +118,12 @@ export function SmartSourcing() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{sourcingRecords.map((r) => (
|
||||
{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">
|
||||
@@ -79,9 +147,9 @@ export function SmartSourcing() {
|
||||
</table>
|
||||
</Card>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 rounded-lg border border-violet-200 bg-violet-50/50 p-3 text-sm text-muted-foreground">
|
||||
<Sparkles className="mt-0.5 size-4 shrink-0 text-violet-500" />
|
||||
小滨已根据采购品类与历史成交,自动从全国公共资源交易平台等渠道匹配并外发邀请,未查看回执的供应商已为您高亮提醒。
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { PackagePlus, Sparkles, ArrowRight, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { PageHeader, RiskBadge } from "../components/shared/common";
|
||||
import { suppliers } from "../data/mock";
|
||||
|
||||
type Status = "待发起" | "审批中" | "已入库";
|
||||
|
||||
export function SupplierInbound() {
|
||||
const [rows, setRows] = useState(
|
||||
suppliers.slice(0, 5).map((s, i) => ({
|
||||
...s,
|
||||
inbound: (i === 0 ? "已入库" : i === 1 ? "审批中" : "待发起") as Status,
|
||||
})),
|
||||
);
|
||||
|
||||
function advance(id: string) {
|
||||
setRows((r) =>
|
||||
r.map((x) =>
|
||||
x.id === id ? { ...x, inbound: x.inbound === "待发起" ? "审批中" : "已入库" } : x,
|
||||
),
|
||||
);
|
||||
toast.success("已发起供应商入库流程,自动推送至 SRM 系统");
|
||||
}
|
||||
|
||||
const statusColor: Record<Status, string> = {
|
||||
待发起: "bg-slate-100 text-slate-600",
|
||||
审批中: "bg-amber-100 text-amber-700",
|
||||
已入库: "bg-emerald-100 text-emerald-700",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="供应商入库管理" description="定标后触发的供应商入库流程,审批通过自动同步至 SRM" />
|
||||
|
||||
<div className="mb-4 flex items-start gap-2 rounded-xl border border-violet-200 bg-violet-50/50 p-4 text-sm text-muted-foreground">
|
||||
<Sparkles className="mt-0.5 size-4 shrink-0 text-violet-500" />
|
||||
以下供应商来自定标后自动触发的入库清单,发起入库后系统将自动同步工商风险信息并推送至 SRM 系统。
|
||||
</div>
|
||||
|
||||
<Card className="gap-0 overflow-hidden p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40 text-left text-muted-foreground">
|
||||
<th className="px-5 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">
|
||||
{rows.map((s) => (
|
||||
<tr key={s.id} className="hover:bg-muted/20">
|
||||
<td className="px-5 py-3 font-medium text-foreground">{s.name}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.category}</td>
|
||||
<td className="px-4 py-3">
|
||||
<RiskBadge risk={s.risk} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs ${statusColor[s.inbound]}`}>{s.inbound}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{s.inbound === "待发起" && (
|
||||
<Button size="sm" className="h-7 gap-1" onClick={() => advance(s.id)}>
|
||||
<PackagePlus className="size-3.5" /> 发起入库
|
||||
</Button>
|
||||
)}
|
||||
{s.inbound === "审批中" && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1" onClick={() => advance(s.id)}>
|
||||
<ArrowRight className="size-3.5" /> 推进完成
|
||||
</Button>
|
||||
)}
|
||||
{s.inbound === "已入库" && (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-emerald-600">
|
||||
<Check className="size-4" /> 已同步 SRM
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
PackagePlus,
|
||||
Building2,
|
||||
Phone,
|
||||
Star,
|
||||
Sparkles,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -42,17 +38,7 @@ export function SupplierManagement() {
|
||||
<div>
|
||||
<PageHeader
|
||||
title="供应商管理"
|
||||
description="管理已注册供应商,工商风险信息由 AI 自动同步"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => toast.success("已发起供应商入库流程")}>
|
||||
<PackagePlus className="size-4" /> 发起入库流程
|
||||
</Button>
|
||||
<Button onClick={() => toast.success("新增供应商")}>
|
||||
<Plus className="size-4" /> 新增供应商
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
description="管理已注册供应商,资质审核通过后系统自动入库,工商风险信息由 AI 自动同步"
|
||||
/>
|
||||
|
||||
<Card className="mb-4 flex-row items-center gap-3 p-4">
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
X,
|
||||
Trash2,
|
||||
PackagePlus,
|
||||
ChevronDown,
|
||||
Sparkles,
|
||||
FileCheck2,
|
||||
Loader2,
|
||||
Megaphone,
|
||||
Send,
|
||||
} from "lucide-react";
|
||||
import { ArrowLeft, Check, X, Trash2, PackagePlus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../components/ui/tabs";
|
||||
import {
|
||||
PageHeader,
|
||||
AiResultTag,
|
||||
RiskBadge,
|
||||
ApprovalTracker,
|
||||
} from "../components/shared/common";
|
||||
import { PageHeader } from "../components/shared/common";
|
||||
import { suppliers } from "../data/mock";
|
||||
|
||||
// 招标方式已在需求委托阶段确定,故本页不再区分"邀请/公开"分支,直接进行长短名单管理。
|
||||
export function SupplierShortlist() {
|
||||
const navigate = useNavigate();
|
||||
const [longList, setLongList] = useState(
|
||||
suppliers.map((s) => ({ ...s, decision: "" as "" | "pass" | "reject" })),
|
||||
);
|
||||
const shortList = longList.filter((s) => s.decision === "pass");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center gap-3">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
@@ -40,154 +27,54 @@ export function SupplierShortlist() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="invite">
|
||||
<TabsList>
|
||||
<TabsTrigger value="invite">邀请招标分支</TabsTrigger>
|
||||
<TabsTrigger value="public">公开招标分支</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="invite" className="mt-5">
|
||||
<InviteBranch />
|
||||
</TabsContent>
|
||||
<TabsContent value="public" className="mt-5">
|
||||
<PublicBranch />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 邀请招标:长名单 / 短名单 ---------------- */
|
||||
function InviteBranch() {
|
||||
const [longList, setLongList] = useState(
|
||||
suppliers.map((s) => ({ ...s, decision: "" as "" | "pass" | "reject" })),
|
||||
);
|
||||
const shortList = longList.filter((s) => s.decision === "pass");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 长名单 */}
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">供应商长名单</h3>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
共 {longList.length} 家
|
||||
</span>
|
||||
<div className="space-y-6">
|
||||
{/* 长名单 */}
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">供应商长名单</h3>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
共 {longList.length} 家
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">对供应商进行初步评审,确定是否进入短名单</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">对供应商进行初步评审,确定是否进入短名单</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{longList.map((s, i) => (
|
||||
<tr key={s.id} className="hover:bg-muted/20">
|
||||
<td className="px-6 py-3 font-medium text-foreground">{s.name}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.contact}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.dept}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.opinion}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={s.decision === "pass" ? "default" : "outline"}
|
||||
className="h-7 gap-1 px-2"
|
||||
onClick={() =>
|
||||
setLongList((l) =>
|
||||
l.map((x, j) => (j === i ? { ...x, decision: "pass" } : x)),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Check className="size-3.5" /> 通过
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={s.decision === "reject" ? "destructive" : "outline"}
|
||||
className="h-7 gap-1 px-2"
|
||||
onClick={() =>
|
||||
setLongList((l) =>
|
||||
l.map((x, j) => (j === i ? { ...x, decision: "reject" } : x)),
|
||||
)
|
||||
}
|
||||
>
|
||||
<X className="size-3.5" /> 不通过
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
|
||||
{/* 短名单 */}
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">供应商短名单</h3>
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
已遴选 {shortList.length} 家
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" disabled={shortList.length === 0} onClick={() => toast.success("短名单已确认,进入邀请阶段")}>
|
||||
确认短名单并发起邀请
|
||||
</Button>
|
||||
</div>
|
||||
{shortList.length === 0 ? (
|
||||
<div className="flex h-40 flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<PackagePlus className="size-8 text-muted-foreground/40" />
|
||||
请在上方长名单中点击「通过」,将供应商加入短名单
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{shortList.map((s) => (
|
||||
{longList.map((s, i) => (
|
||||
<tr key={s.id} className="hover:bg-muted/20">
|
||||
<td className="px-6 py-3 font-medium text-foreground">{s.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
{s.inLibrary ? (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-600">
|
||||
<Check className="size-4" /> 库内
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-amber-600">库外</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.conclusion}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.contact}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.opinion}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2">
|
||||
{!s.inLibrary && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 px-2" onClick={() => toast.success("已发起入库流程")}>
|
||||
<PackagePlus className="size-3.5" /> 发起入库
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 gap-1 px-2 text-destructive hover:text-destructive"
|
||||
variant={s.decision === "pass" ? "default" : "outline"}
|
||||
className="h-7 gap-1 px-2"
|
||||
onClick={() =>
|
||||
setLongList((l) =>
|
||||
l.map((x) => (x.id === s.id ? { ...x, decision: "" } : x)),
|
||||
)
|
||||
setLongList((l) => l.map((x, j) => (j === i ? { ...x, decision: "pass" } : x)))
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" /> 移除
|
||||
<Check className="size-3.5" /> 通过
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={s.decision === "reject" ? "destructive" : "outline"}
|
||||
className="h-7 gap-1 px-2"
|
||||
onClick={() =>
|
||||
setLongList((l) => l.map((x, j) => (j === i ? { ...x, decision: "reject" } : x)))
|
||||
}
|
||||
>
|
||||
<X className="size-3.5" /> 不通过
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -195,175 +82,69 @@ function InviteBranch() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</Card>
|
||||
|
||||
/* ---------------- 公开招标:寻源公告 + 报名审核 ---------------- */
|
||||
function PublicBranch() {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="col-span-2 space-y-6">
|
||||
<AnnouncementEditor />
|
||||
<RegistrationReview />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<Card className="gap-0 p-6">
|
||||
<h4 className="mb-4 text-foreground">公告审批状态</h4>
|
||||
<ApprovalTracker
|
||||
nodes={[
|
||||
{ role: "经办人提交", name: "李娜 · 招标专员", status: "completed", time: "06-18 10:00" },
|
||||
{ role: "部门负责人审批", name: "安全环保部 · 已通过", status: "completed", time: "06-18 15:20" },
|
||||
{ role: "合规审批", name: "法务合规 · 审批中", status: "current" },
|
||||
{ role: "公告发布", name: "对外发布寻源公告", status: "pending" },
|
||||
]}
|
||||
/>
|
||||
{/* 短名单 */}
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">供应商短名单</h3>
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
已遴选 {shortList.length} 家
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" disabled={shortList.length === 0} onClick={() => toast.success("短名单已确认,进入邀请阶段")}>
|
||||
确认短名单并发起邀请
|
||||
</Button>
|
||||
</div>
|
||||
{shortList.length === 0 ? (
|
||||
<div className="flex h-40 flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<PackagePlus className="size-8 text-muted-foreground/40" />
|
||||
请在上方长名单中点击「通过」,将供应商加入短名单
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{shortList.map((s) => (
|
||||
<tr key={s.id} className="hover:bg-muted/20">
|
||||
<td className="px-6 py-3 font-medium text-foreground">{s.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
{s.inLibrary ? (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-600">
|
||||
<Check className="size-4" /> 库内
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-amber-600">库外(审核通过后自动入库)</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{s.conclusion}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 gap-1 px-2 text-destructive hover:text-destructive"
|
||||
onClick={() =>
|
||||
setLongList((l) => l.map((x) => (x.id === s.id ? { ...x, decision: "" } : x)))
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" /> 移除
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnnouncementEditor() {
|
||||
return (
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-foreground">寻源公告编辑</h3>
|
||||
<Button size="sm" variant="outline">
|
||||
<Sparkles className="size-4" /> AI 优化公告
|
||||
</Button>
|
||||
</div>
|
||||
{/* 富文本工具栏 */}
|
||||
<div className="flex items-center gap-1 rounded-t-lg border border-b-0 bg-muted/40 px-3 py-2 text-sm text-muted-foreground">
|
||||
{["B", "I", "U", "H1", "H2", "•", "1.", "🔗"].map((b) => (
|
||||
<button key={b} className="rounded px-2 py-1 hover:bg-muted">
|
||||
{b}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-b-lg border bg-card p-4 text-sm leading-relaxed text-foreground">
|
||||
<p className="font-medium">滨化集团厂区污水处理提标改造工程公开招标公告</p>
|
||||
<p className="mt-3 text-muted-foreground">
|
||||
滨化集团股份有限公司就「厂区污水处理提标改造工程」进行公开招标,欢迎符合条件的供应商参与报名。
|
||||
</p>
|
||||
<p className="mt-3 font-medium">一、项目概况</p>
|
||||
<p className="text-muted-foreground">
|
||||
本工程对现有污水处理系统进行提标改造,出水水质须达到《城镇污水处理厂污染物排放标准》一级 A 标准,工期 180 日历天。
|
||||
</p>
|
||||
<p className="mt-3 font-medium">二、供应商资格要求</p>
|
||||
<p className="text-muted-foreground">
|
||||
具有独立法人资格;具备环保工程专业承包二级及以上资质;近三年承担过同类污水处理工程业绩不少于 2 项。
|
||||
</p>
|
||||
<p className="mt-3 font-medium">三、报名时间</p>
|
||||
<p className="text-muted-foreground">2026 年 6 月 25 日 至 2026 年 7 月 5 日(每日 9:00-17:00)。</p>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button variant="outline">保存草稿</Button>
|
||||
<Button onClick={() => toast.success("公告已提交审批")}>
|
||||
<Send className="size-4" /> 提交审批
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RegistrationReview() {
|
||||
const applicants = suppliers.slice(0, 4);
|
||||
const [expanded, setExpanded] = useState<string | null>(applicants[0].id);
|
||||
const [approved, setApproved] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<Card className="gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Megaphone 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">
|
||||
{applicants.length} 家报名
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
审核通过后自动进入「报名通过单位名单」(已通过 {approved.length} 家)
|
||||
</span>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{applicants.map((s) => {
|
||||
const open = expanded === s.id;
|
||||
const isApproved = approved.includes(s.id);
|
||||
return (
|
||||
<div key={s.id}>
|
||||
<button
|
||||
className="flex w-full items-center gap-3 px-6 py-4 text-left hover:bg-muted/20"
|
||||
onClick={() => setExpanded(open ? null : s.id)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">{s.name}</span>
|
||||
{s.aiResult && <AiResultTag result={s.aiResult} />}
|
||||
{isApproved && (
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-600">已通过</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
联系人 {s.contact} · {s.phone}
|
||||
</div>
|
||||
</div>
|
||||
<RiskBadge risk={s.risk} />
|
||||
<ChevronDown className={`size-4 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="bg-muted/20 px-6 py-4">
|
||||
<div className="mb-3 text-sm font-medium text-foreground">提交的资质文件</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{["营业执照.pdf", "环保工程资质证书.pdf", "近三年业绩证明.pdf"].map((f) => (
|
||||
<div key={f} className="flex items-center gap-2 rounded-lg border bg-card px-3 py-2 text-sm">
|
||||
<FileCheck2 className="size-4 text-primary" />
|
||||
<span className="flex-1 truncate text-foreground">{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* AI 审核结果 */}
|
||||
<div className="mt-3 rounded-lg border border-violet-200 bg-violet-50/60 p-3">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-4 text-violet-500" /> AI 辅助审核结果
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm text-muted-foreground">
|
||||
{s.aiResult === "资质有效"
|
||||
? "资质文件齐全有效,工商信息正常,未发现关联投标及失信风险,建议通过。"
|
||||
: "检测到该供应商与其他报名单位存在股权关联,建议人工进一步核实后决定。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<Button size="sm" variant="outline" className="gap-1">
|
||||
<Sparkles className="size-3.5" /> AI 辅助审核
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-destructive"
|
||||
onClick={() => toast.error("已驳回该供应商报名")}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isApproved}
|
||||
onClick={() => {
|
||||
setApproved((a) => [...a, s.id]);
|
||||
toast.success(`${s.name} 审核通过,已进入报名通过名单`);
|
||||
}}
|
||||
>
|
||||
<Check className="size-3.5" /> 审核通过
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ import { useNavigate } from "react-router";
|
||||
import {
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
GitMerge,
|
||||
Bot,
|
||||
Activity,
|
||||
ShieldAlert,
|
||||
Network,
|
||||
TrendingUp,
|
||||
Gavel,
|
||||
CheckCircle2,
|
||||
FileText,
|
||||
FilePlus2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { AiBadge } from "../components/shared/common";
|
||||
import { AgentTaskMonitor } from "../components/AgentTaskMonitor";
|
||||
import { useRole } from "../role-context";
|
||||
import { WORKBENCH } from "../data/workbench";
|
||||
import { mergeCandidates, riskWarnings, bidProjects, approvals } from "../data/mock";
|
||||
import { riskWarnings, bidProjects } from "../data/mock";
|
||||
|
||||
const priorityStyle: Record<string, string> = {
|
||||
高: "bg-red-50 text-red-600",
|
||||
@@ -69,7 +68,7 @@ export function Workbench() {
|
||||
<Card className="col-span-2 gap-0 p-0">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground">「事找人」待办中心</h3>
|
||||
<h3 className="text-foreground">待办中心</h3>
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">{totalTodo} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +112,13 @@ export function Workbench() {
|
||||
{/* 角色专属附加区 */}
|
||||
<div className="space-y-6">{wb.extra && <RoleExtra extra={wb.extra} />}</div>
|
||||
</div>
|
||||
|
||||
{/* 后台智能体任务监控(招标专员 / 系统管理员) */}
|
||||
{wb.agentMonitor && (
|
||||
<div className="mt-6">
|
||||
<AgentTaskMonitor />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,24 +130,47 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
return (
|
||||
<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-primary" />
|
||||
<h4 className="text-foreground">合并招标提醒</h4>
|
||||
<Bot className="size-4 text-primary" />
|
||||
<h4 className="text-foreground">AI 智能体概览</h4>
|
||||
<AiBadge label="AI" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
检测到 <span className="font-semibold text-foreground">{mergeCandidates.length}</span> 个同类型可合并委托,预计可节省采购周期 ~15 天。
|
||||
当前有 <span className="font-semibold text-foreground">2</span> 个后台智能体任务正在执行,详见下方「后台智能体任务」看板。
|
||||
</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{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-primary">重叠 {m.overlap}%</span>
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
{[
|
||||
{ l: "🔍 AI 智慧寻源", v: "70%" },
|
||||
{ l: "🔬 AI 雷同检测", v: "30%" },
|
||||
].map((x) => (
|
||||
<div key={x.l} className="flex items-center justify-between rounded-lg border bg-card px-3 py-2">
|
||||
<span className="line-clamp-1 flex-1 text-foreground">{x.l}</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-amber-600">{x.v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button className="mt-4 w-full" onClick={() => navigate("/app/merge")}>
|
||||
查看合并建议
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (extra === "requester") {
|
||||
return (
|
||||
<Card className="gap-0 border-primary/20 bg-secondary/50 p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<FilePlus2 className="size-4 text-primary" />
|
||||
<h4 className="text-foreground">快速发起采购需求</h4>
|
||||
<AiBadge label="AI" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
用自然语言描述需求,AI 自动生成《招标委托书》,也可导入采购计划或选择模板。
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Button className="w-full" onClick={() => navigate("/app/delegation")}>
|
||||
<FilePlus2 className="size-4" /> 发起招标委托
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={() => navigate("/app/projects")}>
|
||||
查看我的项目
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -176,29 +205,6 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (extra === "approver") {
|
||||
return (
|
||||
<Card className="gap-0 p-5">
|
||||
<h4 className="mb-3 text-foreground">最近待审批</h4>
|
||||
<div className="space-y-2.5">
|
||||
{approvals.slice(0, 3).map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
onClick={() => navigate("/app/approvals")}
|
||||
className="w-full rounded-lg border bg-card p-3 text-left hover:border-primary"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-600">{a.type}</span>
|
||||
{a.urgent && <span className="rounded bg-red-50 px-1.5 py-0.5 text-xs text-red-600">紧急</span>}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-1 text-sm text-foreground">{a.project}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (extra === "admin") {
|
||||
return (
|
||||
<Card className="gap-0 p-5">
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Workflow, Plus, Settings, ArrowRight, History, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Card } from "../../components/ui/card";
|
||||
import { PageHeader } from "../../components/shared/common";
|
||||
|
||||
const nodes = [
|
||||
{ name: "经办人提交", rule: "招标专员发起" },
|
||||
{ name: "部门负责人", rule: "本部门负责人" },
|
||||
{ name: "分管领导", rule: "金额 ≥ 500 万" },
|
||||
{ name: "总经理审批", rule: "金额 ≥ 2000 万" },
|
||||
];
|
||||
|
||||
const versions = [
|
||||
{ v: "v3.2", date: "2026-06-01", status: "当前版本", by: "运维管理员" },
|
||||
{ v: "v3.1", date: "2026-04-15", status: "历史", by: "运维管理员" },
|
||||
{ v: "v3.0", date: "2026-01-10", status: "历史", by: "运维管理员" },
|
||||
];
|
||||
|
||||
export function ProcessConfig() {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="流程配置中心"
|
||||
description="可视化配置审批流程节点、审批人规则与版本管理"
|
||||
actions={
|
||||
<Button onClick={() => toast.success("已发布新流程版本")}>发布流程</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<Card className="col-span-2 gap-0 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Workflow className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">定标审批流程(可视化编辑)</h3>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="size-4" /> 添加节点
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-dashed bg-muted/20 p-6">
|
||||
{nodes.map((n, i) => (
|
||||
<div key={n.name} className="flex items-center gap-2">
|
||||
<div className="w-36 rounded-lg border bg-card p-3 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-foreground">{n.name}</span>
|
||||
<Settings className="size-3.5 cursor-pointer text-muted-foreground hover:text-primary" />
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{n.rule}</div>
|
||||
</div>
|
||||
{i < nodes.length - 1 && <ArrowRight className="size-4 shrink-0 text-muted-foreground" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<h4 className="mb-3 text-foreground">审批人规则(按金额自动匹配)</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
["金额 < 500 万", "部门负责人审批"],
|
||||
["500 万 ≤ 金额 < 2000 万", "部门负责人 + 分管领导"],
|
||||
["金额 ≥ 2000 万", "部门负责人 + 分管领导 + 总经理"],
|
||||
].map(([cond, route]) => (
|
||||
<div key={cond} className="flex items-center justify-between rounded-lg border bg-card px-4 py-2.5 text-sm">
|
||||
<span className="text-muted-foreground">{cond}</span>
|
||||
<span className="font-medium text-foreground">{route}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<History className="size-5 text-primary" />
|
||||
<h4 className="text-foreground">流程版本管理</h4>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{versions.map((v) => (
|
||||
<div key={v.v} className="flex items-center justify-between rounded-lg border bg-card p-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">{v.v}</span>
|
||||
{v.status === "当前版本" && (
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-600">{v.status}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">{v.date} · {v.by}</div>
|
||||
</div>
|
||||
{v.status !== "当前版本" && (
|
||||
<Button size="sm" variant="ghost" className="h-7 gap-1" onClick={() => toast.success(`已回滚至 ${v.v}`)}>
|
||||
<RotateCcw className="size-3.5" /> 回滚
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+38
-60
@@ -1,12 +1,16 @@
|
||||
// 滨化集团智慧招标平台 (STP) - 多角色配置 V1.3
|
||||
// 滨化集团智慧招标平台 (STP) - 多角色配置 V1.4
|
||||
// 说明:依据内部评审意见调整——
|
||||
// 1)统一科技蓝主题(呼应滨化 LOGO);
|
||||
// 2)合并"需求部门人员/采购专员"为"采购需求人员";
|
||||
// 3)移除"审批人"(审批在 OA 中体现);
|
||||
// 4)发起招标委托归属"采购需求人员",招标专员不含该功能;
|
||||
// 5)移除合并招标、供应商入库、合同管理、审批中心、流程配置等入口。
|
||||
|
||||
export type RoleId =
|
||||
| "requester"
|
||||
| "specialist"
|
||||
| "purchaser"
|
||||
| "expert"
|
||||
| "committee"
|
||||
| "approver"
|
||||
| "supplier"
|
||||
| "admin"
|
||||
| "auditor";
|
||||
@@ -23,24 +27,31 @@ export interface RoleConfig {
|
||||
name: string; // 角色名
|
||||
user: string; // 示例用户/企业名
|
||||
dept: string;
|
||||
color: string; // 辅助色
|
||||
color: string; // 辅助色(统一科技蓝)
|
||||
bannerFrom: string;
|
||||
bannerTo: string;
|
||||
nav: NavItem[];
|
||||
}
|
||||
|
||||
// 全平台统一科技蓝
|
||||
const BRAND_FROM = "#0d47a1";
|
||||
const BRAND_TO = "#1e88e5";
|
||||
const BRAND_DOT = "#1565c0";
|
||||
|
||||
export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
requester: {
|
||||
id: "requester",
|
||||
name: "需求部门人员",
|
||||
name: "采购需求人员",
|
||||
user: "刘晓敏",
|
||||
dept: "氯碱生产部",
|
||||
color: "#0ea5e9",
|
||||
bannerFrom: "#0c4a6e",
|
||||
bannerTo: "#0ea5e9",
|
||||
dept: "氯碱生产部 / 采购供应部",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/delegation", label: "发起委托", icon: "FilePlus2" },
|
||||
{ to: "/app/projects", label: "我的项目", icon: "FolderKanban" },
|
||||
{ to: "/app/deposits", label: "保证金管理", icon: "Wallet" },
|
||||
],
|
||||
},
|
||||
specialist: {
|
||||
@@ -48,43 +59,26 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "招标专员",
|
||||
user: "张伟",
|
||||
dept: "招标采购中心",
|
||||
color: "#00695c",
|
||||
bannerFrom: "#00695c",
|
||||
bannerTo: "#00897b",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/projects", label: "项目管理", icon: "FolderKanban" },
|
||||
{ to: "/app/merge", label: "合并招标", icon: "GitMerge" },
|
||||
{ to: "/app/suppliers", label: "供应商管理", icon: "Building2" },
|
||||
{ to: "/app/experts", label: "专家管理", icon: "UserCog" },
|
||||
{ to: "/app/deposits", label: "保证金管理", icon: "Wallet" },
|
||||
{ to: "/app/reports", label: "报表中心", icon: "BarChart3" },
|
||||
],
|
||||
},
|
||||
purchaser: {
|
||||
id: "purchaser",
|
||||
name: "采购专员",
|
||||
user: "陈晨",
|
||||
dept: "采购供应部",
|
||||
color: "#6366f1",
|
||||
bannerFrom: "#312e81",
|
||||
bannerTo: "#6366f1",
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/projects", label: "我的项目", icon: "FolderKanban" },
|
||||
{ to: "/app/contracts", label: "合同管理", icon: "FileSignature" },
|
||||
{ to: "/app/inbound", label: "供应商入库", icon: "PackagePlus" },
|
||||
{ to: "/app/deposits", label: "保证金管理", icon: "Wallet" },
|
||||
],
|
||||
},
|
||||
expert: {
|
||||
id: "expert",
|
||||
name: "评标专家",
|
||||
user: "郑国华",
|
||||
dept: "外部专家 · 化工工艺",
|
||||
color: "#1565c0",
|
||||
bannerFrom: "#0d47a1",
|
||||
bannerTo: "#1976d2",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/expert-tasks", label: "评标任务", icon: "Gavel" },
|
||||
@@ -95,35 +89,22 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "评标委员会工作领导小组",
|
||||
user: "孙建华",
|
||||
dept: "评标委员会",
|
||||
color: "#dc2626",
|
||||
bannerFrom: "#7f1d1d",
|
||||
bannerTo: "#dc2626",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/exceptions", label: "异常决策", icon: "ShieldAlert" },
|
||||
],
|
||||
},
|
||||
approver: {
|
||||
id: "approver",
|
||||
name: "审批人",
|
||||
user: "王立强",
|
||||
dept: "分管副总",
|
||||
color: "#10b981",
|
||||
bannerFrom: "#064e3b",
|
||||
bannerTo: "#10b981",
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/approvals", label: "待审批", icon: "ClipboardCheck" },
|
||||
],
|
||||
},
|
||||
supplier: {
|
||||
id: "supplier",
|
||||
name: "供应商",
|
||||
user: "浙江中控技术股份有限公司",
|
||||
dept: "投标用户",
|
||||
color: "#c2185b",
|
||||
bannerFrom: "#c2185b",
|
||||
bannerTo: "#e91e63",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/bidding", label: "投标中心", icon: "PackageOpen" },
|
||||
@@ -134,13 +115,12 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "系统管理员",
|
||||
user: "运维管理员",
|
||||
dept: "信息中心",
|
||||
color: "#475569",
|
||||
bannerFrom: "#1e293b",
|
||||
bannerTo: "#475569",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/admin/users", label: "用户与权限", icon: "Users" },
|
||||
{ to: "/app/admin/process", label: "流程配置", icon: "Workflow" },
|
||||
{ to: "/app/admin/keys", label: "密钥管理", icon: "KeyRound" },
|
||||
{ to: "/app/suppliers", label: "供应商管理", icon: "Building2" },
|
||||
{ to: "/app/experts", label: "专家管理", icon: "UserCog" },
|
||||
@@ -153,9 +133,9 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
name: "合规 / 审计",
|
||||
user: "李审计",
|
||||
dept: "审计监察部",
|
||||
color: "#0d9488",
|
||||
bannerFrom: "#134e4a",
|
||||
bannerTo: "#0d9488",
|
||||
color: BRAND_DOT,
|
||||
bannerFrom: BRAND_FROM,
|
||||
bannerTo: BRAND_TO,
|
||||
nav: [
|
||||
{ to: "/app", label: "工作台", icon: "LayoutDashboard", end: true },
|
||||
{ to: "/app/audit-logs", label: "审计日志", icon: "ScrollText" },
|
||||
@@ -169,10 +149,8 @@ export const ROLES: Record<RoleId, RoleConfig> = {
|
||||
export const ROLE_ORDER: RoleId[] = [
|
||||
"specialist",
|
||||
"requester",
|
||||
"purchaser",
|
||||
"expert",
|
||||
"committee",
|
||||
"approver",
|
||||
"supplier",
|
||||
"admin",
|
||||
"auditor",
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
--card-foreground: #1a2332;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1a2332;
|
||||
--primary: #c2185b;
|
||||
--primary: #1565c0;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #fce4ec;
|
||||
--secondary-foreground: #ad1457;
|
||||
--secondary: #e3f2fd;
|
||||
--secondary-foreground: #0d47a1;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #fce4ec;
|
||||
--accent-foreground: #ad1457;
|
||||
--accent: #e3f2fd;
|
||||
--accent-foreground: #0d47a1;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
@@ -24,24 +24,24 @@
|
||||
--switch-background: #cbd5e1;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-normal: 400;
|
||||
--ring: #c2185b;
|
||||
--chart-1: #c2185b;
|
||||
--chart-2: #1565c0;
|
||||
--chart-3: #00897b;
|
||||
--chart-4: #f59e0b;
|
||||
--chart-5: #9c27b0;
|
||||
--ring: #1565c0;
|
||||
--chart-1: #1565c0;
|
||||
--chart-2: #00897b;
|
||||
--chart-3: #f59e0b;
|
||||
--chart-4: #7e57c2;
|
||||
--chart-5: #ef5350;
|
||||
--radius: 0.75rem;
|
||||
/* 品牌渐变(全平台统一): #c2185b → #e91e63 */
|
||||
--brand-from: #c2185b;
|
||||
--brand-to: #e91e63;
|
||||
--sidebar: #2d0a19;
|
||||
--sidebar-foreground: #f4d9e4;
|
||||
--sidebar-primary: #e91e63;
|
||||
/* 品牌渐变(全平台统一科技蓝,呼应滨化 LOGO): #0d47a1 → #1e88e5 */
|
||||
--brand-from: #0d47a1;
|
||||
--brand-to: #1e88e5;
|
||||
--sidebar: #0d2137;
|
||||
--sidebar-foreground: #d6e4f5;
|
||||
--sidebar-primary: #1e88e5;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #6d1338;
|
||||
--sidebar-accent: #16375a;
|
||||
--sidebar-accent-foreground: #ffffff;
|
||||
--sidebar-border: #4a0e26;
|
||||
--sidebar-ring: #e91e63;
|
||||
--sidebar-border: #16375a;
|
||||
--sidebar-ring: #1e88e5;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
Reference in New Issue
Block a user