shimai
2026-04-03 e4dfe9f17d64d016376872b786d2987805ee41ef
Merge branch 'test/tony.cheng/260312' of http://120.79.193.119:9090/r/~chengmw/cloud-melody-front into test/shimai.huang/260309
23 files added
5 files modified
1791 ■■■■■ changed files
document/原型_V2/解纷智能体-产品首页.jpg patch | view | raw | blame | history
openspec/changes/upgrade-homepage-layout/design.md 163 ●●●●● patch | view | raw | blame | history
openspec/changes/upgrade-homepage-layout/proposal.md 88 ●●●●● patch | view | raw | blame | history
openspec/changes/upgrade-homepage-layout/specs/homepage-layout/spec.md 166 ●●●●● patch | view | raw | blame | history
openspec/changes/upgrade-homepage-layout/tasks.md 79 ●●●●● patch | view | raw | blame | history
web-app/public/ai-bg.png patch | view | raw | blame | history
web-app/public/app_logo.png patch | view | raw | blame | history
web-app/public/in_person.png patch | view | raw | blame | history
web-app/public/join.png patch | view | raw | blame | history
web-app/public/mom.png patch | view | raw | blame | history
web-app/public/to_person.png patch | view | raw | blame | history
web-app/public/warning.png patch | view | raw | blame | history
web-app/src/App.css 88 ●●●●● patch | view | raw | blame | history
web-app/src/App.js 10 ●●●●● patch | view | raw | blame | history
web-app/src/components/common/AppHeader.css 131 ●●●●● patch | view | raw | blame | history
web-app/src/components/common/AppHeader.jsx 124 ●●●●● patch | view | raw | blame | history
web-app/src/components/common/OutboundCallWidget.jsx 32 ●●●●● patch | view | raw | blame | history
web-app/src/components/common/WarningAlert.css 127 ●●●●● patch | view | raw | blame | history
web-app/src/components/common/WarningAlert.jsx 119 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/AISuggestionCard.css 80 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/AISuggestionCard.jsx 53 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/NegotiationProgress.css 79 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/NegotiationProgress.jsx 88 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/PartyInfoCard.css 105 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/PartyInfoCard.jsx 142 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/TabContainer.jsx 84 ●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/TopSection.jsx 2 ●●● patch | view | raw | blame | history
web-app/src/services/MediationTimelineAPIService.js 31 ●●●●● patch | view | raw | blame | history
document/原型_V2/解纷智能体-产品首页.jpg
openspec/changes/upgrade-homepage-layout/design.md
New file
@@ -0,0 +1,163 @@
# Design: 首页布局升级优化
## Context
当前首页(index.html)需要按照最新原型进行全面升级,涉及多个新组件的添加和现有组件的修改。本次升级主要目标是提升调解员工作效率,通过可视化展示关键信息。
**原型参考**: `document/原型/index.html` 和用户提供的原型图片
## Goals / Non-Goals
### Goals
- 1:1还原原型图视觉效果
- 新增蓝色顶部Header,统一系统导航风格
- 提供预警消息实时提示,提升调解员响应速度
- 可视化展示申请双方信息和协商沟通进度
- 增强调解成功率数据展示,提供同比分析
### Non-Goals
- 不修改现有API调用机制,复用CaseDataContext
- 不修改底部悬浮控制面板
- "查看详细策略建议"功能暂不实现跳转
## Decisions
### 1. 组件架构
**Decision**: 新增独立组件,不侵入现有组件逻辑
```
components/
├── common/
│   ├── AppHeader.jsx          # 顶部Header(新增)
│   └── WarningAlert.jsx       # 预警提示(新增)
└── dashboard/
    ├── PartyInfoCard.jsx      # 申请双方信息(新增)
    ├── NegotiationProgress.jsx # 协商沟通(新增)
    ├── AISuggestionCard.jsx   # AI调解建议(新增)
    └── TabContainer.jsx       # 修改MediationDataBoard
```
**Rationale**: 保持单一职责,便于维护和测试
### 2. 数据流设计
**Decision**: 复用现有CaseDataContext,新增两个API调用
```
CaseDataContext
├── caseData (现有)
│   ├── mediation.success_rate
│   ├── mediation.yoy_success_rate
│   ├── mediation.yoy_before_hours
│   └── mediation.mediation_count
├── processNodes (现有)
└── 新增API调用:
    ├── getWarningNotifyList(mediationId)
    └── getPersonList(caseId)
```
**Rationale**:
- 预警消息和当事人列表数据独立于timeline,需单独API获取
- 成功率同比数据从现有timeline字段获取,无需新API
### 3. Header数据来源
**Decision**: 调解员信息从URL参数获取,使用getMergedParams工具
```javascript
// 从URL参数获取
const params = getMergedParams();
const mediatorInfo = {
  trueName: params.trueName || '调解员',
  unit: params.unit || '',
  roleName: params.roleName || '',
  avatar: params.avatar || DEFAULT_AVATAR
};
```
**Rationale**: 调解员信息由外部系统传入,保持与现有参数获取机制一致
### 4. 预警消息展示策略
**Decision**:
- 单条消息: 直接在消息条展示完整内容
- 多条消息: 展示第一条 + "还有N条预警" + "查看更多"按钮
- 点击"查看更多": 弹出Modal展示全部预警列表
### 5. 协商沟通进度计算
**Decision**:
```javascript
const currentRound = timeline.mediation?.mediation_count || 0;
const nodeCount = processNodes.length;
// 总次数计算
let totalRounds = nodeCount;
if (currentRound > nodeCount && !isLastNode) {
  totalRounds = currentRound + 1;
}
// 渲染点线块
const dots = Array(totalRounds).fill(false).map((_, i) => i < currentRound);
```
## Layout Structure
```
┌────────────────────────────────────────────────────────┐
│  AppHeader (蓝色背景)                                    │
│  [矛盾纠纷应用]                    [🔔2] [👤李X萌 XX区..]│
├────────────────────────────────────────────────────────┤
│  TopSection (现有案件信息头部)                           │
├────────────────────────────────────────────────────────┤
│  MediationProgress (调解进度条)                          │
├────────────────────────────────────────────────────────┤
│  WarningAlert (浅黄色预警提示)                           │
│  ⚠️ 预警:申请人张三情绪激动...        [还有2条] [查看更多]│
├────────────────────────────────────────────────────────┤
│  TabContainer                                          │
│  ┌─────────────────────────────────────────────────────┤
│  │ MediationDataBoard (调解分析Tab)                     │
│  │ ┌──────────────────────┬─────────────────────────┐  │
│  │ │ 诉求差距分析          │ 申请双方                 │  │
│  │ │ ┌────────────────┐   │ [👤申请人] VS [🏢被申请人]│  │
│  │ │ │ 主要分歧点...   │   │                         │  │
│  │ │ └────────────────┘   │ 预计调解成功率            │  │
│  │ │                      │ 68% ↑+8% 较3小时前       │  │
│  │ │ AI调解建议            │                         │  │
│  │ │ ┌────────────────┐   │ 协商沟通                 │  │
│  │ │ │ 建议调解员...   │   │ 第6轮                   │  │
│  │ │ │ [查看详细策略]  │   │ ●━●━●━●━●━○             │  │
│  │ │ └────────────────┘   │                         │  │
│  │ └──────────────────────┴─────────────────────────┘  │
│  └─────────────────────────────────────────────────────┤
└────────────────────────────────────────────────────────┘
```
## Risks / Trade-offs
### Risk 1: API响应延迟
- **风险**: 预警消息和当事人列表API可能响应慢
- **缓解**: 组件内部loading状态,不阻塞整体页面渲染
### Risk 2: URL参数缺失
- **风险**: 调解员信息URL参数可能为空
- **缓解**: 提供默认值和默认头像
### Risk 3: 布局兼容性
- **风险**: 新增Header可能影响现有页面滚动
- **缓解**: Header使用fixed定位,调整body padding-top
## Migration Plan
1. 先创建API服务和新组件(不影响现有功能)
2. 在App.js中添加AppHeader(影响最小)
3. 在MediationProgress下方添加WarningAlert
4. 修改MediationDataBoard布局,集成新组件
5. 调整CSS样式,验证视觉效果
## Open Questions
- [已解答] AI调解建议内容来源? → 先使用Mock数据
- [已解答] "查看详细策略建议"跳转? → 暂不跳转,提示"功能升级中"
openspec/changes/upgrade-homepage-layout/proposal.md
New file
@@ -0,0 +1,88 @@
# Change: 首页布局升级优化
## Why
当前首页缺少顶部导航Header、预警提示、申请双方信息展示、协商沟通可视化和AI调解建议等关键功能模块,需要按照最新原型图进行布局升级,提升调解员工作效率和用户体验。
## What Changes
- **新增**: 蓝色顶部Header组件,包含系统名称、通知图标(带消息数量气泡)、调解员信息
- **新增**: 预警提示消息条组件(浅黄色背景),支持多条消息查看弹窗
- **新增**: 申请双方信息组件,展示申请人/被申请人信息及情绪标签
- **修改**: 预计调解成功率组件,增加同比数据展示(如 +8% 较3小时前)
- **新增**: 协商沟通面板组件,点线式可视化展示沟通轮次
- **新增**: AI调解建议面板,展示调解建议内容和详情按钮
- **新增**: 预警消息API服务 (`/api/v1/mediation-timeline/v2/warning-notify-list/{mediation_id}`)
- **新增**: 当事人列表API服务 (`/api/v1/mediation-timeline/v2/person-list/{case_id}`)
## Impact
- **Affected specs**: homepage-layout
- **Affected code**:
  - `web-app/src/App.js` - 添加Header组件
  - `web-app/src/App.css` - 新增Header和相关组件样式
  - `web-app/src/components/dashboard/TopSection.jsx` - 添加预警提示
  - `web-app/src/components/dashboard/TabContainer.jsx` - 修改MediationDataBoard组件
  - 新增组件:
    - `web-app/src/components/common/AppHeader.jsx` - 顶部Header
    - `web-app/src/components/common/WarningAlert.jsx` - 预警提示
    - `web-app/src/components/dashboard/PartyInfoCard.jsx` - 申请双方信息
    - `web-app/src/components/dashboard/NegotiationProgress.jsx` - 协商沟通
    - `web-app/src/components/dashboard/AISuggestionCard.jsx` - AI调解建议
  - `web-app/src/services/MediationTimelineAPIService.js` - 新增API服务
