Compare commits
20 Commits
637105cf64
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dba6db9d2 | |||
| 41fc6e86dd | |||
| aa023a6d45 | |||
| e0ae0e4007 | |||
| d81ac936e7 | |||
| 38a804ce41 | |||
| 3a50a3f3ed | |||
| 282f1f1b92 | |||
| 787de5aa88 | |||
| f7a49ffb78 | |||
| 5af4f768f5 | |||
| f1cd487478 | |||
| 3a7a94235a | |||
| f24c503d9d | |||
| 71ba72ad2a | |||
| 851bb530ac | |||
| b5261609ff | |||
| 20bcc2b2c9 | |||
| 3bd2a0a31d | |||
| 36add99032 |
@@ -0,0 +1,15 @@
|
||||
# 依赖
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
**/dist/
|
||||
.vite/
|
||||
**/.vite/
|
||||
|
||||
# 系统 / 编辑器
|
||||
.DS_Store
|
||||
*.log
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,434 @@
|
||||
# STP 智慧招标平台 —— 供应商工作台详细设计说明书
|
||||
|
||||
| 文档名称 | 供应商工作台详细设计 |
|
||||
| :------- | :---------- |
|
||||
| **文档版本** | V1.0 |
|
||||
| **编制日期** | 2026-07-02 |
|
||||
| **关联文档** | [[需求分析说明书]] |
|
||||
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
供应商工作台是供应商登录后默认进入的页面,作为其日常操作的核心枢纽。设计目标包括:
|
||||
- **任务驱动**:集中展示待办事项(报名、缴费、上传投标文件等),引导供应商快速完成关键动作。
|
||||
- **信息整合**:汇总投标项目进展、通知消息、企业信息状态,减少页面跳转。
|
||||
- **个性化推荐**:根据供应商关注的品类推送最新招标公告,提升参与效率。
|
||||
- **快捷入口**:提供企业信息维护、资质文件管理、安全设置等常用功能的快速通道。
|
||||
|
||||
### 1.2 适用角色
|
||||
- 已注册并激活的供应商(包括新注册供应商和 SRM 存量绑定供应商)。
|
||||
- 不同供应商看到的内容根据其投标记录、关注品类差异化展示。
|
||||
|
||||
### 1.3 设计原则
|
||||
| 原则 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **一目了然** | 核心数据(待办数量、项目进度)以卡片/数字突出显示。 |
|
||||
| **操作便捷** | 主要操作(报名、上传文件)入口直接放在工作台,缩短路径。 |
|
||||
| **实时同步** | 待办事项、项目状态、通知均从后端实时获取,保持最新。 |
|
||||
| **角色聚焦** | 只展示与供应商相关的功能,不显示招标方或专家内容。 |
|
||||
|
||||
|
||||
## 2. 页面布局总览
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 900" font-family="'Microsoft YaHei', sans-serif">
|
||||
<defs>
|
||||
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
<filter id="shadowDeep" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<linearGradient id="headerBg" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#c2185b"/>
|
||||
<stop offset="100%" stop-color="#e91e63"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#c2185b"/><stop offset="100%" stop-color="#e91e63"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cardGrad1" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e8f5e9"/><stop offset="100%" stop-color="#c8e6c9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cardGrad2" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e3f2fd"/><stop offset="100%" stop-color="#bbdefb"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cardGrad3" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#fff3e0"/><stop offset="100%" stop-color="#ffe0b2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="1200" height="900" fill="#f5f7fa"/>
|
||||
|
||||
<!-- ===== 顶部导航栏(与首页一致,但用户区显示已登录) ===== -->
|
||||
<rect width="1200" height="64" fill="url(#headerBg)"/>
|
||||
<text x="40" y="42" font-size="22" font-weight="bold" fill="#fff">智慧招标平台</text>
|
||||
<text x="190" y="42" font-size="13" fill="rgba(255,255,255,0.8)">Smart Tendering Platform</text>
|
||||
|
||||
<!-- 导航菜单(保留首页菜单,但“首页”高亮) -->
|
||||
<text x="440" y="42" font-size="15" fill="#fff" font-weight="bold">首页</text>
|
||||
<text x="520" y="42" font-size="15" fill="rgba(255,255,255,0.8)">招标公告</text>
|
||||
<text x="620" y="42" font-size="15" fill="rgba(255,255,255,0.8)">定标公告</text>
|
||||
<text x="720" y="42" font-size="15" fill="rgba(255,255,255,0.8)">企业新闻</text>
|
||||
|
||||
<!-- 已登录用户 -->
|
||||
<circle cx="1030" cy="32" r="16" fill="rgba(255,255,255,0.3)"/>
|
||||
<text x="1030" y="37" text-anchor="middle" font-size="12" fill="#fff">李</text>
|
||||
<text x="1060" y="37" font-size="14" fill="#fff">李强</text>
|
||||
<text x="1110" y="37" font-size="12" fill="rgba(255,255,255,0.7)">▼</text>
|
||||
|
||||
<!-- ===== 内容区:两栏布局 ===== -->
|
||||
<!-- 左侧:主内容区(宽度 780px) -->
|
||||
<!-- 右侧:侧边栏(宽度 300px,间距 20px) -->
|
||||
|
||||
<!-- 左侧主内容 -->
|
||||
<rect x="40" y="90" width="780" height="770" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 欢迎语 + 日期 -->
|
||||
<text x="70" y="125" font-size="20" font-weight="bold" fill="#1a2332">👋 欢迎回来,李强</text>
|
||||
<text x="70" y="150" font-size="14" fill="#8895aa">今天是 2026年7月2日,星期四</text>
|
||||
|
||||
<!-- ===== 待办事项卡片(横向滚动或图标) ===== -->
|
||||
<rect x="70" y="180" width="720" height="100" rx="10" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="90" y="210" font-size="15" font-weight="bold" fill="#4a5568">📋 待办事项</text>
|
||||
|
||||
<!-- 待办项1 -->
|
||||
<rect x="100" y="225" width="150" height="40" rx="8" fill="#ffebee" stroke="#ef9a9a"/>
|
||||
<text x="175" y="250" text-anchor="middle" font-size="13" fill="#c62828">保证金缴纳(2)</text>
|
||||
|
||||
<!-- 待办项2 -->
|
||||
<rect x="270" y="225" width="150" height="40" rx="8" fill="#fff3e0" stroke="#ffcc80"/>
|
||||
<text x="345" y="250" text-anchor="middle" font-size="13" fill="#e65100">投标报名(1)</text>
|
||||
|
||||
<!-- 待办项3 -->
|
||||
<rect x="440" y="225" width="150" height="40" rx="8" fill="#e3f2fd" stroke="#90caf9"/>
|
||||
<text x="515" y="250" text-anchor="middle" font-size="13" fill="#0d47a1">待上传投标文件(3)</text>
|
||||
|
||||
<!-- 待办项4 -->
|
||||
<rect x="610" y="225" width="150" height="40" rx="8" fill="#e8f5e9" stroke="#a5d6a7"/>
|
||||
<text x="685" y="250" text-anchor="middle" font-size="13" fill="#2e7d32">待查看结果(2)</text>
|
||||
|
||||
<!-- ===== 我的投标项目 ===== -->
|
||||
<text x="70" y="320" font-size="15" font-weight="bold" fill="#4a5568">📂 我的投标项目</text>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<rect x="70" y="335" width="720" height="160" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
|
||||
<!-- 表头 -->
|
||||
<rect x="70" y="335" width="720" height="32" rx="8" fill="#f0f2f7"/>
|
||||
<text x="90" y="356" font-size="13" font-weight="bold" fill="#4a5568">项目名称</text>
|
||||
<text x="260" y="356" font-size="13" font-weight="bold" fill="#4a5568">公告日期</text>
|
||||
<text x="380" y="356" font-size="13" font-weight="bold" fill="#4a5568">状态</text>
|
||||
<text x="500" y="356" font-size="13" font-weight="bold" fill="#4a5568">截止日期</text>
|
||||
<text x="650" y="356" font-size="13" font-weight="bold" fill="#4a5568">操作</text>
|
||||
|
||||
<!-- 行1 -->
|
||||
<rect x="80" y="375" width="700" height="36" rx="4" fill="#fff"/>
|
||||
<text x="90" y="398" font-size="13" fill="#1a2332">年产5万吨聚丙烯项目设备采购</text>
|
||||
<text x="260" y="398" font-size="13" fill="#666">2026-07-01</text>
|
||||
<rect x="380" y="385" width="60" height="18" rx="4" fill="#ff9800"/>
|
||||
<text x="410" y="398" text-anchor="middle" font-size="11" fill="#fff">报名中</text>
|
||||
<text x="500" y="398" font-size="13" fill="#666">2026-07-15</text>
|
||||
<text x="650" y="398" font-size="13" fill="#1565c0" text-decoration="underline">继续报名</text>
|
||||
|
||||
<!-- 行2 -->
|
||||
<rect x="80" y="415" width="700" height="36" rx="4" fill="#fff"/>
|
||||
<text x="90" y="438" font-size="13" fill="#1a2332">数字化交付平台开发服务</text>
|
||||
<text x="260" y="438" font-size="13" fill="#666">2026-06-29</text>
|
||||
<rect x="380" y="425" width="60" height="18" rx="4" fill="#4caf50"/>
|
||||
<text x="410" y="438" text-anchor="middle" font-size="11" fill="#fff">已投标</text>
|
||||
<text x="500" y="438" font-size="13" fill="#666">2026-07-10</text>
|
||||
<text x="650" y="438" font-size="13" fill="#9e9e9e">等待开标</text>
|
||||
|
||||
<!-- 行3 -->
|
||||
<rect x="80" y="455" width="700" height="36" rx="4" fill="#fff"/>
|
||||
<text x="90" y="478" font-size="13" fill="#1a2332">2026年度防腐保温工程框架</text>
|
||||
<text x="260" y="478" font-size="13" fill="#666">2026-06-28</text>
|
||||
<rect x="380" y="465" width="60" height="18" rx="4" fill="#9e9e9e"/>
|
||||
<text x="410" y="478" text-anchor="middle" font-size="11" fill="#fff">已截止</text>
|
||||
<text x="500" y="478" font-size="13" fill="#666">已过期</text>
|
||||
<text x="650" y="478" font-size="13" fill="#9e9e9e">-</text>
|
||||
|
||||
<!-- 查看更多 -->
|
||||
<text x="700" y="515" font-size="13" fill="#c2185b" text-decoration="underline">查看全部投标项目 →</text>
|
||||
|
||||
<!-- ===== 最新通知 ===== -->
|
||||
<text x="70" y="555" font-size="15" font-weight="bold" fill="#4a5568">🔔 最新通知</text>
|
||||
<rect x="70" y="570" width="720" height="95" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
|
||||
<rect x="80" y="580" width="700" height="28" rx="4" fill="#fff"/>
|
||||
<circle cx="95" cy="594" r="4" fill="#c2185b"/>
|
||||
<text x="110" y="600" font-size="13" fill="#1a2332">【系统通知】您的“数字化交付平台”项目已完成开标,请查看结果。</text>
|
||||
|
||||
<rect x="80" y="612" width="700" height="28" rx="4" fill="#fff"/>
|
||||
<circle cx="95" cy="626" r="4" fill="#c2185b"/>
|
||||
<text x="110" y="632" font-size="13" fill="#1a2332">【报名提醒】“年产5万吨聚丙烯项目”报名即将截止(7月15日)。</text>
|
||||
|
||||
<text x="700" y="680" font-size="13" fill="#c2185b" text-decoration="underline">查看所有通知 →</text>
|
||||
|
||||
<!-- ===== 右侧边栏 ===== -->
|
||||
<!-- 企业信息概览 -->
|
||||
<rect x="840" y="90" width="320" height="170" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="870" y="125" font-size="15" font-weight="bold" fill="#4a5568">🏢 企业信息</text>
|
||||
<text x="870" y="155" font-size="14" fill="#1a2332">滨化新材料有限公司</text>
|
||||
<text x="870" y="180" font-size="13" fill="#8895aa">统一社会信用代码:91370100MA3XXXXXXX</text>
|
||||
<text x="870" y="205" font-size="13" fill="#8895aa">联系人:李强 | 手机:138****8001</text>
|
||||
<text x="870" y="230" font-size="13" fill="#4caf50">账户状态:✅ 已激活</text>
|
||||
<rect x="870" y="245" width="90" height="28" rx="6" fill="#e3f2fd" stroke="#1565c0"/>
|
||||
<text x="915" y="264" text-anchor="middle" font-size="12" fill="#1565c0">完善资料</text>
|
||||
|
||||
<!-- 快速入口 -->
|
||||
<rect x="840" y="280" width="320" height="200" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="870" y="315" font-size="15" font-weight="bold" fill="#4a5568">⚡ 快捷入口</text>
|
||||
|
||||
<rect x="870" y="335" width="120" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="930" y="360" text-anchor="middle" font-size="20">📄</text>
|
||||
<text x="930" y="382" text-anchor="middle" font-size="12" fill="#4a5568">资质文件</text>
|
||||
|
||||
<rect x="1000" y="335" width="120" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="1060" y="360" text-anchor="middle" font-size="20">🔐</text>
|
||||
<text x="1060" y="382" text-anchor="middle" font-size="12" fill="#4a5568">安全设置</text>
|
||||
|
||||
<rect x="870" y="405" width="120" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="930" y="430" text-anchor="middle" font-size="20">📊</text>
|
||||
<text x="930" y="452" text-anchor="middle" font-size="12" fill="#4a5568">我的投标</text>
|
||||
|
||||
<rect x="1000" y="405" width="120" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="1060" y="430" text-anchor="middle" font-size="20">💬</text>
|
||||
<text x="1060" y="452" text-anchor="middle" font-size="12" fill="#4a5568">联系客服</text>
|
||||
|
||||
<!-- 推荐招标公告 -->
|
||||
<rect x="840" y="500" width="320" height="200" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="870" y="535" font-size="15" font-weight="bold" fill="#4a5568">📌 推荐招标</text>
|
||||
|
||||
<rect x="870" y="555" width="270" height="32" rx="6" fill="#fafafa"/>
|
||||
<text x="885" y="575" font-size="13" fill="#1a2332">• 催化剂原料采购项目</text>
|
||||
|
||||
<rect x="870" y="595" width="270" height="32" rx="6" fill="#fafafa"/>
|
||||
<text x="885" y="615" font-size="13" fill="#1a2332">• 智能仓储系统建设</text>
|
||||
|
||||
<rect x="870" y="635" width="270" height="32" rx="6" fill="#fafafa"/>
|
||||
<text x="885" y="655" font-size="13" fill="#1a2332">• 污水处理工程(二期)</text>
|
||||
|
||||
<text x="1050" y="685" font-size="13" fill="#c2185b" text-decoration="underline">更多推荐 →</text>
|
||||
|
||||
<!-- ===== AI 助手浮动入口(同首页) ===== -->
|
||||
<circle cx="1110" cy="840" r="42" fill="url(#brandGrad)" filter="url(#shadowDeep)"/>
|
||||
<text x="1110" y="835" text-anchor="middle" font-size="16" fill="#fff" font-weight="bold">小滨</text>
|
||||
<text x="1110" y="855" text-anchor="middle" font-size="10" fill="rgba(255,255,255,0.9)">AI 助手</text>
|
||||
<circle cx="1110" cy="840" r="50" fill="none" stroke="#e91e63" stroke-width="2" opacity="0.3">
|
||||
<animate attributeName="r" from="42" to="55" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" from="0.6" to="0" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## 3. 各模块详细说明
|
||||
|
||||
### 3.1 顶部导航栏
|
||||
- **与首页保持一致**,品牌区 + 导航菜单(首页、招标公告、定标公告、企业新闻)。
|
||||
- **用户区**:显示供应商头像(首字母),名称,下拉菜单(个人中心、我的投标、安全设置、退出)。
|
||||
- **导航高亮**:当前“首页”高亮。
|
||||
|
||||
### 3.2 欢迎区
|
||||
- **位置**:左上角,距顶部 90px。
|
||||
- **内容**:供应商名称 + 当前日期。
|
||||
- **数据来源**:从用户登录信息中获取姓名,日期从服务器时间获取。
|
||||
- **交互**:无,纯展示。
|
||||
|
||||
### 3.3 待办事项卡片
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **位置** | 紧接欢迎区下方 |
|
||||
| **尺寸** | 720 × 100px,浅灰背景,圆角 10px |
|
||||
| **标题** | “📋 待办事项” |
|
||||
| **待办项** | 以标签形式横向排列(可滚动),每个标签显示分类名称 + 数字,点击跳转至对应列表 |
|
||||
| **分类** | 保证金缴纳、投标报名、待上传投标文件、待查看结果、待签合同等 |
|
||||
| **数据来源** | 实时查询后端 API,统计各状态下的数量 |
|
||||
| **交互** | 点击标签跳转至对应功能模块(如“保证金缴纳”跳转至保证金管理页) |
|
||||
|
||||
**待办项标签样式**:
|
||||
- 保证金缴纳:红色背景(#ffebee),文字 #c62828
|
||||
- 投标报名:橙色背景(#fff3e0),文字 #e65100
|
||||
- 待上传投标文件:蓝色背景(#e3f2fd),文字 #0d47a1
|
||||
- 待查看结果:绿色背景(#e8f5e9),文字 #2e7d32
|
||||
|
||||
### 3.4 我的投标项目
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **位置** | 待办卡片下方 |
|
||||
| **尺寸** | 720 × 约 200px,带表格 |
|
||||
| **标题** | “📂 我的投标项目” |
|
||||
| **表格列** | 项目名称、公告日期、状态、截止日期、操作 |
|
||||
| **数据范围** | 显示最新的 3 条投标记录(按状态排序:进行中 > 即将截止 > 已截止) |
|
||||
| **状态标签** | 报名中(橙色)、已投标(绿色)、待开标(蓝色)、已截止(灰色)、已中标(金色)等 |
|
||||
| **操作列** | 根据状态显示不同操作按钮:继续报名、查看详情、上传投标文件、查看结果等 |
|
||||
| **数据来源** | 查询供应商参与的投标项目列表,实时获取状态 |
|
||||
| **查看更多** | 底部“查看全部投标项目 →”链接至完整投标列表 |
|
||||
|
||||
**状态与操作映射**:
|
||||
|
||||
| 状态 | 标签颜色 | 操作按钮文字 | 跳转目标 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 报名中 | 橙色 | 继续报名 | 报名详情页 |
|
||||
| 待上传投标文件 | 蓝色 | 上传文件 | 投标文件上传页 |
|
||||
| 已投标 | 绿色 | 查看详情 | 投标详情页 |
|
||||
| 待开标 | 蓝色(浅) | 等待开标 | —(只读) |
|
||||
| 已截止 | 灰色 | — | — |
|
||||
| 已中标 | 金色 | 查看合同 | 合同详情页 |
|
||||
| 未中标 | 灰色 | 查看结果 | 结果公示页 |
|
||||
|
||||
### 3.5 最新通知
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **位置** | 我的投标项目下方 |
|
||||
| **尺寸** | 720 × 约 110px |
|
||||
| **标题** | “🔔 最新通知” |
|
||||
| **通知数量** | 展示 2 条最新通知(按时间倒序) |
|
||||
| **每条通知** | 红色圆点 + 通知内容(截取前 30 字) |
|
||||
| **通知类型** | 系统通知(开标结果、报名提醒、审核通知等) |
|
||||
| **数据来源** | 通知中心 API,只取未读或最近 5 条 |
|
||||
| **查看全部** | “查看所有通知 →”跳转至通知列表页 |
|
||||
| **交互** | 点击通知可标记为已读并跳转至详情 |
|
||||
|
||||
### 3.6 右侧边栏
|
||||
|
||||
#### 3.6.1 企业信息概览
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **尺寸** | 320 × 170px |
|
||||
| **标题** | “🏢 企业信息” |
|
||||
| **内容** | 企业全称、统一社会信用代码、联系人姓名、手机号、账户状态 |
|
||||
| **账户状态** | 已激活(绿色)、待完善(橙色)、已锁定(红色) |
|
||||
| **按钮** | “完善资料”跳转至企业信息管理页面 |
|
||||
| **数据来源** | 从供应商档案中读取 |
|
||||
|
||||
#### 3.6.2 快捷入口
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :------- | :-------------------------- |
|
||||
| **尺寸** | 320 × 200px |
|
||||
| **标题** | “⚡ 快捷入口” |
|
||||
| **入口图标** | 4 个图标按钮,2×2 网格排列 |
|
||||
| **入口列表** | 资质文件、安全设置、我的投标、联系客服 |
|
||||
| **交互** | 点击跳转至对应页面;联系客服打开 AI 助手或客服通道 |
|
||||
|
||||
#### 3.6.3 推荐招标
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **尺寸** | 320 × 200px |
|
||||
| **标题** | “📌 推荐招标” |
|
||||
| **内容** | 根据供应商历史浏览或关注的品类,推荐 3 条招标公告 |
|
||||
| **展示** | 仅显示项目名称(点击跳转至公告详情) |
|
||||
| **更多** | “更多推荐 →”跳转至招标公告列表(筛选条件为关注品类) |
|
||||
| **推荐算法** | 基于供应商主产品、历史投标记录、浏览行为等进行协同过滤或标签匹配 |
|
||||
|
||||
### 3.7 AI 助手浮动入口
|
||||
- **位置**:页面右下角固定。
|
||||
- **功能**:与首页一致,提供智能问答、操作引导、待办提醒等。
|
||||
- **主动显示内容**:主动提醒用户紧急待办事项,含链接,直接跳转到待办页面。(不要太多,最多三项)
|
||||
|
||||
### 3.8 页面滚动与响应
|
||||
- 左侧主内容可滚动,右侧边栏固定(或跟随滚动,取决于设计)。
|
||||
- 推荐采用 **左侧内容滚动 + 右侧边栏 sticky** 的方式,便于同时查看待办和快捷入口。
|
||||
|
||||
### 3.9 空状态处理
|
||||
- 若无待办事项,显示“🎉 暂无待办,继续保持!”。
|
||||
- 若无投标项目,显示“您还未参与任何投标,去浏览招标公告吧 →”。
|
||||
- 若无通知,显示“暂无新通知”。
|
||||
|
||||
|
||||
## 4. 交互流程
|
||||
|
||||
### 4.1 待办事项点击
|
||||
- 点击“保证金缴纳(2)”→ 跳转至保证金管理页面,列出需要缴纳保证金的项目。
|
||||
- 点击“投标报名(1)”→ 跳转至报名页面,直接进入待报名项目。
|
||||
- 点击“待上传投标文件(3)”→ 跳转至“我的投标”列表,筛选出“待上传”状态的项目。
|
||||
|
||||
### 4.2 投标项目操作
|
||||
- 点击“继续报名”→ 进入报名详情页,可修改报名信息或提交补充材料。
|
||||
- 点击“上传文件”→ 跳转至投标文件上传页,支持加密上传。
|
||||
- 点击“查看结果”→ 跳转至结果公示页。
|
||||
- 点击项目名称 → 跳转至项目详情页,查看完整招标信息。
|
||||
|
||||
### 4.3 通知交互
|
||||
- 点击通知 → 标记为已读,跳转至详情(如结果通知跳转至评标报告)。
|
||||
- 支持批量标记已读。
|
||||
|
||||
### 4.4 快捷入口
|
||||
- 资质文件 → 跳转至“企业信息管理”的“资质文件”Tab。
|
||||
- 安全设置 → 跳转至“企业信息管理”的“安全设置”Tab。
|
||||
- 我的投标 → 跳转至完整的投标列表。
|
||||
- 联系客服 → 弹窗提示 AI 助手或人工客服联系方式。
|
||||
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
### 5.1 待办统计 API
|
||||
```
|
||||
GET /api/v1/supplier/todo/count
|
||||
响应:
|
||||
{
|
||||
"pendingPayment": 2, // 待缴纳保证金
|
||||
"pendingBid": 1, // 待报名
|
||||
"pendingUpload": 3, // 待上传投标文件
|
||||
"pendingResult": 2 // 待查看结果
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 投标项目列表(工作台用)
|
||||
```
|
||||
GET /api/v1/supplier/bids/dashboard
|
||||
参数:limit=3
|
||||
响应:[
|
||||
{
|
||||
"projectId": "P20260701",
|
||||
"projectName": "年产5万吨聚丙烯项目设备采购",
|
||||
"announceDate": "2026-07-01",
|
||||
"status": "BIDDING", // BIDDING, BID_SUBMITTED, WAITING_OPEN, CLOSED, WIN, LOSE
|
||||
"deadline": "2026-07-15",
|
||||
"action": "CONTINUE_BID" // CONTINUE_BID, UPLOAD_FILE, VIEW_RESULT, NONE
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 5.3 通知列表
|
||||
```
|
||||
GET /api/v1/supplier/notifications/latest
|
||||
响应:[{ id, title, content, createTime, isRead, link }]
|
||||
```
|
||||
|
||||
### 5.4 推荐招标
|
||||
```
|
||||
GET /api/v1/supplier/recommended-bids
|
||||
参数:limit=3
|
||||
响应:[{ projectId, projectName, category, publishDate }]
|
||||
```
|
||||
|
||||
|
||||
## 6. 权限与状态
|
||||
|
||||
| 功能 | 访问条件 |
|
||||
| :--- | :--- |
|
||||
| 工作台展示 | 任何已登录供应商 |
|
||||
| 待办事项 | 实时计算,所有供应商可见 |
|
||||
| 投标项目 | 仅显示当前供应商参与的 |
|
||||
| 推荐招标 | 根据供应商信息个性化推荐 |
|
||||
| 企业信息 | 显示当前供应商自己的信息 |
|
||||
|
||||
**状态变化**:当供应商完成某项操作(如缴纳保证金),工作台待办计数自动更新(通过 WebSocket 或定时刷新)。
|
||||
|
||||
## 7. 性能与实现建议
|
||||
|
||||
- **刷新机制**:工作台数据通过 WebSocket 或轮询(每 30 秒)更新待办数量。
|
||||
- **缓存**:企业信息可缓存 5 分钟,投标列表缓存 1 分钟。
|
||||
- **懒加载**:通知和推荐招标在滚动至可视区域时加载。
|
||||
- **前端框架**:Vue 3 / React,使用 Composition API 或 Hooks 管理状态。
|
||||
|
||||
@@ -1029,16 +1029,16 @@ flowchart LR
|
||||
|
||||
## 15. 安全与合规
|
||||
|
||||
| 安全维度 | 具体措施 |
|
||||
| :--- | :--- |
|
||||
| **传输安全** | 强制 HTTPS + TLS 1.3,HSTS 头部强制。 |
|
||||
| 安全维度 | 具体措施 |
|
||||
| :--------- | :----------------------------------------------------------------------------------- |
|
||||
| **传输安全** | 强制 HTTPS + TLS 1.3,HSTS 头部强制。 |
|
||||
| **敏感字段加密** | `credit_code`、`contact_phone` 使用 **AES-256-GCM** 应用层加密,密钥由KMS托管,内存动态漂移(参见全局安全方案4.2)。 |
|
||||
| **可搜索加密** | 唯一性校验使用字段的 **MD5/SHA-256** 盲索引(不可逆),不直接对密文进行`SELECT`。 |
|
||||
| **数据脱敏展示** | 后台展示信用代码为 `913701**********`,手机号为 `138****8001`。 |
|
||||
| **审计防篡改** | 所有审计日志采用 **哈希链+数字签名**(参见需求说明书4.8.1),保留≥7年。 |
|
||||
| **防机器人** | 注册提交需验证图形验证码(极验),手机验证码发送每日限5次。 |
|
||||
| **密码安全** | 首次激活密码强度≥8位,含大小写+数字+特殊字符,不得为弱口令(密码字典检测)。 |
|
||||
| **合规性** | 满足《个人信息保护法》第13条(告知同意)、第17条(隐私政策),注册前勾选同意《用户协议》与《隐私政策》。 |
|
||||
| **可搜索加密** | 唯一性校验使用字段的 **MD5/SHA-256** 盲索引(不可逆),不直接对密文进行`SELECT`。 |
|
||||
| **数据脱敏展示** | 后台展示信用代码为 `913701**********`,手机号为 `138****8001`。 |
|
||||
| **审计防篡改** | 所有审计日志采用 **哈希链+数字签名**(参见需求说明书4.8.1),保留≥7年。 |
|
||||
| **防机器人** | 注册提交需验证图形验证码(极验),手机验证码发送每日限5次。 |
|
||||
| **密码安全** | 首次激活密码强度≥8位,含大小写+数字+特殊字符,不得为弱口令(密码字典检测)。 |
|
||||
| **合规性** | 满足《个人信息保护法》第13条(告知同意)、第17条(隐私政策),注册前勾选同意《用户协议》、《隐私政策》及《反商业贿赂承诺书》。 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
# # STP 智慧招标平台首页详细设计说明书
|
||||
|
||||
| 文档名称 | STP 首页详细设计 |
|
||||
| :------- | :----------------------------------- |
|
||||
| **文档版本** | V1.0 |
|
||||
| **修订说明** | 移除“帮助中心”菜单项(由 AI 助手承担)、移除统计数字区,优化布局。 |
|
||||
| **编制日期** | 2026-07-01 |
|
||||
| **关联文档** | 《[[需求分析说明书.md]]》《[[登录模块.md]]》 |
|
||||
|
||||
|
||||
## 目录
|
||||
1. [概述](#1-概述)
|
||||
2. [页面布局总览](#2-页面布局总览svg-线框图)
|
||||
3. [各模块详细说明](#3-各模块详细说明)
|
||||
4. [交互与动态效果详细说明](#4-交互与动态效果详细说明)
|
||||
5. [登录前后差异化展示](#5-登录前后差异化展示)
|
||||
6. [技术实现建议](#6-技术实现建议)
|
||||
7. [性能与可访问性](#7-性能与可访问性)
|
||||
8. [后续迭代方向](#8-后续迭代方向)
|
||||
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
STP 首页作为平台的门户,需同时满足**公开访问**和**登录后个性化**两种场景。核心目标包括:
|
||||
- **信息聚合**:集中展示招标公告、定标公告、企业新闻等动态信息。
|
||||
- **角色引导**:快速引导供应商、专家、内部员工进入对应操作路径。
|
||||
- **品牌信任**:通过声明、合规提示建立公信力。
|
||||
- **智能体验**:AI 助手作为统一帮助入口,提供主动式智能服务,替代传统帮助中心。
|
||||
|
||||
### 1.2 设计原则
|
||||
| 原则 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **清晰的信息层级** | 视觉重心依次为:导航 → 核心公告 → 快捷入口 → 声明。 |
|
||||
| **响应式适配** | 自适应 PC 端(≥1280px)和平板端(≥768px)。 |
|
||||
| **动效适度** | 运用滚动动画、轮播切换、微交互提升体验,不喧宾夺主。 |
|
||||
| **角色感知** | 登录前后内容差异化展示(如未登录显示注册入口,已登录显示待办提醒)。 |
|
||||
| **AI 优先** | AI 助手作为帮助入口,所有操作引导类需求通过对话完成。 |
|
||||
|
||||
|
||||
## 2. 页面布局总览
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 960" font-family="'Microsoft YaHei', sans-serif">
|
||||
<defs>
|
||||
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
<filter id="shadowDeep" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<linearGradient id="headerBg" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#c2185b"/>
|
||||
<stop offset="100%" stop-color="#e91e63"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bannerBg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#fce4ec"/>
|
||||
<stop offset="100%" stop-color="#f3e5f5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#c2185b"/><stop offset="100%" stop-color="#e91e63"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="1200" height="960" fill="#f5f7fa"/>
|
||||
|
||||
<!-- ========== 顶部导航栏 ========== -->
|
||||
<rect width="1200" height="72" fill="url(#headerBg)"/>
|
||||
<text x="40" y="46" font-size="24" font-weight="bold" fill="#fff">智慧招标平台</text>
|
||||
<text x="200" y="46" font-size="14" fill="rgba(255,255,255,0.8)">Smart Tendering Platform</text>
|
||||
|
||||
<!-- 导航菜单(移除“帮助中心”) -->
|
||||
<text x="480" y="46" font-size="15" fill="#fff" font-weight="bold">首页</text>
|
||||
<text x="560" y="46" font-size="15" fill="rgba(255,255,255,0.8)">招标公告</text>
|
||||
<text x="660" y="46" font-size="15" fill="rgba(255,255,255,0.8)">定标公告</text>
|
||||
<text x="760" y="46" font-size="15" fill="rgba(255,255,255,0.8)">企业新闻</text>
|
||||
|
||||
<!-- 登录/注册入口 -->
|
||||
<rect x="980" y="20" width="80" height="32" rx="6" fill="rgba(255,255,255,0.2)" stroke="#fff"/>
|
||||
<text x="1020" y="42" text-anchor="middle" font-size="14" fill="#fff">登录</text>
|
||||
<rect x="1070" y="20" width="90" height="32" rx="6" fill="#fff"/>
|
||||
<text x="1115" y="42" text-anchor="middle" font-size="14" fill="#c2185b" font-weight="bold">注册</text>
|
||||
|
||||
<!-- ========== Banner 轮播区 ========== -->
|
||||
<rect x="40" y="100" width="1120" height="280" rx="12" fill="#fff" filter="url(#shadowDeep)"/>
|
||||
<rect x="40" y="100" width="1120" height="280" rx="12" fill="url(#bannerBg)"/>
|
||||
|
||||
<!-- Banner 内容 -->
|
||||
<text x="160" y="210" font-size="32" font-weight="bold" fill="#c2185b">AI 赋能的智慧招标</text>
|
||||
<text x="160" y="250" font-size="18" fill="#4a5568">全流程线上闭环,主动智能寻源,自动风险评估</text>
|
||||
<text x="160" y="290" font-size="14" fill="#8895aa">• 智能招标委托 • 供应商 AI 寻源 • 自动化评标辅助</text>
|
||||
<rect x="160" y="320" width="140" height="40" rx="8" fill="#c2185b" filter="url(#shadow)"/>
|
||||
<text x="230" y="346" text-anchor="middle" font-size="15" fill="#fff" font-weight="bold">了解更多 →</text>
|
||||
|
||||
<!-- 轮播指示器(带高亮动画) -->
|
||||
<circle cx="900" cy="340" r="7" fill="#c2185b"/>
|
||||
<circle cx="925" cy="340" r="6" fill="#d1d5db"/>
|
||||
<circle cx="950" cy="340" r="6" fill="#d1d5db"/>
|
||||
<circle cx="975" cy="340" r="6" fill="#d1d5db"/>
|
||||
|
||||
<!-- ========== 公告区(三列,上移填补统计区空缺) ========== -->
|
||||
<text x="60" y="430" font-size="20" font-weight="bold" fill="#1a2332">📢 最新公告</text>
|
||||
|
||||
<!-- 左:招标公告 -->
|
||||
<rect x="40" y="455" width="360" height="330" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="40" y="455" width="360" height="42" rx="12" fill="#c2185b"/>
|
||||
<text x="220" y="483" text-anchor="middle" font-size="16" font-weight="bold" fill="#fff">招标公告</text>
|
||||
<text x="220" y="505" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.8)">最新发布</text>
|
||||
|
||||
<rect x="60" y="520" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="75" y="540" font-size="14" fill="#1a2332">年产5万吨聚丙烯项目设备采购</text>
|
||||
<text x="75" y="555" font-size="12" fill="#8895aa">2026-07-01 | 货物 | 报名中</text>
|
||||
|
||||
<rect x="60" y="570" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="75" y="590" font-size="14" fill="#1a2332">数字化交付平台开发服务</text>
|
||||
<text x="75" y="605" font-size="12" fill="#8895aa">2026-06-29 | 服务 | 即将截止</text>
|
||||
|
||||
<rect x="60" y="620" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="75" y="640" font-size="14" fill="#1a2332">2026年度防腐保温工程框架</text>
|
||||
<text x="75" y="655" font-size="12" fill="#8895aa">2026-06-28 | 工程 | 已截止</text>
|
||||
|
||||
<!-- 招标公告更多按钮 -->
|
||||
<rect x="280" y="760" width="100" height="28" rx="14" fill="#fce4ec"/>
|
||||
<text x="330" y="780" text-anchor="middle" font-size="13" fill="#c2185b" font-weight="bold">查看全部 →</text>
|
||||
|
||||
<!-- 中:定标公告 -->
|
||||
<rect x="420" y="455" width="360" height="330" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="420" y="455" width="360" height="42" rx="12" fill="#1565c0"/>
|
||||
<text x="600" y="483" text-anchor="middle" font-size="16" font-weight="bold" fill="#fff">定标公告</text>
|
||||
<text x="600" y="505" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.8)">最新定标</text>
|
||||
|
||||
<rect x="440" y="520" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="455" y="540" font-size="14" fill="#1a2332">催化剂采购项目(包1)</text>
|
||||
<text x="455" y="555" font-size="12" fill="#8895aa">2026-07-01 | 中标方:滨化催化剂有限公司</text>
|
||||
|
||||
<rect x="440" y="570" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="455" y="590" font-size="14" fill="#1a2332">厂区安防监控升级项目</text>
|
||||
<text x="455" y="605" font-size="12" fill="#8895aa">2026-06-30 | 中标方:海康威视</text>
|
||||
|
||||
<rect x="440" y="620" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="455" y="640" font-size="14" fill="#1a2332">办公楼装修工程</text>
|
||||
<text x="455" y="655" font-size="12" fill="#8895aa">2026-06-28 | 中标方:滨州建工集团</text>
|
||||
|
||||
<!-- 定标公告更多按钮 -->
|
||||
<rect x="660" y="760" width="100" height="28" rx="14" fill="#e3f2fd"/>
|
||||
<text x="710" y="780" text-anchor="middle" font-size="13" fill="#1565c0" font-weight="bold">查看全部 →</text>
|
||||
|
||||
<!-- 右:企业新闻 -->
|
||||
<rect x="800" y="455" width="360" height="330" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="800" y="455" width="360" height="42" rx="12" fill="#e65100"/>
|
||||
<text x="980" y="483" text-anchor="middle" font-size="16" font-weight="bold" fill="#fff">企业新闻</text>
|
||||
<text x="980" y="505" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.8)">集团动态</text>
|
||||
|
||||
<rect x="820" y="520" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="835" y="540" font-size="14" fill="#1a2332">滨化集团获评“省级智能工厂”</text>
|
||||
<text x="835" y="555" font-size="12" fill="#8895aa">2026-07-01 | 集团动态</text>
|
||||
|
||||
<rect x="820" y="570" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="835" y="590" font-size="14" fill="#1a2332">2026年供应商大会圆满举办</text>
|
||||
<text x="835" y="605" font-size="12" fill="#8895aa">2026-06-25 | 合作伙伴</text>
|
||||
|
||||
<rect x="820" y="620" width="320" height="40" rx="6" fill="#fafafa"/>
|
||||
<text x="835" y="640" font-size="14" fill="#1a2332">智慧招标平台正式上线运营</text>
|
||||
<text x="835" y="655" font-size="12" fill="#8895aa">2026-06-01 | 平台公告</text>
|
||||
|
||||
<!-- 企业新闻更多按钮 -->
|
||||
<rect x="1040" y="760" width="100" height="28" rx="14" fill="#fff3e0"/>
|
||||
<text x="1090" y="780" text-anchor="middle" font-size="13" fill="#e65100" font-weight="bold">查看全部 →</text>
|
||||
|
||||
<!-- ========== 底部声明区 ========== -->
|
||||
<rect x="40" y="815" width="1120" height="110" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="80" y="850" font-size="14" font-weight="bold" fill="#4a5568">免责声明</text>
|
||||
<text x="80" y="875" font-size="12" fill="#8895aa">本平台所有招标信息均由滨化集团或其下属公司发布,信息真实有效。供应商应对投标行为负责,本平台保留对违规行为的处理权。</text>
|
||||
<text x="80" y="900" font-size="12" fill="#8895aa">隐私政策 | 服务协议 | 法律声明 | © 2026 滨化集团 | 鲁ICP备XXXXXXXX号</text>
|
||||
|
||||
<!-- ========== AI 助手浮动入口 ========== -->
|
||||
<circle cx="1110" cy="890" r="42" fill="url(#brandGrad)" filter="url(#shadowDeep)"/>
|
||||
<text x="1110" y="885" text-anchor="middle" font-size="16" fill="#fff" font-weight="bold">小滨</text>
|
||||
<text x="1110" y="905" text-anchor="middle" font-size="10" fill="rgba(255,255,255,0.9)">AI 助手</text>
|
||||
|
||||
<!-- AI 助手呼吸光环动画示意 -->
|
||||
<circle cx="1110" cy="890" r="50" fill="none" stroke="#e91e63" stroke-width="2" opacity="0.3">
|
||||
<animate attributeName="r" from="42" to="55" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" from="0.6" to="0" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## 3. 各模块详细说明
|
||||
|
||||
### 3.1 顶部导航栏
|
||||
|
||||
| 元素 | 说明 |
|
||||
| :-------- | :-------------------------------------------------------------------------- |
|
||||
| **品牌区** | 平台 Logo + 名称(“智慧招标平台”),点击返回首页。右侧显示英文副标题。 |
|
||||
| **主导航** | 首页、招标公告、定标公告、企业新闻。高亮当前所在页面。 |
|
||||
| **用户操作区** | **未登录**:显示“登录”和“注册”按钮。**已登录**:显示用户头像/名称,下拉菜单包含“个人中心”、“我的投标/评审”、“安全设置”、“退出”。 |
|
||||
| **动态效果** | 导航项 hover 时下划线滑出动画(0.3s);登录/注册按钮 hover 时微变色/上浮。 |
|
||||
|
||||
> **设计决策**:移除“帮助中心”菜单项。所有操作引导、常见问题解答、流程咨询等功能由右下角 **AI 助手“小滨”** 统一承担,降低用户学习成本,提供更智能、更个性化的帮助体验。
|
||||
|
||||
### 3.2 Banner 轮播区
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **尺寸** | 1120 × 280px,圆角 12px,带卡片阴影 |
|
||||
| **功能** | 展示平台核心价值、最新活动、重要通知等,支持自动轮播和手动切换 |
|
||||
| **轮播数量** | 3~5 张 Banner |
|
||||
| **每张内容** | 主标题(32px)、副标题(18px)、特性标签列表、CTA 按钮 |
|
||||
| **切换方式** | 自动轮播(每 5 秒切换)+ 底部指示器手动切换 |
|
||||
| **动效** | 淡入淡出过渡(0.8s 缓动),指示器高亮同步变化 |
|
||||
| **CTA 按钮** | “了解更多”跳转至平台介绍页,“立即参与”跳转至公告列表 |
|
||||
|
||||
### 3.3 公告区
|
||||
|
||||
#### 3.3.1 招标公告(左列)
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题区** | 红色渐变背景(#c2185b),显示“招标公告”+“最新发布”标签 |
|
||||
| **列表项** | 展示 3 条最新招标信息 |
|
||||
| **每条信息** | 标题(14px,加粗)+ 发布时间、品类、状态标签 |
|
||||
| **状态标签** | 报名中(绿色)、即将截止(橙色)、已截止(灰色) |
|
||||
| **更多按钮** | “查看全部 →”,跳转至招标公告列表页(支持分页、搜索、筛选) |
|
||||
|
||||
#### 3.3.2 定标公告(中列)
|
||||
| 属性 | 说明 |
|
||||
| :------- | :-------------------------------- |
|
||||
| **标题区** | 蓝色渐变背景(#1565c0),显示“定标公告”+“最新定标”标签 |
|
||||
| **列表项** | 展示 3 条最新定标结果 |
|
||||
| **每条信息** | 项目名称(14px,加粗)+ 定标日期、中标方 |
|
||||
| **更多按钮** | “查看全部 →”,跳转至定标公告列表页 |
|
||||
|
||||
#### 3.3.3 企业新闻(右列)
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题区** | 橙色渐变背景(#e65100),显示“企业新闻”+“集团动态”标签 |
|
||||
| **列表项** | 展示 3 条最新集团动态 |
|
||||
| **每条信息** | 标题(14px,加粗)+ 发布日期、分类标签 |
|
||||
| **更多按钮** | “查看全部 →”,跳转至新闻列表页 |
|
||||
|
||||
#### 3.3.4 动态效果
|
||||
| 交互 | 效果 |
|
||||
| :--- | :--- |
|
||||
| **列表项 hover** | 背景色变为 #f0f2f7,过渡 0.2s |
|
||||
| **更多按钮 hover** | 文字颜色加深,下划线展开 |
|
||||
| **状态标签** | 报名中:绿色圆点 + “报名中”文字 |
|
||||
|
||||
### 3.4 底部声明区
|
||||
| 元素 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **免责声明** | “本平台所有招标信息均由滨化集团或其下属公司发布,信息真实有效。供应商应对投标行为负责,本平台保留对违规行为的处理权。” |
|
||||
| **政策链接** | 隐私政策、服务协议、法律声明(可点击跳转) |
|
||||
| **版权信息** | 集团名称 + 备案号 |
|
||||
| **样式** | 浅灰背景,文字 12px,版权信息居中对齐 |
|
||||
|
||||
### 3.5 AI 助手浮动入口
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--------- | :------------------------------------------------------------------------------------------------------- |
|
||||
| **位置** | 页面右下角固定,`position: fixed`,不随滚动移动 |
|
||||
| **视觉** | 圆形渐变按钮(直径 84px),显示“小滨”和“AI 助手”文字 |
|
||||
| **动态效果** | 呼吸光环动画(圆形脉冲,周期 2s),悬停时放大 1.05 倍 |
|
||||
| **交互** | 点击展开聊天窗口(浮窗),可最小化/关闭 |
|
||||
| **功能定位** | **替代传统帮助中心**。提供:<br>• 平台操作引导(“如何发起委托?”“怎么报名?”)<br>• 常见问题解答<br>• 流程状态查询(“我的注册进度?”)<br>• 智能推荐(根据角色推荐下一步操作) |
|
||||
| **登录前后差异** | 未登录:通用问答 + 注册引导;<br>已登录:个性化问答 + 待办提醒;<br>**注:登录后AI主动显示待办提醒,每一条提醒后面有链接直通处理页面。** |
|
||||
|
||||
## 4. 交互与动态效果详细说明
|
||||
|
||||
| 元素 | 触发事件 | 效果描述 | 持续时间 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **导航菜单** | hover | 文字下方滑出下划线(从左至右) | 0.3s |
|
||||
| **登录/注册按钮** | hover | 背景色微变,轻微上浮 2px | 0.2s |
|
||||
| **Banner 轮播** | 自动/点击指示器 | 内容淡入淡出切换,指示器高亮 | 0.8s |
|
||||
| **Banner CTA 按钮** | hover | 背景色加深,轻微放大 | 0.2s |
|
||||
| **公告列表项** | hover | 背景色变为 #f0f2f7 | 0.2s |
|
||||
| **“查看全部”按钮** | hover | 文字颜色加深,下划线展开 | 0.3s |
|
||||
| **AI 助手浮标** | hover | 放大 1.05 倍,阴影加深 | 0.2s |
|
||||
| **AI 助手光环** | 自动 | 脉冲扩散动画(呼吸效果) | 2s 循环 |
|
||||
| **页面加载** | onLoad | 公告列表从下往上淡入(stagger 100ms) | 0.6s |
|
||||
|
||||
### 4.1 关键帧动画示例(CSS)
|
||||
|
||||
```css
|
||||
/* AI 助手呼吸光环 */
|
||||
@keyframes breathe {
|
||||
0% { r: 42; opacity: 0.6; }
|
||||
100% { r: 58; opacity: 0; }
|
||||
}
|
||||
|
||||
/* 公告列表入场 */
|
||||
@keyframes slideUp {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 导航下划线 */
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.nav-item:hover::after { width: 100%; }
|
||||
```
|
||||
|
||||
|
||||
## 5. 登录前后差异化展示
|
||||
|
||||
| 模块 | 未登录 | 已登录 |
|
||||
| :--- | :--- | :--- |
|
||||
| **顶部用户区** | 显示“登录”+“注册”按钮 | 显示用户头像、名称及下拉菜单(个人中心/我的投标/安全设置/退出) |
|
||||
| **Banner 区** | 展示平台宣传内容(AI赋能、全流程等) | 可增加“待办提醒”Banner(如“您有 X 个项目待投标”)或个性化推荐 |
|
||||
| **公告区** | 全部公开,按时间倒序展示 | 可标注已读状态,或按供应商关注的品类/行业进行排序推荐 |
|
||||
| **AI 助手** | 通用问答 + 注册引导 | 根据角色推送个性化帮助:<br>• 供应商:“您关注的品类有新公告”<br>• 专家:“您有新的评标邀请”<br>• 招标专员:“待办任务提醒” |
|
||||
|
||||
|
||||
## 6. 技术实现建议
|
||||
|
||||
| 领域 | 建议 |
|
||||
| :--- | :--- |
|
||||
| **前端框架** | Vue 3 / React,采用组件化开发,公告列表、Banner 等均封装为独立组件 |
|
||||
| **数据交互** | • 公告列表:REST API 分页获取(`/api/announcements`)<br>• Banner 内容:由后台 CMS 管理(`/api/banners`)<br>• 统计数据:实时查询或 Redis 缓存(5分钟) |
|
||||
| **状态管理** | Pinia(Vue)/ Redux(React),管理用户登录状态、公告缓存 |
|
||||
| **动画库** | CSS Animation + 少量 GSAP(数字滚动效果) |
|
||||
| **响应式** | 使用 Grid / Flexbox,移动端适配为单列布局(≥768px 为三列,<768px 为单列) |
|
||||
| **AI 助手** | WebSocket 实时通信,支持流式输出(SSE),后端对接大语言模型 |
|
||||
|
||||
|
||||
## 7. 性能与可访问性
|
||||
|
||||
| 维度 | 要求 |
|
||||
| :--- | :--- |
|
||||
| **首屏加载** | ≤ 1.5 秒(LCP) |
|
||||
| **图片优化** | Banner 使用 WebP 格式,支持懒加载 |
|
||||
| **SEO** | 公告标题使用语义化 HTML(`<h1>`~`<h3>`),meta 信息完整 |
|
||||
| **可访问性** | • 颜色对比度 ≥ 4.5:1(WCAG AA)<br>• 支持键盘操作(Tab 切换、Enter 点击)<br>• 轮播指示器有 `aria-label` |
|
||||
| **缓存策略** | 公告列表缓存 60 秒,Banner 缓存 300 秒 |
|
||||
|
||||
|
||||
## 8. 后续迭代方向
|
||||
|
||||
| 方向 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **个性化推荐** | 根据用户历史浏览记录、关注的品类,推荐可能感兴趣的招标项目 |
|
||||
| **智能搜索** | 顶部导航增加全局搜索框,支持关键词检索公告、供应商、专家 |
|
||||
| **订阅功能** | 供应商可订阅关注的品类,新公告发布时通过站内信/邮件推送 |
|
||||
| **多语言支持** | 预留中英文切换接口(i18n),后续拓展 |
|
||||
| **数据看板** | 登录后首页可展示个人投标/评审数据统计 |
|
||||
|
||||
|
||||
## 9. 附录:模块映射表
|
||||
|
||||
| 序号 | 模块名称 | SVG Y坐标 | 高度 | 功能 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 1 | 顶部导航栏 | 0 | 72 | 品牌+导航+用户操作 |
|
||||
| 2 | Banner 轮播区 | 100 | 280 | 核心宣传+活动通知 |
|
||||
| 3 | 招标公告 | 455 | 330 | 最新招标信息(3条) |
|
||||
| 4 | 定标公告 | 455 | 330 | 最新定标结果(3条) |
|
||||
| 5 | 企业新闻 | 455 | 330 | 最新集团动态(3条) |
|
||||
| 6 | 底部声明区 | 815 | 110 | 免责声明+政策+版权 |
|
||||
| 7 | AI 助手浮标 | 848(固定) | 84 | 智能帮助入口 |
|
||||
@@ -0,0 +1,618 @@
|
||||
# 招标专员工作台详细设计说明书
|
||||
|
||||
| 文档名称 | 招标专员工作台详细设计 |
|
||||
| :------- | :---------- |
|
||||
| **文档版本** | V1.0 |
|
||||
| **编制日期** | 2026-07-06 |
|
||||
| **关联文档** | 《需求分析说明书》 |
|
||||
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
招标专员工作台是招标专员登录后默认进入的页面,作为其日常招标业务操作的核心枢纽。设计核心围绕 **“任务驱动”** 与 **“AI主动辅助”** 两大理念:
|
||||
- **任务驱动**:集中展示待办任务(委托确认、寻源确认、立项审批、评标组织、定标发起等),按招标流程阶段分类,引导专员高效推进。
|
||||
- **AI主动辅助**:AI助手“小滨”以悬浮窗口形式常驻,主动分析当前待办任务,推送优先级建议、风险预警和操作指引,实现“事找人”的智能工作模式。
|
||||
|
||||
### 1.2 适用角色
|
||||
- 集团及下属公司的招标专员、招标管理部人员。
|
||||
- 不同专员的待办任务根据其被分配的项目和流程节点差异化展示。
|
||||
|
||||
### 1.3 设计原则
|
||||
| 原则 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **流程驱动** | 任务按招标全生命周期(委托→寻源→立项→开标→评标→定标)自然排列,符合业务认知。 |
|
||||
| **AI优先** | AI助手悬浮窗主动推送待办建议,减少专员主动查找的认知负担。 |
|
||||
| **效率优先** | 高频操作(确认、审批、发起)一键可达,缩短操作路径。 |
|
||||
| **实时同步** | 所有任务状态、待办数量实时更新,确保不遗漏关键节点。 |
|
||||
|
||||
|
||||
## 2. 页面布局总览
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 980" font-family="'Microsoft YaHei', sans-serif">
|
||||
<defs>
|
||||
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
<filter id="shadowDeep" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<linearGradient id="headerBg" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#00695c"/><stop offset="100%" stop-color="#00897b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00695c"/><stop offset="100%" stop-color="#00897b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cardGrad1" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e0f2f1"/><stop offset="100%" stop-color="#b2dfdb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="1200" height="980" fill="#f5f7fa"/>
|
||||
|
||||
<!-- ===== 顶部导航栏(绿色系,与供应商/专家区分) ===== -->
|
||||
<rect width="1200" height="64" fill="url(#headerBg)"/>
|
||||
<text x="40" y="42" font-size="22" font-weight="bold" fill="#fff">智慧招标平台</text>
|
||||
<text x="190" y="42" font-size="13" fill="rgba(255,255,255,0.8)">Smart Tendering Platform</text>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<text x="440" y="42" font-size="15" fill="rgba(255,255,255,0.8)">首页</text>
|
||||
<text x="520" y="42" font-size="15" fill="#fff" font-weight="bold">工作台</text>
|
||||
<text x="620" y="42" font-size="15" fill="rgba(255,255,255,0.8)">项目管理</text>
|
||||
<text x="720" y="42" font-size="15" fill="rgba(255,255,255,0.8)">供应商管理</text>
|
||||
<text x="820" y="42" font-size="15" fill="rgba(255,255,255,0.8)">专家管理</text>
|
||||
|
||||
<!-- 已登录用户 -->
|
||||
<circle cx="1010" cy="32" r="16" fill="rgba(255,255,255,0.3)"/>
|
||||
<text x="1010" y="37" text-anchor="middle" font-size="12" fill="#fff">张</text>
|
||||
<text x="1040" y="37" font-size="14" fill="#fff">张婷</text>
|
||||
<text x="1090" y="37" font-size="12" fill="rgba(255,255,255,0.7)">▼</text>
|
||||
|
||||
<!-- ===== 内容区 ===== -->
|
||||
|
||||
<!-- 欢迎横幅 + 今日概览 -->
|
||||
<rect x="40" y="90" width="1120" height="70" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="70" y="120" font-size="18" font-weight="bold" fill="#1a2332">👋 早上好,张婷</text>
|
||||
<text x="70" y="145" font-size="14" fill="#8895aa">今日待办:<tspan fill="#00695c" font-weight="bold">8</tspan> 项 | 进行中项目:<tspan fill="#00695c" font-weight="bold">5</tspan> 个</text>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ===== 新增:快捷入口(8个图标,2行×4列) ===== -->
|
||||
<!-- ============================================================ -->
|
||||
<text x="70" y="200" font-size="16" font-weight="bold" fill="#1a2332">⚡ 快捷入口</text>
|
||||
|
||||
<!-- 行1 -->
|
||||
<!-- 入口1:新建委托 -->
|
||||
<rect x="40" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="40" y="220" width="130" height="4" rx="2" fill="#e91e63"/>
|
||||
<text x="105" y="252" text-anchor="middle" font-size="24">📝</text>
|
||||
<text x="105" y="280" text-anchor="middle" font-size="13" fill="#4a5568">新建委托</text>
|
||||
|
||||
<!-- 入口2:我的项目 -->
|
||||
<rect x="185" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="185" y="220" width="130" height="4" rx="2" fill="#1565c0"/>
|
||||
<text x="250" y="252" text-anchor="middle" font-size="24">📂</text>
|
||||
<text x="250" y="280" text-anchor="middle" font-size="13" fill="#4a5568">我的项目</text>
|
||||
|
||||
<!-- 入口3:供应商库 -->
|
||||
<rect x="330" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="330" y="220" width="130" height="4" rx="2" fill="#ff9800"/>
|
||||
<text x="395" y="252" text-anchor="middle" font-size="24">🏢</text>
|
||||
<text x="395" y="280" text-anchor="middle" font-size="13" fill="#4a5568">供应商库</text>
|
||||
|
||||
<!-- 入口4:专家库 -->
|
||||
<rect x="475" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="475" y="220" width="130" height="4" rx="2" fill="#1a237e"/>
|
||||
<text x="540" y="252" text-anchor="middle" font-size="24">👨🏫</text>
|
||||
<text x="540" y="280" text-anchor="middle" font-size="13" fill="#4a5568">专家库</text>
|
||||
|
||||
<!-- 入口5:模板库 -->
|
||||
<rect x="620" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="620" y="220" width="130" height="4" rx="2" fill="#4caf50"/>
|
||||
<text x="685" y="252" text-anchor="middle" font-size="24">📄</text>
|
||||
<text x="685" y="280" text-anchor="middle" font-size="13" fill="#4a5568">模板库</text>
|
||||
|
||||
<!-- 入口6:报表中心 -->
|
||||
<rect x="765" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="765" y="220" width="130" height="4" rx="2" fill="#e65100"/>
|
||||
<text x="830" y="252" text-anchor="middle" font-size="24">📊</text>
|
||||
<text x="830" y="280" text-anchor="middle" font-size="13" fill="#4a5568">报表中心</text>
|
||||
|
||||
<!-- 入口7:保证金管理 -->
|
||||
<rect x="910" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="910" y="220" width="130" height="4" rx="2" fill="#9c27b0"/>
|
||||
<text x="975" y="252" text-anchor="middle" font-size="24">💰</text>
|
||||
<text x="975" y="280" text-anchor="middle" font-size="13" fill="#4a5568">保证金管理</text>
|
||||
|
||||
<!-- 入口8:定标审批 -->
|
||||
<rect x="1055" y="220" width="130" height="80" rx="10" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="1055" y="220" width="130" height="4" rx="2" fill="#c62828"/>
|
||||
<text x="1120" y="252" text-anchor="middle" font-size="24">✅</text>
|
||||
<text x="1120" y="280" text-anchor="middle" font-size="13" fill="#4a5568">定标审批</text>
|
||||
|
||||
<!-- 行2:新增推荐入口(AI智能推荐,带小滨角标) -->
|
||||
<text x="70" y="330" font-size="14" font-weight="bold" fill="#00695c">🤖 小滨推荐</text>
|
||||
|
||||
<!-- 推荐1:AI寻源 -->
|
||||
<rect x="40" y="350" width="200" height="60" rx="10" fill="#e8f5e9" stroke="#4caf50" filter="url(#shadow)"/>
|
||||
<text x="70" y="378" font-size="20">🔍</text>
|
||||
<text x="100" y="375" font-size="14" font-weight="bold" fill="#2e7d32">AI智慧寻源</text>
|
||||
<text x="100" y="396" font-size="12" fill="#4a5568">2个项目待AI寻源</text>
|
||||
<rect x="190" y="355" width="30" height="16" rx="8" fill="#ff9800"/>
|
||||
<text x="205" y="367" text-anchor="middle" font-size="10" fill="#fff">2</text>
|
||||
|
||||
<!-- 推荐2:待定标 -->
|
||||
<rect x="260" y="350" width="200" height="60" rx="10" fill="#fff3e0" stroke="#ffcc80" filter="url(#shadow)"/>
|
||||
<text x="290" y="378" font-size="20">📋</text>
|
||||
<text x="320" y="375" font-size="14" font-weight="bold" fill="#e65100">待定标项目</text>
|
||||
<text x="320" y="396" font-size="12" fill="#4a5568">1个项目待发起定标</text>
|
||||
<rect x="410" y="355" width="30" height="16" rx="8" fill="#f44336"/>
|
||||
<text x="425" y="367" text-anchor="middle" font-size="10" fill="#fff">1</text>
|
||||
|
||||
<!-- ===== 待办任务看板(4个阶段卡片) ===== -->
|
||||
<text x="70" y="460" font-size="16" font-weight="bold" fill="#1a2332">📋 待办任务</text>
|
||||
|
||||
<!-- 卡片1:待处理委托 -->
|
||||
<rect x="40" y="480" width="260" height="170" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="40" y="480" width="260" height="6" rx="3" fill="#e91e63"/>
|
||||
<text x="60" y="508" font-size="14" font-weight="bold" fill="#e91e63">📝 待确认委托</text>
|
||||
<text x="60" y="534" font-size="32" font-weight="bold" fill="#1a2332">3</text>
|
||||
<text x="60" y="554" font-size="13" fill="#8895aa">个委托单待审核确认</text>
|
||||
<text x="60" y="580" font-size="13" fill="#e91e63" text-decoration="underline">查看全部 →</text>
|
||||
<rect x="60" y="600" width="80" height="24" rx="12" fill="#ffebee"/>
|
||||
<text x="100" y="617" text-anchor="middle" font-size="11" fill="#c62828">新委托</text>
|
||||
|
||||
<!-- 卡片2:待寻源确认 -->
|
||||
<rect x="320" y="480" width="260" height="170" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="320" y="480" width="260" height="6" rx="3" fill="#ff9800"/>
|
||||
<text x="340" y="508" font-size="14" font-weight="bold" fill="#e65100">🔍 待寻源确认</text>
|
||||
<text x="340" y="534" font-size="32" font-weight="bold" fill="#1a2332">2</text>
|
||||
<text x="340" y="554" font-size="13" fill="#8895aa">个寻源结果待确认</text>
|
||||
<text x="340" y="580" font-size="13" fill="#e65100" text-decoration="underline">查看全部 →</text>
|
||||
<rect x="340" y="600" width="80" height="24" rx="12" fill="#fff3e0"/>
|
||||
<text x="380" y="617" text-anchor="middle" font-size="11" fill="#e65100">AI已完成</text>
|
||||
|
||||
<!-- 卡片3:待立项审批 -->
|
||||
<rect x="600" y="480" width="260" height="170" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="600" y="480" width="260" height="6" rx="3" fill="#1565c0"/>
|
||||
<text x="620" y="508" font-size="14" font-weight="bold" fill="#1565c0">📄 待立项审批</text>
|
||||
<text x="620" y="534" font-size="32" font-weight="bold" fill="#1a2332">1</text>
|
||||
<text x="620" y="554" font-size="13" fill="#8895aa">个招标方案待审批</text>
|
||||
<text x="620" y="580" font-size="13" fill="#1565c0" text-decoration="underline">查看全部 →</text>
|
||||
<rect x="620" y="600" width="80" height="24" rx="12" fill="#e3f2fd"/>
|
||||
<text x="660" y="617" text-anchor="middle" font-size="11" fill="#0d47a1">待发起</text>
|
||||
|
||||
<!-- 卡片4:待开标/评标 -->
|
||||
<rect x="880" y="480" width="260" height="170" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<rect x="880" y="480" width="260" height="6" rx="3" fill="#4caf50"/>
|
||||
<text x="900" y="508" font-size="14" font-weight="bold" fill="#2e7d32">⚖️ 待开标/评标</text>
|
||||
<text x="900" y="534" font-size="32" font-weight="bold" fill="#1a2332">2</text>
|
||||
<text x="900" y="554" font-size="13" fill="#8895aa">个项目待组织评标</text>
|
||||
<text x="900" y="580" font-size="13" fill="#2e7d32" text-decoration="underline">查看全部 →</text>
|
||||
<rect x="900" y="600" width="80" height="24" rx="12" fill="#e8f5e9"/>
|
||||
<text x="940" y="617" text-anchor="middle" font-size="11" fill="#2e7d32">即将截止</text>
|
||||
|
||||
<!-- ===== 我的在办项目(表格) ===== -->
|
||||
<text x="70" y="700" font-size="16" font-weight="bold" fill="#1a2332">📂 我的在办项目</text>
|
||||
|
||||
<rect x="40" y="720" width="1120" height="180" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 表头 -->
|
||||
<rect x="40" y="720" width="1120" height="36" rx="12" fill="#f0f2f7"/>
|
||||
<text x="70" y="744" font-size="13" font-weight="bold" fill="#4a5568">项目编号</text>
|
||||
<text x="200" y="744" font-size="13" font-weight="bold" fill="#4a5568">项目名称</text>
|
||||
<text x="420" y="744" font-size="13" font-weight="bold" fill="#4a5568">当前阶段</text>
|
||||
<text x="560" y="744" font-size="13" font-weight="bold" fill="#4a5568">所属委托</text>
|
||||
<text x="700" y="744" font-size="13" font-weight="bold" fill="#4a5568">截止日期</text>
|
||||
<text x="840" y="744" font-size="13" font-weight="bold" fill="#4a5568">状态</text>
|
||||
<text x="980" y="744" font-size="13" font-weight="bold" fill="#4a5568">操作</text>
|
||||
|
||||
<!-- 行1 -->
|
||||
<rect x="50" y="765" width="1100" height="36" rx="4" fill="#fff"/>
|
||||
<text x="70" y="788" font-size="13" fill="#1a2332">BID-2026-001</text>
|
||||
<text x="200" y="788" font-size="13" fill="#1a2332">年产5万吨聚丙烯项目设备采购</text>
|
||||
<rect x="420" y="776" width="60" height="18" rx="4" fill="#ff9800"/>
|
||||
<text x="450" y="789" text-anchor="middle" font-size="11" fill="#fff">寻源中</text>
|
||||
<text x="560" y="788" font-size="13" fill="#666">技术部-李工</text>
|
||||
<text x="700" y="788" font-size="13" fill="#666">2026-07-15</text>
|
||||
<rect x="840" y="776" width="60" height="18" rx="4" fill="#4caf50"/>
|
||||
<text x="870" y="789" text-anchor="middle" font-size="11" fill="#fff">进行中</text>
|
||||
<text x="980" y="788" font-size="13" fill="#00695c" text-decoration="underline">继续</text>
|
||||
|
||||
<!-- 行2 -->
|
||||
<rect x="50" y="805" width="1100" height="36" rx="4" fill="#fff"/>
|
||||
<text x="70" y="828" font-size="13" fill="#1a2332">BID-2026-003</text>
|
||||
<text x="200" y="828" font-size="13" fill="#1a2332">数字化交付平台开发服务</text>
|
||||
<rect x="420" y="816" width="60" height="18" rx="4" fill="#1565c0"/>
|
||||
<text x="450" y="829" text-anchor="middle" font-size="11" fill="#fff">立项中</text>
|
||||
<text x="560" y="828" font-size="13" fill="#666">数字化部-王工</text>
|
||||
<text x="700" y="828" font-size="13" fill="#666">2026-07-10</text>
|
||||
<rect x="840" y="816" width="60" height="18" rx="4" fill="#ff9800"/>
|
||||
<text x="870" y="829" text-anchor="middle" font-size="11" fill="#fff">即将截止</text>
|
||||
<text x="980" y="828" font-size="13" fill="#00695c" text-decoration="underline">处理</text>
|
||||
|
||||
<!-- 行3 -->
|
||||
<rect x="50" y="845" width="1100" height="36" rx="4" fill="#fff"/>
|
||||
<text x="70" y="868" font-size="13" fill="#1a2332">BID-2026-005</text>
|
||||
<text x="200" y="868" font-size="13" fill="#1a2332">2026年度防腐保温工程框架</text>
|
||||
<rect x="420" y="856" width="60" height="18" rx="4" fill="#e91e63"/>
|
||||
<text x="450" y="869" text-anchor="middle" font-size="11" fill="#fff">待开标</text>
|
||||
<text x="560" y="868" font-size="13" fill="#666">工程部-刘工</text>
|
||||
<text x="700" y="868" font-size="13" fill="#666">2026-07-08</text>
|
||||
<rect x="840" y="856" width="60" height="18" rx="4" fill="#f44336"/>
|
||||
<text x="870" y="869" text-anchor="middle" font-size="11" fill="#fff">紧急</text>
|
||||
<text x="980" y="868" font-size="13" fill="#00695c" text-decoration="underline">组织评标</text>
|
||||
|
||||
<!-- 查看更多 -->
|
||||
<text x="1020" y="890" font-size="13" fill="#00695c" text-decoration="underline">查看全部项目 →</text>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ===== AI 助手“小滨”悬浮窗 ===== -->
|
||||
<!-- ============================================================ -->
|
||||
<g transform="translate(800, 480)">
|
||||
<rect x="0" y="0" width="340" height="400" rx="16" fill="rgba(0,0,0,0.15)" filter="url(#shadowDeep)"/>
|
||||
<rect x="0" y="0" width="340" height="400" rx="16" fill="#ffffff" filter="url(#shadowDeep)"/>
|
||||
|
||||
<!-- 头部 -->
|
||||
<rect x="0" y="0" width="340" height="52" rx="16" fill="url(#headerBg)"/>
|
||||
<rect x="0" y="28" width="340" height="24" fill="url(#headerBg)"/>
|
||||
<circle cx="24" cy="26" r="10" fill="rgba(255,255,255,0.3)"/>
|
||||
<text x="24" y="30" text-anchor="middle" font-size="10" fill="#fff" font-weight="bold">滨</text>
|
||||
<text x="42" y="30" font-size="15" font-weight="bold" fill="#fff">小滨 AI 助手</text>
|
||||
<text x="310" y="24" font-size="16" fill="rgba(255,255,255,0.7)">−</text>
|
||||
<text x="325" y="24" font-size="16" fill="rgba(255,255,255,0.7)">✕</text>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<rect x="16" y="62" width="308" height="28" rx="6" fill="#e8f5e9"/>
|
||||
<circle cx="32" cy="76" r="5" fill="#4caf50"/>
|
||||
<text x="44" y="81" font-size="12" fill="#2e7d32">● 在线 · 已为您分析今日待办</text>
|
||||
|
||||
<!-- 消息1 -->
|
||||
<rect x="16" y="100" width="308" height="64" rx="8" fill="#fff3e0" stroke="#ffcc80"/>
|
||||
<text x="28" y="122" font-size="13" font-weight="bold" fill="#e65100">🔥 紧急:BID-2026-005 今日需开标</text>
|
||||
<text x="28" y="144" font-size="12" fill="#bf360c">“2026年度防腐保温工程框架”已到开标时间,请立即组织评标</text>
|
||||
<text x="290" y="144" font-size="12" fill="#e65100" font-weight="bold" text-decoration="underline">去处理 →</text>
|
||||
|
||||
<!-- 消息2 -->
|
||||
<rect x="16" y="174" width="308" height="64" rx="8" fill="#e8f5e9" stroke="#a5d6a7"/>
|
||||
<text x="28" y="196" font-size="13" font-weight="bold" fill="#2e7d32">✅ AI 已完成:寻源报告生成</text>
|
||||
<text x="28" y="218" font-size="12" fill="#1b5e20">“年产5万吨聚丙烯项目”已自动寻源,匹配到12家潜在供应商</text>
|
||||
<text x="290" y="218" font-size="12" fill="#2e7d32" font-weight="bold" text-decoration="underline">查看 →</text>
|
||||
|
||||
<!-- 消息3 -->
|
||||
<rect x="16" y="248" width="308" height="64" rx="8" fill="#e3f2fd" stroke="#90caf9"/>
|
||||
<text x="28" y="270" font-size="13" font-weight="bold" fill="#0d47a1">📋 您有 3 个委托单待审核</text>
|
||||
<text x="28" y="292" font-size="12" fill="#0d47a1">来自技术部、采购部的委托申请已提交,请及时审核确认</text>
|
||||
<text x="290" y="292" font-size="12" fill="#0d47a1" font-weight="bold" text-decoration="underline">去审核 →</text>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<rect x="16" y="340" width="240" height="40" rx="20" fill="#f5f7fa" stroke="#e0e4ec"/>
|
||||
<text x="30" y="365" font-size="13" fill="#b0bbcc">输入您的问题...</text>
|
||||
<rect x="268" y="340" width="56" height="40" rx="20" fill="url(#brandGrad)"/>
|
||||
<text x="296" y="366" text-anchor="middle" font-size="18" fill="#fff">➤</text>
|
||||
</g>
|
||||
|
||||
<!-- 小窗边缘提示角标 -->
|
||||
<circle cx="1140" y="490" r="10" fill="#f44336"/>
|
||||
<text x="1140" y="495" text-anchor="middle" font-size="10" fill="#fff" font-weight="bold">5</text>
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## 3. 各模块详细说明
|
||||
|
||||
### 3.1 顶部导航栏
|
||||
- **品牌区**:平台Logo + 名称,点击返回首页。
|
||||
- **导航菜单**:首页、工作台(高亮)、项目管理、供应商管理、专家管理。
|
||||
- **用户区**:显示招标专员头像(首字母)、姓名,下拉菜单含个人设置、退出等。
|
||||
- **颜色主题**:绿色系(#00695c → #00897b),与供应商(红色)、专家(蓝紫色)区分。
|
||||
|
||||
### 3.2 快捷入口模块
|
||||
|
||||
> 位于欢迎横幅与待办看板之间,提供高频功能的一键直达入口。
|
||||
|
||||
#### 3.2.1 布局与尺寸
|
||||
|
||||
| 属性 | 说明 |
|
||||
| -------- | ------------------------------------- |
|
||||
| **位置** | 欢迎横幅下方,待办看板上方 |
|
||||
| **布局** | 两行:第一行为 8 个固定入口(2行×4列),第二行为“小滨推荐”动态入口 |
|
||||
| **入口尺寸** | 宽 130px,高 80px,间距 15px |
|
||||
| **背景** | 白色卡片,圆角 10px,带浅阴影 |
|
||||
| **顶部色条** | 每个入口顶部有 4px 高的彩色条,用于视觉区分 |
|
||||
|
||||
#### 3.1.2 固定快捷入口列表
|
||||
|
||||
|序号|图标|名称|顶部色条|跳转目标|说明|
|
||||
|---|---|---|---|---|---|
|
||||
|1|📝|新建委托|红色(#e91e63)|委托创建页|快速发起新的招标委托|
|
||||
|2|📂|我的项目|蓝色(#1565c0)|项目管理列表|查看所有负责的项目|
|
||||
|3|🏢|供应商库|橙色(#ff9800)|供应商管理页|查询/管理供应商信息|
|
||||
|4|👨🏫|专家库|深蓝(#1a237e)|专家管理页|查询/管理评审专家|
|
||||
|5|📄|模板库|绿色(#4caf50)|模板管理页|招标文件/评分模板|
|
||||
|6|📊|报表中心|深橙(#e65100)|报表统计页|招标数据统计分析|
|
||||
|7|💰|保证金管理|紫色(#9c27b0)|保证金管理页|保证金缴纳/退还管理|
|
||||
|8|✅|定标审批|深红(#c62828)|定标审批列表|快速发起定标审批|
|
||||
|
||||
### 3.3 待办任务看板(4个阶段卡片)
|
||||
|
||||
> 按招标全生命周期阶段组织,每个卡片突出显示待办数量,支持点击查看明细。
|
||||
|
||||
| 卡片 | 颜色主题 | 数据来源 | 任务类型 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **待确认委托** | 红色(#e91e63) | 委托流程 | 委托审核确认、委托补充 |
|
||||
| **待寻源确认** | 橙色(#ff9800) | 供应商寻源流程 | AI寻源结果确认、短名单确认 |
|
||||
| **待立项审批** | 蓝色(#1565c0) | 招标立项流程 | 招标方案评审、审批发起 |
|
||||
| **待开标/评标** | 绿色(#4caf50) | 开标及评标流程 | 截标确认、专家抽取、组织评标 |
|
||||
|
||||
**交互规则**:
|
||||
- 点击卡片任意区域 → 跳转至对应任务列表页。
|
||||
- 点击“查看全部” → 跳转至带筛选条件的完整任务列表。
|
||||
- 右上角标签 → 显示该阶段最高优先级状态(如“新委托”、“即将截止”)。
|
||||
- 数字实时更新,有变化时数字闪烁(0.5s 呼吸动画)。
|
||||
|
||||
### 3.4 我的在办项目
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **位置** | 待办看板下方 |
|
||||
| **尺寸** | 1120 × 200px |
|
||||
| **展示逻辑** | 当前专员负责的、状态为“进行中”的项目,按紧急程度排序(即将截止优先) |
|
||||
| **表格列** | 项目编号、项目名称、当前阶段、所属委托、截止日期、状态、操作 |
|
||||
| **状态标签** | 进行中(绿色)、即将截止(橙色)、紧急(红色) |
|
||||
| **操作列** | 根据项目阶段动态变化:继续、处理、组织评标、查看等 |
|
||||
| **查看更多** | 跳转至完整项目列表页 |
|
||||
|
||||
**操作列映射表**:
|
||||
|
||||
| 项目阶段 | 操作按钮 | 跳转目标 |
|
||||
| :--- | :--- | :--- |
|
||||
| 委托中 | 确认 | 委托详情页 |
|
||||
| 寻源中 | 查看报告 | AI寻源结果页 |
|
||||
| 立项中 | 编辑方案 | 招标方案编辑页 |
|
||||
| 待开标 | 组织评标 | 开标准备页 |
|
||||
| 评标中 | 进入评标 | 评标管理页 |
|
||||
| 定标中 | 发起审批 | 定标审批页 |
|
||||
|
||||
## 3.5 后台智能体任务监控
|
||||
|
||||
### 3.5.1 模块定位
|
||||
|
||||
| 属性 | 说明 |
|
||||
| -------- | ------------------------------------------------------------ |
|
||||
| **模块名称** | 后台智能体任务(AI Agent Task Monitor) |
|
||||
| **位置** | 待办任务看板下方,在办项目列表上方 |
|
||||
| **功能** | 实时展示后台异步 AI 智能体任务的执行进度,包括任务名称、关联项目、执行状态、进度百分比、关键中间结果、预计剩余时间等 |
|
||||
| **刷新机制** | 每 3 秒轮询 / WebSocket 推送(推荐) |
|
||||
|
||||
### 3.5.2 任务类型清单
|
||||
|
||||
|任务图标|任务名称|触发时机|典型耗时|输出产物|
|
||||
|---|---|---|---|---|
|
||||
|🔍|AI 智慧寻源|委托确认后自动触发|2-10 分钟|潜在供应商名单 + 推荐报告|
|
||||
|🔬|AI 雷同检测|投标文件上传完成后|1-8 分钟|雷同检测报告|
|
||||
|📊|AI 价格分析|开标完成后|1-3 分钟|价格偏离度分析报告|
|
||||
|👨🏫|AI 专家推荐|评标前准备阶段|30 秒-2 分钟|匹配专家列表|
|
||||
|📄|AI 评标报告生成|评标打分完成后|1-2 分钟|评标报告初稿|
|
||||
|🧩|AI 合并招标建议|委托生效时|1-3 分钟|合并招标建议书|
|
||||
|📋|AI 资格初审|供应商报名后|1-5 分钟|资格预审结果|
|
||||
|
||||
### 3.5.3 任务状态与视觉规范
|
||||
|
||||
|状态|色条颜色|状态标签|进度条样式|说明|
|
||||
|---|---|---|---|---|
|
||||
|**待执行**|灰色(#9e9e9e)|⏸ 待执行|灰色空进度条|任务在队列中等待|
|
||||
|**执行中**|橙色(#ff9800)|⏳ 执行中|橙色渐变进度条 + 百分比|正在运行,显示进度|
|
||||
|**已完成**|绿色(#4caf50)|✅ 已完成|绿色实心进度条|任务成功完成|
|
||||
|**失败**|红色(#f44336)|❌ 执行失败|红色进度条 + 错误标记|任务异常终止|
|
||||
|
||||
### 3.5.4 单条任务卡片结构
|
||||
|
||||
|区域|元素|说明|
|
||||
|---|---|---|
|
||||
|**左侧**|色条|6px 宽,颜色对应任务状态|
|
||||
|**图标区**|任务图标 + 圆形背景|28px 圆,背景色与状态匹配|
|
||||
|**任务信息**|任务名称(加粗)+ 关联项目名称|显示任务所属项目|
|
||||
|**状态区**|状态标签 + 进度条 + 百分比|展示当前执行进度|
|
||||
|**详情区**|关键中间结果|如“已匹配 8/20 家供应商”|
|
||||
|**时间区**|预计剩余时间 / 完成时间|执行中显示剩余,已完成显示完成时间|
|
||||
|**操作区**|取消 / 查看报告|执行中可取消,已完成可查看报告|
|
||||
|
||||
### 3.5.5 任务卡片示例数据
|
||||
|
||||
|任务|状态|进度|中间结果|预计剩余|操作|
|
||||
|---|---|---|---|---|---|
|
||||
|AI 智慧寻源|执行中|70%|已匹配:8 家供应商|2 分钟|取消|
|
||||
|AI 雷同检测|执行中|30%|已扫描:3/10 份|5 分钟|取消|
|
||||
|AI 价格分析|已完成|100%|发现 1 份报价偏离 >20%|09:35 完成|查看报告|
|
||||
|AI 专家推荐|待执行|0%|—|—|—|
|
||||
|
||||
### 3.5.6 点击交互
|
||||
|
||||
|交互|说明|
|
||||
|---|---|
|
||||
|**点击任务卡片**|跳转至任务详情页(展示完整的执行日志和中间结果)|
|
||||
|**点击“取消”**|弹出二次确认弹窗,确认后终止任务(仅限执行中状态)|
|
||||
|**点击“查看报告”**|在新窗口打开或下载生成的报告文件|
|
||||
|**悬停进度条**|显示详细进度信息(Tooltip)|
|
||||
|
||||
### 3.5.7 智能体任务与 AI 助手的协同
|
||||
|
||||
当智能体任务状态发生变化时,AI助手“小滨”会同步推送主动提醒:
|
||||
|
||||
|任务状态变化|小滨推送内容|示例|
|
||||
|---|---|---|
|
||||
|任务完成|“✅ AI [任务名称] 已完成,请查看报告”|“✅ AI 价格分析已完成,发现 1 份报价偏离度 >20%”|
|
||||
|任务失败|“❌ AI [任务名称] 执行失败,请重试或联系管理员”|“❌ AI 雷同检测执行失败,请检查投标文件格式”|
|
||||
|关键节点|“📌 AI [任务名称] 已找到 [数量] 个潜在供应商”|“📌 AI 智慧寻源已找到 12 家潜在供应商”|
|
||||
|
||||
|
||||
### 3.6 AI 助手“小滨”悬浮窗(核心特色)
|
||||
|
||||
#### 3.6.1 视觉设计
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **位置** | `position: fixed`,默认位于右下角(距底部80px,距右侧40px) |
|
||||
| **尺寸** | 宽340px,高400px |
|
||||
| **可拖动** | 用户可通过拖拽头部区域移动悬浮窗位置 |
|
||||
| **可折叠** | 点击头部“−”按钮最小化到右下角图标,点击“✕”关闭(可在系统设置中重新开启) |
|
||||
|
||||
#### 3.6.2 主动提示能力
|
||||
|
||||
AI助手“小滨”**不需要用户主动提问**,而是主动分析当前专员的待办任务状态,推送以下类型提示:
|
||||
|
||||
| 提示类型 | 触发条件 | 样式 | 示例 |
|
||||
| :----------- | :------------ | :------------ | :------------------- |
|
||||
| **🔥 紧急提醒** | 任务截止时间 < 24小时 | 橙色背景(#fff3e0) | “BID-2026-005 今日需开标” |
|
||||
| **✅ AI完成通知** | AI自动完成阶段性工作 | 绿色背景(#e8f5e9) | “✅ AI已完成:寻源报告生成” |
|
||||
| **📋 待办汇总** | 日常任务堆积提醒 | 蓝色背景(#e3f2fd) | “您有3个委托单待审核” |
|
||||
| **⚠️ 风险预警** | 检测到异常或风险 | 红色背景(#ffebee) | “⚠️ 供应商不足3家,需二次寻源” |
|
||||
| **💡 操作建议** | 下一步最佳操作 | 灰色背景(#f5f5f5) | “💡 建议优先处理即将截止的委托” |
|
||||
提示示例:
|
||||
您当前有3项待确认委托,一个立项待审批,防腐保温工程最紧急,点击*这里*(链接)进行处理。
|
||||
|
||||
#### 3.5.3 消息推送规则
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[定时扫描待办任务<br>(每30秒)] --> B{检测到待办?}
|
||||
B -->|无| C[显示“暂无待办,继续保持!”]
|
||||
B -->|有| D[按优先级排序]
|
||||
D --> E{优先级判定}
|
||||
E -->|截止时间<24h| F[生成“紧急提醒”]
|
||||
E -->|AI自动完成| G[生成“AI完成通知”]
|
||||
E -->|常规任务| H[生成“待办汇总”]
|
||||
E -->|异常检测| I[生成“风险预警”]
|
||||
F --> J[按时间倒序展示在悬浮窗]
|
||||
G --> J
|
||||
H --> J
|
||||
I --> J
|
||||
J --> K[最多显示4条消息]
|
||||
K --> L[用户点击“去处理”→跳转对应页面]
|
||||
```
|
||||
|
||||
#### 3.5.4 消息交互
|
||||
|
||||
| 交互 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **点击消息卡片** | 跳转至对应的任务处理页面(如点击“委托审核”→委托审核列表) |
|
||||
| **点击“去处理”** | 同点击消息卡片,跳转至对应页面 |
|
||||
| **输入框提问** | 用户可在底部输入框主动提问,小滨基于知识库+当前任务上下文回答 |
|
||||
| **消息自动消失** | 任务处理后,该消息在下次刷新时自动消失 |
|
||||
| **消息手动关闭** | 点击消息右上角“✕”可手动关闭该条消息 |
|
||||
|
||||
#### 3.5.5 状态指示器
|
||||
|
||||
- 在线状态:绿色圆点 + “在线 · 已为您分析今日待办”
|
||||
- 处理中状态(AI执行任务时):橙色圆点 + “正在分析任务...”
|
||||
- 离线/错误状态:红色圆点 + “连接异常,点击重试”
|
||||
|
||||
|
||||
## 4. 交互流程
|
||||
|
||||
### 4.1 待办任务处理完整流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[专员登录工作台] --> B[小滨自动扫描待办]
|
||||
B --> C[待办卡片显示数量]
|
||||
B --> D[小滨窗口推送优先级消息]
|
||||
D --> E{专员操作}
|
||||
E -->|点击消息卡片| F[跳转至对应任务页]
|
||||
E -->|点击待办卡片| F
|
||||
E -->|点击项目操作按钮| G[跳转至具体操作页]
|
||||
F --> H[完成任务操作]
|
||||
G --> H
|
||||
H --> I[返回工作台]
|
||||
I --> J[小滨自动刷新待办状态]
|
||||
J --> K{有待办?}
|
||||
K -->|有| L[更新待办数量+消息列表]
|
||||
K -->|无| M[显示“🎉 所有任务已处理完毕!”]
|
||||
```
|
||||
|
||||
### 4.2 小滨主动提醒触发场景
|
||||
|
||||
| 场景 | 触发时机 | 小滨动作 |
|
||||
| :--- | :--- | :--- |
|
||||
| 新委托到达 | 委托审核通过 | 推送“📋 新的委托单已到达,请确认” |
|
||||
| AI寻源完成 | 寻源任务完成 | 推送“✅ AI已完成供应商寻源,请确认短名单” |
|
||||
| 开标时间临近 | 开标前24小时 | 推送“🔥 项目XX即将开标,请确认准备工作” |
|
||||
| 有效投标人不足 | 报名截止后 | 推送“⚠️ 有效投标人不足3家,建议二次寻源” |
|
||||
| 定标待审批 | 评标完成 | 推送“📋 评标已完成,请发起定标审批” |
|
||||
| 无待办任务 | 所有任务完成 | 推送“🎉 今日所有任务已处理完毕!” |
|
||||
|
||||
### 4.3 项目列表操作交互
|
||||
|
||||
- 点击项目行任意位置 → 跳转至项目详情页。
|
||||
- 点击操作列按钮 → 跳转至具体任务处理页(如“组织评标”→评标准备页)。
|
||||
- 支持行内快速操作(如状态变更下拉)。
|
||||
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
### 5.1 待办统计 API
|
||||
```
|
||||
GET /api/v1/buyer/todo/statistics
|
||||
响应:
|
||||
{
|
||||
"pendingEntrust": 3, // 待确认委托
|
||||
"pendingSourcing": 2, // 待寻源确认
|
||||
"pendingApproval": 1, // 待立项审批
|
||||
"pendingBidOpen": 2, // 待开标/评标
|
||||
"todayTotal": 8 // 今日待办总计
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 在办项目列表
|
||||
```
|
||||
GET /api/v1/buyer/projects/ongoing
|
||||
参数:limit=5
|
||||
响应:[{
|
||||
"projectId": "BID-2026-001",
|
||||
"projectName": "年产5万吨聚丙烯项目设备采购",
|
||||
"phase": "SOURCING", // ENTRUST/SOURCING/APPROVAL/BID_OPEN/EVALUATION/FINALIZE
|
||||
"entrustDept": "技术部-李工",
|
||||
"deadline": "2026-07-15",
|
||||
"status": "ONGOING", // ONGOING/URGENT/EXPIRING
|
||||
"action": "VIEW_REPORT" // CONFIRM/EDIT/ORGANIZE/VIEW等
|
||||
}]
|
||||
```
|
||||
|
||||
### 5.3 小滨消息列表(由后端实时生成)
|
||||
```
|
||||
GET /api/v1/buyer/ai-messages
|
||||
响应:[{
|
||||
"id": "MSG_001",
|
||||
"type": "URGENT", // URGENT/AI_COMPLETE/TODO/RISK/SUGGESTION
|
||||
"title": "BID-2026-005 今日需开标",
|
||||
"content": "“2026年度防腐保温工程框架”已到开标时间,请立即组织评标",
|
||||
"action": "ORGANIZE_BID", // 对应跳转路由
|
||||
"actionLabel": "去处理",
|
||||
"createTime": "2026-07-06T09:00:00Z",
|
||||
"expireTime": "2026-07-07T09:00:00Z"
|
||||
}]
|
||||
```
|
||||
|
||||
|
||||
## 6. 业务规则
|
||||
|
||||
| ID | 规则 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| R01 | 待办看板按流程阶段分组,每个阶段独立计数 | 委托/寻源/立项/开标评标 |
|
||||
| R02 | 小滨消息最多展示4条,按优先级排序 | 紧急>风险>AI完成>待办汇总 |
|
||||
| R03 | 消息有效期24小时,超时自动归档 | 避免信息过载 |
|
||||
| R04 | 任务处理后,相关消息在下次刷新时自动消失 | 实时同步 |
|
||||
| R05 | 待办卡片数字变化时触发呼吸动画 | 吸引注意 |
|
||||
| R06 | 小滨悬浮窗默认展开,支持最小化 | 用户可控制 |
|
||||
|
||||
|
||||
## 7. 安全与性能
|
||||
|
||||
| 维度 | 措施 |
|
||||
| :--- | :--- |
|
||||
| **权限控制** | 仅招标专员可见工作台,数据按专员权限隔离 |
|
||||
| **数据实时性** | 待办统计每30秒轮询更新,WebSocket推送状态变更 |
|
||||
| **消息历史** | AI助手消息保留7天,可查询历史记录 |
|
||||
| **防打扰** | 支持关闭小滨主动推送(仅保留角标提醒) |
|
||||
@@ -0,0 +1,474 @@
|
||||
# 评标专家信息维护模块详细设计
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
评标专家信息维护模块是专家登录后管理个人档案的核心枢纽,设计目标包括:
|
||||
- **信息统一管理**:集中维护个人基本信息、专业资质、执业资格、联系方式等。
|
||||
- **财务信息独立维护**:管理银行账户信息,确保评审劳务报酬准确发放。
|
||||
- **安全自主可控**:支持密码修改、手机/邮箱更换、登录设备管理等安全操作。
|
||||
|
||||
### 1.2 适用角色
|
||||
- 已注册并通过审核的外部评标专家。
|
||||
- 集团内部专家仅适用于部分功能。
|
||||
- 不同专家根据其专业领域和职称等级,展示差异化内容(如高级职称可参评更高级别项目)。
|
||||
|
||||
### 1.3 核心功能模块
|
||||
| 模块 | 功能简述 |
|
||||
| :--- | :--- |
|
||||
| **个人资料** | 查看与修改基本信息(工作单位、从事专业、职称等),敏感字段变更需审核 |
|
||||
| **专业资质** | 上传/更新职称证书、执业资格证书,AI辅助识别与真伪校验 |
|
||||
| **银行账户** | 维护开户银行及账号(加密存储),用于劳务报酬发放 |
|
||||
| **安全设置** | 修改密码、更换手机号/邮箱、踢出设备、查看登录日志 |
|
||||
|
||||
|
||||
## 2. 页面布局总览
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 820" font-family="'Microsoft YaHei', sans-serif">
|
||||
<defs>
|
||||
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
<filter id="shadowDeep" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<linearGradient id="headerBg" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1a237e"/><stop offset="100%" stop-color="#283593"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1a237e"/><stop offset="100%" stop-color="#283593"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="1200" height="820" fill="#f5f7fa"/>
|
||||
|
||||
<!-- ===== 顶部导航栏 ===== -->
|
||||
<rect width="1200" height="64" fill="url(#headerBg)"/>
|
||||
<text x="40" y="42" font-size="22" font-weight="bold" fill="#fff">智慧招标平台</text>
|
||||
<text x="190" y="42" font-size="13" fill="rgba(255,255,255,0.8)">Smart Tendering Platform</text>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<text x="440" y="42" font-size="15" fill="rgba(255,255,255,0.8)">首页</text>
|
||||
<text x="520" y="42" font-size="15" fill="rgba(255,255,255,0.8)">招标公告</text>
|
||||
<text x="620" y="42" font-size="15" fill="rgba(255,255,255,0.8)">定标公告</text>
|
||||
<text x="720" y="42" font-size="15" fill="rgba(255,255,255,0.8)">企业新闻</text>
|
||||
|
||||
<!-- 已登录用户 -->
|
||||
<circle cx="1030" cy="32" r="16" fill="rgba(255,255,255,0.3)"/>
|
||||
<text x="1030" y="37" text-anchor="middle" font-size="12" fill="#fff">王</text>
|
||||
<text x="1060" y="37" font-size="14" fill="#fff">王建国</text>
|
||||
<text x="1110" y="37" font-size="12" fill="rgba(255,255,255,0.7)">▼</text>
|
||||
|
||||
<!-- ===== 内容区:左侧菜单 + 右侧内容 ===== -->
|
||||
|
||||
<!-- 左侧菜单栏(精简为4项) -->
|
||||
<rect x="40" y="90" width="220" height="690" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 专家头像 & 基本信息 -->
|
||||
<circle cx="150" cy="145" r="50" fill="#e8eaf6" stroke="#c5cae9" stroke-width="2"/>
|
||||
<text x="150" y="150" text-anchor="middle" font-size="28" fill="#1a237e" font-weight="bold">王</text>
|
||||
<text x="150" y="210" text-anchor="middle" font-size="16" font-weight="bold" fill="#1a2332">王建国</text>
|
||||
<text x="150" y="235" text-anchor="middle" font-size="13" fill="#4a5568">高级工程师 · 化工专业</text>
|
||||
<rect x="80" y="250" width="140" height="24" rx="12" fill="#4caf50"/>
|
||||
<text x="150" y="267" text-anchor="middle" font-size="12" fill="#fff">✅ 专家库已入库</text>
|
||||
|
||||
<line x1="60" y1="290" x2="240" y2="290" stroke="#f0f2f7" stroke-width="1"/>
|
||||
|
||||
<!-- 菜单项(仅保留4项) -->
|
||||
<rect x="60" y="310" width="180" height="40" rx="8" fill="#e8eaf6"/>
|
||||
<text x="150" y="336" text-anchor="middle" font-size="14" font-weight="bold" fill="#1a237e">👤 个人资料</text>
|
||||
|
||||
<rect x="60" y="365" width="180" height="40" rx="8" fill="#f5f5f5"/>
|
||||
<text x="150" y="391" text-anchor="middle" font-size="14" fill="#4a5568">🎓 专业资质</text>
|
||||
|
||||
<rect x="60" y="420" width="180" height="40" rx="8" fill="#f5f5f5"/>
|
||||
<text x="150" y="446" text-anchor="middle" font-size="14" fill="#4a5568">🏦 银行账户</text>
|
||||
|
||||
<rect x="60" y="475" width="180" height="40" rx="8" fill="#f5f5f5"/>
|
||||
<text x="150" y="501" text-anchor="middle" font-size="14" fill="#4a5568">🔐 安全设置</text>
|
||||
|
||||
<!-- ===== 右侧内容区 ===== -->
|
||||
<rect x="280" y="90" width="880" height="690" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<text x="310" y="130" font-size="20" font-weight="bold" fill="#1a2332">👤 个人资料</text>
|
||||
<text x="310" y="158" font-size="14" fill="#8895aa">维护您的个人信息,确保资料准确无误</text>
|
||||
|
||||
<line x1="310" y1="175" x2="1120" y2="175" stroke="#f0f2f7" stroke-width="2"/>
|
||||
|
||||
<!-- 表单区(两列布局) -->
|
||||
<!-- 第一列 -->
|
||||
<text x="310" y="215" font-size="14" fill="#4a5568">真实姓名 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="310" y="225" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="325" y="250" font-size="14" fill="#1a2332">王建国</text>
|
||||
|
||||
<text x="610" y="215" font-size="14" fill="#4a5568">性别 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="610" y="225" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="625" y="250" font-size="14" fill="#1a2332">男</text>
|
||||
|
||||
<!-- 第二行 -->
|
||||
<text x="310" y="295" font-size="14" fill="#4a5568">身份证号码 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="310" y="305" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="325" y="330" font-size="14" fill="#1a2332">110101********4477</text>
|
||||
<text x="560" y="330" font-size="12" fill="#4caf50">✅ 已实名认证</text>
|
||||
|
||||
<text x="610" y="295" font-size="14" fill="#4a5568">出生日期</text>
|
||||
<rect x="610" y="305" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="625" y="330" font-size="14" fill="#1a2332">1975-03-07</text>
|
||||
|
||||
<!-- 第三行 -->
|
||||
<text x="310" y="375" font-size="14" fill="#4a5568">工作单位 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="310" y="385" width="240" height="38" rx="6" fill="#ffffff" stroke="#e0e4ec"/>
|
||||
<text x="325" y="410" font-size="14" fill="#b0bbcc">请输入工作单位</text>
|
||||
|
||||
<text x="610" y="375" font-size="14" fill="#4a5568">现从事专业 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="610" y="385" width="240" height="38" rx="6" fill="#ffffff" stroke="#e0e4ec"/>
|
||||
<text x="625" y="410" font-size="14" fill="#b0bbcc">请输入专业名称</text>
|
||||
|
||||
<!-- 第四行 -->
|
||||
<text x="310" y="455" font-size="14" fill="#4a5568">技术职称 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="310" y="465" width="240" height="38" rx="6" fill="#ffffff" stroke="#e0e4ec"/>
|
||||
<text x="325" y="490" font-size="14" fill="#b0bbcc">请选择职称等级</text>
|
||||
|
||||
<text x="610" y="455" font-size="14" fill="#4a5568">职称专业 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="610" y="465" width="240" height="38" rx="6" fill="#ffffff" stroke="#e0e4ec"/>
|
||||
<text x="625" y="490" font-size="14" fill="#b0bbcc">请输入职称专业</text>
|
||||
|
||||
<!-- 第五行 -->
|
||||
<text x="310" y="535" font-size="14" fill="#4a5568">手机号码 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="310" y="545" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="325" y="570" font-size="14" fill="#1a2332">138****8000</text>
|
||||
<text x="560" y="570" font-size="12" fill="#4caf50">✅ 已验证</text>
|
||||
|
||||
<text x="610" y="535" font-size="14" fill="#4a5568">电子邮箱 <tspan fill="#e91e63">*</tspan></text>
|
||||
<rect x="610" y="545" width="240" height="38" rx="6" fill="#f5f6fa" stroke="#e0e4ec"/>
|
||||
<text x="625" y="570" font-size="14" fill="#1a2332">wangjg@example.com</text>
|
||||
<text x="860" y="570" font-size="12" fill="#4caf50">✅ 已验证</text>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<line x1="310" y1="620" x2="1120" y2="620" stroke="#f0f2f7" stroke-width="2"/>
|
||||
|
||||
<rect x="900" y="640" width="120" height="40" rx="8" fill="#e0e4ec"/>
|
||||
<text x="960" y="666" text-anchor="middle" font-size="15" fill="#666">取消</text>
|
||||
|
||||
<rect x="1040" y="640" width="120" height="40" rx="8" fill="url(#brandGrad)"/>
|
||||
<text x="1100" y="666" text-anchor="middle" font-size="15" font-weight="bold" fill="#fff">保存修改</text>
|
||||
|
||||
<!-- 右侧小滨 AI 助手 -->
|
||||
<rect x="840" y="660" width="220" height="105" rx="10" fill="#ffffff" stroke="#e0e4ec" filter="url(#shadow)"/>
|
||||
<circle cx="860" cy="685" r="14" fill="#1a237e"/>
|
||||
<text x="860" y="690" text-anchor="middle" font-size="10" fill="#fff" font-weight="bold">小滨</text>
|
||||
<text x="885" y="690" font-size="13" font-weight="bold" fill="#1a2332">AI 助手</text>
|
||||
<rect x="845" y="705" width="200" height="45" rx="6" fill="#e8eaf6" stroke="#c5cae9"/>
|
||||
<text x="860" y="725" font-size="11" fill="#1a237e">💡 温馨提示</text>
|
||||
<text x="860" y="742" font-size="10" fill="#4a5568">· 修改手机号/邮箱需双因子验证</text>
|
||||
<text x="860" y="752" font-size="10" fill="#4a5568">· 银行账号变更需验证登录密码</text>
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## 3. 各功能模块详细说明
|
||||
|
||||
### 3.1 左侧菜单栏
|
||||
|
||||
| 菜单项 | 图标 | 说明 | 高亮规则 |
|
||||
| :------- | :-- | :-------------------------- | :------------------- |
|
||||
| **个人资料** | 👤 | 基本信息维护(姓名、身份证、单位、职称、联系方式等) | 默认高亮 |
|
||||
| **专业资质** | 🎓 | 职称证书、执业资格证书的上传与更新,AI辅助识别 | 点击切换 |
|
||||
| **银行账户** | 🏦 | 维护开户行、户名、银行账号,用于评审劳务报酬发放 | **仅适用外部专家** |
|
||||
| **安全设置** | 🔐 | 修改密码、更换手机号/邮箱、登录设备管理、查看登录日志 | 密码修改和更换手机、邮箱仅适用外部专家。 |
|
||||
|
||||
### 3.2 个人资料(默认激活页)
|
||||
|
||||
**页面功能**:
|
||||
- 展示并允许修改专家的基本档案信息。
|
||||
- **所有字段均来自注册时提交的数据,除联系方式外,其他字段(姓名、身份证、职称等)变更需经过“AI审核”或“人工复核”**(与供应商营业执照变更逻辑一致,确保资质真实性)。
|
||||
|
||||
**字段列表**:
|
||||
|
||||
| 字段名 | 组件类型 | 是否必填 | 适用对象 | 备注 |
|
||||
| :---- | :----- | :--- | :--- | :---------------- |
|
||||
| 真实姓名 | 只读文本 | 是 | 全体 | 变更需提交证明材料人工审核 |
|
||||
| 性别 | 只读文本 | 是 | 全体 | 从身份证自动识别 |
|
||||
| 身份证号码 | 只读(脱敏) | 是 | 外部专家 | 变更需人工审核(关键标识) |
|
||||
| 出生日期 | 只读文本 | 否 | 外部专家 | 从身份证自动计算 |
|
||||
| 工作单位 | 文本输入框 | 是 | 全体 | 可修改(需人工复核) |
|
||||
| 现从事专业 | 文本输入框 | 是 | 全体 | 可修改 |
|
||||
| 技术职称 | 下拉选择 | 是 | 全体 | 高级/副高级/中级/初级 |
|
||||
| 职称专业 | 文本输入框 | 是 | 全体 | 如:化学工程与工艺 |
|
||||
| 手机号码 | 文本输入框 | 是 | 外部专家 | **修改需双因子验证(旧+新)** |
|
||||
| 电子邮箱 | 文本输入框 | 是 | 外部专家 | **修改需双因子验证(旧+新)** |
|
||||
|
||||
**关键字段变更规则**:
|
||||
|
||||
| 字段 | 变更是否需要审核 | 审核方式 |
|
||||
| :--- | :--- | :--- |
|
||||
| 姓名 | 是 | 需上传身份证正反面扫描件,AI比对+人工复核 |
|
||||
| 身份证号 | 是(阻断) | **系统唯一标识,变更走特殊流程(人工/线下)** |
|
||||
| 职称/专业 | 是 | 需上传新职称证书扫描件,AI识别+人工审核 |
|
||||
| 工作单位 | 是 | 人工审核(原单位离职风险) |
|
||||
| 手机/邮箱 | 否(即时生效) | 双因子验证通过即可 |
|
||||
|
||||
### 3.3 专业资质
|
||||
|
||||
**页面功能**:
|
||||
- 展示当前上传的职称/执业资格证书文件列表(类似供应商的资质文件管理)。
|
||||
- 支持上传新证书、重命名、删除旧证书。
|
||||
- AI 自动识别证书真伪(防篡改检测)和信息提取(证书编号、发证机关、有效期等)。
|
||||
|
||||
**文件列表示例**:
|
||||
|
||||
| 文件名称 | 上传时间 | 大小 | 状态 | 操作 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 职称证书_教授.pdf | 2026-06-15 | 2.1MB | ✅ 已认证 | 重命名 / 删除 |
|
||||
| 注册化工工程师资格证书.pdf | 2026-05-20 | 3.4MB | ⏳ 审核中 | 删除 |
|
||||
|
||||
**操作**:
|
||||
- 上传新资质文件(支持 PDF/JPG/PNG,≤20MB)。
|
||||
- 删除/重命名。
|
||||
- 点击文件名预览文件。
|
||||
|
||||
### 3.4 银行账户
|
||||
|
||||
**页面功能**:
|
||||
- 维护专家的银行账户信息,用于支付评审报酬。
|
||||
- 字段:开户银行名称、开户行网点、户名、银行账号。
|
||||
- **银行账号加密存储**(AES-256-GCM,与全局安全策略一致)。
|
||||
- 修改需验证当前登录密码(或短信验证码)防止盗用。
|
||||
|
||||
**字段列表**:
|
||||
|
||||
| 字段名 | 组件类型 | 是否必填 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 开户银行名称 | 下拉选择/文本输入 | 是 | 如:中国银行、工商银行 |
|
||||
| 开户行网点 | 文本输入框 | 是 | 如:中国银行北京分行营业部 |
|
||||
| 户名 | 只读文本 | 是 | 与专家真实姓名一致(自动关联) |
|
||||
| 银行账号 | 文本输入框(掩码) | 是 | 显示后4位,修改时需输入完整账号 |
|
||||
| 确认银行账号 | 文本输入框(掩码) | 是 | 修改时需二次输入确认 |
|
||||
|
||||
**安全要求**:
|
||||
- 银行账号仅显示后 4 位(如 `**** **** **** 8888`)。
|
||||
- 修改时需进行身份二次验证(短信验证码或当前登录密码)。
|
||||
|
||||
### 3.5 安全设置
|
||||
|
||||
**功能与供应商安全设置一致**,包括:
|
||||
|
||||
| 功能项 | 验证强度 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **修改登录密码** | 高(验证旧密码) | 强制密码复杂度;修改后清除所有会话,重新登录 |
|
||||
| **更换绑定手机号** | 最高(旧+新双因子) | 新号全局唯一,30天内限3次 |
|
||||
| **更换绑定邮箱** | 最高(旧+新双因子) | 新邮箱全局唯一,30天内限3次 |
|
||||
| **登录设备管理** | 中(登录态+二次确认) | 查看在线设备列表,支持“踢出所有设备” |
|
||||
| **登录日志查看** | 低(仅登录态) | 最近90天登录记录,异常登录自动标记(红底) |
|
||||
|
||||
|
||||
## 4. 关键业务流程
|
||||
|
||||
### 4.1 联系人信息变更(手机号/邮箱)流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[专家点击修改手机号] --> B[输入新手机号]
|
||||
B --> C[系统校验新号是否已被占用]
|
||||
C -->|已被占用| D[提示“该手机号已被其他专家绑定”]
|
||||
C -->|未被占用| E[发送验证码至旧手机]
|
||||
E --> F[专家输入旧手机验证码]
|
||||
F --> G{验证通过?}
|
||||
G -->|否| H[提示错误,可重试(5次锁定)]
|
||||
G -->|是| I[发送验证码至新手机]
|
||||
I --> J[输入新手机验证码]
|
||||
J --> K{验证通过?}
|
||||
K -->|否| L[提示错误]
|
||||
K -->|是| M[提交变更请求]
|
||||
M --> N[数据库更新手机号]
|
||||
N --> O[更新登录名(若为登录凭证)]
|
||||
O --> P[清除所有验证码缓存]
|
||||
P --> Q[记录审计日志]
|
||||
Q --> R[发送通知至新旧手机号]
|
||||
```
|
||||
|
||||
### 4.2 资质证书更新(AI审核)流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[专家上传新职称证书] --> B[文件上传至OSS]
|
||||
B --> C[调用OCR服务提取文字字段]
|
||||
C --> D[AI真实性审查(ELA/翻拍检测)]
|
||||
D -->|不通过| E[驳回:证书疑似伪造或翻拍]
|
||||
D -->|通过| F[AI提取信息:证书编号/发证机关/有效期等]
|
||||
F --> G[与数据库现有字段比对]
|
||||
G --> H{关键信息是否变更?}
|
||||
H -->|否| I[仅更新文件URL]
|
||||
H -->|是| J[记录变更差异,触发人工复核]
|
||||
J --> K[推送待办至管理员]
|
||||
K --> L[管理员审核]
|
||||
L -->|通过| M[更新主表+记录审计]
|
||||
L -->|不通过| N[驳回并通知专家]
|
||||
```
|
||||
|
||||
### 4.3 银行账户修改流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[专家点击“修改银行账户”] --> B[弹窗显示当前账户信息(脱敏)]
|
||||
B --> C[输入新开户行、新账号、确认账号]
|
||||
C --> D[点击“下一步”]
|
||||
D --> E[发送短信验证码至绑定手机]
|
||||
E --> F[输入短信验证码]
|
||||
F --> G{验证通过?}
|
||||
G -->|否| H[提示错误,可重试]
|
||||
G -->|是| I[提交变更请求]
|
||||
I --> J[数据库更新银行账号(加密存储)]
|
||||
J --> K[记录审计日志(含新旧账号)]
|
||||
K --> L[发送通知至手机/邮箱]
|
||||
```
|
||||
|
||||
### 4.4 安全设置 —— 修改密码流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 专家
|
||||
participant FE as 前端
|
||||
participant Svc as 账户服务
|
||||
participant DB as 数据库
|
||||
participant Cache as Redis
|
||||
|
||||
U->>FE: 填写旧密码+新密码
|
||||
FE->>FE: 前端校验(非空/强度/新旧不同)
|
||||
FE->>Svc: PUT /api/v1/expert/password
|
||||
Svc->>DB: 查询当前密码哈希
|
||||
DB-->>Svc: password_hash
|
||||
Svc->>Svc: 比对旧密码
|
||||
alt 旧密码错误
|
||||
Svc->>Cache: INCR fail_count
|
||||
alt fail_count >= 5
|
||||
Svc-->>FE: 403 账号已锁定30分钟
|
||||
else <5
|
||||
Svc-->>FE: 401 当前密码错误
|
||||
end
|
||||
else 旧密码正确
|
||||
Svc->>Cache: DEL fail_count
|
||||
Svc->>Svc: 新密码哈希
|
||||
Svc->>DB: UPDATE password_hash
|
||||
Svc->>Cache: DEL 所有会话
|
||||
Svc-->>FE: 200 成功,请重新登录
|
||||
FE->>FE: 清除本地Token,跳转登录页
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
### 5.1 专家信息表 `t_expert_profile`(核心字段)
|
||||
|
||||
| 字段名 | 类型 | 约束 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `expert_id` | VARCHAR(32) | PK | 专家档案编号 |
|
||||
| `user_id` | VARCHAR(32) | FK | 关联STP用户账号表 |
|
||||
| `real_name` | VARCHAR(64) | NOT NULL | 真实姓名 |
|
||||
| `id_card_no` | VARCHAR(128) | NOT NULL, UNIQUE | 身份证号(AES-256加密) |
|
||||
| `id_card_md5` | CHAR(32) | UNIQUE | MD5盲索引 |
|
||||
| `gender` | TINYINT | | 1-男,2-女 |
|
||||
| `birth_date` | DATE | | 出生日期 |
|
||||
| `company` | VARCHAR(128) | | 工作单位 |
|
||||
| `profession` | VARCHAR(64) | NOT NULL | 从事专业 |
|
||||
| `title` | VARCHAR(32) | NOT NULL | 职称等级 |
|
||||
| `title_major` | VARCHAR(64) | | 职称专业 |
|
||||
| `phone` | VARCHAR(128) | NOT NULL, UNIQUE | 手机号(加密) |
|
||||
| `phone_md5` | CHAR(32) | UNIQUE | MD5 |
|
||||
| `email` | VARCHAR(128) | NOT NULL | 邮箱 |
|
||||
| `bank_name` | VARCHAR(64) | | 开户银行 |
|
||||
| `bank_branch` | VARCHAR(128) | | 开户行网点 |
|
||||
| `bank_account` | VARCHAR(128) | | 银行账号(加密) |
|
||||
| `bank_account_md5` | CHAR(32) | | MD5 |
|
||||
| `status` | VARCHAR(20) | NOT NULL | ACTIVE/LOCKED/REJECTED |
|
||||
| `activated_at` | DATETIME | | 激活时间 |
|
||||
| `updated_at` | DATETIME | | 更新时间 |
|
||||
|
||||
### 5.2 资质证书表 `t_expert_qualification`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `id` | BIGINT | PK |
|
||||
| `expert_id` | VARCHAR(32) | FK |
|
||||
| `file_name` | VARCHAR(256) | 文件显示名称 |
|
||||
| `file_url` | VARCHAR(512) | OSS存储URL |
|
||||
| `file_size` | BIGINT | 文件大小(字节) |
|
||||
| `ocr_result` | JSON | OCR识别结果(含证书编号、发证机关、有效期) |
|
||||
| `audit_status` | VARCHAR(20) | PASS / PENDING / FAIL |
|
||||
| `audit_comment` | TEXT | 审核备注 |
|
||||
| `upload_time` | DATETIME | 上传时间 |
|
||||
| `status` | VARCHAR(20) | ACTIVE / DELETED |
|
||||
|
||||
|
||||
## 6. API 接口设计
|
||||
|
||||
### 6.1 获取专家信息
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `GET /api/v1/expert/profile` |
|
||||
| **响应** | 返回完整的专家信息(敏感字段脱敏) |
|
||||
|
||||
### 6.2 更新专家信息(普通字段)
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `PUT /api/v1/expert/profile` |
|
||||
| **请求体** | `{ "company": "新单位", "profession": "新专业" }` |
|
||||
| **限制** | 姓名、身份证号、职称不允许通过此接口修改(或触发审核) |
|
||||
|
||||
### 6.3 更新手机号/邮箱
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `PUT /api/v1/expert/contact` |
|
||||
| **请求体** | `{ "field":"phone", "newValue":"...", "oldOtpToken":"...", "newOtpToken":"..." }` |
|
||||
|
||||
### 6.4 上传资质证书
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `POST /api/v1/expert/qualification` |
|
||||
| **方法** | `multipart/form-data` |
|
||||
| **响应** | `{ "fileId":"...", "auditStatus":"PENDING" }` |
|
||||
|
||||
### 6.5 删除资质证书
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `DELETE /api/v1/expert/qualification/{fileId}` |
|
||||
|
||||
### 6.6 更新银行账户
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `PUT /api/v1/expert/bank-account` |
|
||||
| **请求体** | `{ "bankName":"...", "bankBranch":"...", "bankAccount":"...", "confirmAccount":"...", "smsCode":"..." }` |
|
||||
|
||||
### 6.7 修改密码
|
||||
| 项目 | 内容 |
|
||||
| :--- | :--- |
|
||||
| **路径** | `PUT /api/v1/expert/password` |
|
||||
| **请求体** | `{ "oldPassword":"...", "newPassword":"..." }` |
|
||||
|
||||
|
||||
## 7. 业务规则汇总
|
||||
|
||||
| ID | 规则 | 适用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| R01 | 身份证号全局唯一,不可重复注册(包括审核中状态) | 注册/信息变更 |
|
||||
| R02 | 修改手机号/邮箱必须双因子验证(旧+新),即时生效 | 联系人变更 |
|
||||
| R03 | 修改姓名、职称、身份证号必须经 AI 审核或人工复核 | 个人资料变更 |
|
||||
| R04 | 银行账号加密存储,修改需短信验证码二次确认 | 银行账户 |
|
||||
| R05 | 上传资质证书自动触发 AI 真实性审查(ELA/翻拍检测) | 专业资质 |
|
||||
| R06 | 所有信息变更均需记录审计日志(哈希链防篡改) | 全场景 |
|
||||
| R07 | 专家账户锁定后,不可参与评标抽取(需管理员解封) | 违规处理 |
|
||||
| R08 | 修改银行账号后,同步通知管理员(用于财务系统更新) | 银行账户 |
|
||||
|
||||
|
||||
## 8. 安全与合规
|
||||
|
||||
| 安全维度 | 措施 |
|
||||
| :--- | :--- |
|
||||
| **敏感信息加密** | 身份证号、手机号、银行账号使用 AES-256-GCM 字段级加密 |
|
||||
| **脱敏展示** | 身份证号 `110101********4477`,手机号 `138****8000`,银行账号 `**** **** **** 8888` |
|
||||
| **审计追溯** | 所有变更操作(含IP、时间、新旧值)写入哈希链,保留≥7年 |
|
||||
| **密码安全** | 同平台全局策略(Argon2id哈希,强度要求≥8位含大小写数字特殊字符) |
|
||||
@@ -0,0 +1,495 @@
|
||||
# STP 智慧招标平台 —— 评标专家工作台详细设计说明书(修订版)
|
||||
|
||||
| 文档名称 | 评标专家工作台详细设计 |
|
||||
| :--- | :--- |
|
||||
| **文档版本** | V2.0 |
|
||||
| **修订说明** | 待办事项分类调整为“待确认评标邀请”和“待完成评标项目”;评审任务列表增加“待确认”状态及对应操作;删除快捷操作区域;“待结算报酬”改为“累计评审费”;增加AI助手主动提醒紧急待办事项。 |
|
||||
| **编制日期** | 2026-07-02 |
|
||||
| **关联文档** | 《评委管理规定》《需求分析说明书》《专家注册模块》《开标及评标流程》 |
|
||||
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
评标专家工作台是外部/内部评标专家登录后默认进入的页面,作为其评审工作的核心枢纽。设计目标包括:
|
||||
- **任务驱动**:集中展示待确认的评标邀请、待完成的评审项目,引导专家优先处理。
|
||||
- **评审引导**:清晰列出评审任务列表,提供一键进入评审室或查看邀请详情的入口。
|
||||
- **合规强化**:突出承诺书签署、回避申报等合规操作,确保专家遵守《评委管理规定》。
|
||||
- **智能提醒**:AI助手主动推送紧急待办事项(不超过3项),提升响应效率。
|
||||
- **数据透明**:展示评审统计、累计评审费等数据,增强专家对自身贡献的感知。
|
||||
|
||||
### 1.2 适用角色
|
||||
- 已注册并激活的外部评标专家(含通过专家注册审核的专家)。
|
||||
- 集团内部评标专家(由系统同步创建)。
|
||||
|
||||
### 1.3 设计原则
|
||||
| 原则 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **任务优先** | 待办事项(邀请确认、待评标)置顶显示,数字醒目。 |
|
||||
| **流程闭环** | 从收到邀请 → 确认/回避/拒绝 → 评审 → 提交,全流程引导。 |
|
||||
| **严谨合规** | 回避申报、承诺书签署等合规操作入口显眼,避免遗漏。 |
|
||||
| **智能辅助** | AI助手主动提醒紧急待办,减少遗漏风险。 |
|
||||
|
||||
|
||||
## 2. 页面布局总览(SVG 线框图)
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 960" font-family="'Microsoft YaHei', sans-serif">
|
||||
<defs>
|
||||
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
<filter id="shadowDeep" x="-2%" y="-2%" width="104%" height="104%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<filter id="shadowBubble" x="-5%" y="-5%" width="110%" height="110%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="16" flood-color="#000" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<linearGradient id="headerBg" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#0d47a1"/>
|
||||
<stop offset="100%" stop-color="#1976d2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0d47a1"/><stop offset="100%" stop-color="#1976d2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="1200" height="960" fill="#f5f7fa"/>
|
||||
|
||||
<!-- ========== 顶部导航栏 ========== -->
|
||||
<rect width="1200" height="64" fill="url(#headerBg)"/>
|
||||
<text x="40" y="42" font-size="22" font-weight="bold" fill="#fff">智慧招标平台</text>
|
||||
<text x="190" y="42" font-size="13" fill="rgba(255,255,255,0.8)">Smart Tendering Platform</text>
|
||||
|
||||
<!-- 专家专属导航菜单 -->
|
||||
<text x="440" y="42" font-size="15" fill="#fff" font-weight="bold">工作台</text>
|
||||
<text x="530" y="42" font-size="15" fill="rgba(255,255,255,0.8)">评审任务</text>
|
||||
<text x="630" y="42" font-size="15" fill="rgba(255,255,255,0.8)">承诺书管理</text>
|
||||
<text x="750" y="42" font-size="15" fill="rgba(255,255,255,0.8)">历史评审</text>
|
||||
<text x="850" y="42" font-size="15" fill="rgba(255,255,255,0.8)">个人资料</text>
|
||||
|
||||
<!-- 已登录用户 -->
|
||||
<circle cx="1030" cy="32" r="16" fill="rgba(255,255,255,0.3)"/>
|
||||
<text x="1030" y="37" text-anchor="middle" font-size="12" fill="#fff">王</text>
|
||||
<text x="1060" y="37" font-size="14" fill="#fff">王建国</text>
|
||||
<text x="1110" y="37" font-size="12" fill="rgba(255,255,255,0.7)">▼</text>
|
||||
|
||||
<!-- ========== 左侧主内容 ========== -->
|
||||
<rect x="40" y="90" width="780" height="830" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 欢迎语 + 专家角色标签 -->
|
||||
<text x="70" y="125" font-size="20" font-weight="bold" fill="#1a2332">👋 欢迎回来,王建国</text>
|
||||
<rect x="280" y="108" width="100" height="24" rx="12" fill="#e3f2fd" stroke="#1565c0"/>
|
||||
<text x="330" y="125" text-anchor="middle" font-size="12" fill="#0d47a1">外部专家</text>
|
||||
<text x="70" y="150" font-size="14" fill="#8895aa">评审专业:化学工程 | 石油化工</text>
|
||||
|
||||
<!-- ========== 待办事项卡片 ========== -->
|
||||
<rect x="70" y="180" width="720" height="100" rx="10" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
<text x="90" y="210" font-size="15" font-weight="bold" fill="#4a5568">📋 待办事项</text>
|
||||
|
||||
<!-- 待办项1 -->
|
||||
<rect x="100" y="225" width="200" height="40" rx="8" fill="#e3f2fd" stroke="#90caf9"/>
|
||||
<text x="200" y="250" text-anchor="middle" font-size="13" fill="#0d47a1">📩 待确认评标邀请(2)</text>
|
||||
|
||||
<!-- 待办项2 -->
|
||||
<rect x="320" y="225" width="200" height="40" rx="8" fill="#fff3e0" stroke="#ffcc80"/>
|
||||
<text x="420" y="250" text-anchor="middle" font-size="13" fill="#e65100">📝 待完成评标项目(3)</text>
|
||||
|
||||
<!-- ========== 当前评审任务列表 ========== -->
|
||||
<text x="70" y="320" font-size="15" font-weight="bold" fill="#4a5568">📂 当前评审任务</text>
|
||||
<text x="70" y="342" font-size="13" fill="#8895aa">请在截止日期前完成评审,未完成将影响评审资格</text>
|
||||
|
||||
<rect x="70" y="360" width="720" height="240" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
|
||||
<!-- 表头 -->
|
||||
<rect x="70" y="360" width="720" height="36" rx="8" fill="#f0f2f7"/>
|
||||
<text x="90" y="384" font-size="13" font-weight="bold" fill="#4a5568">项目名称</text>
|
||||
<text x="260" y="384" font-size="13" font-weight="bold" fill="#4a5568">专业领域</text>
|
||||
<text x="380" y="384" font-size="13" font-weight="bold" fill="#4a5568">评审截止</text>
|
||||
<text x="500" y="384" font-size="13" font-weight="bold" fill="#4a5568">状态</text>
|
||||
<text x="630" y="384" font-size="13" font-weight="bold" fill="#4a5568">操作</text>
|
||||
|
||||
<!-- 行1 -->
|
||||
<rect x="80" y="404" width="700" height="44" rx="4" fill="#fff"/>
|
||||
<text x="90" y="430" font-size="13" fill="#1a2332">年产5万吨聚丙烯项目(设备采购)</text>
|
||||
<text x="260" y="430" font-size="13" fill="#666">化学工程</text>
|
||||
<text x="380" y="430" font-size="13" fill="#666">2026-07-20</text>
|
||||
<rect x="500" y="416" width="60" height="18" rx="4" fill="#1565c0"/>
|
||||
<text x="530" y="429" text-anchor="middle" font-size="11" fill="#fff">待确认</text>
|
||||
<rect x="620" y="410" width="80" height="30" rx="6" fill="#1565c0"/>
|
||||
<text x="660" y="430" text-anchor="middle" font-size="12" fill="#fff">查看</text>
|
||||
|
||||
<!-- 行2 -->
|
||||
<rect x="80" y="452" width="700" height="44" rx="4" fill="#fff"/>
|
||||
<text x="90" y="478" font-size="13" fill="#1a2332">数字化交付平台开发服务</text>
|
||||
<text x="260" y="478" font-size="13" fill="#666">信息技术</text>
|
||||
<text x="380" y="478" font-size="13" fill="#666">2026-07-18</text>
|
||||
<rect x="500" y="464" width="70" height="18" rx="4" fill="#ff9800"/>
|
||||
<text x="535" y="477" text-anchor="middle" font-size="11" fill="#fff">待评审</text>
|
||||
<rect x="620" y="458" width="90" height="30" rx="6" fill="#1565c0"/>
|
||||
<text x="665" y="478" text-anchor="middle" font-size="12" fill="#fff">进入评审</text>
|
||||
|
||||
<!-- 行3 -->
|
||||
<rect x="80" y="500" width="700" height="44" rx="4" fill="#fff"/>
|
||||
<text x="90" y="526" font-size="13" fill="#1a2332">催化剂原料采购项目(包2)</text>
|
||||
<text x="260" y="526" font-size="13" fill="#666">化工材料</text>
|
||||
<text x="380" y="526" font-size="13" fill="#666">2026-07-12</text>
|
||||
<rect x="500" y="512" width="70" height="18" rx="4" fill="#4caf50"/>
|
||||
<text x="535" y="525" text-anchor="middle" font-size="11" fill="#fff">评审中</text>
|
||||
<rect x="620" y="506" width="90" height="30" rx="6" fill="#1565c0"/>
|
||||
<text x="665" y="526" text-anchor="middle" font-size="12" fill="#fff">继续评审</text>
|
||||
|
||||
<!-- 行4 -->
|
||||
<rect x="80" y="548" width="700" height="44" rx="4" fill="#f5f5f5"/>
|
||||
<text x="90" y="574" font-size="13" fill="#1a2332">罐区安全改造项目</text>
|
||||
<text x="260" y="574" font-size="13" fill="#666">安全工程</text>
|
||||
<text x="380" y="574" font-size="13" fill="#666">2026-06-30</text>
|
||||
<rect x="500" y="560" width="70" height="18" rx="4" fill="#9e9e9e"/>
|
||||
<text x="535" y="573" text-anchor="middle" font-size="11" fill="#fff">已截止</text>
|
||||
<text x="650" y="574" font-size="13" fill="#9e9e9e">已超时</text>
|
||||
|
||||
<text x="700" y="620" font-size="13" fill="#1565c0" text-decoration="underline">查看全部评审任务 →</text>
|
||||
|
||||
<!-- ========== 最近评审记录 ========== -->
|
||||
<text x="70" y="660" font-size="15" font-weight="bold" fill="#4a5568">📊 最近评审记录</text>
|
||||
<rect x="70" y="675" width="720" height="80" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
|
||||
|
||||
<rect x="80" y="685" width="700" height="28" rx="4" fill="#fff"/>
|
||||
<text x="100" y="705" font-size="13" fill="#1a2332">滨化集团_办公楼装修工程(已评)</text>
|
||||
<text x="480" y="705" font-size="12" fill="#8895aa">2026-06-28</text>
|
||||
<text x="620" y="705" font-size="12" fill="#4caf50">已提交</text>
|
||||
|
||||
<rect x="80" y="717" width="700" height="28" rx="4" fill="#fff"/>
|
||||
<text x="100" y="737" font-size="13" fill="#1a2332">厂区安防监控升级项目(已评)</text>
|
||||
<text x="480" y="737" font-size="12" fill="#8895aa">2026-06-20</text>
|
||||
<text x="620" y="737" font-size="12" fill="#4caf50">已提交</text>
|
||||
|
||||
<!-- ========== 右侧边栏 ========== -->
|
||||
<!-- 专家信息概览 -->
|
||||
<rect x="840" y="90" width="320" height="220" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
|
||||
<circle cx="900" cy="135" r="32" fill="#1565c0"/>
|
||||
<text x="900" y="141" text-anchor="middle" font-size="20" fill="#fff" font-weight="bold">王</text>
|
||||
|
||||
<text x="950" y="135" font-size="16" font-weight="bold" fill="#1a2332">王建国</text>
|
||||
<text x="950" y="158" font-size="13" fill="#8895aa">专家编号:EXP_20260601_002</text>
|
||||
|
||||
<rect x="840" y="175" width="320" height="1" fill="#f0f2f7"/>
|
||||
|
||||
<text x="870" y="200" font-size="13" fill="#4a5568">👨🔬 专业领域:</text>
|
||||
<text x="970" y="200" font-size="13" fill="#1a2332">化学工程、石油化工</text>
|
||||
|
||||
<text x="870" y="225" font-size="13" fill="#4a5568">📋 职称:</text>
|
||||
<text x="940" y="225" font-size="13" fill="#1a2332">教授级高级工程师</text>
|
||||
|
||||
<text x="870" y="250" font-size="13" fill="#4a5568">🏦 收款账户:</text>
|
||||
<text x="990" y="250" font-size="13" fill="#1a2332">6217***********1234</text>
|
||||
<text x="870" y="270" font-size="12" fill="#4caf50">✅ 已验证</text>
|
||||
|
||||
<!-- 评审统计 -->
|
||||
<rect x="840" y="330" width="320" height="110" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="870" y="365" font-size="15" font-weight="bold" fill="#4a5568">📈 评审统计</text>
|
||||
|
||||
<text x="870" y="395" font-size="13" fill="#4a5568">累计评审项目:</text>
|
||||
<text x="1020" y="395" font-size="18" font-weight="bold" fill="#1565c0">24</text>
|
||||
|
||||
<text x="870" y="420" font-size="13" fill="#4a5568">本年度评审:</text>
|
||||
<text x="1000" y="420" font-size="16" font-weight="bold" fill="#0d47a1">8</text>
|
||||
|
||||
<text x="870" y="445" font-size="13" fill="#4a5568">累计评审费:</text>
|
||||
<text x="1000" y="445" font-size="16" font-weight="bold" fill="#e65100">¥23,800.00</text>
|
||||
|
||||
<!-- 通知 -->
|
||||
<rect x="840" y="460" width="320" height="200" rx="12" fill="#fff" filter="url(#shadow)"/>
|
||||
<text x="870" y="495" font-size="15" font-weight="bold" fill="#4a5568">🔔 通知</text>
|
||||
|
||||
<rect x="870" y="515" width="270" height="28" rx="4" fill="#fafafa"/>
|
||||
<circle cx="885" cy="529" r="4" fill="#1565c0"/>
|
||||
<text x="900" y="535" font-size="13" fill="#1a2332">您被抽取为“催化剂采购”项目评委</text>
|
||||
|
||||
<rect x="870" y="547" width="270" height="28" rx="4" fill="#fafafa"/>
|
||||
<circle cx="885" cy="561" r="4" fill="#1565c0"/>
|
||||
<text x="900" y="567" font-size="13" fill="#1a2332">承诺书签署提醒(即将到期)</text>
|
||||
|
||||
<rect x="870" y="579" width="270" height="28" rx="4" fill="#fafafa"/>
|
||||
<circle cx="885" cy="593" r="4" fill="#1565c0"/>
|
||||
<text x="900" y="599" font-size="13" fill="#1a2332">2026年度继续教育通知</text>
|
||||
|
||||
<rect x="870" y="611" width="270" height="28" rx="4" fill="#fafafa"/>
|
||||
<circle cx="885" cy="625" r="4" fill="#1565c0"/>
|
||||
<text x="900" y="631" font-size="13" fill="#1a2332">评审费结算通知(3月)</text>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ===== AI 助手浮动对话框(已更新为浮窗样式) ===== -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- AI助手浮动对话框 -->
|
||||
<rect x="780" y="700" width="380" height="230" rx="16" fill="#ffffff" filter="url(#shadowBubble)" stroke="#e0e4ec" stroke-width="1"/>
|
||||
|
||||
<!-- 对话框头部 -->
|
||||
<rect x="780" y="700" width="380" height="40" rx="16" fill="url(#brandGrad)"/>
|
||||
<rect x="780" y="724" width="380" height="16" fill="url(#brandGrad)"/>
|
||||
<text x="810" y="726" font-size="15" font-weight="bold" fill="#fff">🤖 小滨</text>
|
||||
<text x="1120" y="726" font-size="16" fill="rgba(255,255,255,0.8)" font-weight="bold">✕</text>
|
||||
|
||||
<!-- 对话内容区 -->
|
||||
<!-- 待办项1:高优先级 -->
|
||||
<rect x="795" y="752" width="350" height="32" rx="6" fill="#ffebee"/>
|
||||
<text x="810" y="766" font-size="12" fill="#c62828" font-weight="bold">🔴</text>
|
||||
<text x="825" y="773" font-size="13" fill="#1a2332">你收到</text>
|
||||
<text x="875" y="773" font-size="13" fill="#1565c0" font-weight="bold" text-decoration="underline">催化剂采购项目</text>
|
||||
<text x="970" y="773" font-size="13" fill="#1a2332">的评标邀请</text>
|
||||
|
||||
<!-- 待办项2:中优先级 -->
|
||||
<rect x="795" y="790" width="350" height="32" rx="6" fill="#fff8e1"/>
|
||||
<text x="810" y="806" font-size="12" fill="#f57f17" font-weight="bold">🟡</text>
|
||||
<text x="825" y="810" font-size="13" fill="#1a2332">请点击</text>
|
||||
<text x="875" y="810" font-size="13" fill="#1565c0" font-weight="bold" text-decoration="underline">这里</text>
|
||||
<text x="900" y="810" font-size="13" fill="#1a2332">查看详情,接受或拒绝或申请回避</text>
|
||||
|
||||
<!-- 待办项3:低优先级 -->
|
||||
<rect x="795" y="828" width="350" height="24" rx="6" fill="#f5f5f5"/>
|
||||
<text x="810" y="844" font-size="12" fill="#1976d2" font-weight="bold">🔵</text>
|
||||
<text x="825" y="844" font-size="12" fill="#666">催化剂项目评审截止剩余不足24小时</text>
|
||||
|
||||
<!-- 底部输入框 -->
|
||||
<line x1="795" y1="880" x2="1145" y2="880" stroke="#e0e4ec" stroke-width="1"/>
|
||||
<rect x="795" y="888" width="290" height="30" rx="6" fill="#f5f7fa" stroke="#e0e4ec" stroke-width="1"/>
|
||||
<text x="805" y="908" font-size="13" fill="#b0bbcc">输入消息...</text>
|
||||
<circle cx="1115" cy="903" r="14" fill="url(#brandGrad)"/>
|
||||
<text x="1115" y="908" text-anchor="middle" font-size="13" fill="#fff">➤</text>
|
||||
|
||||
<!-- ===== AI助手头像(对话框下方) ===== -->
|
||||
<circle cx="1110" cy="940" r="34" fill="url(#brandGrad)" filter="url(#shadowDeep)"/>
|
||||
<text x="1110" y="935" text-anchor="middle" font-size="14" fill="#fff" font-weight="bold">小滨</text>
|
||||
<text x="1110" y="952" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)">AI 助手</text>
|
||||
|
||||
<!-- 呼吸光环 -->
|
||||
<circle cx="1110" cy="940" r="42" fill="none" stroke="#1976d2" stroke-width="2" opacity="0.3">
|
||||
<animate attributeName="r" from="36" to="48" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" from="0.6" to="0" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- 对话框与AI头像连接箭头示意(小三角) -->
|
||||
<polygon points="1070,930 1085,920 1085,930" fill="#ffffff" stroke="#e0e4ec" stroke-width="0.5"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## 3. 各模块详细说明
|
||||
|
||||
### 3.1 顶部导航栏
|
||||
| 元素 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **品牌区** | 平台 Logo + 名称“智慧招标平台”,点击返回首页。 |
|
||||
| **导航菜单** | 工作台(当前高亮)、评审任务、承诺书管理、历史评审、个人资料。 |
|
||||
| **用户区** | 显示专家姓名“王建国”,点击展开下拉菜单(个人资料、安全设置、退出登录)。 |
|
||||
| **配色** | 采用蓝色系(#0d47a1 → #1976d2),与供应商的红色系形成角色区分。 |
|
||||
|
||||
### 3.2 欢迎区
|
||||
| 属性 | 说明 |
|
||||
| :------- | :-------------------------- |
|
||||
| **内容** | 欢迎语 + 专家姓名 + 职称(如“王建国 教授”) |
|
||||
| **角色标签** | “外部专家”或“内部专家”标识,圆角标签样式 |
|
||||
| **专业信息** | 显示专家的核心专业领域(如“化学工程 | 石油化工”) |
|
||||
| **数据来源** | 从专家档案中读取 |
|
||||
|
||||
### 3.3 待办事项卡片
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题** | “📋 待办事项” |
|
||||
| **待办项分类** | **待确认评标邀请**:已被抽取为评标专家,需要确认邀请并签署《评标纪律承诺书》,或申请回避,或因时间冲突拒绝邀请。点击跳转至“评审任务”列表并自动筛选“待确认”状态。<br>**待完成评标项目**:已经确认接受评标邀请,待评标的项目。点击跳转至“评审任务”列表并自动筛选“待评审”或“评审中”状态。 |
|
||||
| **数据来源** | 实时查询后端 API,统计各状态数量(待确认邀请数、待评审项目数)。 |
|
||||
| **交互** | 点击任意待办项,跳转至“评审任务”列表页并应用对应状态筛选。 |
|
||||
|
||||
### 3.4 当前评审任务列表
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题** | “📂 当前评审任务” + 辅助文字“请在截止日期前完成评审...” |
|
||||
| **表格列** | 项目名称、专业领域、评审截止日期、状态、操作 |
|
||||
| **数据范围** | 展示最近或进行中的 4 条评审任务(含待确认、待评审、评审中、已截止) |
|
||||
| **状态标签** | **待确认**(蓝色):已抽取但未确认邀请<br>**待评审**(橙色):已确认,尚未开始评分<br>**评审中**(绿色):已部分提交或已进入<br>**已截止**(灰色):已过截止日期 |
|
||||
| **操作列** | - **待确认**:显示“查看”按钮,点击跳转至**评标邀请详情页**,专家可在该页面选择接受、回避或拒绝。<br>- **待评审/评审中**:显示“进入评审”或“继续评审”按钮,点击跳转至**在线盲评/打分页面**。<br>- **已截止**:显示“已超时”或“查看”只读模式。 |
|
||||
| **查看邀请详情** | 邀请详情页包含:项目基本信息、抽取记录、回避声明选项、承诺书签署入口、接受/回避/拒绝按钮。 |
|
||||
| **查看更多** | “查看全部评审任务 →”跳转至完整评审任务列表(支持分页、按状态筛选)。 |
|
||||
|
||||
### 3.5 最近评审记录
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题** | “📊 最近评审记录” |
|
||||
| **内容** | 展示最近 2 条已完成的评审记录,方便专家快速回顾 |
|
||||
| **展示信息** | 项目名称、评审完成日期、状态(已提交) |
|
||||
| **交互** | 点击记录跳转至历史评审详情页(只读模式) |
|
||||
|
||||
### 3.6 右侧边栏
|
||||
|
||||
#### 3.6.1 专家信息概览
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **头像** | 专家姓名首字母圆形图标(蓝色背景) |
|
||||
| **姓名与编号** | 姓名 + 专家编号(如 EXP_20260601_002) |
|
||||
| **专业领域** | 展示 1~2 个主要评审专业 |
|
||||
| **职称** | 如教授级高级工程师、副教授等 |
|
||||
| **收款账户** | 脱敏显示银行账号(如 6217***********1234),状态标签“已验证” |
|
||||
| **数据来源** | 专家档案表 |
|
||||
|
||||
#### 3.6.2 评审统计
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题** | “📈 评审统计” |
|
||||
| **统计项** | 累计评审项目总数、本年度评审数量、**累计评审费**(替代原“待结算报酬”) |
|
||||
| **数据来源** | 实时统计(累计评审数、本年度评审数、已结算评审费总额) |
|
||||
| **作用** | 帮助专家了解自身贡献与累计收入,体现平台透明性 |
|
||||
|
||||
#### 3.6.3 通知
|
||||
| 属性 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **标题** | “🔔 通知” |
|
||||
| **数量** | 展示 4 条最新通知(因删除快捷操作,空间充裕) |
|
||||
| **通知类型** | 评审邀请、承诺书签署提醒、系统通知、培训通知、结算通知等 |
|
||||
| **交互** | 点击通知可标记为已读,跳转至通知详情 |
|
||||
| **数据来源** | 通知中心 API |
|
||||
|
||||
### 3.7 AI 助手主动提醒待办事项
|
||||
|
||||
| 属性 | 说明 |
|
||||
| :------- | :------------------------------------------- |
|
||||
| **触发条件** | 当专家存在**紧急待办**时,AI助手自动弹出提醒。 |
|
||||
| **内容示例** | 你收到催化剂采购项目的评标邀请,请点击`这里`查看详情,你可以接受邀请或拒绝或申请回避。 |
|
||||
| **交互** | 点周内容中的链接直接跳转到评标邀请页面。 |
|
||||
| **数量限制** | 最多展示 3 项最紧急的待办事项,按紧急程度(截止时间)排序。 |
|
||||
| **表现形式** | 浮动对话框,上方显示对话内容,下方为用户输出框。 |
|
||||
|
||||
|
||||
|
||||
## 4. 交互流程详细说明
|
||||
|
||||
### 4.1 核心操作:查看评标邀请(待确认状态)
|
||||
| 步骤 | 说明 |
|
||||
| :--- | :--- |
|
||||
| 1 | 在“当前评审任务”列表中,点击状态为“待确认”项目的“查看”按钮。 |
|
||||
| 2 | 跳转至**评标邀请详情页**,该页面展示:<br>• 项目基本信息(项目名称、采购方、评标时间、地点/线上方式)<br>• 评标工作内容简述<br>• 回避声明条款(需逐项勾选确认)<br>• 《评标纪律承诺书》预览及签署按钮<br>• 三个操作按钮:**接受邀请**、**申请回避**、**拒绝邀请** |
|
||||
| 3 | 专家选择操作:<br>• **接受邀请**:需先签署承诺书(若未签署),签署后自动确认,状态变为“待评审”。<br>• **申请回避**:填写回避原因并提交,系统推送至招标专员审核。<br>• **拒绝邀请**:填写拒绝原因(如时间冲突),系统自动从评审名单中移除。 |
|
||||
| 4 | 操作完成后,跳转回工作台,任务列表中该任务状态相应更新。 |
|
||||
|
||||
### 4.2 核心操作:进入评审
|
||||
|
||||
| 步骤 | 说明 |
|
||||
| :--- | :--- |
|
||||
| 1 | 点击“进入评审”或“继续评审”按钮(仅当状态为待评审或评审中)。 |
|
||||
| 2 | 系统二次校验:确认专家已签署当前项目的《评标纪律承诺书》,若未签署则引导补签。 |
|
||||
| 3 | 进入**在线盲评室**页面。该页面包含:<br>• 项目基本信息(脱敏展示)<br>• 已加密/脱敏的投标文件列表(技术标、商务标分开)<br>• 评分维度表(根据招标文件设定的评标办法)<br>• 打分输入框与评语输入框<br>• 提交/暂存按钮 |
|
||||
| 4 | 专家逐项打分并填写评语,可“暂存”草稿。 |
|
||||
| 5 | 全部完成后点击“提交评分”,系统校验是否漏项。 |
|
||||
| 6 | 提交后不可修改,状态变更为“已提交”,数据自动存证(哈希链)。 |
|
||||
| 7 | 跳转回工作台,评审任务列表中该任务消失或状态更新。 |
|
||||
### 4.3 通知交互
|
||||
| 步骤 | 说明 |
|
||||
| :--- | :--- |
|
||||
| 1 | 点击通知列表中的任一通知。 |
|
||||
| 2 | 通知标记为“已读”状态。 |
|
||||
| 3 | 根据不同通知类型跳转至对应详情页。 |
|
||||
|
||||
|
||||
## 5. 数据模型与 API
|
||||
|
||||
### 5.1 专家待办统计
|
||||
```
|
||||
GET /api/v1/expert/todo/count
|
||||
响应:
|
||||
{
|
||||
"pendingInvitations": 2, // 待确认评标邀请数
|
||||
"pendingReviews": 3 // 待完成评标项目数(含待评审、评审中)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 评审任务列表(含待确认状态)
|
||||
```
|
||||
GET /api/v1/expert/review-tasks?limit=4
|
||||
响应:[
|
||||
{
|
||||
"projectId": "P20260701",
|
||||
"projectName": "年产5万吨聚丙烯项目(设备采购)",
|
||||
"category": "化学工程",
|
||||
"deadline": "2026-07-20",
|
||||
"status": "PENDING_CONFIRM", // 新增状态
|
||||
"action": "VIEW_INVITATION" // 查看邀请
|
||||
},
|
||||
{
|
||||
"projectId": "P20260702",
|
||||
"projectName": "数字化交付平台开发服务",
|
||||
"category": "信息技术",
|
||||
"deadline": "2026-07-18",
|
||||
"status": "PENDING_REVIEW",
|
||||
"action": "START_REVIEW"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 5.3 评审统计
|
||||
```
|
||||
GET /api/v1/expert/statistics
|
||||
响应:
|
||||
{
|
||||
"totalReviewed": 24,
|
||||
"yearlyReviewed": 8,
|
||||
"totalFee": 23800.00 // 累计评审费(替代pendingPayment)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 紧急待办提醒
|
||||
```
|
||||
GET /api/v1/expert/urgent-todos
|
||||
响应:[
|
||||
{
|
||||
"type": "CONFIRM_INVITATION",
|
||||
"projectId": "P20260701",
|
||||
"message": "确认评标邀请(剩余12小时)",
|
||||
"deadline": "2026-07-03 09:00"
|
||||
},
|
||||
{
|
||||
"type": "SIGN_COMMITMENT",
|
||||
"message": "签署《评标纪律承诺书》",
|
||||
"deadline": "2026-07-15"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 6. 状态与空状态设计
|
||||
|
||||
### 6.1 任务状态映射
|
||||
| 评审任务状态 | 标签颜色 | 操作按钮 |
|
||||
| :----- | :---------- | :--------- |
|
||||
| 待确认 | 蓝色(#1565c0) | 查看(跳转邀请详情) |
|
||||
| 待评审 | 橙色(#ff9800) | 进入评审 |
|
||||
| 评审中 | 绿色(#4caf50) | 继续评审 |
|
||||
| 已完成 | 灰色(#9e9e9e) | 查看详情 |
|
||||
|
||||
### 6.2 空状态说明
|
||||
| 场景 | 展示内容 |
|
||||
| :--- | :--- |
|
||||
| **无待办事项** | “🎉 暂无待办,感谢您的付出!” |
|
||||
| **无当前评审任务** | “📭 当前没有待评审项目,有新任务时会第一时间通知您。” |
|
||||
| **无通知** | “暂无新通知” |
|
||||
|
||||
|
||||
## 7. 权限与角色差异
|
||||
|
||||
| 功能 | 外部专家 | 内部专家 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 查看工作台 | ✅ | ✅ | 默认页面 |
|
||||
| 查看/确认邀请 | ✅ | ✅ | 需在邀请详情页操作 |
|
||||
| 进入评审 | ✅ | ✅ | 需已确认邀请并签署承诺书 |
|
||||
| 签署承诺书 | ✅ | ✅ | 独立签署 |
|
||||
| 回避申报 | ✅ | ✅ | 可在邀请详情页或专门入口申报 |
|
||||
| 查看累计评审费 | ✅ | ✅ | 外部专家关注,内部专家也可能有 |
|
||||
|
||||
|
||||
## 8. 安全与合规要点
|
||||
|
||||
- **评审盲评**:进入评审室前系统强制校验承诺书签署状态,未签署不可查看投标文件。
|
||||
- **数据脱敏**:专家手机号、银行账号在侧边栏中脱敏展示。
|
||||
- **操作存证**:每次评分提交、承诺书签署、回避申报均触发哈希链存证。
|
||||
- **回避校验**:AI 自动抽取时会校验回避关系,专家在邀请确认时需人工确认。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 滨化集团智慧招标平台(STP)用户需求分析说明书
|
||||
|
||||
**版本:** V1.4
|
||||
**编制日期:** 2026年6月25日
|
||||
**版本:** V1.5
|
||||
**编制日期:** 2026年7月6日
|
||||
**编制依据:**
|
||||
- 《招标管理制度》(2026年4月版)
|
||||
- 《招标评委管理规定》(2026年4月版)
|
||||
@@ -259,7 +259,6 @@ graph TD
|
||||
```
|
||||
|
||||
|
||||
**==流程合并待确认==**
|
||||
#### 关键表单
|
||||
- **《供应商长名单》、《供应商短名单》、《报名通过单位名单》**(来自8.4.4.1流程)
|
||||
- **《评标委员会》**(BH-SCM-JL-011):在立项阶段初步确定工作领导小组。
|
||||
@@ -311,8 +310,8 @@ graph TD
|
||||
| **输出** | 审核通过的供应商名单、投标文件存证。 |
|
||||
| **角色操作** | - **供应商**:注册、报名、缴费、上传标书。<br>- **AI**:自动审核资质、验真、对账。<br>- **相关部门人员**:对报名单位业绩等进行评审。 |
|
||||
| **标书费收取** | ==是否收取标书费以及标书费的金额在招标方案制定确定。<br>供应商缴纳标书费后向平台提交转账凭证,平台发起审批向财务确认,确认后才能查看招标文件内容。== |
|
||||
| **缴纳保证金** | ==供应商缴纳保证金后,向平台提交保证金转账证据,平台发起审批向财务确认,确认后自动转为保证金缴纳状态。== |
|
||||
| **业务规则** | - 投标保证金≤预算2%,最高80万元(仅提醒)(制度5.9.3.1)。<br>- 投标文件必须加密(**==系统加密/个人加密,待确认==**),开标前不可解密。 |
|
||||
| **缴纳保证金** | ==供应商缴纳保证金后,向平台提交保证金转账证据,财务系统API-推送至STP,确认后自动转为保证金缴纳状态。== |
|
||||
| **业务规则** | - 投标保证金≤预算2%,最高80万元(仅提醒)(制度5.9.3.1)。<br>- 投标文件必须加密(**==由系统自动加密==**),开标前不可解密。 |
|
||||
| **异常处理** | - 缴费未到账:系统轮询支付网关 |
|
||||
| **AI赋能** | - **OCR识别**:提取资质证书关键字段。<br>- **真伪核验**:对接工商/税务API验真。<br>- **自动对账**:支付回调后更新缴费状态。<br>- **区块链存证**:存证投标文件哈希。 |
|
||||
|
||||
@@ -463,8 +462,10 @@ graph TD
|
||||
A[合同签订完成] --> B[系统自动收集全流程资料]
|
||||
B --> C[按制度5.16清单整理]
|
||||
C --> D[生成电子归档文件包]
|
||||
D --> E[推送至电子档案系统]
|
||||
E --> F[归档完成]
|
||||
D --> E1[通过OA审批转交]
|
||||
D -.->|同步| E2[提供下载]
|
||||
E1 --> F[归档完成]
|
||||
E2 -.->F
|
||||
F --> G[审计人员随时查阅]
|
||||
G --> H[BI看板实时展示]
|
||||
```
|
||||
@@ -480,7 +481,7 @@ graph TD
|
||||
| **输出** | 归档文件包、审计日志。 |
|
||||
| **角色操作** | - **系统**:自动收集、整理、推送。<br>- **审计/合规**:查询、导出。 |
|
||||
| **业务规则** | - 归档资料必须包括制度5.16列明的所有文件。 |
|
||||
| **归档系统** | 紫光档案管理系统 |
|
||||
| **归档方式** | ==在stp上面打包成压缩包,全流程快照。<br>人工下载之后保存留档 & 通过OA转交给他人<br>(两个功能都要有,用户任选)== |
|
||||
| **异常处理** | - 资料缺失:系统提示缺失项,要求补传。 |
|
||||
| **AI赋能** | - **智能分类**:自动分类并按规范命名。<br>- **缺失检测**:对比归档清单,自动识别缺失文件。<br>- **审计辅助**:支持自然语言查询(如“查询2025年所有围标疑似项目”)。 |
|
||||
|
||||
@@ -752,12 +753,14 @@ Agent 在调用任何业务数据前,必须通过统一数据网关验证请
|
||||
|
||||
## 第六章 待确定事项汇总
|
||||
|
||||
* **委托自动合并功能**(同类型多立项合并)暂缓落地,根据业务使用频次确定是否开发。
|
||||
* **投标文件加密方案**
|
||||
* **委托自动合并功能(已确定)**(同类型多立项合并)**暂缓落地**,根据业务使用频次确定是否开发。
|
||||
* **投标文件加密方案(已确定)**
|
||||
两种方案待定:
|
||||
①供应商自主设密钥,开标现场输密码解密(弊端:联系人失联无法开标);
|
||||
②系统统一加密、平台自动解密(弊端供应商不信任数据安全)。
|
||||
经商定,以方案②,系统统一加密的方式进行。
|
||||
* **标书费配置**
|
||||
**经商定,以方案②,系统统一加密的方式进行。**
|
||||
* **标书费配置(已确定)**
|
||||
1. 收费节点定于标书发放前,付费后解锁招标文件查看权限;
|
||||
2. 招标方案制定时可选择是否收取标书费;
|
||||
3. 暂时保留接口,详细过程还需要商酌。
|
||||
- **保证金退还具体流程** ==还需要财务部确定==。
|
||||
|
||||
|
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",
|
||||
@@ -5667,22 +5715,6 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,12 @@ import { RoleProvider } from "./role-context";
|
||||
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";
|
||||
@@ -11,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";
|
||||
@@ -27,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() {
|
||||
@@ -39,6 +38,12 @@ export default function App() {
|
||||
<BrowserRouter>
|
||||
<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 />} />
|
||||
@@ -49,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 />} />
|
||||
{/* 评标专家 */}
|
||||
@@ -75,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,158 +1,499 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Sparkles, X, Send, Bot, Lock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
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";
|
||||
|
||||
interface Proactive {
|
||||
type: ProactiveType;
|
||||
title: string;
|
||||
content: string;
|
||||
to?: string;
|
||||
actionLabel?: string;
|
||||
}
|
||||
|
||||
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-[#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: "💡" },
|
||||
};
|
||||
|
||||
// 各角色的主动提醒(最多 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/P2026005/sourcing", actionLabel: "查看结果" },
|
||||
{ type: "todo", title: "您有 3 个委托单待受理", content: "来自采购需求人员的委托申请已提交,请及时受理并立项。", to: "/app/projects", 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: "您有一个「保温材料招标项目」的评标邀请,可以选择接受或拒绝,点击查看详情。", to: "/app/expert-tasks", actionLabel: "查看详情" },
|
||||
{ type: "todo", title: "《评标纪律承诺书》待签署", content: "进入评审前需先签署当前项目的评标纪律承诺书。", to: "/app/expert-tasks", 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 个项目触发异常上报(有效投标人不足 3 家),需领导小组决策。", to: "/app/exceptions", actionLabel: "去处理" },
|
||||
],
|
||||
auditor: [
|
||||
{ type: "risk", title: "围串标风险提示", content: "AI 检测到 1 组投标文件相似度偏高,建议核查。", to: "/app/collusion", actionLabel: "去核查" },
|
||||
],
|
||||
};
|
||||
|
||||
const quickReplies = [
|
||||
"如何发起招标委托?",
|
||||
"邀请招标和公开招标有什么区别?",
|
||||
"帮我统计本年度项目预算总和",
|
||||
"在线评标怎么打分?",
|
||||
"供应商如何入库?",
|
||||
"公开招标和邀请招标的区别?",
|
||||
];
|
||||
|
||||
const knowledge: Record<string, string> = {
|
||||
委托:
|
||||
"发起招标委托很简单:① 点击工作台或项目管理页的「发起委托」按钮;② 在「填写需求」步骤用自然语言描述需求,点击「AI 智能解析」即可自动生成《招标委托表》;③ 确认预填单后提交审批即可。需要我带您过去吗?",
|
||||
区别:
|
||||
"公开招标面向所有符合条件的供应商发布寻源公告,参与方需报名审核;邀请招标则由您从长名单中筛选短名单进行定向邀请。两者在本平台均支持全流程线上闭环。",
|
||||
评标:
|
||||
"进入「在线评标室」后:左栏选择匿名投标人,中栏查看脱敏标书,右栏按技术分/商务分/价格分录入分数并填写评分说明,系统会同步展示 AI 雷同检测与价格合理性分析,最后点击「提交评分」。",
|
||||
入库:
|
||||
"在「供应商管理」页点击「发起入库流程」,或在短名单管理中对供应商点击「发起入库」,系统将自动同步工商风险信息并进入审批。",
|
||||
合并:
|
||||
"您好,当系统中存在同类型未完成立项的委托时,系统会自动推送合并建议。您可以在立项阶段的「合并招标建议」卡片中查看并确认合并。需要我为您演示具体步骤吗?",
|
||||
上传:
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,只有您本人可查看和修改,投标截止前还可撤回重传。",
|
||||
};
|
||||
interface Knowledge {
|
||||
text: string;
|
||||
actions?: MsgAction[];
|
||||
}
|
||||
|
||||
const knowledge: { key: string; ans: Knowledge }[] = [
|
||||
{
|
||||
key: "委托",
|
||||
ans: {
|
||||
text:
|
||||
"发起招标委托很简单:①点击工作台或项目管理页的「发起委托」按钮;②在「填写需求」步骤用自然语言描述需求,点击「AI 智能解析」即可自动生成《招标委托书》;③确认预填单后提交审批即可。",
|
||||
actions: [{ label: "现在去发起委托", to: "/app/delegation" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "统计",
|
||||
ans: {
|
||||
text: "据统计,本年度一共有 13 个项目,预算总和为 12,348 万元。",
|
||||
actions: [{ label: "查看项目列表", to: "/app/projects" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "预算",
|
||||
ans: {
|
||||
text: "据统计,本年度一共有 13 个项目,预算总和为 12,348 万元。",
|
||||
actions: [{ label: "查看项目列表", to: "/app/projects" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "区别",
|
||||
ans: {
|
||||
text:
|
||||
"公开招标面向所有符合条件的供应商发布寻源公告,参与方需报名审核;邀请招标则由您从长名单中筛选短名单进行定向邀请。招标方式在需求委托阶段即已确定,两者均支持全流程线上闭环。",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "评标",
|
||||
ans: {
|
||||
text:
|
||||
"进入「在线评标室」后:左栏选择匿名投标人,中栏查看脱敏标书,右栏按技术分/商务分/价格分录入分数并填写评分说明,系统会同步展示 AI 雷同检测与价格合理性分析,最后点击「提交评分」。",
|
||||
actions: [{ label: "进入在线评标室", to: "/app/projects/P2026001/evaluation" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "入库",
|
||||
ans: {
|
||||
text:
|
||||
"供应商在「供应商管理」中提交入库申请,AI 会自动核验工商与风险信息;审核通过后系统将自动入库,无需单独操作。",
|
||||
actions: [{ label: "前往供应商管理", to: "/app/suppliers" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "上传",
|
||||
ans: {
|
||||
text:
|
||||
"进入「投标中心」→ 选择项目 → 在「上传投标文件」区域上传文件,系统会自动加密并上链存证。开标前任何人都无法查看,投标截止前还可撤回重传。",
|
||||
actions: [{ label: "进入投标中心", to: "/app/bidding" }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dataKeywords = ["报价", "投标价", "多少钱", "金额", "中标", "得分", "分数", "名单", "谁"];
|
||||
|
||||
export function XiaobinAssistant() {
|
||||
const { sensitive } = useRole();
|
||||
const [open, setOpen] = useState(false);
|
||||
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(pageType === "app");
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
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(() => {
|
||||
if (pageType === "app") setProactive(PROACTIVE[roleId] ?? []);
|
||||
}, [roleId, pageType]);
|
||||
|
||||
useEffect(() => {
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, open]);
|
||||
if (showChat) endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, showChat]);
|
||||
|
||||
function answer(q: string) {
|
||||
// 流程敏感期:拒答数据类问题
|
||||
if (sensitive && dataKeywords.some((k) => q.includes(k))) {
|
||||
return "当前为教学模式(流程敏感期),数据类问题暂无法回答。暂未到查看时间,请等待系统通知。我可以继续为您讲解系统操作哦~";
|
||||
// 初始位置(右下角)
|
||||
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);
|
||||
}
|
||||
const key = Object.keys(knowledge).find((k) => q.includes(k));
|
||||
return key
|
||||
? knowledge[key]
|
||||
: "这是一个很好的问题。作为操作助手,我建议您查阅对应模块的帮助说明,或描述更具体的操作场景,我会尽力为您讲解系统使用方法。(温馨提示:业务决策需由有权限的人员完成)";
|
||||
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): Knowledge {
|
||||
if (sensitive && dataKeywords.some((k) => q.includes(k))) {
|
||||
return {
|
||||
text: "当前为流程敏感期(如开标前),数据类问题暂无法回答。请等待系统通知。我可以继续为您讲解系统操作哦~",
|
||||
};
|
||||
}
|
||||
const hit = knowledge.find((k) => q.includes(k.key));
|
||||
return (
|
||||
hit?.ans ?? {
|
||||
text:
|
||||
"这是一个很好的问题。作为操作助手,我建议您查阅对应模块的帮助说明,或描述更具体的操作场景,我会尽力为您讲解系统使用方法。(温馨提示:业务决策需由有权限的人员完成)",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function send(text: string) {
|
||||
const q = text.trim();
|
||||
if (!q) return;
|
||||
setShowChat(true);
|
||||
setMsgs((m) => [...m, { role: "user", text: q }]);
|
||||
setInput("");
|
||||
setTimeout(() => {
|
||||
setMsgs((m) => [...m, { role: "bot", text: answer(q) }]);
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full bg-gradient-to-r from-violet-500 to-blue-600 px-4 py-3 text-white shadow-lg shadow-blue-500/30 transition-transform hover:scale-105"
|
||||
>
|
||||
<Sparkles className="size-5" />
|
||||
<span className="text-sm font-medium">小滨助手</span>
|
||||
</button>
|
||||
<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",
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex h-[540px] w-[380px] flex-col overflow-hidden rounded-2xl border bg-card shadow-2xl">
|
||||
<div className="flex items-center justify-between bg-gradient-to-r from-violet-500 to-blue-600 px-4 py-3 text-white">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-9 items-center justify-center rounded-full bg-white/20">
|
||||
<Bot className="size-5" />
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">智能助手 · 小滨</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/80">
|
||||
<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">
|
||||
<X 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>
|
||||
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 && (
|
||||
<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="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-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>
|
||||
)}
|
||||
|
||||
<div className="flex-1 space-y-3 overflow-y-auto bg-muted/30 p-4">
|
||||
{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>
|
||||
))}
|
||||
{msgs.length <= 1 && (
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="text-xs text-muted-foreground">猜你想问:</p>
|
||||
{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 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-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Button size="icon" onClick={() => send(input)} className="size-9 shrink-0">
|
||||
<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">
|
||||
全流程线上闭环,事找人主动式智能体验
|
||||
|
||||
@@ -8,7 +8,7 @@ export function AiBadge({ label = "AI 生成", className }: { label?: string; cl
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md bg-gradient-to-r from-violet-500 to-blue-500 px-2 py-0.5 text-xs font-medium text-white",
|
||||
"inline-flex items-center gap-1 rounded-md bg-brand-gradient px-2 py-0.5 text-xs font-medium text-white",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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-violet-200 bg-gradient-to-br from-violet-50 to-blue-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}>
|
||||
@@ -165,9 +224,9 @@ export function Delegation() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 bg-gradient-to-br from-violet-50 to-blue-50 p-6">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 步骤二:确认预填单 */}
|
||||
|
||||
@@ -106,7 +106,7 @@ function Detail({ item, onBack }: { item: Exception; onBack: () => void }) {
|
||||
<Card className="gap-0 p-6">
|
||||
<h3 className="mb-2 text-foreground">问题描述</h3>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{item.desc}</p>
|
||||
<div className="mt-4 rounded-lg border border-violet-200 bg-gradient-to-br from-violet-50 to-blue-50 p-4">
|
||||
<div className="mt-4 rounded-lg border border-primary/20 bg-secondary/50 p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-4 text-violet-500" /> AI 分析摘要
|
||||
</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,119 +1,199 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, Link } from "react-router";
|
||||
import { Hexagon, User, Lock, ShieldCheck, Sparkles, ArrowRight } 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";
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [mode, setMode] = useState<Mode>("vendor");
|
||||
const [tab, setTab] = useState<Tab>("account");
|
||||
const [account, setAccount] = useState("zhangwei");
|
||||
const [pwd, setPwd] = useState("123456");
|
||||
const [showPwd, setShowPwd] = useState(false);
|
||||
const [phone, setPhone] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [remember, setRemember] = useState(true);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
|
||||
function sendCode() {
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
toast.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
toast.success("验证码已发送,请注意查收");
|
||||
setCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((c) => {
|
||||
if (c <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return c - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (mode === "employee") {
|
||||
toast.info("正在跳转至滨化集团 IAM 统一身份认证…");
|
||||
setTimeout(() => navigate("/app"), 800);
|
||||
return;
|
||||
}
|
||||
navigate("/app");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-slate-50">
|
||||
{/* 左侧品牌区 */}
|
||||
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-gradient-to-br from-[#0f1e3d] via-[#13306b] to-[#2563eb] p-12 text-white lg:flex">
|
||||
<div 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/60">Binhua Smart Tendering Platform</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="relative z-10">
|
||||
<div className="mb-4 inline-flex items-center gap-2 rounded-full bg-white/10 px-3 py-1 text-sm backdrop-blur">
|
||||
<Sparkles className="size-4" /> AI 原生 · 全流程线上闭环
|
||||
</div>
|
||||
<h1 className="text-white" style={{ fontSize: 40, lineHeight: 1.2, fontWeight: 700 }}>
|
||||
智慧招标,AI 赋能
|
||||
</h1>
|
||||
<p className="mt-4 max-w-md text-white/70">
|
||||
从需求提报到合同归档,角色化工作台与智能助手让复杂招标流程直观、高效,实现「事找人」的主动式智能体验。
|
||||
<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="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/60">{s.v}</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex border bg-[#f8fafc] p-1">
|
||||
{(
|
||||
[
|
||||
{ id: "employee" as Mode, label: "内部员工" },
|
||||
{ id: "vendor" as Mode, label: "外部账号" },
|
||||
]
|
||||
).map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => setMode(m.id)}
|
||||
className={cn(
|
||||
"flex-1 py-2 text-sm",
|
||||
mode === m.id ? "bg-primary font-medium text-white" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-white/40">© 2026 滨化集团股份有限公司 · 版权所有</div>
|
||||
|
||||
{/* 装饰 */}
|
||||
<div className="pointer-events-none absolute -right-20 top-1/4 size-72 rounded-full bg-blue-400/20 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-10 right-1/3 size-60 rounded-full bg-violet-400/20 blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* 右侧表单区 */}
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-8 lg:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hexagon className="size-7 text-primary" />
|
||||
<span className="font-semibold">滨化智慧招标平台</span>
|
||||
</div>
|
||||
<div className="mt-3 flex border-b border-border">
|
||||
{(
|
||||
[
|
||||
{ id: "account" as Tab, label: "账号登录" },
|
||||
{ id: "mobile" as Tab, label: "手机验证码" },
|
||||
]
|
||||
).map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={() => setTab(t.id)}
|
||||
className={cn(
|
||||
"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-1 bottom-0 h-0.5 bg-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="text-foreground">账号登录</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">欢迎回来,请登录您的工作账号</p>
|
||||
|
||||
<form
|
||||
className="mt-8 space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/app");
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm">账号</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
|
||||
placeholder="请输入账号"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm">密码</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="password"
|
||||
value={pwd}
|
||||
onChange={(e) => setPwd(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<ShieldCheck className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none focus:border-primary"
|
||||
placeholder="验证码"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-11 w-28 items-center justify-center rounded-lg bg-gradient-to-r from-blue-100 to-violet-100 font-mono tracking-widest text-primary select-none">
|
||||
8 K 2 7
|
||||
</div>
|
||||
</div>
|
||||
<form className="mt-4 space-y-3" onSubmit={submit}>
|
||||
{tab === "account" ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">账号 / 手机号</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
placeholder="请输入您的账号"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">密码</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type={showPwd ? "text" : "password"}
|
||||
value={pwd}
|
||||
onChange={(e) => setPwd(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-10 text-sm outline-none transition-colors focus:border-primary"
|
||||
placeholder="请输入您的密码"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPwd((s) => !s)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPwd ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">手机号</label>
|
||||
<div className="relative">
|
||||
<Smartphone className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
placeholder="请输入11位手机号"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">验证码</label>
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<ShieldCheck className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
className="h-11 w-full rounded-lg border bg-input-background pl-10 pr-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
placeholder="6 位验证码"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={sendCode}
|
||||
disabled={countdown > 0}
|
||||
className="h-11 w-28 shrink-0 rounded-lg border border-primary text-sm font-medium text-primary transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:border-border disabled:text-muted-foreground"
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s 后重发` : "发送验证码"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<label className="flex items-center gap-2 text-muted-foreground">
|
||||
<input type="checkbox" className="size-4 rounded border-border accent-[var(--primary)]" defaultChecked />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
className="size-4 rounded border-border accent-[var(--primary)]"
|
||||
/>
|
||||
记住我
|
||||
</label>
|
||||
<button type="button" className="text-primary hover:underline">
|
||||
@@ -121,19 +201,72 @@ export function LoginPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="h-11 w-full">
|
||||
登录 <ArrowRight className="size-4" />
|
||||
<Button type="submit" className="h-10 w-full text-base">
|
||||
登 录
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-muted-foreground">
|
||||
还不是平台供应商?
|
||||
<Link to="/register" className="ml-1 font-medium text-primary hover:underline">
|
||||
立即注册
|
||||
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||
还没有账号?
|
||||
{mode === "employee" ? (
|
||||
<span className="ml-1 text-muted-foreground/70">员工账号由 HR 系统统一同步</span>
|
||||
) : (
|
||||
<Link to="/register" className="ml-1 font-medium text-primary hover:underline">
|
||||
立即注册
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 border bg-[#fffbea] px-3 py-2 text-xs text-[#92400e]">
|
||||
原 SRM 注册用户,可直接使用 SRM 账号登录,无需重复注册。
|
||||
</div>
|
||||
|
||||
<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-violet-200 bg-gradient-to-r from-violet-50 to-blue-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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
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";
|
||||
|
||||
type Notice = { title: string; date: string };
|
||||
|
||||
const NOTICE_TABS = ["招标公告", "资格预审公告", "中标候选人公示", "结果公告", "终止公告"];
|
||||
const BANNERS = [
|
||||
{
|
||||
title: "廉洁过节",
|
||||
subtitle: "阳光招采 · 公开透明 · 全流程可追溯",
|
||||
style: "from-[#1f4d8f] via-[#2e6dc2] to-[#5c8fe0]",
|
||||
},
|
||||
{
|
||||
title: "上线试运行",
|
||||
subtitle: "智慧招标平台已上线,欢迎采购人、供应商、专家使用",
|
||||
style: "from-[#0d5f89] via-[#0f7da8] to-[#4ba6c9]",
|
||||
},
|
||||
{
|
||||
title: "轮播图片2",
|
||||
subtitle: "统一门户、统一身份、统一公告发布",
|
||||
style: "from-[#2f5f3f] via-[#3b8460] to-[#66a37f]",
|
||||
},
|
||||
];
|
||||
|
||||
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" },
|
||||
],
|
||||
};
|
||||
|
||||
function NoticeList({ items }: { items: Notice[] }) {
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<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 [activeTab, setActiveTab] = useState(NOTICE_TABS[0]);
|
||||
const [activeBanner, setActiveBanner] = useState(0);
|
||||
const banner = BANNERS[activeBanner];
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<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 bg-gradient-to-br from-violet-50/60 to-blue-50/60 p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-3.5 text-violet-500" /> {c.t}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{c.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
点击「生成招标方案」,小滨将基于委托单自动生成标段划分、评标办法与推荐短名单建议。
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 行动卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ActionCard
|
||||
icon={Sparkles}
|
||||
title="AI 智慧寻源"
|
||||
desc="AI 主动外发邀请与回执跟踪"
|
||||
desc="从互联网 / SRM 库 / 临时供应商池寻源"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/sourcing`)}
|
||||
/>
|
||||
<ActionCard
|
||||
@@ -155,7 +111,7 @@ export function ProjectDetail() {
|
||||
<ActionCard
|
||||
icon={Gavel}
|
||||
title="开标大厅"
|
||||
desc="自动开标、解密与哈希上链存证"
|
||||
desc="在线开标、解密与哈希上链存证"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/opening`)}
|
||||
/>
|
||||
<ActionCard
|
||||
@@ -164,18 +120,6 @@ export function ProjectDetail() {
|
||||
desc="专家脱敏评标与 AI 辅助分析"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/evaluation`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={Award}
|
||||
title="定标审批"
|
||||
desc="评标报告预览与定标决策"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/award`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={FileSignature}
|
||||
title="合同生成"
|
||||
desc="自动拟稿、差异对比与电子签章"
|
||||
onClick={() => navigate(`/app/projects/${project.id}/contract`)}
|
||||
/>
|
||||
<ActionCard
|
||||
icon={Building2}
|
||||
title="供应商库"
|
||||
@@ -184,18 +128,17 @@ export function ProjectDetail() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 招标文件 */}
|
||||
{/* 招标文件(方案与文件合一) */}
|
||||
<Card className="gap-0 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-foreground">招标文件</h3>
|
||||
{fileDone && <AiBadge label="AI 生成" />}
|
||||
</div>
|
||||
{!fileDone ? (
|
||||
<Button onClick={runFile} disabled={genFile}>
|
||||
{genFile ? <Loader2 className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
|
||||
{genFile ? "AI 生成文件中…" : "AI 生成文件"}
|
||||
{genFile ? "AI 生成中…" : "AI 生成招标文件"}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
@@ -209,21 +152,37 @@ export function ProjectDetail() {
|
||||
)}
|
||||
</div>
|
||||
{fileDone ? (
|
||||
<div className="mt-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<FileText className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{project.name}-招标文件.docx</div>
|
||||
<div className="text-xs text-muted-foreground">AI 生成 · 共 42 页 · 支持在线编辑</div>
|
||||
</div>
|
||||
<Check className="size-5 text-emerald-500" />
|
||||
<>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ t: "标段划分建议", d: "建议划分为 2 个标段:①控制系统硬件 ②系统集成与服务,便于专业化竞争。" },
|
||||
{ t: "推荐评标办法", d: "综合评估法:技术 60 分 / 商务 20 分 / 价格 20 分,技术权重较高。" },
|
||||
{ t: "推荐供应商短名单", d: "基于合格供方库与历史业绩,推荐浙江中控、和利时、西门子等 3 家。" },
|
||||
].map((c) => (
|
||||
<div key={c.t} className="rounded-xl border border-primary/15 bg-secondary/40 p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Sparkles className="size-3.5 text-primary" /> {c.t}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{c.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<FileText className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">{project.name}-招标文件.docx</div>
|
||||
<div className="text-xs text-muted-foreground">AI 生成 · 共 42 页 · 支持在线编辑</div>
|
||||
</div>
|
||||
<Check className="size-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
点击「AI 生成文件」,系统将根据招标方案自动生成招标文件初稿,支持在线预览、编辑与一键发布公告。
|
||||
点击「AI 生成招标文件」,小滨将基于委托单自动生成标段划分、评标办法、推荐短名单与完整招标文件初稿,支持在线预览、编辑与一键发布公告。
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Plus, Search, Filter, ChevronRight } from "lucide-react";
|
||||
import { Search, Filter, ChevronRight, ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, Columns3, Send } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import {
|
||||
@@ -10,66 +11,184 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../components/ui/dropdown-menu";
|
||||
import { PageHeader, StatusBadge } from "../components/shared/common";
|
||||
import { projects } from "../data/mock";
|
||||
|
||||
type ColKey = "type" | "budget" | "owner" | "dept" | "status" | "progress";
|
||||
type SortKey = "name" | "budget" | "progress";
|
||||
|
||||
const COLUMNS: { key: ColKey; label: string; sortable?: SortKey }[] = [
|
||||
{ key: "type", label: "招标方式" },
|
||||
{ key: "budget", label: "预算(万元)", sortable: "budget" },
|
||||
{ key: "owner", label: "委托人" },
|
||||
{ key: "dept", label: "委托单位" },
|
||||
{ key: "status", label: "当前阶段" },
|
||||
{ key: "progress", label: "进度", sortable: "progress" },
|
||||
];
|
||||
|
||||
export function ProjectList() {
|
||||
const navigate = useNavigate();
|
||||
const [q, setQ] = useState("");
|
||||
const [type, setType] = useState("all");
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const [owner, setOwner] = useState("");
|
||||
const [dept, setDept] = useState("");
|
||||
const [minBudget, setMinBudget] = useState("");
|
||||
const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" } | null>(null);
|
||||
const [cols, setCols] = useState<Record<ColKey, boolean>>({
|
||||
type: true,
|
||||
budget: true,
|
||||
owner: true,
|
||||
dept: true,
|
||||
status: true,
|
||||
progress: true,
|
||||
});
|
||||
|
||||
const list = projects.filter(
|
||||
(p) =>
|
||||
(type === "all" || p.type === type) &&
|
||||
(p.name.includes(q) || p.code.includes(q)),
|
||||
);
|
||||
const list = useMemo(() => {
|
||||
let r = projects.filter(
|
||||
(p) =>
|
||||
(type === "all" || p.type === type) &&
|
||||
(p.name.includes(q) || p.code.includes(q)) &&
|
||||
(!owner || p.owner.includes(owner)) &&
|
||||
(!dept || p.dept.includes(dept)) &&
|
||||
(!minBudget || p.budget >= Number(minBudget)),
|
||||
);
|
||||
if (sort) {
|
||||
r = [...r].sort((a, b) => {
|
||||
let cmp = 0;
|
||||
if (sort.key === "name") cmp = a.name.localeCompare(b.name, "zh");
|
||||
else cmp = (a[sort.key] as number) - (b[sort.key] as number);
|
||||
return sort.dir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}, [q, type, owner, dept, minBudget, sort]);
|
||||
|
||||
function toggleSort(key: SortKey) {
|
||||
setSort((s) =>
|
||||
s?.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" },
|
||||
);
|
||||
}
|
||||
|
||||
function SortIcon({ k }: { k: SortKey }) {
|
||||
if (sort?.key !== k) return <ChevronsUpDown className="size-3.5 text-muted-foreground/50" />;
|
||||
return sort.dir === "asc" ? <ArrowUp className="size-3.5 text-primary" /> : <ArrowDown className="size-3.5 text-primary" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="项目管理"
|
||||
description="管理全部招标项目,覆盖从需求委托到合同归档的全生命周期"
|
||||
description="管理全部招标项目,覆盖从需求委托到归档的全生命周期"
|
||||
actions={
|
||||
<Button onClick={() => navigate("/app/delegation")}>
|
||||
<Plus className="size-4" /> 发起委托
|
||||
<Button onClick={() => toast.success("已提交至 OA 审批流,请在 OA 中查看审批进度")}>
|
||||
<Send className="size-4" /> 提交审批
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card className="mb-4 flex-row items-center gap-3 p-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索项目名称或编号"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
<Card className="mb-4 gap-3 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索项目名称或编号"
|
||||
className="h-9 w-full rounded-lg border bg-input-background pl-9 pr-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger className="w-40">
|
||||
<Filter className="size-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部招标方式</SelectItem>
|
||||
<SelectItem value="公开招标">公开招标</SelectItem>
|
||||
<SelectItem value="邀请招标">邀请招标</SelectItem>
|
||||
<SelectItem value="竞争性谈判">竞争性谈判</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant={advanced ? "default" : "outline"} onClick={() => setAdvanced((v) => !v)}>
|
||||
<SlidersHorizontal className="size-4" /> 高级搜索
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns3 className="size-4" /> 列设置
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuLabel>显示的列</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{COLUMNS.map((c) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={c.key}
|
||||
checked={cols[c.key]}
|
||||
onCheckedChange={(v) => setCols((s) => ({ ...s, [c.key]: !!v }))}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{c.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger className="w-40">
|
||||
<Filter className="size-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部招标方式</SelectItem>
|
||||
<SelectItem value="公开招标">公开招标</SelectItem>
|
||||
<SelectItem value="邀请招标">邀请招标</SelectItem>
|
||||
<SelectItem value="竞争性谈判">竞争性谈判</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{advanced && (
|
||||
<div className="grid grid-cols-4 gap-3 rounded-lg border bg-muted/20 p-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">委托人</label>
|
||||
<input value={owner} onChange={(e) => setOwner(e.target.value)} placeholder="按委托人筛选" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">委托单位</label>
|
||||
<input value={dept} onChange={(e) => setDept(e.target.value)} placeholder="按委托单位筛选" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-muted-foreground">最低预算(万元)</label>
|
||||
<input value={minBudget} onChange={(e) => setMinBudget(e.target.value.replace(/\D/g, ""))} placeholder="如 500" className="h-9 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button variant="outline" className="h-9" onClick={() => { setOwner(""); setDept(""); setMinBudget(""); }}>
|
||||
重置条件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="gap-0 overflow-hidden p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40 text-left text-muted-foreground">
|
||||
<th className="px-5 py-3 font-medium">项目名称 / 编号</th>
|
||||
<th className="px-5 py-3 font-medium">招标方式</th>
|
||||
<th className="px-5 py-3 font-medium">预算(万元)</th>
|
||||
<th className="px-5 py-3 font-medium">负责人</th>
|
||||
<th className="px-5 py-3 font-medium">当前阶段</th>
|
||||
<th className="px-5 py-3 font-medium">进度</th>
|
||||
<th className="px-5 py-3 font-medium">
|
||||
<button onClick={() => toggleSort("name")} className="flex items-center gap-1 hover:text-foreground">
|
||||
项目名称 / 编号 <SortIcon k="name" />
|
||||
</button>
|
||||
</th>
|
||||
{COLUMNS.map((c) =>
|
||||
cols[c.key] ? (
|
||||
<th key={c.key} className="px-5 py-3 font-medium">
|
||||
{c.sortable ? (
|
||||
<button onClick={() => toggleSort(c.sortable!)} className="flex items-center gap-1 hover:text-foreground">
|
||||
{c.label} <SortIcon k={c.sortable} />
|
||||
</button>
|
||||
) : (
|
||||
c.label
|
||||
)}
|
||||
</th>
|
||||
) : null,
|
||||
)}
|
||||
<th className="px-5 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -84,20 +203,25 @@ export function ProjectList() {
|
||||
<div className="font-medium text-foreground">{p.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{p.code}</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-muted-foreground">{p.type}</td>
|
||||
<td className="px-5 py-4 font-medium text-foreground">{p.budget.toLocaleString()}</td>
|
||||
<td className="px-5 py-4 text-muted-foreground">{p.owner}</td>
|
||||
<td className="px-5 py-4">
|
||||
<StatusBadge status={p.status} />
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary" style={{ width: `${p.progress}%` }} />
|
||||
{cols.type && <td className="px-5 py-4 text-muted-foreground">{p.type}</td>}
|
||||
{cols.budget && <td className="px-5 py-4 font-medium text-foreground">{p.budget.toLocaleString()}</td>}
|
||||
{cols.owner && <td className="px-5 py-4 text-muted-foreground">{p.owner}</td>}
|
||||
{cols.dept && <td className="px-5 py-4 text-muted-foreground">{p.dept}</td>}
|
||||
{cols.status && (
|
||||
<td className="px-5 py-4">
|
||||
<StatusBadge status={p.status} />
|
||||
</td>
|
||||
)}
|
||||
{cols.progress && (
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary" style={{ width: `${p.progress}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{p.progress}%</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{p.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-5 py-4 text-right">
|
||||
<ChevronRight className="size-4 text-muted-foreground" />
|
||||
</td>
|
||||
|
||||
@@ -1,81 +1,591 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate, Link } from "react-router";
|
||||
import { Hexagon, ArrowLeft, CheckCircle2 } from "lucide-react";
|
||||
import {
|
||||
Hexagon,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
UploadCloud,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
XCircle,
|
||||
Building2,
|
||||
UserRound,
|
||||
ShieldCheck,
|
||||
X,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { cn } from "../components/ui/utils";
|
||||
|
||||
const fields = [
|
||||
{ label: "企业全称", placeholder: "请输入营业执照上的企业全称", col: 2 },
|
||||
{ label: "统一社会信用代码", placeholder: "18 位信用代码" },
|
||||
{ label: "企业类型", placeholder: "如:有限责任公司" },
|
||||
{ label: "法定代表人", placeholder: "请输入法人姓名" },
|
||||
{ label: "注册资本(万元)", placeholder: "请输入注册资本" },
|
||||
{ label: "联系人", placeholder: "请输入联系人姓名" },
|
||||
{ label: "联系电话", placeholder: "请输入手机号码" },
|
||||
{ label: "电子邮箱", placeholder: "用于接收平台通知" },
|
||||
{ label: "经营类别", placeholder: "如:自动化控制 / 环保工程" },
|
||||
{ label: "企业地址", placeholder: "请输入详细注册地址", col: 2 },
|
||||
type Registrant = "supplier" | "expert";
|
||||
type AuditState = "idle" | "auditing" | "success" | "rejected";
|
||||
|
||||
const SUPPLIER_STEPS = ["企业信息", "联系人", "提交 / 自动审核"];
|
||||
const EXPERT_STEPS = ["基本信息", "专业资质 / 收款", "提交审核"];
|
||||
|
||||
interface PwdRule {
|
||||
label: string;
|
||||
test: (v: string) => boolean;
|
||||
}
|
||||
|
||||
const PWD_RULES: PwdRule[] = [
|
||||
{ label: "长度 8~20 位", test: (v) => v.length >= 8 && v.length <= 20 },
|
||||
{ label: "包含大写字母(A-Z)与小写字母(a-z)", test: (v) => /[A-Z]/.test(v) && /[a-z]/.test(v) },
|
||||
{ label: "包含数字(0-9)与特殊字符(!@#$%^&*)", test: (v) => /\d/.test(v) && /[!@#$%^&*()_+\-=]/.test(v) },
|
||||
{ label: "禁止连续/重复字符(如 12345678、aaaa)", test: (v) => !/(.)\1{3,}/.test(v) && !/0123|1234|2345|3456|4567|5678|6789/.test(v) },
|
||||
{ label: "非常见弱口令(password、admin123 等)", test: (v) => !["password", "admin123", "12345678", "qwerty"].includes(v.toLowerCase()) },
|
||||
];
|
||||
|
||||
function Field({
|
||||
label,
|
||||
placeholder,
|
||||
required,
|
||||
hint,
|
||||
className,
|
||||
type = "text",
|
||||
readOnly,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
className?: string;
|
||||
type?: string;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
{label} {required && <span className="text-[#e91e63]">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type={type}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
"h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary",
|
||||
readOnly && "bg-muted text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepBar({ steps, current }: { steps: string[]; current: number }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-full border bg-white px-6 py-4 shadow-sm">
|
||||
{steps.map((s, i) => (
|
||||
<div key={s} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-9 items-center justify-center rounded-full text-sm font-bold transition-colors",
|
||||
i <= current ? "bg-brand-gradient text-white" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{i < current ? <CheckCircle2 className="size-5" /> : i + 1}
|
||||
</div>
|
||||
<span className={cn("mt-2 text-xs", i <= current ? "font-medium text-primary" : "text-muted-foreground")}>{s}</span>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div className={cn("mx-3 mb-6 h-0.5 w-16 rounded-full sm:w-24", i < current ? "bg-brand-gradient" : "bg-border")} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Notice({ items }: { items: string[] }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#ffcc80] bg-[#fff3e0] p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold text-[#e65100]">
|
||||
<Info className="size-4" /> 注册须知
|
||||
</div>
|
||||
<ol className="mt-2 space-y-1.5 text-xs leading-relaxed text-[#bf360c]">
|
||||
{items.map((t, i) => (
|
||||
<li key={i}>
|
||||
{i + 1}. {t}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
const [registrant, setRegistrant] = useState<Registrant>("supplier");
|
||||
const [step, setStep] = useState(0);
|
||||
const [ocr, setOcr] = useState<"none" | "scanning" | "done">("none");
|
||||
const [audit, setAudit] = useState<AuditState>("idle");
|
||||
const [products, setProducts] = useState<string[]>(["工业阀门", "管道配件"]);
|
||||
const [productInput, setProductInput] = useState("");
|
||||
const [pwd, setPwd] = useState("");
|
||||
const [confirmPwd, setConfirmPwd] = useState("");
|
||||
const [showPwd, setShowPwd] = useState(false);
|
||||
const [smsCountdown, setSmsCountdown] = useState(0);
|
||||
|
||||
const steps = registrant === "supplier" ? SUPPLIER_STEPS : EXPERT_STEPS;
|
||||
|
||||
const passedRules = useMemo(() => PWD_RULES.map((r) => r.test(pwd)), [pwd]);
|
||||
const passedCount = passedRules.filter(Boolean).length;
|
||||
const strength: 0 | 1 | 2 = passedCount >= 5 && pwd.length >= 12 ? 2 : passedCount >= 4 ? 1 : 0;
|
||||
|
||||
function switchRegistrant(r: Registrant) {
|
||||
setRegistrant(r);
|
||||
setStep(0);
|
||||
setAudit("idle");
|
||||
setOcr("none");
|
||||
}
|
||||
|
||||
function triggerOcr() {
|
||||
setOcr("scanning");
|
||||
setTimeout(() => {
|
||||
setOcr("done");
|
||||
toast.success("营业执照识别完成,已自动回填企业信息");
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function addProduct() {
|
||||
const v = productInput.trim();
|
||||
if (v && products.length < 20 && !products.includes(v)) {
|
||||
setProducts((p) => [...p, v]);
|
||||
}
|
||||
setProductInput("");
|
||||
}
|
||||
|
||||
function sendSms() {
|
||||
toast.success("验证码已发送");
|
||||
setSmsCountdown(60);
|
||||
const t = setInterval(() => {
|
||||
setSmsCountdown((c) => {
|
||||
if (c <= 1) {
|
||||
clearInterval(t);
|
||||
return 0;
|
||||
}
|
||||
return c - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function submitAudit() {
|
||||
setStep(steps.length - 1);
|
||||
setAudit("auditing");
|
||||
setTimeout(() => {
|
||||
setAudit("success");
|
||||
}, 3800);
|
||||
}
|
||||
|
||||
const isLastFormStep = step === steps.length - 2;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<div className="min-h-screen bg-[#f5f7fa]">
|
||||
<header className="flex h-16 items-center justify-between border-b bg-card px-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hexagon className="size-6 text-primary" />
|
||||
<span className="font-semibold">滨化集团智慧招标平台 · 供应商注册</span>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-8 items-center justify-center rounded-lg bg-brand-gradient">
|
||||
<Hexagon className="size-5 text-white" />
|
||||
</div>
|
||||
<span className="font-semibold">滨化集团智慧招标平台 · 新用户注册</span>
|
||||
</div>
|
||||
<Link to="/" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary">
|
||||
<ArrowLeft className="size-4" /> 返回登录
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto max-w-3xl px-6 py-10">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-foreground">供应商入驻注册</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
完善企业信息提交后,平台将自动同步工商风险信息并进入资质审核,预计 1-3 个工作日完成。
|
||||
</p>
|
||||
<div className="mx-auto max-w-5xl px-6 py-8">
|
||||
{/* 注册对象选择 */}
|
||||
<div className="mb-6 flex justify-center gap-3">
|
||||
{(
|
||||
[
|
||||
{ id: "supplier" as Registrant, label: "供应商注册", icon: Building2 },
|
||||
{ id: "expert" as Registrant, label: "外部专家注册", icon: UserRound },
|
||||
]
|
||||
).map((r) => {
|
||||
const Icon = r.icon;
|
||||
return (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => switchRegistrant(r.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full border px-5 py-2.5 text-sm font-medium transition-all",
|
||||
registrant === r.id
|
||||
? "border-transparent bg-brand-gradient text-white shadow-sm"
|
||||
: "border-border bg-white text-muted-foreground hover:border-primary hover:text-primary",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" /> {r.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-8">
|
||||
<h3 className="mb-5 text-foreground">企业基本信息</h3>
|
||||
<form
|
||||
className="grid grid-cols-2 gap-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.success("注册申请已提交,平台正在进行资质审核");
|
||||
setTimeout(() => navigate("/"), 1200);
|
||||
}}
|
||||
>
|
||||
{fields.map((f) => (
|
||||
<div key={f.label} className={f.col === 2 ? "col-span-2" : ""}>
|
||||
<label className="mb-1.5 block text-sm">{f.label}</label>
|
||||
<input
|
||||
className="h-10 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
placeholder={f.placeholder}
|
||||
<div className="mb-6 flex items-center justify-between gap-4">
|
||||
<StepBar steps={steps} current={step} />
|
||||
<div className="hidden shrink-0 items-center gap-1.5 rounded-full border border-[#4caf50] bg-[#e8f5e9] px-3 py-1.5 text-xs text-[#2e7d32] lg:flex">
|
||||
<ShieldCheck className="size-3.5" /> AI 即时审核 · 秒级通过
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ============ 供应商注册 ============ */}
|
||||
{registrant === "supplier" && (
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_300px]">
|
||||
<div className="rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">供应商注册</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">请填写企业真实信息,上传营业执照,系统将自动识别并回填</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
|
||||
{/* 营业执照上传 */}
|
||||
<div
|
||||
onClick={triggerOcr}
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary bg-[#fafbfc] py-6 transition-colors hover:bg-secondary/40"
|
||||
>
|
||||
<UploadCloud className="mb-2 size-7 text-primary" />
|
||||
<div className="text-sm font-medium text-primary">上传营业执照(JPG/PNG/PDF,≤10MB)</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">支持拖拽或点击上传,AI 将自动识别企业信息并填充表单</div>
|
||||
</div>
|
||||
|
||||
{ocr === "scanning" && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg bg-[#fff8e1] px-3 py-2 text-sm text-[#e65100]">
|
||||
<Loader2 className="size-4 animate-spin" /> 正在识别营业执照...
|
||||
</div>
|
||||
)}
|
||||
{ocr === "done" && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-[#a5d6a7] bg-[#e8f5e9] px-3 py-2 text-sm text-[#2e7d32]">
|
||||
<CheckCircle2 className="size-4" /> 已识别:营业执照信息提取成功(统一信用代码:91370100XXXXXXXXXX)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<Field label="企业全称" required placeholder="系统自动识别或手动输入" className="col-span-2" />
|
||||
<Field label="统一社会信用代码" required placeholder="18 位代码" />
|
||||
<Field label="注册资本(万元)" required placeholder="请输入数字" />
|
||||
<Field label="法定代表人" required placeholder="请输入法定代表人姓名" />
|
||||
<Field label="行业分类" required placeholder="请选择或搜索行业(GB/T 4754)" />
|
||||
<Field label="注册地址" required placeholder="请填写详细地址" className="col-span-2" />
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
经营范围 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="系统自动识别或手动输入,不超过 500 字"
|
||||
className="w-full resize-none rounded-lg border bg-input-background px-3 py-2 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
主要产品 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={productInput}
|
||||
onChange={(e) => setProductInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addProduct())}
|
||||
placeholder="输入产品名称,回车添加"
|
||||
className="h-11 flex-1 rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<Button type="button" variant="outline" className="h-11" onClick={addProduct}>
|
||||
+ 添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{products.map((p) => (
|
||||
<span
|
||||
key={p}
|
||||
className="flex items-center gap-1 rounded-full border border-primary bg-secondary px-3 py-1 text-xs text-primary"
|
||||
>
|
||||
{p}
|
||||
<button onClick={() => setProducts((ps) => ps.filter((x) => x !== p))}>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Notice
|
||||
items={[
|
||||
"请确保信息与营业执照一致",
|
||||
"统一社会信用代码将作为唯一标识",
|
||||
"提交后由 AI 即时自动审核",
|
||||
"审核通过后可参与投标报名",
|
||||
"已有 SRM 账号请直接登录",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm">营业执照 / 资质文件</label>
|
||||
<div className="flex h-28 cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30 text-sm text-muted-foreground hover:border-primary">
|
||||
<CheckCircle2 className="mb-2 size-6 text-muted-foreground/60" />
|
||||
点击上传或拖拽文件至此(支持 PDF / JPG / PNG)
|
||||
{step === 1 && (
|
||||
<div className="mx-auto max-w-2xl rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">联系人信息</h3>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="space-y-4">
|
||||
<Field label="联系人姓名" required placeholder="请输入联系人姓名" hint="必填,最大 64 字符" />
|
||||
<Field label="联系电话(手机号)" required placeholder="请输入 11 位手机号" hint="必填,格式校验,全局唯一" />
|
||||
<Field label="电子邮箱" required placeholder="请输入常用邮箱" hint="必填,格式校验,全局唯一" />
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
短信验证码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
placeholder="点击发送验证码"
|
||||
className="h-11 flex-1 rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={sendSms}
|
||||
disabled={smsCountdown > 0}
|
||||
className="h-11 w-32 shrink-0 rounded-lg bg-brand-gradient text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{smsCountdown > 0 ? `${smsCountdown}s 后重发` : "发送验证码"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">用于验证手机号真实性(注册必验)</p>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
输入密码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPwd ? "text" : "password"}
|
||||
value={pwd}
|
||||
onChange={(e) => setPwd(e.target.value)}
|
||||
placeholder="请设置登录密码"
|
||||
className="h-11 w-full rounded-lg border bg-input-background px-3 pr-10 text-sm outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPwd((s) => !s)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showPwd ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
确认密码 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type={showPwd ? "text" : "password"}
|
||||
value={confirmPwd}
|
||||
onChange={(e) => setConfirmPwd(e.target.value)}
|
||||
placeholder="请再次输入密码"
|
||||
className={cn(
|
||||
"h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none transition-colors focus:border-primary",
|
||||
confirmPwd && confirmPwd !== pwd && "border-destructive",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 密码复杂度要求 */}
|
||||
<div className="rounded-xl border bg-[#f8fafc] p-4">
|
||||
<div className="text-sm font-medium text-[#374151]">密码复杂度要求</div>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{PWD_RULES.map((r, i) => (
|
||||
<li key={r.label} className="flex items-center gap-2 text-[13px]">
|
||||
<span className={passedRules[i] ? "text-[#10b981]" : "text-[#ef4444]"}>{passedRules[i] ? "✔" : "✘"}</span>
|
||||
<span className="text-[#374151]">{r.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* 强度条 */}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground">密码强度</span>
|
||||
<div className="h-2 w-40 overflow-hidden rounded-full bg-[#e5e7eb]">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all",
|
||||
strength === 0 ? "w-1/3 bg-[#ef4444]" : strength === 1 ? "w-2/3 bg-[#f59e0b]" : "w-full bg-[#10b981]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
strength === 0 ? "text-[#ef4444]" : strength === 1 ? "text-[#f59e0b]" : "text-[#10b981]",
|
||||
)}
|
||||
>
|
||||
{strength === 0 ? "弱" : strength === 1 ? "中" : "强"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-span-2 mt-2 flex items-center gap-3">
|
||||
<Button type="submit" className="h-11 flex-1">
|
||||
提交注册申请
|
||||
{/* ============ 外部专家注册 ============ */}
|
||||
{registrant === "expert" && (
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_300px]">
|
||||
<div className="rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">外部专家注册</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">请完整填写以下信息,所有数据将受到严格加密保护</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<Field label="真实姓名" required placeholder="请输入中文姓名" />
|
||||
<Field label="性别" required placeholder="请选择" />
|
||||
<Field label="身份证号码" required placeholder="请输入 18 位身份证号" hint="🔒 将使用 AES-256 加密存储" className="col-span-2" />
|
||||
<Field label="出生日期" placeholder="由身份证自动识别" readOnly />
|
||||
<Field label="年龄" placeholder="自动计算" readOnly />
|
||||
<Field label="手机号码" required placeholder="请输入手机号" />
|
||||
<Field label="电子邮箱" required placeholder="请输入常用邮箱" />
|
||||
<Field label="工作单位" placeholder="请输入单位全称" />
|
||||
<Field label="现从事专业" required placeholder="如:化工工艺、设备管理" />
|
||||
<Field label="技术职称" required placeholder="高级 / 副高级 / 中级" />
|
||||
<Field label="职称专业" placeholder="如:化学工程" />
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">
|
||||
职称 / 资质证书扫描件 <span className="text-[#e91e63]">*</span>
|
||||
</label>
|
||||
<div className="flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary bg-[#fafbfc] py-5 hover:bg-secondary/40">
|
||||
<UploadCloud className="mb-1.5 size-6 text-primary" />
|
||||
<div className="text-sm text-primary">点击上传或拖拽文件(支持 PDF/JPG/PNG,≤20MB)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Notice
|
||||
items={[
|
||||
"所有信息须与身份证及职称证书一致",
|
||||
"滨化集团将对信息进行真实性核验",
|
||||
"入库专家须遵守《评委管理规定》",
|
||||
"至少具备中级及以上职称方可入库",
|
||||
"个人信息变更需及时在线更新",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="mx-auto max-w-2xl rounded-2xl bg-card p-8 shadow-sm">
|
||||
<h3 className="text-foreground">专业资质与收款账户</h3>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="grid grid-cols-2 gap-x-5 gap-y-4">
|
||||
<div className="col-span-2">
|
||||
<label className="mb-1.5 block text-sm text-[#4a5568]">擅长领域标签(用于智能匹配)</label>
|
||||
<input
|
||||
placeholder="如:石油化工、反应工程、分离技术,逗号分隔"
|
||||
className="h-11 w-full rounded-lg border bg-input-background px-3 text-sm outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Field label="开户银行" placeholder="如:中国银行北京分行" />
|
||||
<Field label="银行账号" placeholder="请输入收款账号" hint="🔒 将使用 AES-256 加密存储" />
|
||||
<label className="col-span-2 mt-2 flex items-start gap-2 rounded-lg border border-[#ffcc80] bg-[#fff3e0] p-3 text-xs text-[#bf360c]">
|
||||
<input type="checkbox" className="mt-0.5 size-4 accent-[var(--primary)]" />
|
||||
我已阅读并同意签署《评标纪律承诺书》,承诺客观公正评审,遵守评委管理规定。
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ============ 提交 / 自动审核态(两种注册对象共用最后一步) ============ */}
|
||||
{step === steps.length - 1 && (
|
||||
<div className="mx-auto max-w-lg">
|
||||
{audit === "auditing" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<Loader2 className="mx-auto size-14 animate-spin text-primary" />
|
||||
<div className="mt-5 text-lg font-bold text-foreground">
|
||||
{registrant === "supplier" ? "AI 正在审核企业信息..." : "已提交,AI 正在初审..."}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">预计 3~8 秒,请勿关闭页面</div>
|
||||
<div className="mt-3 text-[13px] text-muted-foreground/80">✓ 工商信息核验 ✓ 风险扫描 ✓ 唯一性检查</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audit === "success" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-[#e8f5e9]">
|
||||
<CheckCircle2 className="size-14 text-[#4caf50]" />
|
||||
</div>
|
||||
<div className="mt-5 text-2xl font-bold text-foreground">🎉 注册成功!</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{registrant === "supplier" ? "您的供应商账号已激活,正在自动登录..." : "您的专家账号已创建,请留意激活通知短信/邮件。"}
|
||||
</p>
|
||||
<div className="my-5 h-px bg-border" />
|
||||
<div className="space-y-1.5 text-sm text-muted-foreground">
|
||||
<div>
|
||||
{registrant === "supplier" ? "供应商编号" : "专家编号"}:
|
||||
<span className="font-semibold text-foreground">
|
||||
{registrant === "supplier" ? "SUP_20260709_0158" : "EXP_20260709_002"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
登录账号:<span className="font-semibold text-foreground">138****8001</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 rounded-lg border border-[#ffcc80] bg-[#fff3e0] px-4 py-2.5 text-xs text-[#e65100]">
|
||||
📌 温馨提示:参与具体项目投标时,需按招标公告要求提交资质审核材料
|
||||
</div>
|
||||
<Button className="mt-6 h-11 w-full bg-brand-gradient hover:opacity-90" onClick={() => navigate("/app")}>
|
||||
进入工作台 <ArrowRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audit === "rejected" && (
|
||||
<div className="rounded-2xl bg-card p-10 text-center shadow-sm">
|
||||
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-[#ffcdd2]">
|
||||
<XCircle className="size-14 text-[#c62828]" />
|
||||
</div>
|
||||
<div className="mt-5 text-2xl font-bold text-[#c62828]">审核未通过</div>
|
||||
<div className="mt-5 rounded-lg border bg-[#fafafa] p-4 text-left text-[13px] text-[#c62828]">
|
||||
<div>• 统一社会信用代码与工商登记信息不匹配</div>
|
||||
<div>• 企业状态为“注销”,不符合注册条件</div>
|
||||
<div className="mt-2 text-[#666]">💡 请核对信息后重新提交,如有疑问请联系客服</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-6 h-11 w-full bg-brand-gradient hover:opacity-90"
|
||||
onClick={() => {
|
||||
setAudit("idle");
|
||||
setStep(0);
|
||||
}}
|
||||
>
|
||||
修改后重新提交
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部导航按钮 */}
|
||||
{audit === "idle" && (
|
||||
<div className="mx-auto mt-6 flex max-w-5xl items-center justify-end gap-3">
|
||||
{step > 0 && (
|
||||
<Button variant="outline" className="h-11 px-6" onClick={() => setStep((s) => s - 1)}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="button" variant="outline" className="h-11" onClick={() => navigate("/")}>
|
||||
取消
|
||||
)}
|
||||
{isLastFormStep ? (
|
||||
<Button className="h-11 bg-brand-gradient px-8 hover:opacity-90" onClick={submitAudit}>
|
||||
提交注册
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<Button className="h-11 bg-brand-gradient px-8 hover:opacity-90" onClick={() => setStep((s) => s + 1)}>
|
||||
下一步 <ArrowRight className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -122,26 +128,49 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
|
||||
if (extra === "specialist") {
|
||||
return (
|
||||
<Card className="gap-0 border-violet-200 bg-gradient-to-br from-violet-50 to-blue-50 p-5">
|
||||
<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-violet-500" />
|
||||
<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-violet-600">重叠 {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>
|
||||
);
|
||||
}
|
||||
@@ -150,7 +179,7 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
return (
|
||||
<Card className="gap-0 p-5">
|
||||
<div className="mb-3 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>
|
||||
<div className="space-y-2.5">
|
||||
@@ -164,7 +193,7 @@ function RoleExtra({ extra }: { extra: string }) {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="line-clamp-1 flex-1 text-sm text-foreground">{p.name}</span>
|
||||
<span className="ml-2 shrink-0 rounded bg-violet-50 px-1.5 py-0.5 text-xs text-violet-600">
|
||||
<span className="ml-2 shrink-0 rounded bg-secondary px-1.5 py-0.5 text-xs text-primary">
|
||||
匹配 {p.match}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -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: "#2563eb",
|
||||
bannerFrom: "#0f1e3d",
|
||||
bannerTo: "#2563eb",
|
||||
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: "#f59e0b",
|
||||
bannerFrom: "#7c2d12",
|
||||
bannerTo: "#f59e0b",
|
||||
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: "#8b5cf6",
|
||||
bannerFrom: "#4c1d95",
|
||||
bannerTo: "#8b5cf6",
|
||||
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,43 +2,46 @@
|
||||
|
||||
:root {
|
||||
--font-size: 14px;
|
||||
--background: #f8fafc;
|
||||
--foreground: #1e293b;
|
||||
--background: #f3f5f8;
|
||||
--foreground: #1a2332;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1e293b;
|
||||
--card-foreground: #1a2332;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1e293b;
|
||||
--primary: #2563eb;
|
||||
--popover-foreground: #1a2332;
|
||||
--primary: #1565c0;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eff6ff;
|
||||
--secondary-foreground: #1d4ed8;
|
||||
--secondary: #eef4fb;
|
||||
--secondary-foreground: #0d47a1;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #eff6ff;
|
||||
--accent-foreground: #1d4ed8;
|
||||
--accent: #eef4fb;
|
||||
--accent-foreground: #0d47a1;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--input: transparent;
|
||||
--input-background: #f8fafc;
|
||||
--input-background: #f9fafb;
|
||||
--switch-background: #cbd5e1;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-normal: 400;
|
||||
--ring: #2563eb;
|
||||
--chart-1: #2563eb;
|
||||
--chart-2: #0ea5e9;
|
||||
--chart-3: #14b8a6;
|
||||
--chart-4: #f59e0b;
|
||||
--chart-5: #8b5cf6;
|
||||
--radius: 0.625rem;
|
||||
--sidebar: #0f1e3d;
|
||||
--sidebar-foreground: #cbd5e1;
|
||||
--sidebar-primary: #2563eb;
|
||||
--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: #1e3a8a;
|
||||
--sidebar-accent: #16375a;
|
||||
--sidebar-accent-foreground: #ffffff;
|
||||
--sidebar-border: #1e293b;
|
||||
--sidebar-ring: #2563eb;
|
||||
--sidebar-border: #16375a;
|
||||
--sidebar-ring: #1e88e5;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -117,6 +120,8 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-brand-from: var(--brand-from);
|
||||
--color-brand-to: var(--brand-to);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -179,3 +184,61 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 平台品牌工具类与动效(统一科技蓝) ===== */
|
||||
@layer utilities {
|
||||
/* 品牌渐变背景 */
|
||||
.bg-brand-gradient {
|
||||
background-image: linear-gradient(135deg, var(--brand-from), var(--brand-to));
|
||||
}
|
||||
.text-brand-gradient {
|
||||
background-image: linear-gradient(135deg, var(--brand-from), var(--brand-to));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* AI 助手呼吸光环 */
|
||||
@keyframes brand-breathe {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.55;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.55);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.animate-breathe {
|
||||
animation: brand-breathe 2s ease-out infinite;
|
||||
}
|
||||
|
||||
/* 列表/卡片入场(自下而上淡入) */
|
||||
@keyframes brand-slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: brand-slide-up 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* 待办数字变化呼吸提醒 */
|
||||
@keyframes brand-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
.animate-pulse-soft {
|
||||
animation: brand-pulse 1s ease-in-out 2;
|
||||
}
|
||||
|
||||
|
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": "密钥漂移二次授权弹窗"
|
||||
}
|
||||
]
|
||||