Compare commits
8 Commits
787de5aa88
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dba6db9d2 | |||
| 41fc6e86dd | |||
| aa023a6d45 | |||
| e0ae0e4007 | |||
| d81ac936e7 | |||
| 38a804ce41 | |||
| 3a50a3f3ed | |||
| 282f1f1b92 |
@@ -450,7 +450,7 @@
|
||||
|
||||
### 3.6 AI 助手“小滨”悬浮窗(核心特色)
|
||||
|
||||
#### 3.5.1 视觉设计
|
||||
#### 3.6.1 视觉设计
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
@@ -459,7 +459,7 @@
|
||||
| **可拖动** | 用户可通过拖拽头部区域移动悬浮窗位置 |
|
||||
| **可折叠** | 点击头部“−”按钮最小化到右下角图标,点击“✕”关闭(可在系统设置中重新开启) |
|
||||
|
||||
#### 3.5.2 主动提示能力
|
||||
#### 3.6.2 主动提示能力
|
||||
|
||||
AI助手“小滨”**不需要用户主动提问**,而是主动分析当前专员的待办任务状态,推送以下类型提示:
|
||||
|
||||
@@ -471,7 +471,7 @@ AI助手“小滨”**不需要用户主动提问**,而是主动分析当前
|
||||
| **⚠️ 风险预警** | 检测到异常或风险 | 红色背景(#ffebee) | “⚠️ 供应商不足3家,需二次寻源” |
|
||||
| **💡 操作建议** | 下一步最佳操作 | 灰色背景(#f5f5f5) | “💡 建议优先处理即将截止的委托” |
|
||||
提示示例:
|
||||
您当前有3项待确认委托,一项审批
|
||||
您当前有3项待确认委托,一个立项待审批,防腐保温工程最紧急,点击*这里*(链接)进行处理。
|
||||
|
||||
#### 3.5.3 消息推送规则
|
||||
|
||||
@@ -616,18 +616,3 @@ GET /api/v1/buyer/ai-messages
|
||||
| **数据实时性** | 待办统计每30秒轮询更新,WebSocket推送状态变更 |
|
||||
| **消息历史** | AI助手消息保留7天,可查询历史记录 |
|
||||
| **防打扰** | 支持关闭小滨主动推送(仅保留角标提醒) |
|
||||
|
||||
|
||||
## 8. 与供应商/专家工作台的差异
|
||||
|
||||
| 维度 | 供应商工作台 | 专家工作台 | 招标专员工作台 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **主题色** | 红色(#c2185b) | 蓝紫色(#1a237e) | 绿色(#00695c) |
|
||||
| **核心任务** | 投标报名、文件上传 | 接收评审邀请 | 流程推进、任务管理 |
|
||||
| **待办分类** | 按操作类型(报名/缴费/上传) | 按评审状态 | **按招标阶段(委托/寻源/立项/开标)** |
|
||||
| **AI助手定位** | 操作问答 | 评审提醒 | **主动流程推进器** |
|
||||
| **数据看板** | 投标项目列表 | 评审历史 | **在办项目+阶段任务** |
|
||||
|
||||
---
|
||||
|
||||
以上为招标专员工作台的完整详细设计,核心特色为 **AI助手“小滨”的悬浮窗主动提示机制**,通过实时扫描待办任务、推送优先级建议和操作指引,帮助招标专员高效推进招标流程,真正实现“事找人”的智能化工作模式。
|
||||
@@ -277,7 +277,7 @@
|
||||
```
|
||||
|
||||
|
||||
## 3. 各模块详细说明(文字描述)
|
||||
## 3. 各模块详细说明
|
||||
|
||||
### 3.1 顶部导航栏
|
||||
| 元素 | 说明 |
|
||||
@@ -322,7 +322,7 @@
|
||||
| **展示信息** | 项目名称、评审完成日期、状态(已提交) |
|
||||
| **交互** | 点击记录跳转至历史评审详情页(只读模式) |
|
||||
|
||||
### 3.6 右侧边栏(删除快捷操作,调整顺序)
|
||||
### 3.6 右侧边栏
|
||||
|
||||
#### 3.6.1 专家信息概览
|
||||
| 属性 | 说明 |
|
||||
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,200 @@
|
||||
# 前端界面截图内部评审意见-20260710
|
||||
|
||||
## 1. 主题颜色还是统一配科技蓝吧,跟滨化的LOGO比较搭
|
||||
## 2. 提醒事项直接让 AI 助手对话窗口显示,用户进入工作台默认就会看到助手的消息,不用点开,但可以收起来。
|
||||
## 3. 合并招标暂时不实现。以后看情况再说。
|
||||
## 4. 提醒事项直接让 AI 助手对话窗口显示,用户进入工作台默认就会看到助手的消息,不用点开,但可以收起来。
|
||||
## 5. “事找人待办中心”,改成 “待办中心” (注:几乎所有工作台都有这个问题)
|
||||
![[Pasted image 20260710172254.png]]
|
||||
|
||||
## 6. 招标专员工作台增加后台智能体任务监控
|
||||
|
||||
### 1 模块定位
|
||||
|
||||
| 属性 | 说明 |
|
||||
| -------- | ------------------------------------------------------------ |
|
||||
| **模块名称** | 后台智能体任务(AI Agent Task Monitor) |
|
||||
| **位置** | 待办任务看板下方,在办项目列表上方 |
|
||||
| **功能** | 实时展示后台异步 AI 智能体任务的执行进度,包括任务名称、关联项目、执行状态、进度百分比、关键中间结果、预计剩余时间等 |
|
||||
| **刷新机制** | 每 3 秒轮询 / WebSocket 推送(推荐) |
|
||||
|
||||
### 2 任务类型清单
|
||||
|
||||
|任务图标|任务名称|触发时机|典型耗时|输出产物|
|
||||
|---|---|---|---|---|
|
||||
|🔍|AI 智慧寻源|委托确认后自动触发|2-10 分钟|潜在供应商名单 + 推荐报告|
|
||||
|🔬|AI 雷同检测|投标文件上传完成后|1-8 分钟|雷同检测报告|
|
||||
|📊|AI 价格分析|开标完成后|1-3 分钟|价格偏离度分析报告|
|
||||
|👨🏫|AI 专家推荐|评标前准备阶段|30 秒-2 分钟|匹配专家列表|
|
||||
|📄|AI 评标报告生成|评标打分完成后|1-2 分钟|评标报告初稿|
|
||||
|🧩|AI 合并招标建议|委托生效时|1-3 分钟|合并招标建议书|
|
||||
|📋|AI 资格初审|供应商报名后|1-5 分钟|资格预审结果|
|
||||
|
||||
### 3 任务状态与视觉规范
|
||||
|
||||
|状态|色条颜色|状态标签|进度条样式|说明|
|
||||
|---|---|---|---|---|
|
||||
|**待执行**|灰色(#9e9e9e)|⏸ 待执行|灰色空进度条|任务在队列中等待|
|
||||
|**执行中**|橙色(#ff9800)|⏳ 执行中|橙色渐变进度条 + 百分比|正在运行,显示进度|
|
||||
|**已完成**|绿色(#4caf50)|✅ 已完成|绿色实心进度条|任务成功完成|
|
||||
|**失败**|红色(#f44336)|❌ 执行失败|红色进度条 + 错误标记|任务异常终止|
|
||||
|
||||
### 4 单条任务卡片结构
|
||||
|
||||
|区域|元素|说明|
|
||||
|---|---|---|
|
||||
|**左侧**|色条|6px 宽,颜色对应任务状态|
|
||||
|**图标区**|任务图标 + 圆形背景|28px 圆,背景色与状态匹配|
|
||||
|**任务信息**|任务名称(加粗)+ 关联项目名称|显示任务所属项目|
|
||||
|**状态区**|状态标签 + 进度条 + 百分比|展示当前执行进度|
|
||||
|**详情区**|关键中间结果|如“已匹配 8/20 家供应商”|
|
||||
|**时间区**|预计剩余时间 / 完成时间|执行中显示剩余,已完成显示完成时间|
|
||||
|**操作区**|取消 / 查看报告|执行中可取消,已完成可查看报告|
|
||||
|
||||
### 5 任务卡片示例数据
|
||||
|
||||
|任务|状态|进度|中间结果|预计剩余|操作|
|
||||
|---|---|---|---|---|---|
|
||||
|AI 智慧寻源|执行中|70%|已匹配:8 家供应商|2 分钟|取消|
|
||||
|AI 雷同检测|执行中|30%|已扫描:3/10 份|5 分钟|取消|
|
||||
|AI 价格分析|已完成|100%|发现 1 份报价偏离 >20%|09:35 完成|查看报告|
|
||||
|AI 专家推荐|待执行|0%|—|—|—|
|
||||
|
||||
### 6 点击交互
|
||||
|
||||
|交互|说明|
|
||||
|---|---|
|
||||
|**点击任务卡片**|跳转至任务详情页(展示完整的执行日志和中间结果)|
|
||||
|**点击“取消”**|弹出二次确认弹窗,确认后终止任务(仅限执行中状态)|
|
||||
|**点击“查看报告”**|在新窗口打开或下载生成的报告文件|
|
||||
|**悬停进度条**|显示详细进度信息(Tooltip)|
|
||||
|
||||
### 7 智能体任务与 AI 助手的协同
|
||||
|
||||
当智能体任务状态发生变化时,AI助手“小滨”会同步推送主动提醒:
|
||||
|
||||
|任务状态变化|小滨推送内容|示例|
|
||||
|---|---|---|
|
||||
|任务完成|“✅ AI [任务名称] 已完成,请查看报告”|“✅ AI 价格分析已完成,发现 1 份报价偏离度 >20%”|
|
||||
|任务失败|“❌ AI [任务名称] 执行失败,请重试或联系管理员”|“❌ AI 雷同检测执行失败,请检查投标文件格式”|
|
||||
|关键节点|“📌 AI [任务名称] 已找到 [数量] 个潜在供应商”|“📌 AI 智慧寻源已找到 12 家潜在供应商”|
|
||||
|
||||
## 8 需求部门人员工作台添加AI 助手提醒
|
||||
例:
|
||||
```text
|
||||
您的阀门采购项目已定标,下一步:进入 SRM 生成采购合同。(点击这里进入)
|
||||
```
|
||||
## 9 采购专员和需求部门人员角色重叠
|
||||
|
||||
合并成采购需求人员吧
|
||||
|
||||
## 10 评标专家工作台,添加小滨信息
|
||||
例:
|
||||
```
|
||||
您有一个保温材料招标项目的评标邀请,你可以选择接受或者拒绝,点击这里查看详情。
|
||||
```
|
||||
|
||||
## 11 审批人角色在OA中体现,不需要登录到 STP ,STP 不需要设计这个角色。
|
||||
|
||||
## 12 供应商工作台增加小滨信息
|
||||
例:
|
||||
```
|
||||
您在聚丙烯项目中的报名已经通过,请点击这里查看招标文件,如果确认投标,可以点击这里交付投标保证金。
|
||||
```
|
||||
|
||||
## 13 系统管理员工作台增加后台工作 Agent 查看内容
|
||||
|
||||
## 14 发起招标委托是采购需求人员的事,不是招标专员的事。
|
||||
|
||||
## 15 智能问答对话例子:
|
||||
- 回时后直接提供链接或按钮供用户选择,不要“需要我带你过去吗?”这样的回答显得有点傻。
|
||||
例:
|
||||
```
|
||||
user:如何发起招标委托
|
||||
AI:发起招标委托很简单:①点击工作台或项目管理页的「发起委托」按钮;②在「填写需求」步骤用自然语言描述需求,点击「AI 智能解析」即可自动生成《招标委托书》;③确认预填单后提交审批即可。
|
||||
现在点击[发起委托](./startMe)开始!
|
||||
```
|
||||
|
||||
![[Pasted image 20260710182754.png]]
|
||||
|
||||
|
||||
## 16 发起招标委托流程
|
||||
- 要增加[导入采购计划]()按钮
|
||||
- 添加[直接填写委托单]按钮
|
||||
- 添加[选择模板]按钮(提供“自动”选项,让AI自己选择。)
|
||||
|
||||
## 17 **项目列表页面**
|
||||
- 招标专员没有“发起委托”功能。
|
||||
- 列表页面的各列表头增加排序按钮"↑""↓"。
|
||||
- 搜索项目名称的两个搜索框功能重叠。
|
||||
- 应增加“高级搜索”功能。
|
||||
- 列名应该增加 委托人,委托单位。
|
||||
- 增加提交审批按钮
|
||||
- 可选择显示的列。
|
||||
- 增加小滨演示例子:
|
||||
```
|
||||
问:帮我统计一下本年度所有项目的预算总和
|
||||
答:据统计,本年度一共有13个项目,预算总和为 12348 万元。
|
||||
```
|
||||
## 18 项目详情页面
|
||||
|
||||
- 上方显示的流程不正确,正确流程是:
|
||||
```mermaid
|
||||
graph LR
|
||||
A[发起委托]-->B[供应商寻源]-->C[招标立项]-->D[供应商投标]-->E[开标评标]-->F[定标]-->G[归档]
|
||||
```
|
||||
- 招标方案“AI生成”标签不需要。
|
||||
- 没有显示委托人信息
|
||||
- “招标方案”和“招标文件”是一回事,不要重复出现。
|
||||
- 增加提交审批按钮
|
||||
- 小滨助手示例:
|
||||
```
|
||||
user:请帮我生成招标方案
|
||||
AI:好的,根据当前项目资料,我将以软件采购模板为基础为您生成完整招标方案,或者您也可以选择不同的模板。[确定按钮]
|
||||
user:<选择确定>
|
||||
AI:好的正在为您生成招标方案,您可在AI任务列表中查看它的进度。[任务列表]
|
||||
```
|
||||
|
||||
## 19 供应商长短名单管理
|
||||
|
||||
- 不需要 “邀请招标分支”和 “公开招标分支“,招标方式在需求委托阶段已经确定。
|
||||
- 不需要显示推荐部门列
|
||||
|
||||
## 20 AI供应商寻源页面
|
||||
- AI寻源结果 没有跟项目关联。
|
||||
- 不仅仅是互联网寻源,还有从 SRM 库寻源,从STP临时供应商池寻源。
|
||||
|
||||
## 21 **开标大厅**
|
||||
- 不需要自动开标
|
||||
|
||||
## 22 定标审批页面
|
||||
- 审批在OA中,不需要体现审批页面。
|
||||
|
||||
## 23 合同生成页面
|
||||
- 合同生成在 SRM 中,STP 中没有此页面
|
||||
|
||||
## 24 合并招标页面
|
||||
- 目前不需要此页面
|
||||
|
||||
## 25 专家管理页面
|
||||
- 回避状态与项目相关,不需要在总体管理页面中体现。
|
||||
- 抽取专家在项目流程,不在总体管理中。
|
||||
|
||||
# 26 供应商管理
|
||||
- 不需要有新增供应商功能。
|
||||
- 不需要供应商入库功能(审核通过即自动入库,无须单独操作)
|
||||
|
||||
## 27 合同管理页面
|
||||
- 不需要。
|
||||
## 28 供应商入库页面
|
||||
- 不需要。
|
||||
|
||||
## 29 **审批中心**
|
||||
- 不需要
|
||||
|
||||
## 30 **流程配置**
|
||||
- 不需要,审批流程在 OA 中配置。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.1.12",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"playwright": "^1.61.1",
|
||||
"tailwindcss": "4.1.12",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
@@ -4766,6 +4767,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.1.12",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"playwright": "^1.61.1",
|
||||
"tailwindcss": "4.1.12",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
@@ -87,4 +88,4 @@
|
||||
"vite": "6.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
SHOTS = os.path.join(ROOT, "shots")
|
||||
OUT = os.path.join(ROOT, "滨化智慧招标平台-前端界面截图汇总.docx")
|
||||
BRAND = RGBColor(0x15, 0x65, 0xC0)
|
||||
BRAND_HEX = "1565C0"
|
||||
PAGE_W_IN = 6.3
|
||||
|
||||
with open(os.path.join(SHOTS, "manifest.json"), "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
order = []
|
||||
for m in manifest:
|
||||
if m["section"] not in order:
|
||||
order.append(m["section"])
|
||||
manifest.sort(key=lambda x: order.index(x["section"]))
|
||||
|
||||
doc = Document()
|
||||
style = doc.styles["Normal"]
|
||||
style.font.name = "Microsoft YaHei"
|
||||
style.font.size = Pt(10.5)
|
||||
style.element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
|
||||
for sec in doc.sections:
|
||||
sec.top_margin = Inches(0.8)
|
||||
sec.bottom_margin = Inches(0.8)
|
||||
sec.left_margin = Inches(0.8)
|
||||
sec.right_margin = Inches(0.8)
|
||||
|
||||
def set_font(run, size=None, bold=None, color=None):
|
||||
run.font.name = "Microsoft YaHei"
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
if size:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.font.bold = bold
|
||||
if color is not None:
|
||||
run.font.color.rgb = color
|
||||
|
||||
# 封面
|
||||
for _ in range(4):
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = p.add_run("滨化集团智慧招标平台")
|
||||
set_font(r, size=30, bold=True, color=BRAND)
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = p.add_run("Smart Tendering Platform (STP)")
|
||||
set_font(r, size=14, color=RGBColor(0x88, 0x88, 0x88))
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = p.add_run("前端界面截图汇总")
|
||||
set_font(r, size=22, bold=True)
|
||||
for _ in range(6):
|
||||
doc.add_paragraph()
|
||||
|
||||
sections_order = []
|
||||
for m in manifest:
|
||||
if m["section"] not in sections_order:
|
||||
sections_order.append(m["section"])
|
||||
|
||||
meta = [
|
||||
("文档类型", "前端界面(UI)截图汇总"),
|
||||
("覆盖范围", "门户页 / 登录页 / 新增导航页 / 工作台 / 业务页 / 弹窗"),
|
||||
("截图数量", f"{len(manifest)} 张"),
|
||||
("章节数量", f"{len(sections_order)} 章"),
|
||||
("生成日期", date.today().strftime("%Y 年 %m 月 %d 日")),
|
||||
]
|
||||
for k, v in meta:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = p.add_run(f"{k}:")
|
||||
set_font(r, size=11, bold=True)
|
||||
r = p.add_run(v)
|
||||
set_font(r, size=11, color=RGBColor(0x44, 0x44, 0x44))
|
||||
|
||||
# 目录
|
||||
doc.add_page_break()
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("目 录")
|
||||
set_font(r, size=18, bold=True, color=BRAND)
|
||||
doc.add_paragraph()
|
||||
for sec in sections_order:
|
||||
cnt = sum(1 for m in manifest if m["section"] == sec)
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(sec)
|
||||
set_font(r, size=12, bold=True)
|
||||
r = p.add_run(f" ({cnt} 张)")
|
||||
set_font(r, size=10.5, color=RGBColor(0x88, 0x88, 0x88))
|
||||
|
||||
fig_no = 0
|
||||
current = None
|
||||
for m in manifest:
|
||||
img = os.path.join(SHOTS, m["file"])
|
||||
if not os.path.exists(img):
|
||||
continue
|
||||
if m["section"] != current:
|
||||
current = m["section"]
|
||||
doc.add_page_break()
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(current)
|
||||
set_font(r, size=17, bold=True, color=BRAND)
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
pbdr = pPr.makeelement(qn("w:pBdr"), {})
|
||||
bottom = pbdr.makeelement(qn("w:bottom"), {
|
||||
qn("w:val"): "single", qn("w:sz"): "12",
|
||||
qn("w:space"): "4", qn("w:color"): BRAND_HEX,
|
||||
})
|
||||
pbdr.append(bottom)
|
||||
pPr.append(pbdr)
|
||||
doc.add_paragraph()
|
||||
|
||||
fig_no += 1
|
||||
cap = doc.add_paragraph()
|
||||
r = cap.add_run(f"图 {fig_no} {m['title']}")
|
||||
set_font(r, size=11, bold=True, color=RGBColor(0x22, 0x22, 0x22))
|
||||
|
||||
with Image.open(img) as im:
|
||||
w, h = im.size
|
||||
ratio = h / w
|
||||
width_in = PAGE_W_IN
|
||||
height_in = width_in * ratio
|
||||
max_h = 8.6
|
||||
if height_in > max_h:
|
||||
height_in = max_h
|
||||
width_in = height_in / ratio
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.add_run().add_picture(img, width=Inches(width_in))
|
||||
doc.add_paragraph()
|
||||
|
||||
doc.save(OUT)
|
||||
print(f"已生成:{OUT}")
|
||||
print(f"共 {fig_no} 张截图,{len(sections_order)} 个章节")
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { chromium } from "playwright";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = path.resolve(__dirname, "../../shots");
|
||||
const BASE = process.env.BASE_URL || "http://localhost:5174";
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
fs.rmSync(OUT, { recursive: true, force: true });
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const manifest = [];
|
||||
let idx = 0;
|
||||
|
||||
async function expand(page) {
|
||||
await page.evaluate(() => {
|
||||
document.querySelectorAll(".h-screen").forEach((el) => {
|
||||
el.style.height = "auto";
|
||||
el.style.minHeight = "100vh";
|
||||
el.style.overflow = "visible";
|
||||
});
|
||||
document.querySelectorAll(".overflow-hidden").forEach((el) => (el.style.overflow = "visible"));
|
||||
document.querySelectorAll("main").forEach((el) => {
|
||||
el.style.overflow = "visible";
|
||||
el.style.height = "auto";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function shot(page, section, title, { full = true, wait = 500, expandLayout = true } = {}) {
|
||||
idx += 1;
|
||||
const file = `${String(idx).padStart(2, "0")}.png`;
|
||||
if (expandLayout) await expand(page).catch(() => {});
|
||||
await sleep(wait);
|
||||
await page.screenshot({ path: path.join(OUT, file), fullPage: full });
|
||||
manifest.push({ file, section, title });
|
||||
console.log(`[${idx}] ${section} / ${title} -> ${file}`);
|
||||
}
|
||||
|
||||
async function goto(page, url) {
|
||||
await page.goto(BASE + url, { waitUntil: "networkidle" }).catch(async () => {
|
||||
await page.goto(BASE + url, { waitUntil: "domcontentloaded" });
|
||||
});
|
||||
await sleep(450);
|
||||
}
|
||||
|
||||
async function clickIf(page, locator, wait = 300) {
|
||||
const n = await locator.count();
|
||||
if (n > 0) {
|
||||
await locator.first().click().catch(() => {});
|
||||
await sleep(wait);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchRole(page, name) {
|
||||
await page.locator("header button:has(svg.lucide-refresh-cw)").first().click();
|
||||
await sleep(250);
|
||||
await page.getByRole("menuitem", { name }).first().click();
|
||||
await sleep(700);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.3 });
|
||||
page.setDefaultTimeout(15000);
|
||||
|
||||
const A = "一、门户与登录";
|
||||
await goto(page, "/portal");
|
||||
await shot(page, A, "首页(门户)", { wait: 700 });
|
||||
await clickIf(page, page.getByRole("link", { name: "交易信息" }));
|
||||
await shot(page, A, "门户导航 · 交易信息页");
|
||||
await goto(page, "/portal/policies");
|
||||
await shot(page, A, "门户导航 · 政策法规页");
|
||||
await goto(page, "/portal/guides");
|
||||
await shot(page, A, "门户导航 · 操作指南页");
|
||||
await goto(page, "/portal/faq");
|
||||
await shot(page, A, "门户导航 · 常见问题页");
|
||||
await goto(page, "/portal/procurement-platform");
|
||||
await shot(page, A, "门户导航 · 采购平台页");
|
||||
await goto(page, "/");
|
||||
await shot(page, A, "登录页(含可拖拽小滨)", { full: false, expandLayout: false });
|
||||
|
||||
const B = "二、角色工作台";
|
||||
await goto(page, "/app");
|
||||
await shot(page, B, "招标专员工作台", { wait: 800 });
|
||||
for (const rn of ["采购需求人员", "评标专家", "评标委员会工作领导小组", "供应商", "系统管理员", "合规 / 审计"]) {
|
||||
await switchRole(page, rn);
|
||||
await shot(page, B, `${rn}工作台`, { wait: 700 });
|
||||
}
|
||||
await switchRole(page, "招标专员");
|
||||
|
||||
const C = "三、核心业务页面";
|
||||
for (const [url, title] of [
|
||||
["/app/delegation", "发起招标委托"],
|
||||
["/app/projects", "项目管理列表"],
|
||||
["/app/projects/P2026001", "项目详情"],
|
||||
["/app/projects/P2026001/shortlist", "供应商长短名单"],
|
||||
["/app/projects/P2026001/sourcing", "AI智慧寻源"],
|
||||
["/app/projects/P2026001/opening", "开标大厅"],
|
||||
["/app/projects/P2026001/evaluation", "在线评标室"],
|
||||
["/app/deposits", "保证金管理"],
|
||||
["/app/suppliers", "供应商管理"],
|
||||
["/app/experts", "专家管理"],
|
||||
["/app/reports", "报表中心"],
|
||||
["/app/exceptions", "异常决策"],
|
||||
["/app/expert-tasks", "评标任务"],
|
||||
["/app/bidding", "投标中心"],
|
||||
["/app/audit-logs", "审计日志"],
|
||||
["/app/collusion", "围串标分析"],
|
||||
["/app/evidence", "存证验证"],
|
||||
["/app/admin/users", "用户与权限管理"],
|
||||
["/app/admin/keys", "密钥管理"],
|
||||
["/app/settings", "系统设置"],
|
||||
]) {
|
||||
await goto(page, url);
|
||||
await shot(page, C, title, { wait: 600 });
|
||||
}
|
||||
|
||||
const D = "四、弹窗与抽屉";
|
||||
await goto(page, "/app/suppliers");
|
||||
await clickIf(page, page.locator("tbody tr"), 700);
|
||||
await shot(page, D, "供应商详情抽屉", { full: false, expandLayout: false });
|
||||
|
||||
await goto(page, "/app/expert-tasks");
|
||||
await clickIf(page, page.getByRole("button", { name: /确认邀请/ }), 500);
|
||||
await clickIf(page, page.getByRole("button", { name: /签署评标承诺书/ }), 700);
|
||||
await shot(page, D, "评标纪律承诺书弹窗", { full: false, expandLayout: false });
|
||||
|
||||
await goto(page, "/app/admin/keys");
|
||||
await clickIf(page, page.getByRole("button", { name: /手动触发密钥漂移/ }), 700);
|
||||
await shot(page, D, "密钥漂移二次授权弹窗", { full: false, expandLayout: false });
|
||||
|
||||
fs.writeFileSync(path.join(OUT, "manifest.json"), JSON.stringify(manifest, null, 2));
|
||||
await browser.close();
|
||||
console.log(`完成:共 ${manifest.length} 张截图`);
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
fs.writeFileSync(path.join(OUT, "manifest.json"), JSON.stringify(manifest, null, 2));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,11 @@ import { AppLayout } from "./components/layout/AppLayout";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { RegisterPage } from "./pages/RegisterPage";
|
||||
import { PortalHome } from "./pages/PortalHome";
|
||||
import { TradingInfoPage } from "./pages/portal/TradingInfoPage";
|
||||
import { PoliciesPage } from "./pages/portal/PoliciesPage";
|
||||
import { GuidesPage } from "./pages/portal/GuidesPage";
|
||||
import { FaqPage } from "./pages/portal/FaqPage";
|
||||
import { ProcurementPlatformPage } from "./pages/portal/ProcurementPlatformPage";
|
||||
import { Workbench } from "./pages/Workbench";
|
||||
import { Delegation } from "./pages/Delegation";
|
||||
import { ProjectList } from "./pages/ProjectList";
|
||||
@@ -12,15 +17,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 +29,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() {
|
||||
@@ -41,6 +39,11 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginPage />} />
|
||||
<Route path="/portal" element={<PortalHome />} />
|
||||
<Route path="/portal/trading-info" element={<TradingInfoPage />} />
|
||||
<Route path="/portal/policies" element={<PoliciesPage />} />
|
||||
<Route path="/portal/guides" element={<GuidesPage />} />
|
||||
<Route path="/portal/faq" element={<FaqPage />} />
|
||||
<Route path="/portal/procurement-platform" element={<ProcurementPlatformPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/app" element={<AppLayout />}>
|
||||
<Route index element={<Workbench />} />
|
||||
@@ -51,20 +54,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 +71,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, Bot } 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,85 +37,256 @@ 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() {
|
||||
type PageType = "app" | "portal" | "login";
|
||||
|
||||
function getPointer(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) {
|
||||
if ("touches" in e && e.touches.length > 0) {
|
||||
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
}
|
||||
if ("changedTouches" in e && e.changedTouches.length > 0) {
|
||||
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
|
||||
}
|
||||
if ("clientX" in e) {
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const ENTRY_PROACTIVE: Proactive[] = [
|
||||
{
|
||||
type: "todo",
|
||||
title: "欢迎使用智慧招标平台",
|
||||
content: "您可以直接从首页查看交易信息、政策法规与操作指南。",
|
||||
to: "/portal/trading-info",
|
||||
actionLabel: "查看交易信息",
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
title: "新用户建议先注册",
|
||||
content: "供应商用户可先完成注册与实名认证,后续即可参与报名与投标。",
|
||||
to: "/register",
|
||||
actionLabel: "立即注册",
|
||||
},
|
||||
];
|
||||
|
||||
export function XiaobinAssistant({ pageType = "app" }: { pageType?: PageType }) {
|
||||
const { sensitive, role, roleId } = useRole();
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
// 工作台默认展开;首页/登录页默认收起,避免遮挡主内容
|
||||
const [open, setOpen] = useState(pageType === "app");
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [proactive, setProactive] = useState<Proactive[]>(PROACTIVE[roleId] ?? []);
|
||||
const [proactive, setProactive] = useState<Proactive[]>(
|
||||
pageType === "app" ? PROACTIVE[roleId] ?? [] : ENTRY_PROACTIVE,
|
||||
);
|
||||
const [msgs, setMsgs] = useState<Msg[]>([
|
||||
{
|
||||
role: "bot",
|
||||
text: "您好,我是智能助手「小滨」👋 我可以为您解答系统操作与业务咨询。注意:我不参与任何业务决策哦。请问有什么可以帮您?",
|
||||
text: "您好,我是智能助手「小滨」👋 我可以为您解答系统操作与业务咨询。注意:我不参与任何业务决策哦。请问有什么可以帮您?",
|
||||
},
|
||||
]);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef({ dragging: false, dx: 0, dy: 0, sx: 0, sy: 0, moved: false });
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const EDGE_GAP = 20;
|
||||
|
||||
const panelSize = open ? { w: 360, h: 520 } : { w: 68, h: 68 };
|
||||
|
||||
function clamp(x: number, y: number) {
|
||||
const pad = EDGE_GAP;
|
||||
const maxX = Math.max(pad, window.innerWidth - panelSize.w - pad);
|
||||
const maxY = Math.max(pad, window.innerHeight - panelSize.h - pad);
|
||||
return {
|
||||
x: Math.min(Math.max(x, pad), maxX),
|
||||
y: Math.min(Math.max(y, pad), maxY),
|
||||
};
|
||||
}
|
||||
|
||||
function startDrag(e: React.MouseEvent | React.TouchEvent) {
|
||||
const p = getPointer(e);
|
||||
dragRef.current.dragging = true;
|
||||
dragRef.current.sx = p.x;
|
||||
dragRef.current.sy = p.y;
|
||||
dragRef.current.moved = false;
|
||||
setDragging(true);
|
||||
dragRef.current.dx = p.x - pos.x;
|
||||
dragRef.current.dy = p.y - pos.y;
|
||||
}
|
||||
|
||||
function snapToEdge(x: number, y: number) {
|
||||
const pad = EDGE_GAP;
|
||||
const maxX = Math.max(pad, window.innerWidth - panelSize.w - pad);
|
||||
const maxY = Math.max(pad, window.innerHeight - panelSize.h - pad);
|
||||
const targetX = x <= maxX / 2 ? pad : maxX;
|
||||
return {
|
||||
x: targetX,
|
||||
y: Math.min(Math.max(y, pad), maxY),
|
||||
};
|
||||
}
|
||||
|
||||
// 切换角色时刷新主动提醒
|
||||
useEffect(() => {
|
||||
setProactive(PROACTIVE[roleId] ?? []);
|
||||
}, [roleId]);
|
||||
if (pageType === "app") setProactive(PROACTIVE[roleId] ?? []);
|
||||
}, [roleId, pageType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showChat) endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, showChat]);
|
||||
|
||||
// 初始位置(右下角)
|
||||
useEffect(() => {
|
||||
const w = pageType === "app" ? 360 : 68;
|
||||
const h = pageType === "app" ? 520 : 68;
|
||||
const p = clamp(window.innerWidth - w - 32, window.innerHeight - h - 32);
|
||||
setPos(p);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pageType]);
|
||||
|
||||
useEffect(() => {
|
||||
function onMove(e: MouseEvent | TouchEvent) {
|
||||
if (!dragRef.current.dragging) return;
|
||||
const p = getPointer(e);
|
||||
if (!dragRef.current.moved && (Math.abs(p.x - dragRef.current.sx) > 4 || Math.abs(p.y - dragRef.current.sy) > 4)) {
|
||||
dragRef.current.moved = true;
|
||||
}
|
||||
const next = clamp(p.x - dragRef.current.dx, p.y - dragRef.current.dy);
|
||||
setPos(next);
|
||||
}
|
||||
function onUp() {
|
||||
if (!dragRef.current.dragging) return;
|
||||
dragRef.current.dragging = false;
|
||||
setDragging(false);
|
||||
setPos((prev) => snapToEdge(prev.x, prev.y));
|
||||
}
|
||||
function onResize() {
|
||||
setPos((prev) => clamp(prev.x, prev.y));
|
||||
}
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("touchmove", onMove, { passive: false });
|
||||
window.addEventListener("mouseup", onUp);
|
||||
window.addEventListener("touchend", onUp);
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("touchmove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
window.removeEventListener("touchend", onUp);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
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,154 +295,205 @@ 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 (
|
||||
<>
|
||||
{/* 悬浮球 + 呼吸光环 */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed z-50",
|
||||
open ? "border bg-card shadow-2xl" : "border-0 bg-transparent shadow-none",
|
||||
open ? "overflow-hidden" : "overflow-visible",
|
||||
dragging ? "" : "transition-[left,top,width,height,border-radius] duration-260 ease-out",
|
||||
)}
|
||||
style={{
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
width: open ? 360 : 68,
|
||||
height: open ? 520 : 68,
|
||||
borderRadius: open ? 16 : "9999px",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!open && !dragRef.current.moved) setOpen(true);
|
||||
}}
|
||||
>
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="group fixed bottom-8 right-8 z-50 flex size-[68px] items-center justify-center rounded-full text-white shadow-xl transition-transform hover:scale-105"
|
||||
<div
|
||||
onMouseDown={startDrag}
|
||||
onTouchStart={startDrag}
|
||||
className="relative flex size-full cursor-grab items-center justify-center rounded-full text-white shadow-xl ring-1 ring-white/35 active:cursor-grabbing"
|
||||
style={{ backgroundImage: gradient }}
|
||||
aria-label="打开小滨 AI 助手"
|
||||
>
|
||||
<span className="pointer-events-none absolute inset-0 rounded-full animate-breathe" style={{ backgroundImage: gradient }} />
|
||||
<span className="relative flex flex-col items-center leading-none">
|
||||
<span className="text-sm font-bold">小滨</span>
|
||||
<span className="mt-0.5 text-[9px] text-white/90">AI 助手</span>
|
||||
<span className="flex size-8 items-center justify-center rounded-full bg-white/22 ring-1 ring-white/35">
|
||||
<Bot className="size-4.5" />
|
||||
</span>
|
||||
<span className="mt-1 text-[10px] font-medium text-white/95">小滨</span>
|
||||
</span>
|
||||
{unread > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-[#f44336] text-[10px] font-bold text-white">
|
||||
<span className="absolute right-0 top-0 z-10 flex size-5 translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border-2 border-white bg-[#f44336] text-[10px] font-bold text-white shadow-sm">
|
||||
{unread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div className="fixed bottom-8 right-8 z-50 flex h-[520px] w-[360px] flex-col overflow-hidden rounded-2xl border bg-card shadow-2xl">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 text-white" style={{ backgroundImage: gradient }}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-white/20 text-sm font-bold">滨</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">小滨 AI 助手</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/85">
|
||||
<span className="size-1.5 rounded-full bg-emerald-300" /> 在线 · 已为您分析今日待办
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{sensitive && (
|
||||
<div className="flex items-center gap-1.5 border-b bg-amber-50 px-4 py-2 text-xs text-amber-700">
|
||||
<Lock className="size-3.5" /> 当前为流程敏感期,数据类问题暂无法回答
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 space-y-3 overflow-y-auto bg-muted/30 p-4">
|
||||
{!showChat ? (
|
||||
<>
|
||||
{proactive.length === 0 && (
|
||||
<div className="rounded-xl border bg-card p-4 text-center text-sm text-muted-foreground">
|
||||
🎉 暂无待办,继续保持!
|
||||
</div>
|
||||
)}
|
||||
{proactive.map((p, i) => {
|
||||
const st = TYPE_STYLE[p.type];
|
||||
return (
|
||||
<div key={i} className={cn("animate-slide-up rounded-xl border p-3", st.bg, st.border)}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className={cn("text-[13px] font-bold", st.title)}>
|
||||
{st.badge} {p.title}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setProactive((ps) => ps.filter((_, idx) => idx !== i))}
|
||||
className="text-muted-foreground/60 hover:text-muted-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-[#4a5568]">{p.content}</p>
|
||||
{p.to && (
|
||||
<button
|
||||
onClick={() => handleAction(p)}
|
||||
className={cn("mt-2 inline-flex items-center gap-0.5 text-xs font-bold underline", st.title)}
|
||||
>
|
||||
{p.actionLabel ?? "去处理"} <ArrowRight className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="pt-1">
|
||||
<p className="mb-2 text-xs text-muted-foreground">猜你想问:</p>
|
||||
<div className="space-y-2">
|
||||
{quickReplies.map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => send(q)}
|
||||
className="block w-full rounded-lg border bg-card px-3 py-2 text-left text-xs text-foreground transition-colors hover:border-primary hover:text-primary"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => setShowChat(false)} className="text-xs text-primary hover:underline">
|
||||
← 返回待办提醒
|
||||
</button>
|
||||
{msgs.map((m, i) => (
|
||||
<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",
|
||||
m.role === "user"
|
||||
? "rounded-br-sm bg-primary text-primary-foreground"
|
||||
: "rounded-bl-sm border bg-card text-card-foreground",
|
||||
)}
|
||||
>
|
||||
{m.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={endRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-t bg-card p-3">
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send(input)}
|
||||
placeholder="输入您的问题…"
|
||||
className="h-9 flex-1 rounded-full border bg-input-background px-3.5 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Button size="icon" onClick={() => send(input)} className="size-9 shrink-0 rounded-full" style={{ backgroundImage: gradient }}>
|
||||
<Send className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col",
|
||||
open ? "opacity-100" : "pointer-events-none opacity-0",
|
||||
dragging ? "" : "transition-opacity duration-180",
|
||||
)}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div
|
||||
className="flex cursor-move items-center justify-between px-4 py-3 text-white"
|
||||
style={{ backgroundImage: gradient }}
|
||||
onMouseDown={startDrag}
|
||||
onTouchStart={startDrag}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-white/20 text-sm font-bold">滨</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">小滨 AI 助手</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/85">
|
||||
<span className="size-1.5 rounded-full bg-emerald-300" /> 在线 · 已为您分析今日待办
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setOpen(false)} className="rounded-md p-1 hover:bg-white/15" title="收起">
|
||||
<Minus className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sensitive && (
|
||||
<div className="flex items-center gap-1.5 border-b bg-amber-50 px-4 py-2 text-xs text-amber-700">
|
||||
<Lock className="size-3.5" /> 当前为流程敏感期,数据类问题暂无法回答
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">
|
||||
🎉 暂无待办,继续保持!
|
||||
</div>
|
||||
)}
|
||||
{proactive.map((p, i) => {
|
||||
const st = TYPE_STYLE[p.type];
|
||||
return (
|
||||
<div key={i} className={cn("animate-slide-up rounded-xl border p-3", st.bg, st.border)}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className={cn("text-[13px] font-bold", st.title)}>
|
||||
{st.badge} {p.title}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setProactive((ps) => ps.filter((_, idx) => idx !== i))}
|
||||
className="text-muted-foreground/60 hover:text-muted-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-[#4a5568]">{p.content}</p>
|
||||
{p.to && (
|
||||
<button
|
||||
onClick={() => handleAction(p)}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="pt-1">
|
||||
<p className="mb-2 text-xs text-muted-foreground">猜你想问:</p>
|
||||
<div className="space-y-2">
|
||||
{quickReplies.map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => send(q)}
|
||||
className="block w-full rounded-lg border bg-card px-3 py-2 text-left text-xs text-foreground transition-colors hover:border-primary hover:text-primary"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => setShowChat(false)} className="text-xs text-primary hover:underline">
|
||||
← 返回待办提醒
|
||||
</button>
|
||||
{msgs.map((m, i) => (
|
||||
<div key={i} className={cn("flex", m.role === "user" ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
className={cn(
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
<div ref={endRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-t bg-card p-3">
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send(input)}
|
||||
placeholder="输入您的问题…"
|
||||
className="h-9 flex-1 rounded-full border bg-input-background px-3.5 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Button size="icon" onClick={() => send(input)} className="size-9 shrink-0 rounded-full" style={{ backgroundImage: gradient }}>
|
||||
<Send className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ export function AppLayout() {
|
||||
const initials = role.user.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
<div className="flex h-screen overflow-hidden bg-[#f3f5f8]">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b bg-card px-6">
|
||||
<header className="flex h-14 shrink-0 items-center justify-between border-b bg-white px-5">
|
||||
<div className="relative w-96 max-w-[40vw]">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
placeholder="搜索项目编号 / 名称…"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
className="h-8 w-full border bg-[#f9fafb] pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -37,10 +37,10 @@ export function AppLayout() {
|
||||
<button
|
||||
onClick={() => setSensitive(!sensitive)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs transition-colors",
|
||||
"flex items-center gap-1.5 border px-2.5 py-1 text-xs transition-colors",
|
||||
sensitive
|
||||
? "border-amber-300 bg-amber-50 text-amber-700"
|
||||
: "border-border text-muted-foreground hover:bg-muted",
|
||||
: "border-border text-muted-foreground hover:bg-[#f3f4f6]",
|
||||
)}
|
||||
title="切换流程敏感期(影响小滨安全模式与数据可见性)"
|
||||
>
|
||||
@@ -50,7 +50,7 @@ export function AppLayout() {
|
||||
|
||||
{/* 角色切换 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs outline-none hover:bg-muted">
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 border px-2.5 py-1 text-xs outline-none hover:bg-[#f3f4f6]">
|
||||
<span className="size-2 rounded-full" style={{ backgroundColor: role.color }} />
|
||||
<span className="font-medium text-foreground">{role.name}</span>
|
||||
<RefreshCw className="size-3.5 text-muted-foreground" />
|
||||
@@ -77,7 +77,7 @@ export function AppLayout() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<button className="relative rounded-lg p-2 text-muted-foreground hover:bg-muted">
|
||||
<button className="relative border p-1.5 text-muted-foreground hover:bg-[#f3f4f6]">
|
||||
<Bell className="size-5" />
|
||||
<span className="absolute right-1.5 top-1.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] text-white">
|
||||
5
|
||||
@@ -85,7 +85,7 @@ export function AppLayout() {
|
||||
</button>
|
||||
<div className="h-6 w-px bg-border" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-2.5 rounded-lg px-2 py-1.5 outline-none hover:bg-muted">
|
||||
<DropdownMenuTrigger className="flex items-center gap-2.5 border px-2 py-1 outline-none hover:bg-[#f3f4f6]">
|
||||
<Avatar className="size-8">
|
||||
<AvatarFallback className="text-xs text-white" style={{ backgroundColor: role.color }}>
|
||||
{initials}
|
||||
@@ -109,7 +109,7 @@ export function AppLayout() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
<main className="flex-1 overflow-y-auto p-4">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,9 @@ export function Sidebar() {
|
||||
const { role } = useRole();
|
||||
|
||||
return (
|
||||
<aside className="flex w-60 shrink-0 flex-col bg-sidebar text-sidebar-foreground">
|
||||
<div className="flex h-16 items-center gap-2.5 px-5">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-primary">
|
||||
<aside className="flex w-[232px] shrink-0 flex-col border-r bg-[#0b2a4a] text-sidebar-foreground">
|
||||
<div className="flex h-14 items-center gap-2.5 px-4">
|
||||
<div className="flex size-8 items-center justify-center bg-primary">
|
||||
<Hexagon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
@@ -21,7 +21,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* 当前角色标识 */}
|
||||
<div className="mx-3 mb-2 flex items-center gap-2 rounded-lg bg-white/5 px-3 py-2">
|
||||
<div className="mx-3 mb-2 flex items-center gap-2 border border-white/10 bg-white/5 px-3 py-2">
|
||||
<span className="size-2 rounded-full" style={{ backgroundColor: role.color }} />
|
||||
<div className="leading-tight">
|
||||
<div className="text-xs font-medium text-white">{role.name}</div>
|
||||
@@ -39,10 +39,10 @@ export function Sidebar() {
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
"flex items-center gap-3 px-3 py-2.5 text-sm transition-colors",
|
||||
active
|
||||
? "bg-primary text-white shadow-sm"
|
||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-white",
|
||||
? "bg-primary text-white"
|
||||
: "text-sidebar-foreground/80 hover:bg-[#143a61] hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-[18px]" />
|
||||
@@ -53,7 +53,7 @@ export function Sidebar() {
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-sidebar-border/40 p-4">
|
||||
<div className="rounded-lg bg-sidebar-accent/40 p-3">
|
||||
<div className="border border-white/10 bg-sidebar-accent/40 p-3">
|
||||
<div className="text-xs font-medium text-white">AI 原生招标平台</div>
|
||||
<p className="mt-1 text-[11px] leading-relaxed text-sidebar-foreground/60">
|
||||
全流程线上闭环,事找人主动式智能体验
|
||||
|
||||
@@ -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,9 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, Link } from "react-router";
|
||||
import { Hexagon, User, Lock, Smartphone, ShieldCheck, ArrowRight, Eye, EyeOff, Info } from "lucide-react";
|
||||
import { Hexagon, User, Lock, Smartphone, ShieldCheck, Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { cn } from "../components/ui/utils";
|
||||
import { XiaobinAssistant } from "../components/XiaobinAssistant";
|
||||
|
||||
type Mode = "employee" | "vendor";
|
||||
type Tab = "account" | "mobile";
|
||||
@@ -41,7 +42,7 @@ export function LoginPage() {
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (mode === "employee") {
|
||||
toast.info("正在跳转至集团 IAM 统一身份认证…");
|
||||
toast.info("正在跳转至滨化集团 IAM 统一身份认证…");
|
||||
setTimeout(() => navigate("/app"), 800);
|
||||
return;
|
||||
}
|
||||
@@ -49,69 +50,26 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#f5f7fa]">
|
||||
{/* 左侧品牌区 */}
|
||||
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-brand-gradient p-12 text-white lg:flex">
|
||||
<Link to="/portal" className="flex items-center gap-3">
|
||||
<div className="flex size-11 items-center justify-center rounded-xl bg-white/15 backdrop-blur">
|
||||
<Hexagon className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">滨化集团智慧招标平台</div>
|
||||
<div className="text-sm text-white/70">Smart Tendering Platform</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h1 className="text-white" style={{ fontSize: 44, lineHeight: 1.2, fontWeight: 700, letterSpacing: 2 }}>
|
||||
智慧招标平台
|
||||
</h1>
|
||||
<p className="mt-5 max-w-md text-white/80">
|
||||
AI 原生 · 全流程线上闭环 · 主动式智能
|
||||
</p>
|
||||
<p className="mt-2 max-w-md text-sm text-white/60">
|
||||
覆盖公开招标 · 邀请招标 · 竞争性谈判 · 单一来源
|
||||
</p>
|
||||
<div className="mt-8 grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ k: "全流程", v: "9 大阶段闭环" },
|
||||
{ k: "AI 寻源", v: "委托效率 +60%" },
|
||||
{ k: "链上存证", v: "投标哈希上链" },
|
||||
].map((s) => (
|
||||
<div key={s.k} className="rounded-xl bg-white/10 p-4 backdrop-blur">
|
||||
<div className="text-lg font-semibold">{s.k}</div>
|
||||
<div className="mt-1 text-xs text-white/70">{s.v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="min-h-screen bg-[#f3f5f8]">
|
||||
<header className="border-b bg-[#0f4ea6] text-white">
|
||||
<div className="mx-auto flex h-14 max-w-[1200px] items-center justify-between px-6">
|
||||
<Link to="/portal" className="flex items-center gap-2">
|
||||
<Hexagon className="size-5" />
|
||||
<span className="text-base font-semibold">滨化电子招标投标交易网</span>
|
||||
</Link>
|
||||
<div className="text-xs text-white/80">欢迎登录 STP 系统</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="text-xs text-white/50">© 2026 滨化集团 | 智慧招标平台 V1.0</div>
|
||||
<main className="mx-auto grid max-w-[1200px] grid-cols-12 gap-4 px-6 py-8">
|
||||
<section className="col-span-12 border bg-white p-5 lg:col-span-4">
|
||||
<h2 className="text-lg font-semibold">用户登录</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
还没有账号?
|
||||
<Link to="/register" className="ml-1 text-primary hover:underline">去注册</Link>
|
||||
</p>
|
||||
|
||||
{/* 装饰 */}
|
||||
<div className="pointer-events-none absolute -right-24 top-1/4 size-80 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-16 right-1/3 size-60 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute left-1/2 top-1/2 size-72 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10" />
|
||||
<div className="pointer-events-none absolute left-1/2 top-1/2 size-52 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10" />
|
||||
</div>
|
||||
|
||||
{/* 右侧表单区 */}
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<div className="w-full max-w-[400px]">
|
||||
<div className="mb-6 lg:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hexagon className="size-7 text-primary" />
|
||||
<span className="font-semibold">滨化智慧招标平台</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-foreground" style={{ fontSize: 24 }}>欢迎登录</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">登录您的账户,开始智能招采之旅</p>
|
||||
</div>
|
||||
|
||||
{/* 角色切换开关: 内部员工 / 外部账号 */}
|
||||
<div className="mt-6 flex rounded-full bg-muted p-1">
|
||||
<div className="mt-4 flex border bg-[#f8fafc] p-1">
|
||||
{(
|
||||
[
|
||||
{ id: "employee" as Mode, label: "内部员工" },
|
||||
@@ -123,20 +81,15 @@ export function LoginPage() {
|
||||
type="button"
|
||||
onClick={() => setMode(m.id)}
|
||||
className={cn(
|
||||
"flex-1 rounded-full py-2 text-sm font-medium transition-all",
|
||||
mode === m.id ? "bg-brand-gradient text-white shadow-sm" : "text-muted-foreground hover:text-foreground",
|
||||
"flex-1 py-2 text-sm",
|
||||
mode === m.id ? "bg-primary font-medium text-white" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-center text-xs text-primary">
|
||||
{mode === "employee" ? "当前:内部员工模式(走 IAM 集团统一认证)" : "当前:外部账号模式(走平台账号 / SRM 认证)"}
|
||||
</p>
|
||||
|
||||
{/* Tab: 账号登录 / 手机验证码 */}
|
||||
<div className="mt-5 flex border-b border-border">
|
||||
<div className="mt-3 flex border-b border-border">
|
||||
{(
|
||||
[
|
||||
{ id: "account" as Tab, label: "账号登录" },
|
||||
@@ -148,17 +101,17 @@ export function LoginPage() {
|
||||
type="button"
|
||||
onClick={() => setTab(t.id)}
|
||||
className={cn(
|
||||
"relative -mb-px px-4 pb-2.5 text-sm transition-colors",
|
||||
tab === t.id ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground",
|
||||
"relative px-4 py-2 text-sm",
|
||||
tab === t.id ? "font-medium text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
{tab === t.id && <span className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-brand-gradient" />}
|
||||
{tab === t.id && <span className="absolute inset-x-1 bottom-0 h-0.5 bg-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form className="mt-5 space-y-4" onSubmit={submit}>
|
||||
<form className="mt-4 space-y-3" onSubmit={submit}>
|
||||
{tab === "account" ? (
|
||||
<>
|
||||
<div>
|
||||
@@ -248,12 +201,12 @@ export function LoginPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="h-11 w-full bg-brand-gradient text-base hover:opacity-90">
|
||||
登 录 <ArrowRight className="size-4" />
|
||||
<Button type="submit" className="h-10 w-full text-base">
|
||||
登 录
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-5 text-center text-sm text-muted-foreground">
|
||||
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||
还没有账号?
|
||||
{mode === "employee" ? (
|
||||
<span className="ml-1 text-muted-foreground/70">员工账号由 HR 系统统一同步</span>
|
||||
@@ -263,22 +216,57 @@ export function LoginPage() {
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 border bg-[#fffbea] px-3 py-2 text-xs text-[#92400e]">
|
||||
原 SRM 注册用户,可直接使用 SRM 账号登录,无需重复注册。
|
||||
</div>
|
||||
|
||||
{/* SRM 温馨提示 */}
|
||||
{mode === "vendor" && (
|
||||
<div className="mt-5 flex items-start gap-2 rounded-lg border border-[#ffcc80] bg-[#fff3e0] px-3 py-2.5 text-xs text-[#e65100]">
|
||||
<Info className="mt-0.5 size-4 shrink-0" />
|
||||
<span>原 SRM 注册用户,可直接使用 SRM 账号密码登录,无需重复注册。</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="mt-4 text-center">
|
||||
<Link to="/portal" className="text-xs text-muted-foreground hover:text-primary">
|
||||
浏览平台首页与公开招标公告 →
|
||||
返回门户首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="col-span-12 space-y-4 lg:col-span-8">
|
||||
<div className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold">交易信息</div>
|
||||
<ul className="space-y-1 p-4 text-sm">
|
||||
{[
|
||||
["[公开招标] 扣电测试柜框架", "2026-07-13"],
|
||||
["[公开招标] 2026年橡胶密封件框架招标O型圈", "2026-07-13"],
|
||||
["[公开招标] 莱州100MW分散式风电项目EPC工程总承包[变更公告]", "2026-07-13"],
|
||||
["[公开招标] 旋塞阀框架包一 金属旋塞阀、包二 衬氟旋塞阀", "2026-07-10"],
|
||||
["[公开招标] 改性TPU熟化料仓", "2026-07-09"],
|
||||
].map(([title, date]) => (
|
||||
<li key={title} className="flex items-center gap-3 border-b border-dashed py-1.5">
|
||||
<span className="line-clamp-1 flex-1 hover:text-primary">{title}</span>
|
||||
<span className="text-xs text-muted-foreground">{date}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold">通知公告</div>
|
||||
<div className="space-y-1 p-4 text-sm text-muted-foreground">
|
||||
<p>蓬莱园区高温管道耐火衬里类物资供应商招募</p>
|
||||
<p>化学桨叶冷却机类供应商招募</p>
|
||||
<p>化学洗车平台服务类供应商招募</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold">操作指南</div>
|
||||
<div className="space-y-1 p-4 text-sm text-muted-foreground">
|
||||
<p>供应商注册操作指南</p>
|
||||
<p>投标文件制作软件操作手册</p>
|
||||
<p>系统使用常见问题处理</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<XiaobinAssistant pageType="login" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,224 +1,198 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { Hexagon, ArrowRight, Sparkles, Bot } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { cn } from "../components/ui/utils";
|
||||
import { PortalShell } from "./portal/PortalShell";
|
||||
import { XiaobinAssistant } from "../components/XiaobinAssistant";
|
||||
|
||||
const NAV = ["首页", "招标公告", "定标公告", "企业新闻"];
|
||||
type Notice = { title: string; date: string };
|
||||
|
||||
const NOTICE_TABS = ["招标公告", "资格预审公告", "中标候选人公示", "结果公告", "终止公告"];
|
||||
const BANNERS = [
|
||||
{
|
||||
title: "AI 赋能的智慧招标",
|
||||
subtitle: "全流程线上闭环,主动智能寻源,自动风险评估",
|
||||
tags: ["智能招标委托", "供应商 AI 寻源", "自动化评标辅助"],
|
||||
title: "廉洁过节",
|
||||
subtitle: "阳光招采 · 公开透明 · 全流程可追溯",
|
||||
style: "from-[#1f4d8f] via-[#2e6dc2] to-[#5c8fe0]",
|
||||
},
|
||||
{
|
||||
title: "一次注册,双平台通用",
|
||||
subtitle: "自动同步 SRM,建立统一供应商身份,秒级 AI 审核即时生效",
|
||||
tags: ["营业执照 OCR", "工商实时验真", "SRM 无感登录"],
|
||||
title: "上线试运行",
|
||||
subtitle: "智慧招标平台已上线,欢迎采购人、供应商、专家使用",
|
||||
style: "from-[#0d5f89] via-[#0f7da8] to-[#4ba6c9]",
|
||||
},
|
||||
{
|
||||
title: "阳光招采,全程存证",
|
||||
subtitle: "投标文件加密上链,评审操作哈希存证,围串标智能预警",
|
||||
tags: ["链上存证", "盲评脱敏", "AI 雷同检测"],
|
||||
title: "轮播图片2",
|
||||
subtitle: "统一门户、统一身份、统一公告发布",
|
||||
style: "from-[#2f5f3f] via-[#3b8460] to-[#66a37f]",
|
||||
},
|
||||
];
|
||||
|
||||
const TENDERS = [
|
||||
{ title: "年产5万吨聚丙烯项目设备采购", meta: "2026-07-01 | 货物", status: "报名中", color: "#2e7d32", dot: "#4caf50" },
|
||||
{ title: "数字化交付平台开发服务", meta: "2026-06-29 | 服务", status: "即将截止", color: "#e65100", dot: "#ff9800" },
|
||||
{ title: "2026年度防腐保温工程框架", meta: "2026-06-28 | 工程", status: "已截止", color: "#9e9e9e", dot: "#bdbdbd" },
|
||||
];
|
||||
const NOTICE_DATA: Record<string, Notice[]> = {
|
||||
招标公告: [
|
||||
{ title: "[公开招标] 2026年地脚螺栓、不锈钢及五金紧固件框架招标", date: "2026-07-13" },
|
||||
{ title: "[公开招标] 莱州100MW分散式风电项目EPC工程总承包[变更公告]", date: "2026-07-13" },
|
||||
{ title: "[公开招标] 2026年橡胶密封件框架招标O型圈", date: "2026-07-13" },
|
||||
{ title: "[公开招标] 旋塞阀框架包一 金属旋塞阀、包二 衬氟旋塞阀", date: "2026-07-10" },
|
||||
{ title: "[公开招标] 改性TPU熟化料仓", date: "2026-07-09" },
|
||||
],
|
||||
资格预审公告: [
|
||||
{ title: "滨州园区危化品运输服务资格预审公告", date: "2026-07-11" },
|
||||
{ title: "公辅系统维保服务资格预审公告", date: "2026-07-10" },
|
||||
{ title: "智慧园区网络安全服务资格预审公告", date: "2026-07-08" },
|
||||
],
|
||||
中标候选人公示: [
|
||||
{ title: "[公开招标] 牵引车租赁服务框架中标候选人公示", date: "2026-07-07" },
|
||||
{ title: "[公开招标] 电池材料项目地质勘察工程中标候选人公示", date: "2026-07-06" },
|
||||
{ title: "[公开招标] 四轮防爆巡检机器人系统中标候选人公示", date: "2026-06-26" },
|
||||
],
|
||||
结果公告: [
|
||||
{ title: "[公开招标] HDPE管道管件框架招标中标结果公告", date: "2026-07-07" },
|
||||
{ title: "[公开招标] 起重框架招标包一 单梁起重机中标结果公告", date: "2026-07-02" },
|
||||
{ title: "[公开招标] 海阳绿电直连光伏组件项目中标结果公告", date: "2026-06-30" },
|
||||
],
|
||||
终止公告: [
|
||||
{ title: "国标切断双偏心蝶阀框架招标废标公告", date: "2026-07-06" },
|
||||
{ title: "TPU四期熟化料仓招标废标公告", date: "2026-07-01" },
|
||||
{ title: "滨州碳酸锂项目冷却窑废标公告", date: "2026-06-24" },
|
||||
],
|
||||
};
|
||||
|
||||
const AWARDS = [
|
||||
{ title: "催化剂采购项目(包1)", meta: "2026-07-01 | 中标方:滨化催化剂有限公司" },
|
||||
{ title: "厂区安防监控升级项目", meta: "2026-06-30 | 中标方:海康威视" },
|
||||
{ title: "办公楼装修工程", meta: "2026-06-28 | 中标方:滨州建工集团" },
|
||||
];
|
||||
|
||||
const NEWS = [
|
||||
{ title: "滨化集团获评“省级智能工厂”", meta: "2026-07-01 | 集团动态" },
|
||||
{ title: "2026 年供应商大会圆满举办", meta: "2026-06-25 | 合作伙伴" },
|
||||
{ title: "智慧招标平台正式上线运营", meta: "2026-06-01 | 平台公告" },
|
||||
];
|
||||
|
||||
function AnnouncementCard({
|
||||
title,
|
||||
tag,
|
||||
headerColor,
|
||||
items,
|
||||
moreBg,
|
||||
moreText,
|
||||
}: {
|
||||
title: string;
|
||||
tag: string;
|
||||
headerColor: string;
|
||||
items: { title: string; meta: string; status?: string; color?: string; dot?: string }[];
|
||||
moreBg: string;
|
||||
moreText: string;
|
||||
}) {
|
||||
function NoticeList({ items }: { items: Notice[] }) {
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden rounded-2xl bg-white shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
|
||||
<div className="px-5 py-3 text-white" style={{ backgroundColor: headerColor }}>
|
||||
<div className="text-base font-bold">{title}</div>
|
||||
<div className="text-[11px] text-white/80">{tag}</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 p-4">
|
||||
{items.map((it) => (
|
||||
<div key={it.title} className="cursor-pointer rounded-lg bg-[#fafafa] px-3 py-2.5 transition-colors hover:bg-[#f0f2f7]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate text-sm text-[#1a2332]">{it.title}</span>
|
||||
{it.status && (
|
||||
<span className="flex shrink-0 items-center gap-1 text-xs" style={{ color: it.color }}>
|
||||
<span className="size-1.5 rounded-full" style={{ backgroundColor: it.dot }} />
|
||||
{it.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[#8895aa]">{it.meta}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
<button
|
||||
className="w-full rounded-full py-1.5 text-xs font-bold transition-opacity hover:opacity-80"
|
||||
style={{ backgroundColor: moreBg, color: moreText }}
|
||||
<ul className="space-y-1.5">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="flex items-center gap-3 border-b border-dashed border-[#e5e7eb] py-1.5 text-sm"
|
||||
>
|
||||
查看全部 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="line-clamp-1 flex-1 text-[#1f2937] hover:text-primary">{item.title}</span>
|
||||
<span className="shrink-0 text-xs text-[#6b7280]">{item.date}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function PortalHome() {
|
||||
const navigate = useNavigate();
|
||||
const [banner, setBanner] = useState(0);
|
||||
const [activeNav, setActiveNav] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setBanner((b) => (b + 1) % BANNERS.length), 5000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const b = BANNERS[banner];
|
||||
const [activeTab, setActiveTab] = useState(NOTICE_TABS[0]);
|
||||
const [activeBanner, setActiveBanner] = useState(0);
|
||||
const banner = BANNERS[activeBanner];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f5f7fa]">
|
||||
{/* 顶部导航 */}
|
||||
<header className="sticky top-0 z-30 bg-brand-gradient shadow-sm">
|
||||
<div className="mx-auto flex h-[68px] max-w-[1200px] items-center px-8">
|
||||
<Link to="/portal" className="flex items-baseline gap-2 text-white">
|
||||
<Hexagon className="size-6 self-center" />
|
||||
<span className="text-xl font-bold">智慧招标平台</span>
|
||||
<span className="hidden text-xs text-white/70 sm:inline">Smart Tendering Platform</span>
|
||||
</Link>
|
||||
<nav className="ml-12 hidden gap-1 md:flex">
|
||||
{NAV.map((n, i) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setActiveNav(i)}
|
||||
className={cn(
|
||||
"group relative px-4 py-2 text-sm text-white/80 transition-colors hover:text-white",
|
||||
activeNav === i && "font-bold text-white",
|
||||
)}
|
||||
>
|
||||
{n}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-x-4 -bottom-0.5 h-0.5 rounded-full bg-white transition-all",
|
||||
activeNav === i ? "opacity-100" : "opacity-0 group-hover:opacity-60",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="rounded-lg border border-white/60 px-4 py-1.5 text-sm text-white transition-colors hover:bg-white/15"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/register")}
|
||||
className="rounded-lg bg-white px-4 py-1.5 text-sm font-bold text-primary transition-transform hover:-translate-y-0.5"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-[1200px] px-8 pb-16">
|
||||
{/* Banner 轮播 */}
|
||||
<section className="relative mt-6 overflow-hidden rounded-2xl bg-gradient-to-br from-[#fce4ec] to-[#f3e5f5] shadow-[0_6px_24px_rgba(0,0,0,0.08)]">
|
||||
<div className="flex min-h-[280px] flex-col justify-center px-12 py-10">
|
||||
<div className="mb-3 inline-flex w-fit items-center gap-1.5 rounded-full bg-white/70 px-3 py-1 text-xs text-primary">
|
||||
<Sparkles className="size-3.5" /> AI 原生 · 全流程线上闭环
|
||||
<PortalShell active="home">
|
||||
<section className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 border bg-white lg:col-span-3">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h2 className="text-base font-semibold">用户登录</h2>
|
||||
<p className="mt-1 text-xs text-[#6b7280]">
|
||||
还没有账号,<button onClick={() => navigate("/register")} className="text-primary hover:underline">去注册</button>
|
||||
</p>
|
||||
</div>
|
||||
<h1 key={banner} className="animate-slide-up text-primary" style={{ fontSize: 34, fontWeight: 700 }}>
|
||||
{b.title}
|
||||
</h1>
|
||||
<p className="mt-3 text-lg text-[#4a5568]">{b.subtitle}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-sm text-[#8895aa]">
|
||||
{b.tags.map((t) => (
|
||||
<span key={t}>• {t}</span>
|
||||
<div className="space-y-3 px-4 pb-4">
|
||||
<input
|
||||
placeholder="账号 / 手机号"
|
||||
className="h-9 w-full border border-[#d9dee7] bg-white px-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
className="h-9 w-full border border-[#d9dee7] bg-white px-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="h-9 w-full bg-primary text-sm font-medium text-white hover:bg-[#0f4ea6]"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 border bg-white lg:col-span-9">
|
||||
<div className="relative h-[310px] overflow-hidden">
|
||||
<div className={cn("absolute inset-0 bg-gradient-to-br", banner.style)} />
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<div className="relative flex h-full flex-col justify-end p-7 text-white">
|
||||
<div className="text-3xl font-bold tracking-wide">{banner.title}</div>
|
||||
<div className="mt-2 text-sm text-white/90">{banner.subtitle}</div>
|
||||
</div>
|
||||
<div className="absolute left-5 top-5 rounded bg-black/35 px-2 py-1 text-xs text-white/90">
|
||||
首页轮播
|
||||
</div>
|
||||
<div className="absolute bottom-4 right-5 flex gap-2">
|
||||
{BANNERS.map((b, idx) => (
|
||||
<button
|
||||
key={b.title}
|
||||
onClick={() => setActiveBanner(idx)}
|
||||
className={cn(
|
||||
"rounded px-2.5 py-1 text-xs",
|
||||
idx === activeBanner ? "bg-white text-[#1f2937]" : "bg-black/35 text-white",
|
||||
)}
|
||||
>
|
||||
{b.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h2 className="text-base font-semibold">交易信息</h2>
|
||||
</div>
|
||||
<div className="border-b px-3">
|
||||
<div className="flex flex-wrap items-center">
|
||||
{NOTICE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
"relative px-4 py-3 text-sm",
|
||||
activeTab === tab ? "font-semibold text-primary" : "text-[#4b5563] hover:text-primary",
|
||||
)}
|
||||
>
|
||||
{tab}
|
||||
{activeTab === tab && <span className="absolute inset-x-2 bottom-0 h-0.5 bg-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate("/register")}
|
||||
className="mt-6 inline-flex w-fit items-center gap-1.5 rounded-lg bg-brand-gradient px-6 py-2.5 text-sm font-bold text-white shadow-md transition-transform hover:scale-105"
|
||||
>
|
||||
了解更多 <ArrowRight className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/* 指示器 */}
|
||||
<div className="absolute bottom-6 right-12 flex gap-2">
|
||||
{BANNERS.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
aria-label={`切换到第 ${i + 1} 张`}
|
||||
onClick={() => setBanner(i)}
|
||||
className={cn("h-2 rounded-full transition-all", i === banner ? "w-6 bg-primary" : "w-2 bg-[#d1d5db]")}
|
||||
<div className="p-4">
|
||||
<NoticeList items={NOTICE_DATA[activeTab]} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 border bg-white lg:col-span-6">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold">通知公告</div>
|
||||
<div className="p-4">
|
||||
<NoticeList
|
||||
items={[
|
||||
{ title: "蓬莱园区高温管道耐火衬里类物资供应商招募", date: "2026-07-10" },
|
||||
{ title: "化学桨叶冷却机类供应商招募", date: "2026-07-10" },
|
||||
{ title: "化学洗车平台服务类供应商招募", date: "2026-06-25" },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 border bg-white lg:col-span-6">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3 text-sm font-semibold">常见问题 / 操作指南</div>
|
||||
<div className="grid grid-cols-2 gap-2 p-4 text-sm">
|
||||
{[
|
||||
"系统使用常见问题处理",
|
||||
"常见问题帮助",
|
||||
"供应商注册操作指南",
|
||||
"投标文件制作软件操作手册",
|
||||
"标证通申请-投标单位操作指南",
|
||||
"浏览器切换IE模式",
|
||||
].map((x) => (
|
||||
<button key={x} className="border px-3 py-2 text-left hover:border-primary hover:bg-[#f8fbff]">
|
||||
{x}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 公告区 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-[#1a2332]" style={{ fontSize: 20, fontWeight: 700 }}>
|
||||
📢 最新公告
|
||||
</h2>
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
<AnnouncementCard title="招标公告" tag="最新发布" headerColor="#c2185b" items={TENDERS} moreBg="#fce4ec" moreText="#c2185b" />
|
||||
<AnnouncementCard title="定标公告" tag="最新定标" headerColor="#1565c0" items={AWARDS} moreBg="#e3f2fd" moreText="#1565c0" />
|
||||
<AnnouncementCard title="企业新闻" tag="集团动态" headerColor="#e65100" items={NEWS} moreBg="#fff3e0" moreText="#e65100" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 免责声明 */}
|
||||
<section className="mt-8 rounded-2xl bg-white p-6 text-center shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
|
||||
<div className="text-sm font-semibold text-[#4a5568]">免责声明</div>
|
||||
<p className="mx-auto mt-2 max-w-3xl text-xs leading-relaxed text-[#8895aa]">
|
||||
本平台所有招标信息均由滨化集团或其下属公司发布,信息真实有效。供应商应对投标行为负责,本平台保留对违规行为的处理权。
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-[#8895aa]">隐私政策 | 服务协议 | 法律声明 | © 2026 滨化集团 | 鲁ICP备XXXXXXXX号</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* AI 助手浮标 */}
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="group fixed bottom-8 right-8 z-50 flex size-[68px] items-center justify-center rounded-full bg-brand-gradient text-white shadow-xl transition-transform hover:scale-105"
|
||||
aria-label="小滨 AI 助手"
|
||||
>
|
||||
<span className="pointer-events-none absolute inset-0 rounded-full bg-brand-gradient animate-breathe" />
|
||||
<span className="relative flex flex-col items-center leading-none">
|
||||
<Bot className="size-5" />
|
||||
<span className="mt-0.5 text-[9px] text-white/90">小滨</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<XiaobinAssistant pageType="portal" />
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -465,7 +465,7 @@ export function RegisterPage() {
|
||||
<Notice
|
||||
items={[
|
||||
"所有信息须与身份证及职称证书一致",
|
||||
"集团公司将对信息进行真实性核验",
|
||||
"滨化集团将对信息进行真实性核验",
|
||||
"入库专家须遵守《评委管理规定》",
|
||||
"至少具备中级及以上职称方可入库",
|
||||
"个人信息变更需及时在线更新",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const users = [
|
||||
{ id: "U1", name: "张伟", dept: "招标采购中心", role: "招标专员", status: "启用" },
|
||||
{ id: "U2", name: "刘晓敏", dept: "氯碱生产部", role: "需求部门人员", status: "启用" },
|
||||
{ id: "U3", name: "陈晨", dept: "采购供应部", role: "采购专员", status: "启用" },
|
||||
{ id: "U4", name: "王立强", dept: "公司领导", role: "审批人", status: "启用" },
|
||||
{ id: "U4", name: "王立强", dept: "集团领导", role: "审批人", status: "启用" },
|
||||
{ id: "U5", name: "孙建华", dept: "评标委员会", role: "领导小组", status: "启用" },
|
||||
{ id: "U6", name: "李审计", dept: "审计监察部", role: "合规/审计", status: "停用" },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { PortalShell } from "./PortalShell";
|
||||
import { cn } from "../../components/ui/utils";
|
||||
|
||||
const FAQS = [
|
||||
{
|
||||
q: "供应商账号如何注册?",
|
||||
a: "点击首页「用户登录」中的「供应商」入口,进入注册页后按营业执照信息与联系人信息提交即可。",
|
||||
},
|
||||
{
|
||||
q: "投标文件制作软件在哪里下载?",
|
||||
a: "请在「操作指南」页面下载「投标文件制作专用软件」,并按手册完成安装配置。",
|
||||
},
|
||||
{
|
||||
q: "忘记密码怎么办?",
|
||||
a: "在登录页点击「忘记密码」,通过注册手机号验证后可重置密码。",
|
||||
},
|
||||
{
|
||||
q: "为什么页面显示异常?",
|
||||
a: "建议使用最新版 Chrome 或 Edge;如为内网环境,请按指南开启 IE 兼容模式。",
|
||||
},
|
||||
];
|
||||
|
||||
export function FaqPage() {
|
||||
const [open, setOpen] = useState(0);
|
||||
return (
|
||||
<PortalShell active="faq">
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h1 className="text-base font-semibold">常见问题</h1>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{FAQS.map((item, idx) => (
|
||||
<div key={item.q} className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setOpen(idx)}
|
||||
className={cn("w-full text-left text-sm", open === idx ? "font-medium text-primary" : "")}
|
||||
>
|
||||
Q{idx + 1}. {item.q}
|
||||
</button>
|
||||
{open === idx && <p className="mt-2 text-sm text-muted-foreground">{item.a}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PortalShell } from "./PortalShell";
|
||||
|
||||
const GUIDES = [
|
||||
"电子招投标平台-供应商注册操作指南",
|
||||
"电子招标投标平台投标文件制作软件操作手册",
|
||||
"标证通申请-投标单位操作指南",
|
||||
"投标文件制作专用软件安装说明",
|
||||
"浏览器切换IE模式",
|
||||
"系统使用常见问题处理",
|
||||
];
|
||||
|
||||
export function GuidesPage() {
|
||||
return (
|
||||
<PortalShell active="guide">
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h1 className="text-base font-semibold">操作指南</h1>
|
||||
</div>
|
||||
<ul className="p-4 text-sm">
|
||||
{GUIDES.map((g, i) => (
|
||||
<li key={g} className="flex items-center gap-3 border-b border-dashed py-2">
|
||||
<span className="text-muted-foreground">{String(i + 1).padStart(2, "0")}.</span>
|
||||
<button className="line-clamp-1 flex-1 text-left hover:text-primary">{g}</button>
|
||||
<button className="shrink-0 border px-2 py-1 text-xs hover:border-primary hover:text-primary">
|
||||
下载
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { PortalShell } from "./PortalShell";
|
||||
|
||||
const POLICIES = [
|
||||
{ title: "中华人民共和国招标投标法", source: "国家法律法规", date: "2026-03-12" },
|
||||
{ title: "电子招标投标办法", source: "国家发展改革委", date: "2026-02-20" },
|
||||
{ title: "滨化集团采购管理办法(2026版)", source: "集团制度", date: "2026-01-15" },
|
||||
{ title: "供应商管理与准入规范", source: "集团制度", date: "2025-12-10" },
|
||||
{ title: "评标专家行为规范与回避制度", source: "集团制度", date: "2025-11-08" },
|
||||
];
|
||||
|
||||
export function PoliciesPage() {
|
||||
return (
|
||||
<PortalShell active="policy">
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h1 className="text-base font-semibold">政策法规</h1>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-3 p-4">
|
||||
{POLICIES.map((p) => (
|
||||
<button
|
||||
key={p.title}
|
||||
className="col-span-12 border px-4 py-3 text-left hover:border-primary hover:bg-[#f8fbff] lg:col-span-6"
|
||||
>
|
||||
<div className="text-sm font-medium">{p.title}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{p.source} | 更新时间:{p.date}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Link } from "react-router";
|
||||
import { Hexagon } from "lucide-react";
|
||||
import { cn } from "../../components/ui/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type NavKey = "home" | "trading" | "policy" | "guide" | "faq" | "platform";
|
||||
|
||||
const NAV_ITEMS: { key: NavKey; label: string; to: string }[] = [
|
||||
{ key: "home", label: "首页", to: "/portal" },
|
||||
{ key: "trading", label: "交易信息", to: "/portal/trading-info" },
|
||||
{ key: "policy", label: "政策法规", to: "/portal/policies" },
|
||||
{ key: "guide", label: "操作指南", to: "/portal/guides" },
|
||||
{ key: "faq", label: "常见问题", to: "/portal/faq" },
|
||||
{ key: "platform", label: "采购平台", to: "/portal/procurement-platform" },
|
||||
];
|
||||
|
||||
export function PortalShell({
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
active: NavKey;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f5f7fa] text-[#1f2937]">
|
||||
<header className="border-b bg-white">
|
||||
<div className="mx-auto flex h-11 max-w-[1260px] items-center px-6 text-xs text-[#6b7280]">
|
||||
<span>欢迎来到智慧电子招标投标交易网!</span>
|
||||
</div>
|
||||
<div className="border-t border-b bg-[#0f4ea6]">
|
||||
<div className="mx-auto flex h-16 max-w-[1260px] items-center px-6">
|
||||
<Link to="/portal" className="flex items-center gap-2 text-white">
|
||||
<Hexagon className="size-6" />
|
||||
<div className="leading-tight">
|
||||
<div className="text-lg font-semibold">智慧招标平台</div>
|
||||
<div className="text-[11px] text-white/80">Smart Tendering Platform</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="border-b bg-white">
|
||||
<div className="mx-auto flex h-11 max-w-[1260px] items-center px-6">
|
||||
{NAV_ITEMS.map((item, i) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
to={item.to}
|
||||
className={cn(
|
||||
"mr-1 border-r px-5 text-sm leading-8",
|
||||
i === NAV_ITEMS.length - 1 && "border-r-0",
|
||||
item.key === active
|
||||
? "bg-primary font-semibold text-white"
|
||||
: "text-[#374151] hover:bg-[#f2f6fc] hover:text-primary",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-[1260px] space-y-4 px-6 py-4">{children}</main>
|
||||
|
||||
<footer className="mt-6 border-t bg-white">
|
||||
<div className="mx-auto max-w-[1260px] space-y-2 px-6 py-5 text-xs text-[#6b7280]">
|
||||
<div>友情链接:滨化集团 | 供应商注册系统 | 智慧招标平台</div>
|
||||
<div>招投标相关网站:中国招投标协会 | 中国招标投标公共服务平台 | 中国国际招标网 | 中国采购与招标网</div>
|
||||
<div>版权所有:滨化集团</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { Building2, Briefcase, UserRound, Users } from "lucide-react";
|
||||
import { PortalShell } from "./PortalShell";
|
||||
|
||||
const ENTRIES = [
|
||||
{
|
||||
title: "采购人入口",
|
||||
desc: "发起采购、发布公告、组织评标与定标管理",
|
||||
icon: Building2,
|
||||
action: "进入采购人工作台",
|
||||
},
|
||||
{
|
||||
title: "供应商入口",
|
||||
desc: "注册报名、下载文件、在线投标、保证金管理",
|
||||
icon: Briefcase,
|
||||
action: "进入供应商工作台",
|
||||
},
|
||||
{
|
||||
title: "专家入口",
|
||||
desc: "接受邀请、签署承诺、在线评标与评分提交",
|
||||
icon: UserRound,
|
||||
action: "进入专家工作台",
|
||||
},
|
||||
{
|
||||
title: "其他入口",
|
||||
desc: "审计监察、系统管理等角色入口",
|
||||
icon: Users,
|
||||
action: "进入其他角色入口",
|
||||
},
|
||||
];
|
||||
|
||||
export function ProcurementPlatformPage() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<PortalShell active="platform">
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h1 className="text-base font-semibold">采购平台</h1>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4 p-4">
|
||||
{ENTRIES.map((entry) => {
|
||||
const Icon = entry.icon;
|
||||
return (
|
||||
<div key={entry.title} className="col-span-12 border p-4 lg:col-span-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="size-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">{entry.title}</h2>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{entry.desc}</p>
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="mt-4 border border-primary px-3 py-1.5 text-xs text-primary hover:bg-[#eef5ff]"
|
||||
>
|
||||
{entry.action}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../components/ui/utils";
|
||||
import { PortalShell } from "./PortalShell";
|
||||
|
||||
type Notice = { title: string; date: string };
|
||||
|
||||
const NOTICE_TABS = ["招标公告", "资格预审公告", "中标候选人公示", "结果公告", "终止公告"];
|
||||
|
||||
const NOTICE_DATA: Record<string, Notice[]> = {
|
||||
招标公告: [
|
||||
{ title: "[公开招标] 扣电测试柜框架", date: "2026-07-13" },
|
||||
{ title: "[公开招标] 2026年地脚螺栓、不锈钢及五金紧固件框架招标", date: "2026-07-13" },
|
||||
{ title: "[公开招标] 莱州100MW分散式风电项目EPC工程总承包[变更公告]", date: "2026-07-13" },
|
||||
{ title: "[公开招标] TPU掺混特种线项目FFS包装机[变更公告]", date: "2026-07-13" },
|
||||
],
|
||||
资格预审公告: [
|
||||
{ title: "滨州园区危化品运输服务资格预审公告", date: "2026-07-11" },
|
||||
{ title: "公辅系统维保服务资格预审公告", date: "2026-07-10" },
|
||||
{ title: "智慧园区网络安全服务资格预审公告", date: "2026-07-08" },
|
||||
],
|
||||
中标候选人公示: [
|
||||
{ title: "[公开招标] 牵引车租赁服务框架中标候选人公示", date: "2026-07-07" },
|
||||
{ title: "[公开招标] 电池材料项目地质勘察工程中标候选人公示", date: "2026-07-06" },
|
||||
{ title: "[公开招标] 四轮防爆巡检机器人系统中标候选人公示", date: "2026-06-26" },
|
||||
],
|
||||
结果公告: [
|
||||
{ title: "[公开招标] HDPE管道管件框架招标中标结果公告", date: "2026-07-07" },
|
||||
{ title: "[公开招标] 起重框架招标包一 单梁起重机中标结果公告", date: "2026-07-02" },
|
||||
{ title: "[公开招标] 海阳绿电直连光伏组件项目中标结果公告", date: "2026-06-30" },
|
||||
],
|
||||
终止公告: [
|
||||
{ title: "国标切断双偏心蝶阀框架招标废标公告", date: "2026-07-06" },
|
||||
{ title: "TPU四期熟化料仓招标废标公告", date: "2026-07-01" },
|
||||
{ title: "滨州碳酸锂项目冷却窑废标公告", date: "2026-06-24" },
|
||||
],
|
||||
};
|
||||
|
||||
export function TradingInfoPage() {
|
||||
const [activeTab, setActiveTab] = useState(NOTICE_TABS[0]);
|
||||
return (
|
||||
<PortalShell active="trading">
|
||||
<section className="border bg-white">
|
||||
<div className="border-b bg-[#f8fafc] px-4 py-3">
|
||||
<h1 className="text-base font-semibold">交易信息</h1>
|
||||
</div>
|
||||
<div className="border-b px-3">
|
||||
<div className="flex flex-wrap items-center">
|
||||
{NOTICE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
"relative px-4 py-3 text-sm",
|
||||
activeTab === tab ? "font-semibold text-primary" : "text-[#4b5563] hover:text-primary",
|
||||
)}
|
||||
>
|
||||
{tab}
|
||||
{activeTab === tab && <span className="absolute inset-x-2 bottom-0 h-0.5 bg-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-1 p-4">
|
||||
{NOTICE_DATA[activeTab].map((n) => (
|
||||
<li key={n.title} className="flex items-center gap-3 border-b border-dashed py-2 text-sm">
|
||||
<span className="line-clamp-1 flex-1 hover:text-primary">{n.title}</span>
|
||||
<span className="text-xs text-muted-foreground">{n.date}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</PortalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,46 +2,46 @@
|
||||
|
||||
:root {
|
||||
--font-size: 14px;
|
||||
--background: #f5f7fa;
|
||||
--background: #f3f5f8;
|
||||
--foreground: #1a2332;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1a2332;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1a2332;
|
||||
--primary: #c2185b;
|
||||
--primary: #1565c0;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #fce4ec;
|
||||
--secondary-foreground: #ad1457;
|
||||
--secondary: #eef4fb;
|
||||
--secondary-foreground: #0d47a1;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #fce4ec;
|
||||
--accent-foreground: #ad1457;
|
||||
--accent: #eef4fb;
|
||||
--accent-foreground: #0d47a1;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--input: transparent;
|
||||
--input-background: #f7f8fc;
|
||||
--input-background: #f9fafb;
|
||||
--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;
|
||||
--radius: 0.75rem;
|
||||
/* 品牌渐变(全平台统一): #c2185b → #e91e63 */
|
||||
--brand-from: #c2185b;
|
||||
--brand-to: #e91e63;
|
||||
--sidebar: #2d0a19;
|
||||
--sidebar-foreground: #f4d9e4;
|
||||
--sidebar-primary: #e91e63;
|
||||
--ring: #1565c0;
|
||||
--chart-1: #1565c0;
|
||||
--chart-2: #00897b;
|
||||
--chart-3: #f59e0b;
|
||||
--chart-4: #7e57c2;
|
||||
--chart-5: #ef5350;
|
||||
--radius: 0.35rem;
|
||||
/* 品牌渐变(全平台统一科技蓝,呼应滨化 LOGO): #0d47a1 → #1e88e5 */
|
||||
--brand-from: #0f4ea6;
|
||||
--brand-to: #0c428d;
|
||||
--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 {
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 平台品牌工具类与动效(依据《详细设计》统一视觉语言) ===== */
|
||||
/* ===== 平台品牌工具类与动效(统一科技蓝) ===== */
|
||||
@layer utilities {
|
||||
/* 品牌渐变背景: #c2185b → #e91e63 */
|
||||
/* 品牌渐变背景 */
|
||||
.bg-brand-gradient {
|
||||
background-image: linear-gradient(135deg, var(--brand-from), var(--brand-to));
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 225 KiB |
@@ -0,0 +1,187 @@
|
||||
[
|
||||
{
|
||||
"file": "01.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "首页(门户)"
|
||||
},
|
||||
{
|
||||
"file": "02.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "门户导航 · 交易信息页"
|
||||
},
|
||||
{
|
||||
"file": "03.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "门户导航 · 政策法规页"
|
||||
},
|
||||
{
|
||||
"file": "04.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "门户导航 · 操作指南页"
|
||||
},
|
||||
{
|
||||
"file": "05.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "门户导航 · 常见问题页"
|
||||
},
|
||||
{
|
||||
"file": "06.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "门户导航 · 采购平台页"
|
||||
},
|
||||
{
|
||||
"file": "07.png",
|
||||
"section": "一、门户与登录",
|
||||
"title": "登录页(含可拖拽小滨)"
|
||||
},
|
||||
{
|
||||
"file": "08.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "招标专员工作台"
|
||||
},
|
||||
{
|
||||
"file": "09.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "采购需求人员工作台"
|
||||
},
|
||||
{
|
||||
"file": "10.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "评标专家工作台"
|
||||
},
|
||||
{
|
||||
"file": "11.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "评标委员会工作领导小组工作台"
|
||||
},
|
||||
{
|
||||
"file": "12.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "供应商工作台"
|
||||
},
|
||||
{
|
||||
"file": "13.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "系统管理员工作台"
|
||||
},
|
||||
{
|
||||
"file": "14.png",
|
||||
"section": "二、角色工作台",
|
||||
"title": "合规 / 审计工作台"
|
||||
},
|
||||
{
|
||||
"file": "15.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "发起招标委托"
|
||||
},
|
||||
{
|
||||
"file": "16.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "项目管理列表"
|
||||
},
|
||||
{
|
||||
"file": "17.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "项目详情"
|
||||
},
|
||||
{
|
||||
"file": "18.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "供应商长短名单"
|
||||
},
|
||||
{
|
||||
"file": "19.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "AI智慧寻源"
|
||||
},
|
||||
{
|
||||
"file": "20.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "开标大厅"
|
||||
},
|
||||
{
|
||||
"file": "21.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "在线评标室"
|
||||
},
|
||||
{
|
||||
"file": "22.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "保证金管理"
|
||||
},
|
||||
{
|
||||
"file": "23.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "供应商管理"
|
||||
},
|
||||
{
|
||||
"file": "24.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "专家管理"
|
||||
},
|
||||
{
|
||||
"file": "25.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "报表中心"
|
||||
},
|
||||
{
|
||||
"file": "26.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "异常决策"
|
||||
},
|
||||
{
|
||||
"file": "27.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "评标任务"
|
||||
},
|
||||
{
|
||||
"file": "28.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "投标中心"
|
||||
},
|
||||
{
|
||||
"file": "29.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "审计日志"
|
||||
},
|
||||
{
|
||||
"file": "30.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "围串标分析"
|
||||
},
|
||||
{
|
||||
"file": "31.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "存证验证"
|
||||
},
|
||||
{
|
||||
"file": "32.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "用户与权限管理"
|
||||
},
|
||||
{
|
||||
"file": "33.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "密钥管理"
|
||||
},
|
||||
{
|
||||
"file": "34.png",
|
||||
"section": "三、核心业务页面",
|
||||
"title": "系统设置"
|
||||
},
|
||||
{
|
||||
"file": "35.png",
|
||||
"section": "四、弹窗与抽屉",
|
||||
"title": "供应商详情抽屉"
|
||||
},
|
||||
{
|
||||
"file": "36.png",
|
||||
"section": "四、弹窗与抽屉",
|
||||
"title": "评标纪律承诺书弹窗"
|
||||
},
|
||||
{
|
||||
"file": "37.png",
|
||||
"section": "四、弹窗与抽屉",
|
||||
"title": "密钥漂移二次授权弹窗"
|
||||
}
|
||||
]
|
||||