## Data Sources
### 1. Header调解员信息
从URL请求参数获取:
- `trueName` - 姓名
- `unit` - 地址
- `roleName` - 角色
- `avatar` - 头像(空则使用默认头像)
### 2. 预警消息API
```
GET /api/v1/mediation-timeline/v2/warning-notify-list/{mediation_id}
Response:
{
  "code": 200,
  "data": [
    {
      "id": "1",
      "content": "申请人张三情绪激动...",
      "create_time": "2026-12-20 12:30:15",
      "level_type": "3"
    }
  ]
}
```
### 3. 当事人列表API
```
GET /api/v1/mediation-timeline/v2/person-list/{case_id}
Response:
{
  "code": 200,
  "data": [
    {
      "id": "202602261114261001",
      "per_type": "15_020008-1",
      "per_type_name": "申请方当事人",
      "true_name": "刘树杰",
      "record_id": "1001",
      "tag_name": "情绪激动",
      "tag_style": "red"
    }
  ]
}
```
### 4. 成功率同比数据
从 `CaseDataContext` 或 `localStorage.case_data_timeline` 获取:
- `mediation.yoy_success_rate` - 同比增长率
- 若为空: `(mediation.success_rate - mediation.last_success_rate) * 100`
- `mediation.yoy_before_hours` - 对比时间(小时),若为空则为0
### 5. 协商沟通轮次
- 当前轮次: `timeline.mediation?.mediation_count`
- 总次数: 默认为流程节点总数
- 当沟通次数超过总次数且未到最后节点时: 总次数 = 沟通次数 + 1
openspec/changes/upgrade-homepage-layout/specs/homepage-layout/spec.md
New file
@@ -0,0 +1,166 @@
# Homepage Layout Specification
## ADDED Requirements
### Requirement: App Header Component
系统 SHALL 在页面顶部显示蓝色背景的Header组件,包含系统名称、通知图标和调解员信息。
#### Scenario: Header displays system name
- **WHEN** 页面加载完成
- **THEN** Header左侧显示"矛盾纠纷应用"系统名称
#### Scenario: Header displays notification icon with badge
- **WHEN** 存在未读通知消息
- **THEN** Header右侧显示通知图标,图标右上角显示红色气泡,气泡内显示未读消息数量
#### Scenario: Header displays mediator info from URL params
- **WHEN** URL参数包含trueName、unit、roleName、avatar
- **THEN** Header右侧显示调解员头像、姓名、"地址|角色"格式的信息
#### Scenario: Header uses default avatar when param empty
- **WHEN** URL参数avatar为空
- **THEN** 使用系统默认头像图片
#### Scenario: Notification popup on icon click
- **WHEN** 用户点击通知图标
- **THEN** 显示通知消息列表弹窗
---
### Requirement: Warning Alert Component
系统 SHALL 在调解进度条下方显示预警提示消息条,支持多条消息查看。
#### Scenario: Single warning message display
- **WHEN** 预警消息API返回1条消息
- **THEN** 预警条直接显示完整消息内容,背景为浅黄色
#### Scenario: Multiple warning messages display
- **WHEN** 预警消息API返回多条消息
- **THEN** 预警条显示第一条消息 + "还有N条预警" + "查看更多"按钮
#### Scenario: Warning modal on view more click
- **WHEN** 用户点击"查看更多"按钮
- **THEN** 弹出Modal显示全部预警消息列表
#### Scenario: Warning API call
- **WHEN** 页面加载且mediationId存在
- **THEN** 调用 `/api/v1/mediation-timeline/v2/warning-notify-list/{mediation_id}` 获取预警数据
---
### Requirement: Party Info Card Component
系统 SHALL 在调解分析面板右侧显示申请双方信息卡片。
#### Scenario: Applicant info display
- **WHEN** 当事人列表API返回申请方当事人数据
- **THEN** 左侧显示申请人头像、姓名,上方显示情绪标签(如有)
#### Scenario: Respondent info display
- **WHEN** 当事人列表API返回被申请方当事人数据
- **THEN** 右侧显示被申请人头像、公司名称,上方显示标签(如有)
#### Scenario: VS separator display
- **WHEN** 同时存在申请人和被申请人
- **THEN** 中间显示VS分隔符图标
#### Scenario: Emotion tag style
- **WHEN** 当事人数据包含tag_name和tag_style
- **THEN** 按tag_style渲染标签颜色(red=红色等)
#### Scenario: Person list API call
- **WHEN** 页面加载且caseId存在
- **THEN** 调用 `/api/v1/mediation-timeline/v2/person-list/{case_id}` 获取当事人数据
---
### Requirement: Success Rate YoY Display
系统 SHALL 在预计调解成功率组件中显示同比数据。
#### Scenario: YoY rate from API field
- **WHEN** timeline.mediation.yoy_success_rate存在
- **THEN** 显示该值作为同比增长率(如+8%)
#### Scenario: YoY rate calculation fallback
- **WHEN** yoy_success_rate为空
- **THEN** 计算 (success_rate - last_success_rate) * 100 作为同比增长率
#### Scenario: YoY time display
- **WHEN** timeline.mediation.yoy_before_hours存在
- **THEN** 显示"较{yoy_before_hours}小时前"
#### Scenario: YoY time default
- **WHEN** yoy_before_hours为空
- **THEN** 显示"较0小时前"
---
### Requirement: Negotiation Progress Component
系统 SHALL 在调解分析面板右侧显示协商沟通进度组件。
#### Scenario: Round count display
- **WHEN** timeline.mediation.mediation_count存在
- **THEN** 显示"第{mediation_count}轮"
#### Scenario: Progress dots default count
- **WHEN** 沟通次数 <= 流程节点总数
- **THEN** 总次数 = 流程节点总数,已完成轮次显示蓝色,未完成显示灰色
#### Scenario: Progress dots dynamic count
- **WHEN** 沟通次数 > 流程节点总数 且 未到最后节点
- **THEN** 总次数 = 沟通次数 + 1
#### Scenario: Progress dots all complete
- **WHEN** 已到达最后节点
- **THEN** 全部点显示蓝色
---
### Requirement: AI Suggestion Card Component
系统 SHALL 在诉求差距分析下方显示AI调解建议面板。
#### Scenario: AI suggestion content display
- **WHEN** 面板加载
- **THEN** 显示AI调解建议内容(Mock数据),背景为浅蓝色
#### Scenario: View detail button display
- **WHEN** 面板加载
- **THEN** 底部显示"查看详细策略建议"按钮
#### Scenario: View detail button click
- **WHEN** 用户点击"查看详细策略建议"按钮
- **THEN** 显示提示消息"该功能正在升级中,敬请期待!"
---
### Requirement: Warning Notify API Service
系统 SHALL 提供预警消息列表API服务方法。
#### Scenario: Get warning notify list
- **WHEN** 调用 getWarningNotifyList(mediationId)
- **THEN** 发送GET请求到 `/api/v1/mediation-timeline/v2/warning-notify-list/{mediationId}`
- **AND** 返回预警消息数组
---
### Requirement: Person List API Service
系统 SHALL 提供当事人列表API服务方法。
#### Scenario: Get person list
- **WHEN** 调用 getPersonList(caseId)
- **THEN** 发送GET请求到 `/api/v1/mediation-timeline/v2/person-list/{caseId}`
- **AND** 返回当事人数组
---
## MODIFIED Requirements
### Requirement: Mediation Data Board Layout
系统 SHALL 将调解数据看板调整为左右两栏布局。
#### Scenario: Left column content
- **WHEN** 调解分析Tab激活
- **THEN** 左列显示:诉求差距分析 + AI调解建议面板
#### Scenario: Right column content
- **WHEN** 调解分析Tab激活
- **THEN** 右列显示:申请双方信息 + 预计调解成功率(含同比) + 协商沟通进度
openspec/changes/upgrade-homepage-layout/tasks.md
New file
@@ -0,0 +1,79 @@
# Tasks: 首页布局升级优化
## 1. API服务层
- [x] 1.1 创建 `MediationTimelineAPIService.js` 服务文件
- [x] 1.2 实现 `getWarningNotifyList(mediationId)` 方法 - 获取预警消息列表
- [x] 1.3 实现 `getPersonList(caseId)` 方法 - 获取当事人列表
## 2. 蓝色顶部Header组件
- [x] 2.1 创建 `AppHeader.jsx` 组件
- [x] 2.2 实现左侧系统名称"矛盾纠纷应用"展示
- [x] 2.3 实现右侧通知图标 + 红色气泡消息数量
- [x] 2.4 实现右侧调解员信息展示(从URL参数获取trueName/unit/roleName/avatar)
- [x] 2.5 实现通知列表弹窗(点击通知图标触发)
- [x] 2.6 添加Header相关CSS样式(蓝色背景渐变)
- [x] 2.7 在 `App.js` 中集成 AppHeader 组件
## 3. 预警提示消息组件
- [x] 3.1 创建 `WarningAlert.jsx` 组件
- [x] 3.2 实现浅黄色预警消息条展示(单条时直接显示)
- [x] 3.3 实现多条消息时右侧显示数量 + "查看更多"按钮
- [x] 3.4 实现预警消息详情弹窗(Modal)
- [x] 3.5 添加预警提示相关CSS样式
- [x] 3.6 在 `MediationProgress` 组件下方集成预警提示
## 4. 申请双方信息组件
- [x] 4.1 创建 `PartyInfoCard.jsx` 组件
- [x] 4.2 实现申请人信息展示(头像 + 姓名 + 情绪标签)
- [x] 4.3 实现中间VS分隔符展示
- [x] 4.4 实现被申请人信息展示(头像 + 公司名称 + 标签)
- [x] 4.5 实现情绪标签样式(根据tag_style显示不同颜色)
- [x] 4.6 添加申请双方信息相关CSS样式
- [x] 4.7 在 `MediationDataBoard` 右侧区域集成组件
## 5. 预计调解成功率组件改进
- [x] 5.1 修改 `MediationDataBoard` 中的成功率展示
- [x] 5.2 实现同比数据获取逻辑(yoy_success_rate或计算差值)
- [x] 5.3 实现"较X小时前"时间展示(yoy_before_hours)
- [x] 5.4 添加成功率同比展示样式(绿色上升箭头 + 百分比)
## 6. 协商沟通组件
- [x] 6.1 创建 `NegotiationProgress.jsx` 组件
- [x] 6.2 实现"协商沟通"标题展示
- [x] 6.3 实现"第N轮"文字展示
- [x] 6.4 实现点线式沟通进度(默认6个点线块)
- [x] 6.5 实现进度着色逻辑(已完成蓝色/未完成灰色)
- [x] 6.6 实现总次数动态计算(基于流程节点和沟通次数)
- [x] 6.7 添加协商沟通相关CSS样式
- [x] 6.8 在 `MediationDataBoard` 右侧区域集成组件
## 7. AI调解建议面板
- [x] 7.1 创建 `AISuggestionCard.jsx` 组件
- [x] 7.2 实现AI建议内容展示(Mock数据)
- [x] 7.3 实现"查看详细策略建议"按钮
- [x] 7.4 实现按钮点击提示("该功能正在升级中,敬请期待!")
- [x] 7.5 添加AI调解建议相关CSS样式(浅蓝色背景)
- [x] 7.6 在诉求差距分析下方集成组件
## 8. 布局调整与整合
- [x] 8.1 调整 `App.css` 整体布局适配新Header
- [x] 8.2 调整 `MediationDataBoard` 布局为左右两栏
- [x] 8.3 确保各组件响应式适配
- [x] 8.4 验证1:1还原原型图效果
## 9. 验证与测试
- [x] 9.1 验证Header通知功能正常
- [x] 9.2 验证预警消息API集成正常
- [x] 9.3 验证当事人列表API集成正常
- [x] 9.4 验证成功率同比数据计算正确
- [x] 9.5 验证协商沟通进度显示正确
- [x] 9.6 整体视觉与原型图对比验收
web-app/public/ai-bg.png
web-app/public/app_logo.png
web-app/public/in_person.png
web-app/public/join.png
web-app/public/mom.png
web-app/public/to_person.png
web-app/public/warning.png
web-app/src/App.css
@@ -715,6 +715,94 @@
  font-weight: 600;
}
/* 预计调解成功率 - 新版样式 */
.success-rate-section {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 14px 16px;
  margin-bottom: 10px;
  text-align: left;
}
.success-rate-label {
  font-size: 0.85rem;
  color: #6c757d;
  margin-bottom: 8px;
}
.success-rate-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 10px;
}
.success-rate-value {
  font-size: 2.2rem;
  font-weight: 700;
  color: #1a6fb8;
  line-height: 1;
}
.success-rate-yoy {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 0.8rem;
}
.yoy-icon-img {
  width: 12px;
  height: 7px;
}
.yoy-rate {
  color: #52c41a;
  font-weight: 600;
}
.yoy-time {
  color: #52c41a;
}
/* 进度条 */
.success-rate-progress {
  width: 100%;
}
.progress-bar-bg {
  width: 100%;
  height: 6px;
  background: #e8e8e8;
  border-radius: 3px;
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  background: #1a6fb8;
  border-radius: 3px;
  transition: width 0.3s ease;
}
/* 调解数据看板 - 左右分栏布局 */
.metric-card.left-column {
  display: flex;
  flex-direction: column;
}
.metric-card.right-column {
  display: flex;
  flex-direction: column;
  padding: 0;
  background: transparent;
  gap: 0px;
}
.metric-card.right-column > *:last-child {
  margin-bottom: 0;
}
/* 模态窗口样式 */
.modal-overlay {
  display: none;
web-app/src/App.js
@@ -16,6 +16,10 @@
import ToolModal from './components/common/ToolModal';
import OutboundCallWidget from './components/common/OutboundCallWidget';
// 新增组件
import AppHeader from './components/common/AppHeader';
import WarningAlert from './components/common/WarningAlert';
// 工具内容组件
import WageCalculatorContent from './components/tools/WageCalculatorContent';
import LawSearchContent from './components/tools/LawSearchContent';
@@ -103,6 +107,9 @@
          href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
        />
        {/* 蓝色顶部Header */}
        <AppHeader />
        {/* 顶部区域 */}
        <TopSection />
@@ -113,6 +120,9 @@
            {/* AI调解进度 */}
            <MediationProgress />
            {/* 预警提示消息 */}
            <WarningAlert />
            {/* 选项卡容器 */}
            <TabContainer ref={tabContainerRef} />
          </div>
web-app/src/components/common/AppHeader.css
New file
@@ -0,0 +1,131 @@
/**
 * AppHeader 组件样式
 * 蓝色顶部导航栏
 */
.app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: linear-gradient(135deg, #1a6fb8, #0d4a8a);
  color: white;
  padding: 10px 24px;
  height: 50px;
  flex-shrink: 0;
}
/* 左侧区域 */
.header-left {
  display: flex;
  align-items: center;
}
.header-title {
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: 1px;
}
/* 右侧区域 */
.header-right {
  display: flex;
  align-items: center;
  gap: 20px;
}
/* 通知图标区域 */
.header-notification {
  cursor: pointer;
  padding: 6px;
  border-radius: 50%;
  transition: background 0.2s;
}
.header-notification:hover {
  background: rgba(255, 255, 255, 0.15);
}
.header-bell-icon {
  font-size: 1.2rem;
  color: white;
}
/* 调解员信息区域 */
.header-mediator {
  display: flex;
  align-items: center;
  gap: 10px;
}
.mediator-avatar {
  width: 32px;
  height: 32px;
  border: 2px solid rgba(255, 255, 255, 0.3);
}
.mediator-details {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}
.mediator-name {
  font-size: 0.9rem;
  font-weight: 600;
}
.mediator-role {
  font-size: 0.75rem;
  opacity: 0.85;
}
.mediator-status-dot {
  width: 8px;
  height: 8px;
  background: #52c41a;
  border-radius: 50%;
  margin-left: 4px;
}
/* 通知列表弹窗 */
.notification-popover .ant-popover-inner {
  max-width: 320px;
}
.notification-list {
  max-height: 300px;
  overflow-y: auto;
}
.notification-item {
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.notification-item:last-child {
  border-bottom: none;
}
.notification-content {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.notification-text {
  font-size: 0.85rem;
  color: #333;
  line-height: 1.4;
}
.notification-time {
  font-size: 0.75rem;
  color: #999;
}
.notification-empty {
  padding: 20px;
  text-align: center;
  color: #999;
  font-size: 0.85rem;
}
web-app/src/components/common/AppHeader.jsx
New file
@@ -0,0 +1,124 @@
/**
 * AppHeader 组件 - 蓝色顶部导航栏
 * 包含系统名称、通知图标、调解员信息
 */
import React, { useState, useCallback } from 'react';
import { Badge, Popover, List, Avatar } from 'antd';
import { BellOutlined, UserOutlined } from '@ant-design/icons';
import { getMergedParams } from '../../utils/urlParams';
import './AppHeader.css';
// 默认头像
const DEFAULT_AVATAR = '/app_logo.png';
/**
 * 从URL参数获取调解员信息
 */
const getMediatorInfo = () => {
  const params = getMergedParams();
  return {
    trueName: params.trueName || '调解员',
    unit: params.unit || '',
    roleName: params.roleName || '管理员',
    avatar: params.avatar || ''
  };
};
/**
 * 通知列表组件
 */
const NotificationList = ({ notifications, onClose }) => {
  if (!notifications?.length) {
    return (
      <div className="notification-empty">
        <span>暂无新通知</span>
      </div>
    );
  }
  return (
    <List
      className="notification-list"
      dataSource={notifications}
      renderItem={(item) => (
        <List.Item className="notification-item">
          <div className="notification-content">
            <span className="notification-text">{item.content || item.title}</span>
            <span className="notification-time">{item.create_time || item.time}</span>
          </div>
        </List.Item>
      )}
    />
  );
};
/**
 * AppHeader 主组件
 */
const AppHeader = ({ notifications = [] }) => {
  const [popoverVisible, setPopoverVisible] = useState(false);
  const mediatorInfo = getMediatorInfo();
  // 处理Popover显示状态
  const handlePopoverChange = useCallback((visible) => {
    setPopoverVisible(visible);
  }, []);
  // 渲染通知图标和气泡
  const renderNotificationIcon = () => (
    <Badge count={notifications.length} offset={[-2, 2]} size="small">
      <BellOutlined className="header-bell-icon" />
    </Badge>
  );
  // 渲染调解员信息
  const renderMediatorInfo = () => (
    <div className="header-mediator">
      <Avatar
        src={mediatorInfo.avatar || DEFAULT_AVATAR}
        icon={!mediatorInfo.avatar && <UserOutlined />}
        className="mediator-avatar"
      />
      <div className="mediator-details">
        <span className="mediator-name">{mediatorInfo.trueName}</span>
        <span className="mediator-role">
          {mediatorInfo.unit && `${mediatorInfo.unit} | `}{mediatorInfo.roleName}
        </span>
      </div>
      <span className="mediator-status-dot" />
    </div>
  );
  return (
    <header className="app-header">
      {/* 左侧:系统名称 */}
      <div className="header-left">
        <span className="header-title">矛盾纠纷应用</span>
      </div>
      {/* 右侧:通知+调解员信息 */}
      <div className="header-right">
        {/* 通知图标 */}
        <Popover
          content={<NotificationList notifications={notifications} />}
          title="系统通知"
          trigger="click"
          open={popoverVisible}
          onOpenChange={handlePopoverChange}
          placement="bottomRight"
          overlayClassName="notification-popover"
        >
          <div className="header-notification">
            {renderNotificationIcon()}
          </div>
        </Popover>
        {/* 调解员信息 */}
        {renderMediatorInfo()}
      </div>
    </header>
  );
};
export default AppHeader;
web-app/src/components/common/OutboundCallWidget.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useCaseData } from '../../contexts/CaseDataContext';
import OutboundBotAPIService from '../../services/OutboundBotAPIService';
import ProcessAPIService from '../../services/ProcessAPIService';
import { message } from 'antd';
const OUTBOUND_JOBS_KEY = 'outbound_call_jobs';
@@ -31,6 +32,7 @@
  const [isVisible, setIsVisible] = useState(false); // 默认隐藏
  const [isMinimized, setIsMinimized] = useState(false); // 默认展开(非最小化)
  const [calls, setCalls] = useState([]);
  const [mediationRecords, setMediationRecords] = useState([]); // AI调解记录
  const isMountedRef = useRef(true);
  
  // 轮询间隔(毫秒)
@@ -41,6 +43,28 @@
  // 获取 caseId
  const caseId = caseData?.caseId || caseData?.case_id;
  // 获取 mediationId
  const mediationId = caseData?.mediation?.id;
  // 加载AI调解记录(无Loading效果)
  const loadMediationRecords = useCallback(async () => {
    if (!mediationId) return;
    try {
      const response = await ProcessAPIService.getProcessRecords({
        mediation_id: mediationId
      });
      if (isMountedRef.current) {
        setMediationRecords(response.data || []);
        console.log('AI调解记录加载成功:', response.data?.length || 0, '条');
      }
    } catch (err) {
      console.error('加载AI调解记录失败:', err);
      // 不显示错误提示,不设置loading状态
    }
  }, [mediationId]);
  // 格式化通话时长
  const formatDuration = (seconds) => {
@@ -346,9 +370,13 @@
    
    // 初始加载
    fetchCallStatus();
    loadMediationRecords(); // 初始加载调解记录
    
    // 设置轮询定时器(10秒间隔)
    const interval = setInterval(fetchCallStatus, POLL_INTERVAL);
    const interval = setInterval(() => {
      fetchCallStatus();
      loadMediationRecords(); // 每10秒加载一次AI调解记录
    }, POLL_INTERVAL);
    
    // 监听外呼任务更新事件(立即刷新)
    const handleOutboundJobsUpdated = () => {
@@ -375,7 +403,7 @@
      window.removeEventListener('mediation-terminated', handleMediationTerminated);
      isMountedRef.current = false;
    };
  }, [fetchCallStatus]);
  }, [fetchCallStatus, loadMediationRecords]);
  // 关闭气泡
  const handleClose = (e) => {
web-app/src/components/common/WarningAlert.css
New file
@@ -0,0 +1,127 @@
/**
 * WarningAlert 组件样式
 * 预警提示消息条
 */
.warning-alert {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: linear-gradient(135deg, #fffbe6, #fff7e0);
  border: 1px solid #ffe58f;
  border-left: 4px solid #faad14;
  border-radius: 6px;
  padding: 10px 16px;
  margin: 0 24px 12px;
}
.warning-alert-content {
  display: flex;
  align-items: center;
  flex: 1;
  gap: 6px;
  min-width: 0;
}
.warning-icon-img {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-right: 6px;
}
.warning-modal-icon {
  width: 16px;
  height: 16px;
  margin-right: 8px;
  vertical-align: middle;
}
.warning-label {
  color: #d46b08;
  font-weight: 600;
  font-size: 0.9rem;
  flex-shrink: 0;
}
.warning-text {
  color: #ad6800;
  font-size: 0.9rem;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.warning-alert-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
  margin-left: 16px;
}
.warning-count {
  color: #d46b08;
  font-size: 0.85rem;
  font-weight: 500;
}
.warning-more-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: 1px solid #faad14;
  color: #d46b08;
  padding: 4px 10px;
  border-radius: 4px;
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.2s;
}
.warning-more-btn:hover {
  background: #faad14;
  color: white;
}
/* 弹窗样式 */
.warning-modal .ant-modal-body {
  max-height: 400px;
  overflow-y: auto;
}
.warning-modal-item {
  display: flex;
  flex-direction: column;
  padding: 12px 0;
  border-bottom: 1px solid #f0f0f0;
}
.warning-modal-item:last-child {
  border-bottom: none;
}
.warning-modal-content {
  display: flex;
  gap: 8px;
  line-height: 1.5;
}
.warning-modal-index {
  color: #faad14;
  font-weight: 600;
  flex-shrink: 0;
}
.warning-modal-text {
  color: #333;
  font-size: 0.9rem;
}
.warning-modal-time {
  color: #999;
  font-size: 0.8rem;
  margin-top: 6px;
}
web-app/src/components/common/WarningAlert.jsx
New file
@@ -0,0 +1,119 @@
/**
 * WarningAlert 组件 - 预警提示消息条
 * 在调解进度条下方显示预警消息
 */
import React, { useState, useEffect, useCallback } from 'react';
import { Modal, List } from 'antd';
import { RightOutlined } from '@ant-design/icons';
import { useCaseData } from '../../contexts/CaseDataContext';
import MediationTimelineAPIService from '../../services/MediationTimelineAPIService';
import './WarningAlert.css';
// 警告图标图片
const WARNING_ICON = '/warning.png';
/**
 * 预警消息列表弹窗
 */
const WarningModal = ({ visible, warnings, onClose }) => (
  <Modal
    title={<span><img src={WARNING_ICON} alt="warning" className="warning-modal-icon" />预警消息列表</span>}
    open={visible}
    onCancel={onClose}
    footer={null}
    width={520}
    className="warning-modal"
  >
    <List
      dataSource={warnings}
      renderItem={(item, index) => (
        <List.Item className="warning-modal-item">
          <div className="warning-modal-content">
            <span className="warning-modal-index">{index + 1}.</span>
            <span className="warning-modal-text">{item.content}</span>
          </div>
          <span className="warning-modal-time">{item.create_time}</span>
        </List.Item>
      )}
    />
  </Modal>
);
/**
 * WarningAlert 主组件
 */
const WarningAlert = () => {
  const { caseData } = useCaseData();
  const [warnings, setWarnings] = useState([]);
  const [modalVisible, setModalVisible] = useState(false);
  const [loading, setLoading] = useState(false);
  const mediationId = caseData?.mediation?.id;
  // 加载预警消息
  const loadWarnings = useCallback(async () => {
    if (!mediationId) return;
    setLoading(true);
    try {
      const response = await MediationTimelineAPIService.getWarningNotifyList(mediationId);
      setWarnings(response.data || []);
    } catch (err) {
      console.error('加载预警消息失败:', err);
      setWarnings([]);
    } finally {
      setLoading(false);
    }
  }, [mediationId]);
  // 组件挂载时加载数据
  useEffect(() => {
    loadWarnings();
  }, [loadWarnings]);
  // 打开弹窗
  const handleOpenModal = useCallback(() => {
    setModalVisible(true);
  }, []);
  // 关闭弹窗
  const handleCloseModal = useCallback(() => {
    setModalVisible(false);
  }, []);
  // 无预警消息时不渲染
  if (!warnings.length || loading) {
    return null;
  }
  const firstWarning = warnings[0];
  const hasMore = warnings.length > 1;
  return (
    <div className="warning-alert">
      <div className="warning-alert-content">
        <img src={WARNING_ICON} alt="warning" className="warning-icon-img" />
        <span className="warning-label">预警:</span>
        <span className="warning-text">{firstWarning.content}</span>
      </div>
      {hasMore && (
        <div className="warning-alert-actions">
          <span className="warning-count">还有{warnings.length - 1}条预警</span>
          <button className="warning-more-btn" onClick={handleOpenModal}>
            查看更多 <RightOutlined />
          </button>
        </div>
      )}
      <WarningModal
        visible={modalVisible}
        warnings={warnings}
        onClose={handleCloseModal}
      />
    </div>
  );
};
export default WarningAlert;
web-app/src/components/dashboard/AISuggestionCard.css
New file
@@ -0,0 +1,80 @@
/**
 * AISuggestionCard 组件样式
 * AI调解建议面板
 */
.ai-suggestion-card {
  background: #1a6fb8;
  border-radius: 12px;
  padding: 14px 16px;
  margin-top: 12px;
  box-shadow: 0px 4px 6px -4px rgba(0,0,0,0.10), 0px 10px 15px -3px rgba(0,0,0,0.10);
  position: relative;
  overflow: hidden;
}
.ai-suggestion-bg {
  position: absolute;
  top: 15px;
  right: 15px;
  width: 48px;
  height: 50px;
  opacity: 0.1;
  pointer-events: none;
}
.ai-suggestion-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.ai-suggestion-icon {
  font-size: 1rem;
  color: #fff;
}
.ai-suggestion-title {
  font-size: 0.9rem;
  font-weight: 600;
  color: #fff;
}
.ai-suggestion-content {
  font-size: 0.85rem;
  line-height: 1.6;
  color: #fff;
  max-height: 80px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
.ai-suggestion-footer {
  margin-top: 10px;
  text-align: center;
}
.ai-suggestion-btn {
  padding: 8px 24px;
  font-size: 0.85rem;
  color: #FFFFFF;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 20px;
  border: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  height: auto;
  line-height: 1.5;
  margin: 10px;
  width: 90%;
  background: rgba(255, 255, 255, 0.20);
  border-radius: 8px;
}
.ai-suggestion-btn:hover {
  color: #0d4a8a;
  background: rgba(255, 255, 255, 1);
}
web-app/src/components/dashboard/AISuggestionCard.jsx
New file
@@ -0,0 +1,53 @@
/**
 * AISuggestionCard 组件 - AI调解建议
 * 展示AI生成的调解建议内容
 */
import React, { useCallback } from 'react';
import { Button, message } from 'antd';
import { BulbOutlined, RightOutlined } from '@ant-design/icons';
import './AISuggestionCard.css';
// Mock数据 - AI调解建议内容
const MOCK_SUGGESTION = `建议调解员优先安抚申请人张三的情绪。目前双方在补偿金额上的差距已缩小至15%,可尝试引入"互谅互让"原则进行最后博弈。建议调解员优先安抚申请人张三的情绪。目前双方在补偿金额上的差距已缩小至15%,可尝试引入"互谅互让"原则进行最后博弈。建议调解员优先安抚申请人张三的情绪。`;
/**
 * AISuggestionCard 主组件
 */
const AISuggestionCard = () => {
  // 点击查看详细策略建议
  const handleViewDetail = useCallback(() => {
    message.info('该功能正在升级中,敬请期待!');
  }, []);
  return (
    <div className="ai-suggestion-card">
      {/* 背景图标 */}
      <img src="/ai-bg.png" alt="" className="ai-suggestion-bg" />
      {/* 标题 */}
      <div className="ai-suggestion-header">
        <BulbOutlined className="ai-suggestion-icon" />
        <span className="ai-suggestion-title">AI 调解建议</span>
      </div>
      {/* 建议内容 */}
      <div className="ai-suggestion-content">
        {MOCK_SUGGESTION}
      </div>
      {/* 查看详情按钮 */}
      <div className="ai-suggestion-footer">
        <Button
          type="link"
          className="ai-suggestion-btn"
          onClick={handleViewDetail}
        >
          查看详细策略建议 <RightOutlined />
        </Button>
      </div>
    </div>
  );
};
export default AISuggestionCard;
web-app/src/components/dashboard/NegotiationProgress.css
New file
@@ -0,0 +1,79 @@
/**
 * NegotiationProgress 组件样式
 * 协商沟通进度点线式展示
 */
.negotiation-progress {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px 16px;
}
.negotiation-header {
  margin-bottom: 8px;
}
.negotiation-title {
  font-size: 0.85rem;
  color: #6c757d;
  font-weight: 500;
}
.negotiation-round {
  margin-bottom: 12px;
}
.round-text {
  font-size: 1.3rem;
  font-weight: 700;
  color: #212529;
}
/* 进度点容器 */
.negotiation-dots {
  display: flex;
  align-items: center;
}
/* 单个进度点+线 */
.progress-dot-wrapper {
  display: flex;
  align-items: center;
  justify-content: flex-start;
}
.progress-dot-wrapper.last {
  flex: 0;
}
/* 进度短横线 */
.progress-dot {
  width: 16px;
  height: 4px;
  border-radius: 2px;
  background: #d9d9d9;
  flex-shrink: 0;
  transition: background 0.3s;
  margin-left: 5px;
}
.progress-dot-wrapper:first-child .progress-dot {
  margin-left: 0;
}
.progress-dot.active {
  background: #1a6fb8;
}
/* 连接线 */
.progress-line {
  flex: 1;
  height: 2px;
  background: #d9d9d9;
  margin: 0 4px;
  transition: background 0.3s;
}
.progress-line.active {
  background: #1a6fb8;
}
web-app/src/components/dashboard/NegotiationProgress.jsx
New file
@@ -0,0 +1,88 @@
/**
 * NegotiationProgress 组件 - 协商沟通进度
 * 点线式可视化展示沟通轮次
 */
import React, { useMemo } from 'react';
import { useCaseData } from '../../contexts/CaseDataContext';
import './NegotiationProgress.css';
/**
 * 计算总轮次和已完成轮次
 */
const calculateRounds = (mediationCount, nodeCount, currentNodeIndex) => {
  const currentRound = mediationCount || 0;
  let totalRounds = nodeCount || 6;
  const isLastNode = currentNodeIndex >= nodeCount - 1;
  // 当沟通次数超过总次数但未到最后节点时,总次数 = 沟通次数 + 1
  if (currentRound > totalRounds && !isLastNode) {
    totalRounds = currentRound + 1;
  }
  return { currentRound, totalRounds, isLastNode };
};
/**
 * 进度点组件
 */
const ProgressDot = ({ isActive, isLast }) => (
  <div className={`progress-dot-wrapper ${isLast ? 'last' : ''}`}>
    <div className={`progress-dot ${isActive ? 'active' : ''}`} />
    {!isLast && <div className={`progress-line ${isActive ? 'active' : ''}`} />}
  </div>
);
/**
 * NegotiationProgress 主组件
 */
const NegotiationProgress = () => {
  const { caseData, processNodes } = useCaseData();
  // 从数据中获取值
  const mediationCount = caseData?.mediation?.mediation_count || 0;
  const nodeCount = processNodes?.length || 6;
  // 计算当前节点索引
  const currentNodeIndex = useMemo(() => {
    if (!processNodes?.length) return 0;
    const activeNode = processNodes.findIndex(n => n.nodeState === 1);
    return activeNode >= 0 ? activeNode : processNodes.length - 1;
  }, [processNodes]);
  // 计算轮次数据
  const { currentRound, totalRounds, isLastNode } = useMemo(() => {
    return calculateRounds(mediationCount, nodeCount, currentNodeIndex);
  }, [mediationCount, nodeCount, currentNodeIndex]);
  // 生成进度点数据
  const dots = useMemo(() => {
    return Array(totalRounds).fill(false).map((_, index) => {
      // 如果是最后节点,全部显示蓝色
      if (isLastNode) return true;
      return index < currentRound;
    });
  }, [totalRounds, currentRound, isLastNode]);
  return (
    <div className="negotiation-progress">
      <div className="negotiation-header">
        <span className="negotiation-title">协商沟通</span>
      </div>
      <div className="negotiation-round">
        <span className="round-text">第{currentRound}轮</span>
      </div>
      <div className="negotiation-dots">
        {dots.map((isActive, index) => (
          <ProgressDot
            key={index}
            isActive={isActive}
            isLast={index === dots.length - 1}
          />
        ))}
      </div>
    </div>
  );
};
export default NegotiationProgress;
web-app/src/components/dashboard/PartyInfoCard.css
New file
@@ -0,0 +1,105 @@
/**
 * PartyInfoCard 组件样式
 * 申请双方信息展示
 */
.party-info-card {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px 16px;
  margin-bottom: 10px;
}
.party-info-title {
  font-size: 0.9rem;
  color: #6c757d;
  margin-bottom: 12px;
  font-weight: 500;
}
.party-info-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
/* 当事人卡片 */
.party-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  flex: 1;
  padding: 8px;
}
/* 情绪标签 - 实心红背景白字 */
.party-tag {
  position: absolute;
  top: -4px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.7rem;
  padding: 2px 8px;
  line-height: 16px;
  border-radius: 10px;
  background-color: #ff4d4f !important;
  color: white !important;
  border: none !important;
}
.party-avatar-img {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  margin-top: 12px;
  margin-bottom: 8px;
  object-fit: cover;
}
.party-role {
  font-size: 0.75rem;
  color: #999;
  margin-bottom: 4px;
}
.party-name {
  font-size: 0.9rem;
  font-weight: 600;
  color: #333;
  text-align: center;
  max-width: 100px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* VS分隔符 */
.vs-separator {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  gap: 8px;
}
.vs-line {
  width: 30px;
  height: 1px;
}
.vs-line-left {
  background: linear-gradient(to right, #E5E6EB, #C9CDD4);
}
.vs-line-right {
  background: linear-gradient(to right, #C9CDD4, #E5E6EB);
}
.vs-icon-img {
  width: 24px;
  height: 24px;
  object-fit: contain;
  flex-shrink: 0;
}
web-app/src/components/dashboard/PartyInfoCard.jsx
New file
@@ -0,0 +1,142 @@
/**
 * PartyInfoCard 组件 - 申请双方信息
 * 展示申请人和被申请人信息,包含头像、姓名和情绪标签
 */
import React, { useState, useEffect, useCallback } from 'react';
import { Tag } from 'antd';
import { useCaseData } from '../../contexts/CaseDataContext';
import MediationTimelineAPIService from '../../services/MediationTimelineAPIService';
import './PartyInfoCard.css';
// 申请人头像图片
const APPLICANT_AVATAR = '/in_person.png';
// 被申请人头像图片
const RESPONDENT_AVATAR = '/to_person.png';
/**
 * 根据per_type判断是申请方还是被申请方
 */
const isApplicant = (perType) => {
  return perType?.includes('15_020008-1') || perType?.toLowerCase().includes('applicant');
};
/**
 * 获取标签颜色样式
 */
const getTagColor = (tagStyle) => {
  const colorMap = {
    red: 'red',
    orange: 'orange',
    yellow: 'gold',
    green: 'green',
    blue: 'blue',
    warning: 'orange',
    danger: 'red',
    success: 'green',
    primary: 'blue'
  };
  return colorMap[tagStyle] || 'default';
};
/**
 * 当事人信息卡片
 */
const PartyCard = ({ person, isApplicantSide }) => {
  const avatarSrc = isApplicantSide ? APPLICANT_AVATAR : RESPONDENT_AVATAR;
  return (
    <div className={`party-card ${isApplicantSide ? 'applicant' : 'respondent'}`}>
      {/* 标签 */}
      {person.tag_name && (
        <Tag color={getTagColor(person.tag_style)} className="party-tag">
          {person.tag_name}
        </Tag>
      )}
      {/* 头像 */}
      <img
        src={avatarSrc}
        alt={isApplicantSide ? '申请人' : '被申请人'}
        className="party-avatar-img"
      />
      {/* 角色标签 */}
      <span className="party-role">
        {isApplicantSide ? '申请人' : '被申请人'}
      </span>
      {/* 姓名/公司名 */}
      <span className="party-name">{person.true_name}</span>
    </div>
  );
};
// VS分隔符图片
const VS_ICON = '/join.png';
/**
 * VS分隔符
 */
const VSSeparator = () => (
  <div className="vs-separator">
    <div className="vs-line vs-line-left"></div>
    <img src={VS_ICON} alt="VS" className="vs-icon-img" />
    <div className="vs-line vs-line-right"></div>
  </div>
);
/**
 * PartyInfoCard 主组件
 */
const PartyInfoCard = () => {
  const { caseData } = useCaseData();
  const [persons, setPersons] = useState([]);
  const [loading, setLoading] = useState(false);
  const caseId = caseData?.caseId || caseData?.case_id;
  // 加载当事人数据
  const loadPersons = useCallback(async () => {
    if (!caseId) return;
    setLoading(true);
    try {
      const response = await MediationTimelineAPIService.getPersonList(caseId);
      setPersons(response.data || []);
    } catch (err) {
      console.error('加载当事人列表失败:', err);
      setPersons([]);
    } finally {
      setLoading(false);
    }
  }, [caseId]);
  // 组件挂载时加载数据
  useEffect(() => {
    loadPersons();
  }, [loadPersons]);
  // 分离申请方和被申请方
  const applicant = persons.find(p => isApplicant(p.per_type));
  const respondent = persons.find(p => !isApplicant(p.per_type));
  if (loading || persons.length === 0) {
    return null;
  }
  return (
    <div className="party-info-card">
      <div className="party-info-title">申请双方</div>
      <div className="party-info-content">
        {applicant && <PartyCard person={applicant} isApplicantSide={true} />}
        <VSSeparator />
        {respondent && <PartyCard person={respondent} isApplicantSide={false} />}
      </div>
    </div>
  );
};
export default PartyInfoCard;
web-app/src/components/dashboard/TabContainer.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { useCaseData } from '../../contexts/CaseDataContext';
import { formatDuration, formatSuccessRate, formatRoundCount } from '../../utils/stateTranslator';
import { formatDuration, formatSuccessRate } from '../../utils/stateTranslator';
import ProcessAPIService from '../../services/ProcessAPIService';
import EvidenceAPIService from '../../services/EvidenceAPIService';
import MediationAgreementAPIService from '../../services/MediationAgreementAPIService';
@@ -8,6 +8,11 @@
import { message, Spin, Tag, Modal, Button, Input, Image } from 'antd';
import { PhoneOutlined } from '@ant-design/icons';
import { CallRecordModal } from '../call-record';
// 新增组件导入
import PartyInfoCard from './PartyInfoCard';
import NegotiationProgress from './NegotiationProgress';
import AISuggestionCard from './AISuggestionCard';
const { TextArea } = Input;
@@ -89,22 +94,53 @@
});
/**
 * 获取成功率同比数据
 */
const getSuccessRateYoY = (mediation) => {
  // 优先使用API返回的同比值
  if (mediation?.yoy_success_rate !== undefined && mediation?.yoy_success_rate !== null) {
    return {
      rate: mediation.yoy_success_rate,
      hours: mediation.yoy_before_hours || 0
    };
  }
  // 计算同比值
  const currentRate = mediation?.success_rate || 0;
  const lastRate = mediation?.last_success_rate || 0;
  const diff = (currentRate - lastRate) * 100;
  return {
    rate: diff,
    hours: mediation?.yoy_before_hours || 0
  };
};
/**
 * 调解数据看板
 */
const MediationDataBoard = () => {
  const { caseData } = useCaseData();
  const timeline = caseData || {};
  const mediation = timeline.mediation || {};
  
  // 从 timeline 获取数据
  const gapContent = timeline.result || '暂无分歧分析';
  const updateTime = formatDuration(timeline.before_duration);
  const successRate = formatSuccessRate(timeline.mediation?.success_rate);
  const roundCount = formatRoundCount(timeline.mediation?.mediation_count);
  const successRate = formatSuccessRate(mediation.success_rate);
  // 获取成功率数值(用于进度条)
  const successRateValue = (mediation.success_rate || 0) * 100;
  // 获取同比数据
  const yoyData = getSuccessRateYoY(mediation);
  const yoyRate = yoyData.rate >= 0 ? `+${yoyData.rate.toFixed(0)}%` : `${yoyData.rate.toFixed(0)}%`;
  const yoyHours = yoyData.hours;
  
  return (
    <div className="mediation-metrics">
      {/* 左侧:诉求差距分析 */}
      <div className="metric-card">
      {/* 左侧:诉求差距分析 + AI建议 */}
      <div className="metric-card left-column">
        <div className="metric-title">
          <i className="fas fa-exclamation-circle"></i>
          <span>诉求差距分析</span>
@@ -120,28 +156,36 @@
              {gapContent}
            </div>
          </div>
          {/* AI调解建议 */}
          <AISuggestionCard />
        </div>
      </div>
      {/* 右侧:调解数据 */}
      <div className="metric-card">
        <div className="metric-title">
          <i className="fas fa-exchange-alt"></i>
          <span>调解数据</span>
        </div>
        <div className="metric-content">
          <div className="success-metric">
            <div className="success-value">{successRate}</div>
            <div className="success-label">预计调解成功概率</div>
            <div className="success-change">
              {/* <i className="fas fa-arrow-up"></i><span>较{updateTime} +8%</span> */}
      {/* 右侧:申请双方 + 成功率 + 协商沟通 */}
      <div className="metric-card right-column">
        {/* 申请双方信息 */}
        <PartyInfoCard />
        {/* 预计调解成功率 */}
        <div className="success-rate-section">
          <div className="success-rate-label">预计调解成功率</div>
          <div className="success-rate-row">
            <span className="success-rate-value">{successRate}</span>
            <div className="success-rate-yoy">
              <img src="/mom.png" alt="" className="yoy-icon-img" />
              <span className="yoy-rate">{yoyRate}</span>
              <span className="yoy-time">较{yoyHours}小时前</span>
            </div>
            <div style={{ marginTop: 15, fontSize: '0.9rem', color: 'var(--gray-color)' }}>
              协商沟通:<span style={{ color: 'var(--dark-color)', fontWeight: 600 }}>{roundCount}</span>
          </div>
          <div className="success-rate-progress">
            <div className="progress-bar-bg">
              <div className="progress-bar-fill" style={{ width: `${successRateValue}%` }}></div>
            </div>
          </div>
        </div>
        {/* 协商沟通进度 */}
        <NegotiationProgress />
      </div>
    </div>
  );
web-app/src/components/dashboard/TopSection.jsx
@@ -23,7 +23,7 @@
          <div className="title-icon">
            <img
              style={{ width: 36 }}
              src="http://gz.hugeinfo.com.cn/dyh/wx414ae04ac3f10b4e/images/pngAI_logo.png"
              src="/app_logo.png"
              // alt="云小调"
            />
          </div>
web-app/src/services/MediationTimelineAPIService.js
New file
@@ -0,0 +1,31 @@
/**
 * 调解时间线扩展API Service
 * 处理预警消息、当事人列表等相关接口
 * 接口前缀: /api/v1/mediation-timeline/*
 */
import { request } from './request';
class MediationTimelineAPIService {
  /**
   * 获取预警消息列表
   * GET /api/v1/mediation-timeline/warning-notify-list/{mediation_id}
   * @param {string} mediationId - 调解ID
   * @returns {Promise<Array>} 预警消息列表
   */
  static getWarningNotifyList(mediationId) {
    return request.get(`/api/v1/mediation-timeline/v2/warning-notify-list/${mediationId}`);
  }
  /**
   * 获取当事人列表
   * GET /api/v1/mediation-timeline/person-list/{case_id}
   * @param {string} caseId - 案件ID
   * @returns {Promise<Array>} 当事人列表
   */
  static getPersonList(caseId) {
    return request.get(`/api/v1/mediation-timeline/v2/person-list/${caseId}`);
  }
}
export default MediationTimelineAPIService;