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
| New file |
| | |
| | | # 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数据 |
| | | - [已解答] "查看详细策略建议"跳转? → 暂不跳转,提示"功能升级中" |
| New file |
| | |
| | | # 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 |
| New file |
| | |
| | | # 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** 右列显示:申请双方信息 + 预计调解成功率(含同比) + 协商沟通进度 |
| New file |
| | |
| | | # 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 整体视觉与原型图对比验收 |
| | |
| | | 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; |
| | |
| | | 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'; |
| | |
| | | href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" |
| | | /> |
| | | |
| | | {/* 蓝色顶部Header */} |
| | | <AppHeader /> |
| | | |
| | | {/* 顶部区域 */} |
| | | <TopSection /> |
| | | |
| | |
| | | {/* AI调解进度 */} |
| | | <MediationProgress /> |
| | | |
| | | {/* 预警提示消息 */} |
| | | <WarningAlert /> |
| | | |
| | | {/* 选项卡容器 */} |
| | | <TabContainer ref={tabContainerRef} /> |
| | | </div> |
| New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| New file |
| | |
| | | /** |
| | | * 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; |
| | |
| | | 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'; |
| | |
| | | const [isVisible, setIsVisible] = useState(false); // 默认隐藏 |
| | | const [isMinimized, setIsMinimized] = useState(false); // 默认展开(非最小化) |
| | | const [calls, setCalls] = useState([]); |
| | | const [mediationRecords, setMediationRecords] = useState([]); // AI调解记录 |
| | | const isMountedRef = useRef(true); |
| | | |
| | | // 轮询间隔(毫秒) |
| | |
| | | |
| | | // 获取 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) => { |
| | |
| | | |
| | | // 初始加载 |
| | | fetchCallStatus(); |
| | | loadMediationRecords(); // 初始加载调解记录 |
| | | |
| | | // 设置轮询定时器(10秒间隔) |
| | | const interval = setInterval(fetchCallStatus, POLL_INTERVAL); |
| | | const interval = setInterval(() => { |
| | | fetchCallStatus(); |
| | | loadMediationRecords(); // 每10秒加载一次AI调解记录 |
| | | }, POLL_INTERVAL); |
| | | |
| | | // 监听外呼任务更新事件(立即刷新) |
| | | const handleOutboundJobsUpdated = () => { |
| | |
| | | window.removeEventListener('mediation-terminated', handleMediationTerminated); |
| | | isMountedRef.current = false; |
| | | }; |
| | | }, [fetchCallStatus]); |
| | | }, [fetchCallStatus, loadMediationRecords]); |
| | | |
| | | // 关闭气泡 |
| | | const handleClose = (e) => { |
| New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| New file |
| | |
| | | /** |
| | | * 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; |
| New file |
| | |
| | | /** |
| | | * 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); |
| | | } |
| New file |
| | |
| | | /** |
| | | * 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; |
| New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| New file |
| | |
| | | /** |
| | | * 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; |
| New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| New file |
| | |
| | | /** |
| | | * 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; |
| | |
| | | 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'; |
| | |
| | | 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; |
| | | |
| | |
| | | }); |
| | | |
| | | /** |
| | | * 获取成功率同比数据 |
| | | */ |
| | | 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> |
| | |
| | | {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> |
| | | ); |
| | |
| | | <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> |
| New file |
| | |
| | | /** |
| | | * 调解时间线扩展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; |