增加临研安报价端管理功能
This commit is contained in:
parent
830b8ccb45
commit
d5ec548602
119
PRD/RMO保险报价流程.md
119
PRD/RMO保险报价流程.md
|
|
@ -1,119 +0,0 @@
|
||||||
# 临床试验责任保险报价流程
|
|
||||||
|
|
||||||
> 基于 ProcessOn 流程图(RMO保险报价.pos)整理
|
|
||||||
|
|
||||||
## 一、泳道角色
|
|
||||||
|
|
||||||
| 泳道 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| **申办者** | 投保人(采购、项目经理等) |
|
|
||||||
| **系统** | RMO 网站及 Smart-Ops 系统 |
|
|
||||||
| **临研安/华泰** | 服务方(TPA 经纪) |
|
|
||||||
| **保司** | 太保/大地、其他保险公司 |
|
|
||||||
|
|
||||||
## 二、流程阶段
|
|
||||||
|
|
||||||
### 阶段一:信息收集与 AI 报价
|
|
||||||
|
|
||||||
1. **登录**(申办者)
|
|
||||||
- 申办者登录系统
|
|
||||||
|
|
||||||
2. **填写项目信息**(申办者)
|
|
||||||
- 上传项目方案、知情同意书或手动填写报价必备资料
|
|
||||||
- 可通过邮件/小程序通知进入此步骤
|
|
||||||
|
|
||||||
3. **标准报价材料**(系统)
|
|
||||||
- 系统生成标准报价材料
|
|
||||||
|
|
||||||
4. **AI 报价**(系统)
|
|
||||||
- 基于标准材料进行 AI 报价
|
|
||||||
|
|
||||||
5. **报价任务创建**(系统)
|
|
||||||
- 生成报价任务文档
|
|
||||||
- 通过 **API Smart-Ops** 与 Smart-Ops 系统对接
|
|
||||||
|
|
||||||
6. **报价任务**(临研安/华泰)
|
|
||||||
- 每保司报价生成一条任务
|
|
||||||
- 跟踪每一任务的完成状态
|
|
||||||
|
|
||||||
7. **报价**(临研安/华泰)
|
|
||||||
- 发件人:RMO@vdano.com
|
|
||||||
- 向各保司发起报价请求
|
|
||||||
|
|
||||||
8. **初步评估**(保司)
|
|
||||||
- 太保/大地、其他保司分别进行初步评估
|
|
||||||
|
|
||||||
9. **核保通过**(保司)
|
|
||||||
- 保司核保通过后,生成正式报价
|
|
||||||
|
|
||||||
10. **正式报价**(保司)
|
|
||||||
- 输出正式报价文档
|
|
||||||
|
|
||||||
### 阶段二:全保司报价
|
|
||||||
|
|
||||||
11. **报价任务完成**(临研安/华泰)
|
|
||||||
- 汇总各保司报价结果
|
|
||||||
|
|
||||||
12. **页面对比呈现**(临研安/华泰)
|
|
||||||
- 保司回复到:RMO@vdano.com
|
|
||||||
- 整理形成可对比的报价方式,呈现给申办者
|
|
||||||
|
|
||||||
13. **价格确定**(保司)
|
|
||||||
- 确定最终报价
|
|
||||||
|
|
||||||
### 阶段三:议价
|
|
||||||
|
|
||||||
14. **是否含报价?**(判断)
|
|
||||||
- **否**:邮件/小程序通知 → 返回「填写项目信息」
|
|
||||||
- **是**:进入「再次登录」流程
|
|
||||||
|
|
||||||
15. **再次登录?**(申办者)
|
|
||||||
- 申办者再次登录系统
|
|
||||||
|
|
||||||
16. **在线议价**(临研安/华泰)
|
|
||||||
- 申办者与经纪人在线沟通、调整报价
|
|
||||||
|
|
||||||
17. **主动人工跟进**(临研安/华泰)
|
|
||||||
- 经纪人对未完成报价的保司进行主动跟进
|
|
||||||
|
|
||||||
## 三、流程示意(Mermaid)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph 信息收集与AI报价
|
|
||||||
A[登录] --> B[填写项目信息]
|
|
||||||
B --> C[标准报价材料]
|
|
||||||
C --> D[AI报价]
|
|
||||||
D --> E[报价任务创建]
|
|
||||||
E --> F[报价任务]
|
|
||||||
F --> G[报价]
|
|
||||||
G --> H[初步评估]
|
|
||||||
H --> I[太保/大地]
|
|
||||||
H --> J[其他保司]
|
|
||||||
I --> K[核保通过]
|
|
||||||
J --> K
|
|
||||||
K --> L[正式报价]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 全保司报价
|
|
||||||
L --> M[报价任务完成]
|
|
||||||
M --> N[页面对比呈现]
|
|
||||||
N --> O[价格确定]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 议价
|
|
||||||
O --> P{是否含报价?}
|
|
||||||
P -->|否| Q[邮件/小程序通知]
|
|
||||||
Q --> B
|
|
||||||
P -->|是| R[再次登录]
|
|
||||||
R --> S[在线议价]
|
|
||||||
S --> T[主动人工跟进]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## 四、关键说明
|
|
||||||
|
|
||||||
- **API Smart-Ops**:报价信息通过 API 传输到 Smart-Ops 系统,用于后续报价、投保、理赔等流程
|
|
||||||
- **任务跟踪**:每保司报价生成一条任务,系统跟踪每一任务的完成状态
|
|
||||||
- **回复渠道**:保司报价回复至 RMO@vdano.com
|
|
||||||
- **数据归属**:所有报价数据属于申办者,在投保人租户下呈现
|
|
||||||
386
PRD/RMO网站需求文档.md
386
PRD/RMO网站需求文档.md
|
|
@ -45,98 +45,109 @@
|
||||||
#### 2.1.1 免登录浏览区(已实现)
|
#### 2.1.1 免登录浏览区(已实现)
|
||||||
```
|
```
|
||||||
首页(免登录)
|
首页(免登录)
|
||||||
├── 主页
|
├── 首页
|
||||||
│ ├── Banner(紧凑型):生命科学风险管理的保险与保证方案;含「获取报价」入口
|
│ ├── Hero:赋能生命科学风险管理,患者安全始终第一;含「获取报价」「了解更多」入口
|
||||||
│ ├── 风险管理体系
|
│ ├── RMO价值主张 / 解决方案
|
||||||
│ │ ├── 法律法规
|
│ ├── 核心能力(患者安全专家、合作保司、团标与数字化、服务时限)
|
||||||
│ │ ├── 实践指南
|
│ ├── 知识资源(法规指南、保险知识、PV与保险、常见问题)
|
||||||
│ │ ├── 行业动态
|
│ └── 联系我们
|
||||||
│ │ └── 药物警戒
|
|
||||||
│ └── 角色专区(各方职责快捷入口)
|
|
||||||
│
|
│
|
||||||
├── 风险职责(原"各方关注")
|
├── 关于RMO(下拉)
|
||||||
│ ├── 风险职责总览
|
│ ├── RMO概述(/about/overview)
|
||||||
│ ├── 申办者职责
|
│ ├── 合作保司、专家经纪、第三方机构
|
||||||
│ ├── 持有人职责
|
│ └── 风险职责
|
||||||
│ ├── 受试者专区
|
│ ├── 风险职责总览(/concern)
|
||||||
│ ├── 研究中心
|
│ ├── 申办者职责(/sponsor)
|
||||||
│ └── CXO职责
|
│ ├── 持有人职责(/holder)
|
||||||
|
│ ├── 研究中心(/institution)
|
||||||
|
│ ├── 参与者(/participant)
|
||||||
|
│ └── CXO职责(/service-provider)
|
||||||
│
|
│
|
||||||
├── 临床试验(原RMO模式)
|
├── 解决方案(下拉)
|
||||||
|
│ ├── 药物警戒:PV服务、AI工具
|
||||||
|
│ ├── 临床保险:保险方案(/rmo-mode/insurance)、保证设计(/rmo-mode/guarantee)
|
||||||
|
│ └── 产品保险:保险方案、保证设计(/post-market/*)
|
||||||
|
│
|
||||||
|
├── 知识资源(下拉)
|
||||||
|
│ ├── PV知识(法规指南、AI应用)
|
||||||
|
│ ├── 保险知识(基础知识、国外比较、条款标准)
|
||||||
|
│ ├── 常见问题(/faq,含职责逻辑、保障范围、PV与保险)
|
||||||
|
│ └── 学习中心(案例学习、培训视频、考试中心)
|
||||||
|
│
|
||||||
|
├── 临床试验(/rmo-mode)
|
||||||
│ ├── 临床试验模块首页
|
│ ├── 临床试验模块首页
|
||||||
│ ├── 保险方案(含「获取报价」入口)
|
│ ├── 保险方案(含「获取报价」入口,跳转项目报价页)
|
||||||
│ ├── 保证方案
|
│ ├── 保证方案
|
||||||
│ └── 保险保证
|
│ └── 保险保证
|
||||||
│
|
│
|
||||||
├── 上市应用
|
├── 上市应用(/post-market)
|
||||||
|
├── 海外风险(/overseas)
|
||||||
│
|
│
|
||||||
├── 海外风险
|
├── 资源中心(/system-management)
|
||||||
│
|
|
||||||
├── 资源中心(原"体系管理")
|
|
||||||
│ ├── 资源中心首页
|
│ ├── 资源中心首页
|
||||||
|
│ ├── 法律法规(/laws)
|
||||||
│ ├── 实践指南
|
│ ├── 实践指南
|
||||||
│ ├── 培训材料
|
│ ├── 培训材料
|
||||||
│ └── 常见问题
|
│ └── 常见问题
|
||||||
│
|
│
|
||||||
└── 登录(右上角)
|
├── 联系我们(/contact)
|
||||||
|
└── 登录(右上角,/login)
|
||||||
```
|
```
|
||||||
### 2.1.2 登录后系统区(待开发)
|
### 2.1.2 登录后系统区(已实现)
|
||||||
|
登录后使用 DashboardLayout,顶部保留首页、风险职责、临床试验、上市应用、海外风险、资源中心等导航入口;侧边栏根据角色显示相应菜单。用户由管理员创建账号,无需自助注册。
|
||||||
#
|
|
||||||
登录后依然可见登录前的菜单。如果用户没有账号,引导其注册账号。所有账号将会有管理员进行审批,以使其获得查看数据的权限。
|
|
||||||
```
|
```
|
||||||
登录后系统(需权限验证)
|
登录后系统(需权限验证,已实现)
|
||||||
├── 工作台
|
├── 工作台(/dashboard)
|
||||||
│ ├── 我的保障
|
│ ├── 数据统计卡片(询价项目、生效保障、全部项目)
|
||||||
│ ├── 我的项目
|
│ ├── 待处理任务(待处理的报价项目、待处理的理赔)
|
||||||
│ ├── 待办任务
|
│ └── 快捷方式(投保人可见:获取报价、报价页面、申请理赔、方案介绍、培训支持)
|
||||||
│ └── 快捷方式(获取报价、申请保障、申请理赔)
|
|
||||||
│
|
│
|
||||||
├── 项目列表
|
├── 项目报价(需权限:投保人,/dashboard/project-quotes)
|
||||||
│ ├── 项目列表(试验项目编号、试验题目、保障范围、承保公司、承保状态)
|
│ └── PV报价、临床试验保险报价、产品责任保险报价(分表单提交)
|
||||||
│ └── 项目明细(详细信息、相关文档、操作按钮)
|
|
||||||
│
|
│
|
||||||
├── 询价列表(需权限:保险人)
|
├── 项目列表(需权限:投保人,/dashboard/projects)
|
||||||
│ └── 询价列表(申办者、项目编号、试验题目、保障范围、特别约定、成交情况)
|
│ ├── 项目列表
|
||||||
|
│ └── 项目明细(/dashboard/projects/:id)
|
||||||
│
|
│
|
||||||
├── 理赔进度(需权限:投保人、保险人)
|
├── 询价列表(需权限:保险人,/dashboard/inquiries)
|
||||||
│ └── 理赔评估列表(项目编号、试验题目、保障范围、承保公司、承保状态)
|
│ └── 询价列表、询价明细(/dashboard/inquiries/:id)
|
||||||
│
|
│
|
||||||
└── 智能工具(所有登录用户可见)
|
├── 理赔进度(需权限:投保人、保险人,/dashboard/claims)
|
||||||
|
│ └── 理赔评估列表、理赔详情(/dashboard/claims/:id)
|
||||||
|
│
|
||||||
|
└── 智能工具(/dashboard/tools)
|
||||||
├── 保费测算工具(所有登录用户可见)
|
├── 保费测算工具(所有登录用户可见)
|
||||||
├── ICF智能修改(需权限:投保人)
|
├── ICF智能修改(需权限:投保人)
|
||||||
└── 方案风险评分(需权限:投保人)
|
├── 方案风险评分(需权限:投保人)
|
||||||
|
├── 方案风险评估(所有登录用户可见,AI 评估方案风险)
|
||||||
|
└── 药安查(所有登录用户可见,药物安全数据查询)
|
||||||
```
|
```
|
||||||
### 2.2 路由与页面结构
|
### 2.2 路由与页面结构
|
||||||
|
|
||||||
#### 2.2.0 路由架构说明
|
#### 2.2.0 路由架构说明
|
||||||
- **免登录浏览区路由**:所有路由在 `App.tsx` 中定义,使用 `Layout` 组件包裹(Header + Footer)
|
- **免登录浏览区路由**:在 `router/index.ts` 中定义,使用 `Layout` 组件包裹(Header + Footer + Breadcrumb)
|
||||||
- **登录后系统路由**:所有 `/dashboard/*` 路由需使用 `ProtectedRoute` 包裹,使用 `DashboardLayout` 组件(侧边栏 + 主内容区)
|
- **登录后系统路由**:所有 `/dashboard/*` 路由使用 `ProtectedRoute` 包裹,内层使用 `DashboardLayout`(侧边栏 + 主内容区)
|
||||||
- **路由守卫**:未登录用户访问登录后页面应重定向到 `/login`
|
- **路由守卫**:`router.beforeEach` 检查登录状态,未登录访问 `/dashboard` 重定向至 `/login` 并携带 `from` 查询参数
|
||||||
|
|
||||||
### 2.3 核心页面详细需求
|
### 2.3 核心页面详细需求
|
||||||
|
|
||||||
#### 2.2.1 首页(已实现)
|
#### 2.2.1 首页(已实现)
|
||||||
**核心元素:**
|
**核心元素:**
|
||||||
- **Banner区域**(紧凑型)
|
- **Hero 区域**(全屏滚动)
|
||||||
- 标题:生命科学风险管理的保险与保证方案
|
- 标题:赋能生命科学风险管理
|
||||||
- 按钮1:保险方案(链接到 `/rmo-mode/insurance`)
|
- 副标题:患者安全始终第一
|
||||||
- 按钮2:保证方案(链接到 `/rmo-mode/guarantee`)
|
- 按钮1:了解更多(链接到 `/about/overview`)
|
||||||
- 按钮3:获取报价(打开报价申请流程:弹窗或跳转报价页面,见 2.3.2.2)
|
- 按钮2:获取报价(未登录时引导登录,已登录跳转 `/dashboard/project-quotes`)
|
||||||
- **风险管理体系区域**
|
- **RMO 价值主张/解决方案**(RmoValueProposition 组件)
|
||||||
- 展示四个环节:法律法规、实践指南、行业动态、药物警戒
|
- **核心能力**(卡片展示)
|
||||||
- 以卡片形式展示,带图标和说明文字
|
- 100+ 患者安全专家、10+ 合作保司、1st 团标与数字化、7/15 服务时限
|
||||||
- **角色专区**(各方职责快捷入口)
|
- **知识资源**(快捷入口)
|
||||||
- 申办者职责(链接到 `/sponsor`)
|
- 法规指南、保险知识、PV与保险、常见问题
|
||||||
- 持有人职责(链接到 `/holder`)
|
- **联系我们**(入口链接)
|
||||||
- 研究中心(链接到 `/institution`)
|
|
||||||
- 受试者专区(链接到 `/participant`)
|
|
||||||
- CXO职责(链接到 `/service-provider`)
|
|
||||||
|
|
||||||
**交互要求:**
|
**交互要求:**
|
||||||
- Banner区域紧凑型设计
|
- 全屏分节滚动,带指示点导航
|
||||||
- 响应式设计,适配不同屏幕尺寸
|
- 响应式设计,适配不同屏幕尺寸
|
||||||
- 所有链接按钮可点击跳转到对应页面
|
|
||||||
|
|
||||||
#### 2.2.2 临床试验页面(原RMO模式,已实现)
|
#### 2.2.2 临床试验页面(原RMO模式,已实现)
|
||||||
**路由结构:**
|
**路由结构:**
|
||||||
|
|
@ -148,18 +159,12 @@
|
||||||
**内容模块:**
|
**内容模块:**
|
||||||
|
|
||||||
1. **保险方案页面**(已实现)
|
1. **保险方案页面**(已实现)
|
||||||
- 基础保障、全面保障说明
|
- **获取报价入口**:页面内提供「获取报价」「前往报价页面」按钮;未登录点击引导登录,已登录跳转 `/dashboard/project-quotes`
|
||||||
- 保险条款标准核心内容
|
- 基础保障、全面保障、保险条款标准核心内容
|
||||||
- **获取报价入口**:页面内提供「获取报价」按钮,点击后打开报价申请流程(弹窗或报价页面,见 2.3.2.2)
|
- 保险服务内容:保险合同审查、理赔审查、保险条款修订、理赔规则制定、条款标准制定
|
||||||
- 保险服务内容:
|
|
||||||
- 保险合同审查
|
|
||||||
- 理赔审查
|
|
||||||
- 保险条款修订
|
|
||||||
- 理赔规则制定
|
|
||||||
- 条款标准制定
|
|
||||||
- 服务供应商展示(保险公司、经纪公司 logo)
|
- 服务供应商展示(保险公司、经纪公司 logo)
|
||||||
|
|
||||||
2. **保证方案页面**(已实现)
|
2. **保证方案页面**(已实现,导航中称为「保证设计」)
|
||||||
- 保证基金的基本逻辑(图示)
|
- 保证基金的基本逻辑(图示)
|
||||||
- 保证基金管理形式比较(表格)
|
- 保证基金管理形式比较(表格)
|
||||||
- **自保(专项风险管理基金)**
|
- **自保(专项风险管理基金)**
|
||||||
|
|
@ -224,54 +229,62 @@
|
||||||
#### 2.2.4 资源中心(原"体系管理",已实现)
|
#### 2.2.4 资源中心(原"体系管理",已实现)
|
||||||
**路由结构:**
|
**路由结构:**
|
||||||
- `/system-management`:资源中心首页(ResourceCenterOverview)
|
- `/system-management`:资源中心首页(ResourceCenterOverview)
|
||||||
|
- `/system-management/laws`:法律法规
|
||||||
- `/system-management/practice-guide`:实践指南
|
- `/system-management/practice-guide`:实践指南
|
||||||
- `/system-management/training`:培训材料
|
- `/system-management/training`:培训材料
|
||||||
- `/system-management/faq` 或 `/faq`:常见问题
|
- `/system-management/faq` 或 `/faq`:常见问题
|
||||||
|
|
||||||
**内容模块:**
|
**内容模块:**
|
||||||
|
|
||||||
1. **实践指南**(已实现)
|
1. **法律法规**(已实现)
|
||||||
|
- 临床试验与风险管理相关法律法规
|
||||||
|
|
||||||
|
2. **实践指南**(已实现)
|
||||||
- 操作指南文档
|
- 操作指南文档
|
||||||
- 最佳实践案例
|
- 最佳实践案例
|
||||||
- 流程规范
|
- 流程规范
|
||||||
|
|
||||||
2. **培训材料**(已实现)
|
3. **培训材料**(已实现)
|
||||||
- 培训视频
|
- 培训视频
|
||||||
- 培训文档
|
- 培训文档
|
||||||
- 培训课程
|
- 培训课程
|
||||||
|
|
||||||
3. **常见问题**(已实现)
|
4. **常见问题**(已实现)
|
||||||
- FAQ列表
|
- FAQ列表
|
||||||
- 问题分类
|
- 问题分类
|
||||||
- 搜索功能(待实现)
|
- 搜索功能(待实现)
|
||||||
|
|
||||||
#### 2.2.5 上市应用(已实现)
|
#### 2.2.5 上市应用(已实现)
|
||||||
- **路由**:`/post-market`
|
- **路由**:`/post-market`、`/post-market/insurance`、`/post-market/guarantee`
|
||||||
- **内容**:上市应用相关说明
|
- **内容**:药品上市后风险管理与药物警戒的保险与保障方案(当前为建设中占位页)
|
||||||
|
|
||||||
#### 2.2.6 海外风险(已实现)
|
#### 2.2.6 海外风险(已实现)
|
||||||
- **路由**:`/overseas`
|
- **路由**:`/overseas`
|
||||||
- **内容**:海外风险相关说明
|
- **内容**:跨境临床试验与海外市场的风险管理保险与保障(当前为建设中占位页)
|
||||||
|
|
||||||
#### 2.2.7 登录页面(已实现)
|
#### 2.2.7 登录页面(已实现)
|
||||||
- **路由**:`/login`
|
- **路由**:`/login`
|
||||||
- **功能**:账号密码登录表单
|
- **功能**:用户名/邮箱 + 密码登录表单;测试账号:admin、policyholder、insurer(密码:123456)
|
||||||
- **后续**:登录成功后根据用户角色跳转到工作台(`/dashboard`)
|
- **登录逻辑**:登录成功后跳转到工作台(`/dashboard`);支持 `from` 查询参数回跳
|
||||||
|
- **说明**:用户由管理员创建,无自助注册;记住我、忘记密码为 UI 占位
|
||||||
|
|
||||||
### 2.3 登录后系统详细需求(待开发)
|
### 2.3 登录后系统详细需求(已实现)
|
||||||
|
|
||||||
#### 2.3.1 系统架构
|
#### 2.3.1 系统架构(已实现)
|
||||||
- **布局**:侧边栏导航 + 主内容区(DashboardLayout)
|
- **布局**:侧边栏导航 + 主内容区(DashboardLayout)✅
|
||||||
- **侧边栏导航结构**:
|
- **顶部导航**:登录后仍可访问首页、风险职责、临床试验、上市应用、海外风险、资源中心
|
||||||
|
- **侧边栏导航结构**(根据角色动态显示):
|
||||||
- 工作台
|
- 工作台
|
||||||
|
- 项目报价(需权限:投保人,`/dashboard/project-quotes`)
|
||||||
- 项目列表(需权限:投保人)
|
- 项目列表(需权限:投保人)
|
||||||
- 询价列表(需权限:保险人)
|
- 询价列表(需权限:保险人)
|
||||||
- 理赔进度(需权限:投保人、保险人)
|
- 理赔进度(需权限:投保人、保险人)
|
||||||
- 智能工具(所有登录用户可见)
|
- 智能工具
|
||||||
- 保费测算工具(所有登录用户可见)
|
- 保费测算工具(所有用户)
|
||||||
- ICF智能修改(需权限:投保人)
|
- ICF智能修改(仅投保人)
|
||||||
- 方案风险评分(需权限:投保人)
|
- 方案风险评分(仅投保人)
|
||||||
- **权限控制**:侧边栏菜单根据用户角色动态显示有权限的菜单项
|
- 方案风险评估(所有用户)
|
||||||
|
- 药安查(所有用户)
|
||||||
|
|
||||||
#### 2.3.2 工作台页面(Dashboard)
|
#### 2.3.2 工作台页面(Dashboard)
|
||||||
**路径**:`/dashboard`
|
**路径**:`/dashboard`
|
||||||
|
|
@ -311,33 +324,20 @@
|
||||||
- 点击可跳转到对应详情页
|
- 点击可跳转到对应详情页
|
||||||
- 其他角色:根据权限显示相应内容
|
- 其他角色:根据权限显示相应内容
|
||||||
|
|
||||||
##### 2.3.2.3 报价申请流程(获取报价)
|
##### 2.3.2.3 报价申请流程(获取报价)(已实现)
|
||||||
**入口**:店家首页 Banner「获取报价」、保险方案页「获取报价」、登录后工作台快捷方式「获取报价」。点击后打开报价申请流程(弹窗或独立报价页面,建议统一为弹窗以保持上下文)。
|
|
||||||
|
|
||||||
**流程对应页面/弹窗需求:**
|
**入口**:首页「获取报价」、保险方案页「获取报价」、工作台快捷方式「获取报价」。当前实现为**跳转至项目报价页面** `/dashboard/project-quotes`;未登录时引导先登录。
|
||||||
|
|
||||||
1. **报价需提交的资料(第一步)**
|
**项目报价页面**(`/dashboard/project-quotes`)已实现三种报价类型,以可折叠卡片展示:
|
||||||
- **表单字段**:项目方案编号、项目标题、申办者、项目分期。
|
1. **PV报价**:药物警戒服务报价,表单含姓名、邮箱、电话、公司、职位、业务问题、验证码、隐私承诺
|
||||||
- **填写方式**:
|
2. **临床试验保险报价**:项目类型、风险等级、保障金额、受试者人数、试验周期、备注等
|
||||||
- **手动填写**:用户逐项输入上述字段。
|
3. **产品责任保险报价**:产品类型、风险等级、保障金额、销售区域等
|
||||||
- **上传项目方案**:用户上传项目方案文件,系统调用 AI 识别并解析,自动填充「项目方案编号、项目标题、申办者、项目分期」;用户可核对并修改。
|
|
||||||
- **操作**:资料填写完成后,显示「生成报价」按钮。
|
|
||||||
|
|
||||||
2. **生成报价(第二步)**
|
**临床试验保险弹窗流程**(QuoteRequestModal,已实现但当前未接入入口):
|
||||||
- 用户点击「生成报价」后,系统调用 AI 服务,基于当前资料自动生成报价。
|
- 弹窗内含:手动填写/上传项目方案(AI 识别)、项目方案编号/标题/申办者/分期、生成报价(AI)、获取精准报价
|
||||||
- 报价结果展示在同一弹窗/页面内(AI 生成报价区域)。
|
- 与需求 3.3.2.1 流程一致,可将入口改为打开弹窗以保持上下文
|
||||||
|
|
||||||
3. **获取精准报价(第三步)**
|
**权限**:项目报价页面仅投保人可见;获取报价入口对免登录用户点击时引导登录。
|
||||||
- 用户点击「获取精准报价」后,系统将报价资料整合为标准化询价内容,以 Email 发送至各保司。
|
|
||||||
- 前端可展示「已发送至保司,等待保司回复」等状态;该次报价申请进入「询价中」状态,可在工作台「待处理的报价项目」中查看。
|
|
||||||
|
|
||||||
4. **精准报价回显(系统侧 + 前端)**
|
|
||||||
- **系统侧**:从 rmo@vdano.com 拉取保司回复邮件,解析并提取各保司报价;由临研安进行审核,审核通过的报价写入报价记录并关联该次申请。
|
|
||||||
- **前端**:报价页面/报价详情支持展示「精准报价」结果(各保司、报价内容等);工作台「待处理的报价项目」中该条状态更新为「已回显精准报价」,点击可进入报价页面查看。
|
|
||||||
|
|
||||||
**路由建议**(若采用独立页面):`/quote` 或 `/dashboard/quote`(新建申请),`/dashboard/quote/:id`(查看某次报价申请及精准报价结果)。若采用弹窗则无需单独路由,弹窗内可提供「查看我的报价」链接至投保人报价列表或工作台待处理任务。
|
|
||||||
|
|
||||||
**权限**:获取报价入口对免登录用户可开放(弹窗内可引导登录后再发精准报价);登录后工作台入口仅投保人可见。
|
|
||||||
|
|
||||||
#### 2.3.3 项目列表页面(ProjectList)
|
#### 2.3.3 项目列表页面(ProjectList)
|
||||||
**路径**:`/dashboard/projects`
|
**路径**:`/dashboard/projects`
|
||||||
|
|
@ -467,17 +467,18 @@
|
||||||
- 相关文档
|
- 相关文档
|
||||||
- 操作按钮:查看明细、处理理赔
|
- 操作按钮:查看明细、处理理赔
|
||||||
|
|
||||||
#### 2.3.8 智能工具页面(Tools)
|
#### 2.3.8 智能工具页面(Tools)(已实现)
|
||||||
**路径**:`/dashboard/tools`
|
**路径**:`/dashboard/tools`
|
||||||
|
|
||||||
**权限说明**:所有登录用户可见
|
**权限说明**:所有登录用户可见
|
||||||
|
|
||||||
**工具入口页**:
|
**工具入口页**(已实现):
|
||||||
- 展示三个工具的入口卡片
|
- 展示五个工具的入口卡片
|
||||||
- 保费测算工具(所有登录用户可见)
|
- 保费测算工具(所有用户)
|
||||||
- ICF智能修改(仅投保人可见)
|
- ICF智能修改(仅投保人)
|
||||||
- 方案风险评分(仅投保人可见)
|
- 方案风险评分(仅投保人)
|
||||||
- 每个工具卡片显示:工具名称、工具描述
|
- 方案风险评估(所有用户,AI 评估方案信息不足风险与偏倚风险)
|
||||||
|
- 药安查(所有用户,药物安全数据查询、不良反应与警戒信息检索)
|
||||||
|
|
||||||
##### 2.3.8.1 保费测算工具(PremiumCalculator)
|
##### 2.3.8.1 保费测算工具(PremiumCalculator)
|
||||||
**路径**:`/dashboard/tools/premium-calculator`
|
**路径**:`/dashboard/tools/premium-calculator`
|
||||||
|
|
@ -512,6 +513,16 @@
|
||||||
- 自动计算风险评分
|
- 自动计算风险评分
|
||||||
- 显示评分结果、风险等级、改进建议
|
- 显示评分结果、风险等级、改进建议
|
||||||
|
|
||||||
|
##### 2.3.8.4 方案风险评估(ProtocolRiskAssessment)(已实现)
|
||||||
|
**路径**:`/dashboard/tools/protocol-risk`
|
||||||
|
|
||||||
|
**功能**:借助 AI 评估方案信息不足风险与偏倚风险
|
||||||
|
|
||||||
|
##### 2.3.8.5 药安查(DrugSafetyQuery)(已实现)
|
||||||
|
**路径**:`/dashboard/tools/drug-safety`
|
||||||
|
|
||||||
|
**功能**:药物安全数据查询,不良反应与警戒信息检索
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三、核心业务逻辑说明
|
## 三、核心业务逻辑说明
|
||||||
|
|
@ -766,14 +777,14 @@ flowchart TB
|
||||||
|
|
||||||
#### 4.1.1 免登录浏览区(已实现)
|
#### 4.1.1 免登录浏览区(已实现)
|
||||||
- **访问方式**:网站免登录访问,所有内容公开
|
- **访问方式**:网站免登录访问,所有内容公开
|
||||||
- **页面范围**:首页、风险职责、临床试验、上市应用、海外风险、资源中心、常见问题
|
- **页面范围**:首页、关于RMO(RMO概述、合作保司、专家经纪、第三方机构、风险职责及子页)、解决方案(药物警戒、临床保险、产品保险)、知识资源、临床试验、上市应用、海外风险、资源中心、常见问题、联系我们
|
||||||
|
|
||||||
#### 4.1.2 登录后系统(待开发)
|
#### 4.1.2 登录后系统(已实现)
|
||||||
- **登录方式**:账号、密码登录
|
- **登录方式**:用户名/邮箱 + 密码登录 ✅
|
||||||
- **用户角色**:投保人、保险人
|
- **用户角色**:投保人、保险人(AuthStore 中 user.role)✅
|
||||||
- **权限控制**:根据用户角色呈现有权限的内容
|
- **权限控制**:侧边栏、工作台内容根据角色动态显示 ✅
|
||||||
- **路由守卫**:未登录用户访问登录后页面应重定向到登录页
|
- **路由守卫**:未登录访问 `/dashboard` 重定向至 `/login`,支持 `from` 回跳 ✅
|
||||||
- **状态管理**:需要全局状态管理用户信息、角色权限、登录状态(建议使用 Context API 或 Zustand)
|
- **状态管理**:Pinia AuthStore 管理用户信息、token、登录状态,localStorage 持久化 ✅
|
||||||
|
|
||||||
#### 4.1.3 数据权限说明
|
#### 4.1.3 数据权限说明
|
||||||
- **数据权限维度**:
|
- **数据权限维度**:
|
||||||
|
|
@ -785,9 +796,9 @@ flowchart TB
|
||||||
- 保险人只能查看和操作分配给自己的项目
|
- 保险人只能查看和操作分配给自己的项目
|
||||||
- **项目状态**:信息收集、询价中、已报价、已成交、已失效
|
- **项目状态**:信息收集、询价中、已报价、已成交、已失效
|
||||||
|
|
||||||
### 4.2 登录后系统功能模块(待开发)
|
### 4.2 登录后系统功能模块(已实现)
|
||||||
|
|
||||||
#### 4.2.1 工作台(Dashboard)
|
#### 4.2.1 工作台(Dashboard)(已实现)
|
||||||
- **路径**:`/dashboard`
|
- **路径**:`/dashboard`
|
||||||
- **布局**:侧边栏导航 + 主内容区(DashboardLayout)
|
- **布局**:侧边栏导航 + 主内容区(DashboardLayout)
|
||||||
- **权限**:所有登录用户可见,但内容根据角色不同
|
- **权限**:所有登录用户可见,但内容根据角色不同
|
||||||
|
|
@ -815,15 +826,13 @@ flowchart TB
|
||||||
- 待处理的报价项目
|
- 待处理的报价项目
|
||||||
- 待处理的理赔
|
- 待处理的理赔
|
||||||
|
|
||||||
#### 4.2.2 报价申请(获取报价)
|
#### 4.2.2 报价申请(获取报价)(已实现)
|
||||||
- **入口**:首页 Banner、保险方案页、登录后工作台「获取报价」
|
- **入口**:首页、保险方案页、工作台「获取报价」→ 跳转 `/dashboard/project-quotes`
|
||||||
- **形式**:弹窗或独立页面(建议弹窗);对应业务流程图见 3.3.2.1
|
- **形式**:独立项目报价页面(已实现);临床试验保险弹窗流程(QuoteRequestModal)已实现但入口未接入
|
||||||
- **功能要点**:
|
- **项目报价页面功能**:
|
||||||
- 报价资料表单:项目方案编号、项目标题、申办者、项目分期;支持手动填写或上传项目方案由 AI 识别并自动填充
|
- PV报价、临床试验保险报价、产品责任保险报价(三种可折叠表单)
|
||||||
- 生成报价:调用 AI 生成报价并展示
|
- 各类型有独立表单字段,提交后展示提交状态
|
||||||
- 获取精准报价:系统整合资料后以 Email 发送至各保司;前端展示「询价中」等状态
|
- **临床试验保险弹窗**(QuoteRequestModal):手动填写/上传方案、AI 识别、生成报价、获取精准报价,与 3.3.2.1 流程一致
|
||||||
- 精准报价回显:系统从 rmo@vdano.com 拉取保司回复,临研安审核通过后,报价回写到该次申请,前端在报价页面/报价详情展示
|
|
||||||
- **投保人**:可在工作台「待处理的报价项目」中查看各次申请状态并进入报价详情查看精准报价
|
|
||||||
|
|
||||||
#### 4.2.3 项目列表(ProjectList)
|
#### 4.2.3 项目列表(ProjectList)
|
||||||
- **路径**:`/dashboard/projects`
|
- **路径**:`/dashboard/projects`
|
||||||
|
|
@ -870,13 +879,15 @@ flowchart TB
|
||||||
- 列表字段:项目编号、试验题目、保障范围、承保公司、承保状态
|
- 列表字段:项目编号、试验题目、保障范围、承保公司、承保状态
|
||||||
- 操作按钮:查看明细、处理理赔
|
- 操作按钮:查看明细、处理理赔
|
||||||
|
|
||||||
#### 4.2.7 智能工具(Tools)
|
#### 4.2.7 智能工具(Tools)(已实现)
|
||||||
- **路径**:`/dashboard/tools`
|
- **路径**:`/dashboard/tools`
|
||||||
- **权限**:所有登录用户可见
|
- **权限**:所有登录用户可见
|
||||||
- **工具入口页**:展示三个工具的入口卡片
|
- **工具入口页**:展示五个工具的入口卡片
|
||||||
- 保费测算工具(所有登录用户可见)
|
- 保费测算工具(所有用户)
|
||||||
- ICF智能修改(仅投保人可见)
|
- ICF智能修改(仅投保人)
|
||||||
- 方案风险评分(仅投保人可见)
|
- 方案风险评分(仅投保人)
|
||||||
|
- 方案风险评估(所有用户)
|
||||||
|
- 药安查(所有用户)
|
||||||
|
|
||||||
##### 4.2.7.1 保费测算工具(PremiumCalculator)
|
##### 4.2.7.1 保费测算工具(PremiumCalculator)
|
||||||
- **路径**:`/dashboard/tools/premium-calculator`
|
- **路径**:`/dashboard/tools/premium-calculator`
|
||||||
|
|
@ -891,6 +902,14 @@ flowchart TB
|
||||||
- **路径**:`/dashboard/tools/risk-scoring`
|
- **路径**:`/dashboard/tools/risk-scoring`
|
||||||
- **功能**:对试验方案进行风险评分
|
- **功能**:对试验方案进行风险评分
|
||||||
|
|
||||||
|
##### 4.2.7.4 方案风险评估(ProtocolRiskAssessment)
|
||||||
|
- **路径**:`/dashboard/tools/protocol-risk`
|
||||||
|
- **功能**:AI 评估方案信息不足风险与偏倚风险(已实现)
|
||||||
|
|
||||||
|
##### 4.2.7.5 药安查(DrugSafetyQuery)
|
||||||
|
- **路径**:`/dashboard/tools/drug-safety`
|
||||||
|
- **功能**:药物安全数据查询、不良反应与警戒信息检索(已实现)
|
||||||
|
|
||||||
### 4.3 内容管理(部分已实现)
|
### 4.3 内容管理(部分已实现)
|
||||||
- **内容发布**:支持发布活动动态、资源文件(待实现后台管理)
|
- **内容发布**:支持发布活动动态、资源文件(待实现后台管理)
|
||||||
- **内容分类**:支持多级分类管理(资源中心已实现分类展示)
|
- **内容分类**:支持多级分类管理(资源中心已实现分类展示)
|
||||||
|
|
@ -959,31 +978,31 @@ flowchart TB
|
||||||
## 七、技术建议
|
## 七、技术建议
|
||||||
|
|
||||||
### 7.1 技术栈(已采用)
|
### 7.1 技术栈(已采用)
|
||||||
- **前端框架**:React 18.2.0 ✅
|
- **前端框架**:Vue 3.4.0 ✅
|
||||||
- **语言**:TypeScript 5.2.2 ✅
|
- **语言**:TypeScript 5.2.2 ✅
|
||||||
- **构建工具**:Vite 5.0.8 ✅
|
- **构建工具**:Vite 5.0.8 ✅
|
||||||
- **路由**:react-router-dom 6.20.0 ✅
|
- **路由**:Vue Router 4.2.0 ✅
|
||||||
|
- **状态管理**:Pinia 2.1.0 ✅
|
||||||
- **样式**:原生 CSS + CSS 变量 ✅
|
- **样式**:原生 CSS + CSS 变量 ✅
|
||||||
- **UI组件库**:无(使用原生 CSS 实现)✅
|
- **UI组件库**:无(使用原生 CSS 实现)✅
|
||||||
|
|
||||||
### 7.2 登录后系统技术栈建议(待实现)
|
### 7.2 登录后系统技术栈(已采用)
|
||||||
- **状态管理**:Context API 或 Zustand(推荐 Zustand,简单易用)
|
- **状态管理**:Pinia(AuthStore 管理登录状态、用户信息、角色)✅
|
||||||
- **数据请求**:axios + React Query 或 SWR(用于服务端数据缓存、同步、重试)
|
- **路由守卫**:ProtectedRoute 组件(检查登录状态,未登录重定向至 /login)✅
|
||||||
- **路由守卫**:ProtectedRoute 组件(检查登录状态)
|
- **权限控制**:基于角色的权限控制(侧边栏根据 user.role 动态显示菜单)✅
|
||||||
- **权限控制**:基于角色的权限控制(RoleGuard 组件或 HOC)
|
- **数据请求**:当前为模拟数据,后续可接入 axios + 接口
|
||||||
- **表单管理**:React Hook Form(如需要复杂表单)
|
|
||||||
|
|
||||||
### 7.3 其他技术建议
|
### 7.3 其他技术
|
||||||
- **图表库**:ECharts / D3.js(用于模式图可视化,如需要)
|
- **图表库**:ECharts / D3.js(用于模式图可视化,如需要)
|
||||||
- **文件预览**:@vue-office 或类似库(用于文档预览)
|
- **文件预览**:@vue-office 或类似库(用于文档预览)
|
||||||
|
|
||||||
### 7.2 后端建议(未来扩展)
|
### 7.4 后端建议(未来扩展)
|
||||||
- **后端框架**:Node.js / Python Django / Java Spring Boot
|
- **后端框架**:Node.js / Python Django / Java Spring Boot
|
||||||
- **数据库**:MySQL / PostgreSQL
|
- **数据库**:MySQL / PostgreSQL
|
||||||
- **文件存储**:OSS / 本地存储
|
- **文件存储**:OSS / 本地存储
|
||||||
- **API设计**:RESTful API
|
- **API设计**:RESTful API
|
||||||
|
|
||||||
### 7.3 部署建议
|
### 7.5 部署建议
|
||||||
- **服务器**:云服务器(阿里云/腾讯云等)
|
- **服务器**:云服务器(阿里云/腾讯云等)
|
||||||
- **CDN**:静态资源CDN加速
|
- **CDN**:静态资源CDN加速
|
||||||
- **域名SSL**:配置HTTPS证书
|
- **域名SSL**:配置HTTPS证书
|
||||||
|
|
@ -1001,36 +1020,25 @@ flowchart TB
|
||||||
6. ✅ 资源中心(实践指南、培训材料、常见问题)
|
6. ✅ 资源中心(实践指南、培训材料、常见问题)
|
||||||
7. ✅ 登录页面(UI已实现,登录逻辑待开发)
|
7. ✅ 登录页面(UI已实现,登录逻辑待开发)
|
||||||
|
|
||||||
### 8.2 第二阶段:登录后系统(待开发)
|
### 8.2 第二阶段:登录后系统(已完成 ✅)
|
||||||
1. ⏳ 用户认证与权限系统
|
1. ✅ 用户认证与权限系统
|
||||||
- 登录接口对接
|
- 登录逻辑(Pinia AuthStore,当前为模拟接口)
|
||||||
- 全局状态管理(AuthContext)
|
- 全局状态管理(AuthStore)
|
||||||
- 路由守卫(ProtectedRoute)
|
- 路由守卫(ProtectedRoute)
|
||||||
- 角色权限控制
|
- 角色权限控制(侧边栏、工作台根据角色显示)
|
||||||
2. ⏳ 工作台页面
|
2. ✅ 工作台页面
|
||||||
- 我的保障、我的项目、待办任务
|
- 数据统计卡片、待处理任务、快捷方式
|
||||||
- 快捷方式(获取报价、申请保障、申请理赔)
|
- 投保人快捷方式:获取报价、报价页面、申请理赔、方案介绍、培训支持
|
||||||
- 待处理的报价项目(含状态与跳转报价详情)
|
3. ✅ 报价申请流程(获取报价)
|
||||||
3. ⏳ 报价申请流程(获取报价)
|
- 多入口跳转至项目报价页(/dashboard/project-quotes)
|
||||||
- 多入口(首页、保险方案页、工作台)打开报价弹窗/页面
|
- 项目报价页含 PV报价、临床试验保险报价、产品责任保险报价
|
||||||
- 报价资料表单(手动填写 + 上传方案 AI 识别)、生成报价、获取精准报价
|
- 临床试验保险弹窗(QuoteRequestModal)已实现,可接入入口
|
||||||
- 报价状态与精准报价回显(系统拉取 rmo@vdano.com、临研安审核后展示)
|
4. ✅ 项目列表模块(带权限过滤)
|
||||||
4. ⏳ 项目列表模块
|
5. ✅ 询价列表模块(保险人权限控制)
|
||||||
- 项目列表(带权限过滤)
|
6. ✅ 理赔进度模块(投保人、保险人可见)
|
||||||
- 项目明细页
|
7. ✅ 智能工具模块
|
||||||
5. ⏳ 询价列表模块(保险人)
|
- 保费测算、ICF智能修改、方案风险评分、方案风险评估、药安查
|
||||||
- 询价列表(需权限控制)
|
8. ✅ DashboardLayout(侧边栏 + 主内容区)
|
||||||
- 询价详情、处理报价
|
|
||||||
6. ⏳ 理赔进度模块(投保人、保险人)
|
|
||||||
- 理赔评估列表(需权限控制)
|
|
||||||
- 理赔详情、处理理赔
|
|
||||||
7. ⏳ 智能工具模块(所有登录用户可见)
|
|
||||||
- 保费测算工具(所有登录用户可见)
|
|
||||||
- ICF智能修改(仅投保人可见)
|
|
||||||
- 方案风险评分(仅投保人可见)
|
|
||||||
8. ⏳ 登录后系统布局
|
|
||||||
- DashboardLayout(侧边栏 + 主内容区)
|
|
||||||
- 侧边栏导航(根据角色动态显示)
|
|
||||||
|
|
||||||
### 8.3 第三阶段:功能增强(未来扩展)
|
### 8.3 第三阶段:功能增强(未来扩展)
|
||||||
1. 在线咨询功能
|
1. 在线咨询功能
|
||||||
|
|
@ -1083,22 +1091,24 @@ flowchart TB
|
||||||
- 相关图片资源:申办者和持有人责任风险管理相关图片
|
- 相关图片资源:申办者和持有人责任风险管理相关图片
|
||||||
|
|
||||||
### 10.3 项目状态
|
### 10.3 项目状态
|
||||||
- **当前版本**:v1.1
|
- **当前版本**:v1.2
|
||||||
- **开发状态**:
|
- **开发状态**:
|
||||||
- ✅ 免登录浏览区:已完成,可演示
|
- ✅ 免登录浏览区:已完成
|
||||||
- ⏳ 登录后系统:待开发
|
- ✅ 登录后系统:已完成(含工作台、项目报价、项目列表、询价列表、理赔进度、智能工具)
|
||||||
|
- ⏳ 待完善:报价精准回显、后端接口对接、上市应用/海外风险内容填充
|
||||||
|
- **技术栈**:Vue 3 + TypeScript + Vite + Pinia + Vue Router
|
||||||
- **最后更新**:2025年2月
|
- **最后更新**:2025年2月
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**文档版本**:v1.1
|
**文档版本**:v1.2
|
||||||
**创建日期**:2025年1月
|
**创建日期**:2025年1月
|
||||||
**最后更新**:2025年2月
|
**最后更新**:2025年2月(根据网站代码同步更新)
|
||||||
|
|
||||||
### 10.4 开发状态说明
|
### 10.4 开发状态说明
|
||||||
- **✅ 已实现**:功能已开发完成,需求描述与代码实现一致
|
- **✅ 已实现**:功能已开发完成,需求文档已与代码实现对齐
|
||||||
- **⏳ 待开发**:功能规划已完成,待开发实现
|
- **⏳ 待完善**:部分流程(如报价精准回显、后端接口)需后续对接
|
||||||
- **未来扩展**:功能需求已明确,优先级较低,后续版本实现
|
- **未来扩展**:上市应用/海外风险内容、在线咨询、后台管理等
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,19 @@
|
||||||
## 报价必备资料
|
## 报价必备资料
|
||||||
- 项目方案编号(必填)
|
- 项目方案编号(必填)
|
||||||
- 项目标题(必填)
|
- 项目标题(必填)
|
||||||
|
- 投保人名称(必填)
|
||||||
- 申办者名称(必填)
|
- 申办者名称(必填)
|
||||||
- 受试药物名称(必填)
|
- 受试药物名称(必填)
|
||||||
- 项目分期(I、II、III、IV期、其他:___)(必填)
|
- 项目分期(I、II、III、IV期、其他:___)(必填)
|
||||||
- 试验受试者人数(必填)
|
- 试验受试者人数(必填)
|
||||||
- 报价用途:制定项目预算;申请试验开展;
|
- 报价用途:制定项目预算;申请试验开展;
|
||||||
- 每人责任限额(万元)(选项:≤10、15、20、30、≥50)[非必填,可以AI推荐];
|
以下四项可以重复,即每次提交信息,可以申请多个保险方案报价,每一个即一个报价方案:
|
||||||
|
{- 每人责任限额(万元)(选项:≤10、15、20、30、≥50)[非必填,可以AI推荐];
|
||||||
- 累计责任限额[非必填,可以AI推荐];
|
- 累计责任限额[非必填,可以AI推荐];
|
||||||
- 每次事故免赔额[非必填,可以AI推荐];
|
- 每次事故免赔额[非必填,可以AI推荐];
|
||||||
|
- 拟投保人数 (默认为受试者总人数);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
## 申请报价页面
|
## 申请报价页面
|
||||||
1. 简洁页面:
|
1. 简洁页面:
|
||||||
|
|
@ -44,11 +49,13 @@
|
||||||
除上述简洁页面需要的字段,补充以下西信息:
|
除上述简洁页面需要的字段,补充以下西信息:
|
||||||
- 疾病类型(选项:癌症、心脏类疾病、生育类疾病、疫苗试验、其他)
|
- 疾病类型(选项:癌症、心脏类疾病、生育类疾病、疫苗试验、其他)
|
||||||
- 每一试验受试者的试验期限(月)(选项:6、(6,12]、(12,24]、(24,36]、(36,48]、(48,60]、(60,72]、(72,84]、>84)
|
- 每一试验受试者的试验期限(月)(选项:6、(6,12]、(12,24]、(24,36]、(36,48]、(48,60]、(60,72]、(72,84]、>84)
|
||||||
|
- 项目预计时长(选项:6、(6,12]、(12,24]、(24,36]、(36,48]、(48,60]、(60,72]、(72,84]、>84)
|
||||||
- 质量管理水平(选项:通过GMP、ISO等认证,质量标准要求高,管理先进、通过必要的认证,质量标准要求较高,管理较先进、其他情况)
|
- 质量管理水平(选项:通过GMP、ISO等认证,质量标准要求高,管理先进、通过必要的认证,质量标准要求较高,管理较先进、其他情况)
|
||||||
- 历史理赔赔付情况(选项:极少、较少、较多、极多)
|
- 历史理赔赔付情况(选项:极少、较少、较多、极多)
|
||||||
- 试验受试者类型(选项:儿童、老年人、孕妇、其他成年人)
|
- 试验受试者类型(选项:儿童、老年人、孕妇、其他成年人)
|
||||||
- 试验受试者健康状况(选项:良好、一般、较差)
|
- 试验受试者健康状况(选项:良好、一般、较差)
|
||||||
- 安全性监测措施(选项:完善、较完善、不完善)
|
- 安全性监测措施(选项:完善、较完善、不完善)
|
||||||
|
- 特别要求(备注对保障范围、理赔条件的要求)
|
||||||
|
|
||||||
# 报价流程
|
# 报价流程
|
||||||
- 投保人填写报价资料;
|
- 投保人填写报价资料;
|
||||||
|
|
@ -96,10 +103,125 @@ flowchart TB
|
||||||
- **回复渠道**:保司报价回复至 RMO@vdano.com
|
- **回复渠道**:保司报价回复至 RMO@vdano.com
|
||||||
- **数据归属**:所有报价数据属于申办者,在投保人租户下呈现
|
- **数据归属**:所有报价数据属于申办者,在投保人租户下呈现
|
||||||
|
|
||||||
|
# 标准报价信息表
|
||||||
|
| 项目 | 【取值】 |
|
||||||
|
|------------------------|------|
|
||||||
|
| **承保险种** | 【保险信息页面的标题】 |
|
||||||
|
| **投保人** | 【投保人名称】 |
|
||||||
|
| **被保险人** | 【申办者名称】 |
|
||||||
|
| **承保试验** | 【项目标题】 |
|
||||||
|
| **方案号** | 【项目方案编号】 |
|
||||||
|
| **研究分期** | 【项目分期】 |
|
||||||
|
| **受试者人数** | 【试验受试者人数】|
|
||||||
|
| **保单期限** | 【项目预计时长】 |
|
||||||
|
| **保单限额** | 【累计责任限额】|
|
||||||
|
| **每人责任限额** | 【每人责任限额】|
|
||||||
|
| **每次事故免赔额** | 【每次事故免赔额】|
|
||||||
|
| **特别约定** | 【特别要求】 |
|
||||||
|
|
||||||
|
|
||||||
# 与Smart-OPS的衔接
|
# 与Smart-OPS的衔接
|
||||||
- 收集到的报价信息,报价信息将传输到Smart-OPS系统,工作台进行后续的报价、投保、理赔等流程;
|
- 收集到的报价信息,报价信息将传输到Smart-OPS系统,工作台进行后续的报价、投保、理赔等流程;
|
||||||
- 临研安人员登录Smart-OPS系统,进行报价,所有报价数据属于申办者所有,该数据将在投保人租户下呈现;
|
- 临研安人员登录Smart-OPS系统,进行报价,所有报价数据属于申办者所有,该数据将在投保人租户下呈现;
|
||||||
- 临研安人员在工作台页面处理所有报价的评估、整理。
|
- 临研安人员在工作台页面处理所有报价的评估、整理。
|
||||||
-
|
|
||||||
|
|
||||||
|
# 在Smart-OPS中的操作
|
||||||
|
- 临研安/华泰经纪工作人员,登录Smart-OPS,进入保险报价菜单;
|
||||||
|
- 显示报价任务列表,列表字段包括:投保人、项目编号、项目标题、主险限额、附加险限额、免赔额、保司、报价状态;操作按钮包括:编辑、查看、发送保司、整理报价
|
||||||
|
## 报价状态包括:
|
||||||
|
- 已创建:报价任务创建;
|
||||||
|
- 初步评估:针对大地、太保广州,临研安给出的初步评估报价。
|
||||||
|
- 正式报价:保司已回复初步报价
|
||||||
|
- 报价完成:报价信息通过API接口返回到保险报价页面。
|
||||||
|
## 报价详情页面:点击编辑或查看后,进入报价任务的详情页面,该页面包括:
|
||||||
|
- 之前收集到的全部信息;
|
||||||
|
- 对应每一方案,发出给保司的邮件、回收的邮件;
|
||||||
|
|
||||||
|
## 数据呈现结构:
|
||||||
|
数据结构设计:项目、报价方案、保司报价任务三级结构
|
||||||
|
|
||||||
|
### 项目(Project)
|
||||||
|
- 项目ID
|
||||||
|
- 项目名称
|
||||||
|
- 其他项目全局信息(如申办者、阶段、试验类型等)
|
||||||
|
- 报价方案列表(QuotationSchemes)
|
||||||
|
- 研究分期
|
||||||
|
|
||||||
|
### 报价方案(QuotationScheme)
|
||||||
|
- 方案ID
|
||||||
|
- 关联项目ID
|
||||||
|
- 方案名称/标题 【以限额作为方案名称】
|
||||||
|
- 方案编号
|
||||||
|
- 受试者人数
|
||||||
|
- 项目周期/保险期限
|
||||||
|
- 主险/附加险需求
|
||||||
|
- 其他对保险的特别要求
|
||||||
|
- 保司报价任务列表(InsurerQuotationTasks)
|
||||||
|
|
||||||
|
### 保司报价任务(InsurerQuotationTask)
|
||||||
|
- 任务ID
|
||||||
|
- 关联方案ID
|
||||||
|
- 保险公司/保司名称
|
||||||
|
- 主险限额/附加险限额
|
||||||
|
- 每人/每次事故限额与免赔额
|
||||||
|
- 报价金额
|
||||||
|
- 特别约定
|
||||||
|
- 审核状态/报价状态(如已创建、初步评估、正式报价、报价完成等)
|
||||||
|
- 任务处理记录(分派、发送、回复等信息)
|
||||||
|
- 关联邮件/回收邮件集合
|
||||||
|
|
||||||
|
## Smart-OPS页面所需操作功能设计
|
||||||
|
|
||||||
|
在Smart-OPS保险报价页面,应增加以下关键操作按钮及其流程说明:
|
||||||
|
|
||||||
|
### 1. 生成标准报价信息
|
||||||
|
- **功能说明**:根据项目及方案已收集到的基础信息,一键生成标准格式的报价表,可导出为Excel或PDF,便于对保司、项目组进行沟通。
|
||||||
|
- **操作入口**:报价任务列表页、报价方案详情页增加“报价信息”按钮。
|
||||||
|
- **流程**:
|
||||||
|
- 点击按钮后,系统根据当前方案或所有报价任务信息,生成符合行业/平台模板的报价文件,可对内容进行预览和导出。
|
||||||
|
|
||||||
|
### 2. 风险评估
|
||||||
|
- **功能说明**:支持RMO团队或临研安对方案进行定量/定性的风险评估,便于后续作为保险条款的输入或决策支持。
|
||||||
|
- **操作入口**:报价方案详情页,增加“风险评估”按钮。
|
||||||
|
- **流程**:
|
||||||
|
- 点击后打开风险评估表单,根据受试药物安全特征、试验设计、受试者状态对项目风险进行评估;
|
||||||
|
- 提交后,评估结果关联于当前方案,风险评估历史可追溯。
|
||||||
|
|
||||||
|
### 3. 申请报价
|
||||||
|
- **功能说明**:将已整理好的方案信息,一键发起给各保险公司(保司)申请正式报价。
|
||||||
|
- **操作入口**:报价任务列表页、方案详情页,增加“申请报价”按钮。
|
||||||
|
- **流程**:
|
||||||
|
- 选择需申请的保司、确认发送内容/模板;
|
||||||
|
- 支持批量申请多个保司;
|
||||||
|
- 系统记录申请时间、发送状态,自动更新报价任务状态。
|
||||||
|
|
||||||
|
### 4. 整理报价
|
||||||
|
- **功能说明**:归集各保司返回的报价,生成汇总对比表,便于客户/投保人选定。
|
||||||
|
- **操作入口**:报价方案详情页、报价任务详情页,增加“整理报价”按钮。
|
||||||
|
- **流程**:
|
||||||
|
- 临研安填写各保司返回的正式报价数据;
|
||||||
|
- 一键合并生成报价对比表,可导出Excel/PDF;
|
||||||
|
- 系统标记哪家保司已完成报价,未完成自动提醒跟进。
|
||||||
|
|
||||||
|
### 5. 返回报价
|
||||||
|
- **功能说明**:选择最终确定的报价,将结果通过系统返回给投保人,并自动同步到项目报价页面。
|
||||||
|
- **操作入口**:报价方案详情页内,完成整理报价后显示“返回报价”按钮。
|
||||||
|
- **流程**:
|
||||||
|
- 确认最终方案和保司,点击“返回报价”;
|
||||||
|
- 支持填写补充说明或上传附件;
|
||||||
|
- 返回信息同步到投保人端页面。
|
||||||
|
|
||||||
|
### 功能按钮汇总(页面动线建议)
|
||||||
|
|
||||||
|
- 报价任务列表页:
|
||||||
|
- [生成标准报价信息] [申请报价] [整理报价]
|
||||||
|
- 报价方案详情页:
|
||||||
|
- [风险评估] [申请报价] [整理报价] [返回报价]
|
||||||
|
- 保司报价任务详情页:
|
||||||
|
- [整理报价] [返回报价]
|
||||||
|
|
||||||
|
> 每一个操作需在系统内形成业务日志,支持后续审计和流程追踪。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -87,6 +87,8 @@ const pathLabels: Record<string, string> = {
|
||||||
// Dashboard
|
// Dashboard
|
||||||
'/dashboard': '工作台',
|
'/dashboard': '工作台',
|
||||||
'/dashboard/project-quotes': '项目报价',
|
'/dashboard/project-quotes': '项目报价',
|
||||||
|
'/dashboard/quote-compare': '报价对比',
|
||||||
|
'/dashboard/smart-ops/quote-tasks': '保险报价(Smart-OPS)',
|
||||||
'/dashboard/projects': '项目列表',
|
'/dashboard/projects': '项目列表',
|
||||||
'/dashboard/projects/:id': '项目详情',
|
'/dashboard/projects/:id': '项目详情',
|
||||||
'/dashboard/inquiries': '询价列表',
|
'/dashboard/inquiries': '询价列表',
|
||||||
|
|
@ -111,6 +113,8 @@ function getLabel(path: string): string {
|
||||||
if (inquiryMatch) return '询价详情'
|
if (inquiryMatch) return '询价详情'
|
||||||
const claimMatch = path.match(/^(\/dashboard\/claims\/)[^/]+$/)
|
const claimMatch = path.match(/^(\/dashboard\/claims\/)[^/]+$/)
|
||||||
if (claimMatch) return '理赔详情'
|
if (claimMatch) return '理赔详情'
|
||||||
|
const quoteTaskMatch = path.match(/^(\/dashboard\/smart-ops\/quote-tasks\/)[^/]+$/)
|
||||||
|
if (quoteTaskMatch) return '报价任务详情'
|
||||||
// 按路径段匹配
|
// 按路径段匹配
|
||||||
const segments = path.split('/').filter(Boolean)
|
const segments = path.split('/').filter(Boolean)
|
||||||
const lastSegment = segments[segments.length - 1]
|
const lastSegment = segments[segments.length - 1]
|
||||||
|
|
@ -146,6 +150,8 @@ function getLabel(path: string): string {
|
||||||
coverage: '保障范围',
|
coverage: '保障范围',
|
||||||
'privacy-policy': '隐私政策',
|
'privacy-policy': '隐私政策',
|
||||||
'project-quotes': '项目报价',
|
'project-quotes': '项目报价',
|
||||||
|
'quote-compare': '报价对比',
|
||||||
|
'quote-tasks': '保险报价',
|
||||||
projects: '项目列表',
|
projects: '项目列表',
|
||||||
inquiries: '询价列表',
|
inquiries: '询价列表',
|
||||||
claims: '理赔进度',
|
claims: '理赔进度',
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@
|
||||||
<span v-show="sidebarOpen" class="nav-text">工作台</span>
|
<span v-show="sidebarOpen" class="nav-text">工作台</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<template v-if="isPolicyholder">
|
<template v-if="isPolicyholder">
|
||||||
<RouterLink to="/dashboard/project-quotes" :class="['nav-item', { active: isActiveParent('/dashboard/project-quotes') }]">
|
<RouterLink to="/dashboard/project-quotes" :class="['nav-item', { active: route.path === '/dashboard/project-quotes' }]">
|
||||||
<span class="nav-icon">💰</span>
|
<span class="nav-icon">💰</span>
|
||||||
<span v-show="sidebarOpen" class="nav-text">项目报价</span>
|
<span v-show="sidebarOpen" class="nav-text">项目报价</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink to="/dashboard/quote-compare" :class="['nav-item', { active: route.path === '/dashboard/quote-compare' }]">
|
||||||
|
<span class="nav-icon">📊</span>
|
||||||
|
<span v-show="sidebarOpen" class="nav-text">报价对比</span>
|
||||||
|
</RouterLink>
|
||||||
<RouterLink to="/dashboard/projects" :class="['nav-item', { active: isActiveParent('/dashboard/projects') }]">
|
<RouterLink to="/dashboard/projects" :class="['nav-item', { active: isActiveParent('/dashboard/projects') }]">
|
||||||
<span class="nav-icon">📋</span>
|
<span class="nav-icon">📋</span>
|
||||||
<span v-show="sidebarOpen" class="nav-text">项目列表</span>
|
<span v-show="sidebarOpen" class="nav-text">项目列表</span>
|
||||||
|
|
@ -34,6 +38,12 @@
|
||||||
<span v-show="sidebarOpen" class="nav-text">询价列表</span>
|
<span v-show="sidebarOpen" class="nav-text">询价列表</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="isTPA">
|
||||||
|
<RouterLink to="/dashboard/smart-ops/quote-tasks" :class="['nav-item', { active: isActiveParent('/dashboard/smart-ops') }]">
|
||||||
|
<span class="nav-icon">📑</span>
|
||||||
|
<span v-show="sidebarOpen" class="nav-text">保险报价(Smart-OPS)</span>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
<template v-if="isPolicyholder || isInsurer">
|
<template v-if="isPolicyholder || isInsurer">
|
||||||
<RouterLink to="/dashboard/claims" :class="['nav-item', { active: isActiveParent('/dashboard/claims') }]">
|
<RouterLink to="/dashboard/claims" :class="['nav-item', { active: isActiveParent('/dashboard/claims') }]">
|
||||||
<span class="nav-icon">📝</span>
|
<span class="nav-icon">📝</span>
|
||||||
|
|
@ -132,10 +142,11 @@ const systemPaths = ['/system-management', '/faq']
|
||||||
|
|
||||||
const isPolicyholder = computed(() => auth.user?.role === '投保人')
|
const isPolicyholder = computed(() => auth.user?.role === '投保人')
|
||||||
const isInsurer = computed(() => auth.user?.role === '保险人')
|
const isInsurer = computed(() => auth.user?.role === '保险人')
|
||||||
|
const isTPA = computed(() => auth.user?.role === '服务方')
|
||||||
|
|
||||||
const isDashboardHome = computed(() =>
|
const isDashboardHome = computed(() =>
|
||||||
route.path === '/dashboard' &&
|
route.path === '/dashboard' &&
|
||||||
!['/dashboard/projects', '/dashboard/project-quotes', '/dashboard/inquiries', '/dashboard/claims', '/dashboard/tools'].some(p => route.path.startsWith(p))
|
!['/dashboard/projects', '/dashboard/project-quotes', '/dashboard/quote-compare', '/dashboard/inquiries', '/dashboard/claims', '/dashboard/tools', '/dashboard/smart-ops'].some(p => route.path.startsWith(p))
|
||||||
)
|
)
|
||||||
|
|
||||||
function isActiveParent(path: string) {
|
function isActiveParent(path: string) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,499 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-quotes-page.clinical-active .page-body {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-quotes-page.clinical-active .section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab 布局:顶部 Tab,内容区全宽 */
|
||||||
|
.quote-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-tab:hover {
|
||||||
|
border-color: rgba(14, 165, 233, 0.5);
|
||||||
|
background: rgba(14, 165, 233, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-tab.active {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
border-color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content-wrap {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel {
|
||||||
|
padding: 0;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel.clinical-panel {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 临床试验 Tab 紧凑单屏布局 */
|
||||||
|
.clinical-compact {
|
||||||
|
height: calc(100vh - 280px);
|
||||||
|
min-height: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clinical-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clinical-card {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clinical-card .form-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-row .form-group.compact {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-row .form-group.compact label {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-row .form-group.compact input[type="file"] {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ai {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-manual {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-manual:hover:not(:disabled) {
|
||||||
|
background: var(--bg-color, #f5f5f5);
|
||||||
|
border-color: rgba(14, 165, 233, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-manual:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognized-fields {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-4 {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.compact {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.compact label {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.compact input,
|
||||||
|
.form-group.compact select {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-block.compact {
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-row .scheme-label {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-fields {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-select,
|
||||||
|
.scheme-input {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-select {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-input {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-input.scheme-num {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clinical-card .btn-add-scheme {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-inline {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-inline .btn-primary {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-row .btn-primary {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result.compact,
|
||||||
|
.success-tip.compact {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result.compact pre {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-tip.compact p {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.form-grid-4 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.clinical-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clinical-compact {
|
||||||
|
height: auto;
|
||||||
|
min-height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-4 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 临床试验 Tab 内样式 */
|
||||||
|
.quote-mode-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab.active {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
border-color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--white);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-mode-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-2 {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-detail {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-detail:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-block {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: rgba(14, 165, 233, 0.04);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-scheme {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-scheme:hover {
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-result pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-tip {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(34, 197, 94, 0.08);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-tip p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn-primary {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn-primary:hover:not(:disabled) {
|
||||||
|
background: var(--brand-primary-dark, #0284c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单通用样式 */
|
||||||
|
.tab-panel .form-group input,
|
||||||
|
.tab-panel .form-group select,
|
||||||
|
.tab-panel .form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel .form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
.quotes-cards-grid {
|
.quotes-cards-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ import ClaimDetail from '@/views/dashboard/ClaimDetail.vue'
|
||||||
import Tools from '@/views/dashboard/Tools.vue'
|
import Tools from '@/views/dashboard/Tools.vue'
|
||||||
import PremiumCalculator from '@/views/dashboard/PremiumCalculator.vue'
|
import PremiumCalculator from '@/views/dashboard/PremiumCalculator.vue'
|
||||||
import ProjectQuotes from '@/views/dashboard/ProjectQuotes.vue'
|
import ProjectQuotes from '@/views/dashboard/ProjectQuotes.vue'
|
||||||
|
import QuoteCompare from '@/views/dashboard/QuoteCompare.vue'
|
||||||
|
import QuoteTaskList from '@/views/dashboard/smart-ops/QuoteTaskList.vue'
|
||||||
|
import QuoteTaskDetail from '@/views/dashboard/smart-ops/QuoteTaskDetail.vue'
|
||||||
import ICFEditor from '@/views/dashboard/ICFEditor.vue'
|
import ICFEditor from '@/views/dashboard/ICFEditor.vue'
|
||||||
import RiskScoring from '@/views/dashboard/RiskScoring.vue'
|
import RiskScoring from '@/views/dashboard/RiskScoring.vue'
|
||||||
import ProtocolRiskAssessment from '@/views/dashboard/ProtocolRiskAssessment.vue'
|
import ProtocolRiskAssessment from '@/views/dashboard/ProtocolRiskAssessment.vue'
|
||||||
|
|
@ -122,6 +125,10 @@ const router = createRouter({
|
||||||
children: [
|
children: [
|
||||||
{ path: '', component: Dashboard },
|
{ path: '', component: Dashboard },
|
||||||
{ path: 'project-quotes', component: ProjectQuotes },
|
{ path: 'project-quotes', component: ProjectQuotes },
|
||||||
|
{ path: 'clinical-trial-quote', redirect: { path: '/dashboard/project-quotes', query: { tab: 'clinical' } } },
|
||||||
|
{ path: 'quote-compare', component: QuoteCompare },
|
||||||
|
{ path: 'smart-ops/quote-tasks', component: QuoteTaskList },
|
||||||
|
{ path: 'smart-ops/quote-tasks/:id', component: QuoteTaskDetail },
|
||||||
{ path: 'projects', component: ProjectList },
|
{ path: 'projects', component: ProjectList },
|
||||||
{ path: 'projects/:id', component: ProjectDetail },
|
{ path: 'projects/:id', component: ProjectDetail },
|
||||||
{ path: 'inquiries', component: InquiryList },
|
{ path: 'inquiries', component: InquiryList },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
export type UserRole = '投保人' | '保险人'
|
export type UserRole = '投保人' | '保险人' | '服务方'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -46,6 +46,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
user: { id: '2', name: '保险人', email: 'insurer@rmo.com', role: '保险人' },
|
user: { id: '2', name: '保险人', email: 'insurer@rmo.com', role: '保险人' },
|
||||||
token: 'mock_token_insurer'
|
token: 'mock_token_insurer'
|
||||||
},
|
},
|
||||||
|
tpa: {
|
||||||
|
user: { id: '3', name: '服务方', email: 'tpa@vdano.com', role: '服务方' },
|
||||||
|
token: 'mock_token_tpa'
|
||||||
|
},
|
||||||
admin: {
|
admin: {
|
||||||
user: { id: '1', name: '投保人', email: 'admin@rmo.com', role: '投保人' },
|
user: { id: '1', name: '投保人', email: 'admin@rmo.com', role: '投保人' },
|
||||||
token: 'mock_token_admin'
|
token: 'mock_token_admin'
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
/>
|
/>
|
||||||
<div class="form-hint">
|
<div class="form-hint">
|
||||||
<small>测试账号:admin, policyholder, insurer(密码:123456)</small>
|
<small>测试账号:admin, policyholder, insurer, tpa(密码:123456)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,224 @@
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div class="project-quotes-page">
|
<div class="project-quotes-page" :class="{ 'clinical-active': activeTab === 'clinical' }">
|
||||||
<PageHeader title="项目报价" description="选择报价类型并填写表单获取报价" />
|
<PageHeader
|
||||||
|
title="项目报价"
|
||||||
|
description="选择报价类型并填写表单获取报价。临床试验保险报价信息将传输至 Smart-OPS,由临研安向各保司询价,保司回复至 RMO@vdano.com。"
|
||||||
|
/>
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="quotes-cards-grid">
|
<!-- Tab 切换 -->
|
||||||
<!-- PV报价卡片 -->
|
<div class="quote-tabs">
|
||||||
<div class="quote-card" :class="{ expanded: expandedCard === 'pv' }">
|
<button
|
||||||
<div class="quote-card-header" @click="toggleCard('pv')">
|
:class="['quote-tab', { active: activeTab === 'pv' }]"
|
||||||
<div class="quote-card-icon">📊</div>
|
@click="activeTab = 'pv'"
|
||||||
<div class="quote-card-title-wrap">
|
>
|
||||||
<h3>PV报价</h3>
|
<span class="tab-icon">📊</span>
|
||||||
<p>药物警戒服务报价咨询</p>
|
<span class="tab-text">PV报价</span>
|
||||||
</div>
|
</button>
|
||||||
<span class="quote-card-arrow">{{ expandedCard === 'pv' ? '▼' : '▶' }}</span>
|
<button
|
||||||
</div>
|
:class="['quote-tab', { active: activeTab === 'clinical' }]"
|
||||||
<div v-show="expandedCard === 'pv'" class="quote-card-body">
|
@click="activeTab = 'clinical'"
|
||||||
<form class="quote-form" @submit.prevent="handlePvSubmit">
|
>
|
||||||
|
<span class="tab-icon">🛡️</span>
|
||||||
|
<span class="tab-text">临床试验保险报价</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['quote-tab', { active: activeTab === 'product' }]"
|
||||||
|
@click="activeTab = 'product'"
|
||||||
|
>
|
||||||
|
<span class="tab-icon">💼</span>
|
||||||
|
<span class="tab-text">产品责任保险报价</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 内容区:全宽展示 -->
|
||||||
|
<div class="tab-content-wrap">
|
||||||
|
<!-- PV报价 -->
|
||||||
|
<div v-show="activeTab === 'pv'" class="tab-panel">
|
||||||
|
<form class="quote-form" @submit.prevent="handlePvSubmit">
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pv-name">姓名 <span class="required">*</span></label>
|
<label for="pv-name">姓名 <span class="required">*</span></label>
|
||||||
<input id="pv-name" v-model="pvForm.name" type="text" placeholder="请输入姓名" required />
|
<input id="pv-name" v-model="pvForm.name" type="text" placeholder="请输入姓名" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label for="pv-email">电子邮箱 <span class="required">*</span></label>
|
||||||
<label for="pv-email">电子邮箱 <span class="required">*</span></label>
|
<input id="pv-email" v-model="pvForm.email" type="email" placeholder="请输入电子邮箱" required />
|
||||||
<input id="pv-email" v-model="pvForm.email" type="email" placeholder="请输入电子邮箱" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="pv-phone">联系电话 <span class="required">*</span></label>
|
|
||||||
<input id="pv-phone" v-model="pvForm.phone" type="tel" placeholder="请输入联系电话" required />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="pv-company">公司 <span class="required">*</span></label>
|
<div class="form-group">
|
||||||
<input id="pv-company" v-model="pvForm.company" type="text" placeholder="请输入公司名称" required />
|
<label for="pv-phone">联系电话 <span class="required">*</span></label>
|
||||||
</div>
|
<input id="pv-phone" v-model="pvForm.phone" type="tel" placeholder="请输入联系电话" required />
|
||||||
<div class="form-group">
|
|
||||||
<label for="pv-position">职位</label>
|
|
||||||
<input id="pv-position" v-model="pvForm.position" type="text" placeholder="请输入职位" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pv-question">业务相关问题 <span class="required">*</span></label>
|
<label for="pv-company">公司 <span class="required">*</span></label>
|
||||||
<textarea id="pv-question" v-model="pvForm.question" placeholder="请描述您的业务问题或需求" rows="4" required></textarea>
|
<input id="pv-company" v-model="pvForm.company" type="text" placeholder="请输入公司名称" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="pv-captcha">验证码 <span class="required">*</span></label>
|
<div class="form-group">
|
||||||
<div class="captcha-row">
|
<label for="pv-position">职位</label>
|
||||||
<input id="pv-captcha" v-model="pvForm.captcha" type="text" placeholder="请输入验证码" maxlength="4" required />
|
<input id="pv-position" v-model="pvForm.position" type="text" placeholder="请输入职位" />
|
||||||
<span class="captcha-code">{{ captchaCode }}</span>
|
</div>
|
||||||
<button type="button" class="btn-captcha-refresh" @click="refreshCaptcha">刷新</button>
|
<div class="form-group">
|
||||||
|
<label for="pv-question">业务相关问题 <span class="required">*</span></label>
|
||||||
|
<textarea id="pv-question" v-model="pvForm.question" placeholder="请描述您的业务问题或需求" rows="4" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pv-captcha">验证码 <span class="required">*</span></label>
|
||||||
|
<div class="captcha-row">
|
||||||
|
<input id="pv-captcha" v-model="pvForm.captcha" type="text" placeholder="请输入验证码" maxlength="4" required />
|
||||||
|
<span class="captcha-code">{{ captchaCode }}</span>
|
||||||
|
<button type="button" class="btn-captcha-refresh" @click="refreshCaptcha">刷新</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="privacy-commitment">
|
||||||
|
<label class="privacy-checkbox-label">
|
||||||
|
<input type="checkbox" v-model="pvPrivacyAgreed" required />
|
||||||
|
<span>我已阅读并理解该承诺、声明</span>
|
||||||
|
</label>
|
||||||
|
<p>达诺/华泰经纪及RMO生态成员承诺对您在本网页下提供的任何信息,包括您的个人信息将按照相关的法律法规及泰格医药《隐私政策》的规定进行严格保密。更多信息,详见<RouterLink to="/privacy-policy" target="_blank">《隐私政策》</RouterLink></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="pvSubmitting || !pvPrivacyAgreed">
|
||||||
|
{{ pvSubmitting ? '提交中...' : '提交' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 临床试验保险报价:紧凑单屏布局 -->
|
||||||
|
<div v-show="activeTab === 'clinical'" class="tab-panel clinical-panel clinical-compact">
|
||||||
|
<div class="clinical-grid">
|
||||||
|
<!-- 1. 报价资料 -->
|
||||||
|
<div class="form-card card clinical-card">
|
||||||
|
<h3 class="form-title">1. 报价资料</h3>
|
||||||
|
<div class="upload-row">
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>项目方案文件 <span class="required">*</span></label>
|
||||||
|
<input type="file" accept=".pdf,.doc,.docx" @change="onProtocolFileChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group compact">
|
||||||
<div class="privacy-commitment">
|
<label>知情同意书</label>
|
||||||
<label class="privacy-checkbox-label">
|
<input type="file" accept=".pdf,.doc,.docx" @change="onIcfFileChange" />
|
||||||
<input type="checkbox" v-model="pvPrivacyAgreed" required />
|
</div>
|
||||||
<span>我已阅读并理解该承诺、声明</span>
|
<button
|
||||||
</label>
|
type="button"
|
||||||
<p>达诺/华泰经纪及RMO生态成员承诺对您在本网页下提供的任何信息,包括您的个人信息将按照相关的法律法规及泰格医药《隐私政策》的规定进行严格保密。更多信息,详见<RouterLink to="/privacy-policy" target="_blank">《隐私政策》</RouterLink></p>
|
class="btn btn-primary btn-ai"
|
||||||
</div>
|
:disabled="!hasProtocolFile || aiRecognizing || manualFillMode"
|
||||||
<div class="form-actions">
|
@click="handleAiRecognize"
|
||||||
<button type="submit" class="btn btn-primary" :disabled="pvSubmitting || !pvPrivacyAgreed">
|
>
|
||||||
{{ pvSubmitting ? '提交中...' : '提交' }}
|
{{ aiRecognizing ? 'AI 识别中…' : 'AI 识别' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-manual"
|
||||||
|
:disabled="manualFillMode || aiRecognized"
|
||||||
|
@click="handleManualFill"
|
||||||
|
>
|
||||||
|
手动填写
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<!-- AI 识别或手动填写后的字段 -->
|
||||||
|
<div v-if="aiRecognized || manualFillMode" class="recognized-fields">
|
||||||
|
<div class="form-grid form-grid-4">
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>项目方案编号</label>
|
||||||
|
<input v-model="clinicalForm.projectCode" placeholder="CT-2025-001" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>项目标题</label>
|
||||||
|
<input v-model="clinicalForm.projectTitle" placeholder="试验方案标题" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>投保人</label>
|
||||||
|
<input v-model="clinicalForm.policyholder" placeholder="投保人名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>申办者</label>
|
||||||
|
<input v-model="clinicalForm.sponsor" placeholder="申办者名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>受试药物</label>
|
||||||
|
<input v-model="clinicalForm.drugName" placeholder="受试药物名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>项目分期</label>
|
||||||
|
<select v-model="clinicalForm.phase">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="I">I 期</option>
|
||||||
|
<option value="II">II 期</option>
|
||||||
|
<option value="III">III 期</option>
|
||||||
|
<option value="IV">IV 期</option>
|
||||||
|
<option value="other">其他</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>受试者人数</label>
|
||||||
|
<input v-model.number="clinicalForm.subjectCount" type="number" placeholder="人数" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label>报价用途</label>
|
||||||
|
<select v-model="clinicalForm.quotePurpose">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="budget">制定项目预算</option>
|
||||||
|
<option value="trial">申请试验开展</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 报价方案(保险方案非AI识别,始终显示) -->
|
||||||
|
<div class="form-card card clinical-card">
|
||||||
|
<h3 class="form-title">2. 报价方案</h3>
|
||||||
|
<div v-for="(scheme, idx) in clinicalForm.schemes" :key="idx" class="scheme-block compact">
|
||||||
|
<div class="scheme-row">
|
||||||
|
<span class="scheme-label">方案{{ idx + 1 }}</span>
|
||||||
|
<div class="scheme-fields">
|
||||||
|
<select v-model="scheme.perPersonLimit" class="scheme-select">
|
||||||
|
<option value="">每人限额</option>
|
||||||
|
<option value="10">≤10万</option>
|
||||||
|
<option value="15">15万</option>
|
||||||
|
<option value="20">20万</option>
|
||||||
|
<option value="30">30万</option>
|
||||||
|
<option value="50">≥50万</option>
|
||||||
|
</select>
|
||||||
|
<input v-model="scheme.aggregateLimit" class="scheme-input" placeholder="累计限额(万)" />
|
||||||
|
<input v-model="scheme.deductible" class="scheme-input" placeholder="免赔额" />
|
||||||
|
<input v-model.number="scheme.insuredCount" type="number" class="scheme-input scheme-num" placeholder="拟投保人数" />
|
||||||
|
</div>
|
||||||
|
<button v-if="clinicalForm.schemes.length > 1" type="button" class="btn-remove" @click="removeScheme(idx)">删</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-add-scheme" @click="addScheme">+ 添加方案</button>
|
||||||
|
<!-- 生成报价(仅在已填写报价资料时显示) -->
|
||||||
|
<div v-if="aiRecognized || manualFillMode" class="generate-inline">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="!canGenerateClinical || generating"
|
||||||
|
@click="handleClinicalGenerate"
|
||||||
|
>
|
||||||
|
{{ generating ? '生成中…' : '生成报价' }}
|
||||||
|
</button>
|
||||||
|
<div v-if="aiQuoteResult && !quoteTaskCreated" class="ai-result compact">
|
||||||
|
<pre>{{ aiQuoteResult }}</pre>
|
||||||
|
</div>
|
||||||
|
<div v-if="quoteTaskCreated" class="success-tip compact">
|
||||||
|
<p>✓ 报价任务已创建</p>
|
||||||
|
<RouterLink to="/dashboard/quote-compare" class="btn btn-primary btn-sm">查看报价对比</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 临床试验保险报价卡片 -->
|
<!-- 产品责任保险报价 -->
|
||||||
<div class="quote-card" :class="{ expanded: expandedCard === 'clinical' }">
|
<div v-show="activeTab === 'product'" class="tab-panel">
|
||||||
<div class="quote-card-header" @click="toggleCard('clinical')">
|
<form class="quote-form" @submit.prevent="handleProductSubmit">
|
||||||
<div class="quote-card-icon">🛡️</div>
|
<div class="form-row">
|
||||||
<div class="quote-card-title-wrap">
|
|
||||||
<h3>临床试验保险报价</h3>
|
|
||||||
<p>临床试验责任保险报价咨询</p>
|
|
||||||
</div>
|
|
||||||
<span class="quote-card-arrow">{{ expandedCard === 'clinical' ? '▼' : '▶' }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-show="expandedCard === 'clinical'" class="quote-card-body">
|
|
||||||
<form class="quote-form" @submit.prevent="handleClinicalSubmit">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-projectType">项目类型</label>
|
|
||||||
<select id="clinical-projectType" v-model="clinicalForm.projectType">
|
|
||||||
<option value="">请选择</option>
|
|
||||||
<option value="phase1">I期临床试验</option>
|
|
||||||
<option value="phase2">II期临床试验</option>
|
|
||||||
<option value="phase3">III期临床试验</option>
|
|
||||||
<option value="phase4">IV期临床试验</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-riskLevel">风险等级</label>
|
|
||||||
<select id="clinical-riskLevel" v-model="clinicalForm.riskLevel">
|
|
||||||
<option value="">请选择</option>
|
|
||||||
<option value="low">低风险</option>
|
|
||||||
<option value="medium">中风险</option>
|
|
||||||
<option value="high">高风险</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-coverage">保障金额(元)</label>
|
|
||||||
<input id="clinical-coverage" v-model="clinicalForm.coverageAmount" type="number" placeholder="请输入保障金额" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-participants">受试者人数</label>
|
|
||||||
<input id="clinical-participants" v-model="clinicalForm.participantCount" type="number" placeholder="请输入受试者人数" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-duration">试验周期(月)</label>
|
|
||||||
<input id="clinical-duration" v-model="clinicalForm.duration" type="number" placeholder="请输入试验周期" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clinical-remark">备注说明</label>
|
|
||||||
<textarea id="clinical-remark" v-model="clinicalForm.remark" placeholder="请输入其他需求说明" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="privacy-commitment">
|
|
||||||
<label class="privacy-checkbox-label">
|
|
||||||
<input type="checkbox" v-model="clinicalPrivacyAgreed" required />
|
|
||||||
<span>我已阅读并理解该承诺、声明</span>
|
|
||||||
</label>
|
|
||||||
<p>达诺/华泰经纪及RMO生态成员承诺对您在本网页下提供的任何信息,包括您的个人信息将按照相关的法律法规及泰格医药《隐私政策》的规定进行严格保密。更多信息,详见<RouterLink to="/privacy-policy" target="_blank">《隐私政策》</RouterLink></p>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-primary" :disabled="clinicalSubmitting || !clinicalPrivacyAgreed">
|
|
||||||
{{ clinicalSubmitting ? '提交中...' : '提交' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 产品责任保险报价卡片 -->
|
|
||||||
<div class="quote-card" :class="{ expanded: expandedCard === 'product' }">
|
|
||||||
<div class="quote-card-header" @click="toggleCard('product')">
|
|
||||||
<div class="quote-card-icon">💼</div>
|
|
||||||
<div class="quote-card-title-wrap">
|
|
||||||
<h3>产品责任保险报价</h3>
|
|
||||||
<p>上市后产品责任保险报价咨询</p>
|
|
||||||
</div>
|
|
||||||
<span class="quote-card-arrow">{{ expandedCard === 'product' ? '▼' : '▶' }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-show="expandedCard === 'product'" class="quote-card-body">
|
|
||||||
<form class="quote-form" @submit.prevent="handleProductSubmit">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="product-productName">产品名称</label>
|
<label for="product-productName">产品名称</label>
|
||||||
<input id="product-productName" v-model="productForm.productName" type="text" placeholder="请输入产品名称" />
|
<input id="product-productName" v-model="productForm.productName" type="text" placeholder="请输入产品名称" />
|
||||||
|
|
@ -159,34 +232,34 @@
|
||||||
<option value="other">其他</option>
|
<option value="other">其他</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-row">
|
||||||
<label for="product-coverage">保障金额(元)</label>
|
<div class="form-group">
|
||||||
<input id="product-coverage" v-model="productForm.coverageAmount" type="number" placeholder="请输入保障金额" />
|
<label for="product-coverage">保障金额(元)</label>
|
||||||
</div>
|
<input id="product-coverage" v-model="productForm.coverageAmount" type="number" placeholder="请输入保障金额" />
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-sales">年销售额(元)</label>
|
|
||||||
<input id="product-sales" v-model="productForm.annualSales" type="number" placeholder="请输入年销售额" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="product-remark">备注说明</label>
|
<label for="product-sales">年销售额(元)</label>
|
||||||
<textarea id="product-remark" v-model="productForm.remark" placeholder="请输入其他需求说明" rows="3"></textarea>
|
<input id="product-sales" v-model="productForm.annualSales" type="number" placeholder="请输入年销售额" />
|
||||||
</div>
|
</div>
|
||||||
<div class="privacy-commitment">
|
</div>
|
||||||
<label class="privacy-checkbox-label">
|
<div class="form-group">
|
||||||
<input type="checkbox" v-model="productPrivacyAgreed" required />
|
<label for="product-remark">备注说明</label>
|
||||||
<span>我已阅读并理解该承诺、声明</span>
|
<textarea id="product-remark" v-model="productForm.remark" placeholder="请输入其他需求说明" rows="3"></textarea>
|
||||||
</label>
|
</div>
|
||||||
<p>达诺/华泰经纪及RMO生态成员承诺对您在本网页下提供的任何信息,包括您的个人信息将按照相关的法律法规及泰格医药《隐私政策》的规定进行严格保密。更多信息,详见<RouterLink to="/privacy-policy" target="_blank">《隐私政策》</RouterLink></p>
|
<div class="privacy-commitment">
|
||||||
</div>
|
<label class="privacy-checkbox-label">
|
||||||
<div class="form-actions">
|
<input type="checkbox" v-model="productPrivacyAgreed" required />
|
||||||
<button type="submit" class="btn btn-primary" :disabled="productSubmitting || !productPrivacyAgreed">
|
<span>我已阅读并理解该承诺、声明</span>
|
||||||
{{ productSubmitting ? '提交中...' : '提交' }}
|
</label>
|
||||||
</button>
|
<p>达诺/华泰经纪及RMO生态成员承诺对您在本网页下提供的任何信息,包括您的个人信息将按照相关的法律法规及泰格医药《隐私政策》的规定进行严格保密。更多信息,详见<RouterLink to="/privacy-policy" target="_blank">《隐私政策》</RouterLink></p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="form-actions">
|
||||||
</div>
|
<button type="submit" class="btn btn-primary" :disabled="productSubmitting || !productPrivacyAgreed">
|
||||||
|
{{ productSubmitting ? '提交中...' : '提交' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -196,13 +269,26 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import PageContainer from '@/components/PageContainer.vue'
|
import PageContainer from '@/components/PageContainer.vue'
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
|
||||||
type ExpandedCard = 'pv' | 'clinical' | 'product' | null
|
const route = useRoute()
|
||||||
|
|
||||||
const expandedCard = ref<ExpandedCard>('pv')
|
type TabType = 'pv' | 'clinical' | 'product'
|
||||||
|
|
||||||
|
interface QuotationScheme {
|
||||||
|
perPersonLimit: string
|
||||||
|
aggregateLimit: string
|
||||||
|
deductible: string
|
||||||
|
insuredCount: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTab = ref<TabType>(
|
||||||
|
(route.query.tab as TabType) === 'clinical' ? 'clinical' :
|
||||||
|
(route.query.tab as TabType) === 'product' ? 'product' : 'pv'
|
||||||
|
)
|
||||||
|
|
||||||
const pvForm = ref({
|
const pvForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -214,13 +300,34 @@ const pvForm = ref({
|
||||||
captcha: ''
|
captcha: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const protocolFile = ref<File | null>(null)
|
||||||
|
const icfFile = ref<File | null>(null)
|
||||||
|
const aiRecognized = ref(false)
|
||||||
|
const aiRecognizing = ref(false)
|
||||||
|
const manualFillMode = ref(false)
|
||||||
|
const generating = ref(false)
|
||||||
|
const aiQuoteResult = ref('')
|
||||||
|
const quoteTaskCreated = ref(false)
|
||||||
|
|
||||||
|
const hasProtocolFile = computed(() => !!protocolFile.value)
|
||||||
|
|
||||||
const clinicalForm = ref({
|
const clinicalForm = ref({
|
||||||
projectType: '',
|
projectCode: '',
|
||||||
riskLevel: '',
|
projectTitle: '',
|
||||||
coverageAmount: '',
|
policyholder: '',
|
||||||
participantCount: '',
|
sponsor: '',
|
||||||
duration: '',
|
drugName: '',
|
||||||
remark: ''
|
phase: '',
|
||||||
|
subjectCount: undefined as number | undefined,
|
||||||
|
quotePurpose: '',
|
||||||
|
schemes: [
|
||||||
|
{
|
||||||
|
perPersonLimit: '',
|
||||||
|
aggregateLimit: '',
|
||||||
|
deductible: '',
|
||||||
|
insuredCount: undefined as number | undefined
|
||||||
|
} as QuotationScheme
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const productForm = ref({
|
const productForm = ref({
|
||||||
|
|
@ -233,12 +340,79 @@ const productForm = ref({
|
||||||
|
|
||||||
const captchaCode = ref('')
|
const captchaCode = ref('')
|
||||||
const pvPrivacyAgreed = ref(false)
|
const pvPrivacyAgreed = ref(false)
|
||||||
const clinicalPrivacyAgreed = ref(false)
|
|
||||||
const productPrivacyAgreed = ref(false)
|
const productPrivacyAgreed = ref(false)
|
||||||
const pvSubmitting = ref(false)
|
const pvSubmitting = ref(false)
|
||||||
const clinicalSubmitting = ref(false)
|
|
||||||
const productSubmitting = ref(false)
|
const productSubmitting = ref(false)
|
||||||
|
|
||||||
|
const canGenerateClinical = computed(() =>
|
||||||
|
clinicalForm.value.projectCode.trim() &&
|
||||||
|
clinicalForm.value.projectTitle.trim() &&
|
||||||
|
clinicalForm.value.policyholder.trim() &&
|
||||||
|
clinicalForm.value.sponsor.trim() &&
|
||||||
|
clinicalForm.value.drugName.trim() &&
|
||||||
|
clinicalForm.value.phase &&
|
||||||
|
(clinicalForm.value.subjectCount ?? 0) > 0 &&
|
||||||
|
clinicalForm.value.quotePurpose
|
||||||
|
)
|
||||||
|
|
||||||
|
function addScheme() {
|
||||||
|
clinicalForm.value.schemes.push({
|
||||||
|
perPersonLimit: '',
|
||||||
|
aggregateLimit: '',
|
||||||
|
deductible: '',
|
||||||
|
insuredCount: clinicalForm.value.subjectCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScheme(idx: number) {
|
||||||
|
clinicalForm.value.schemes.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProtocolFileChange(e: Event) {
|
||||||
|
protocolFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIcfFileChange(e: Event) {
|
||||||
|
icfFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAiRecognize() {
|
||||||
|
if (!protocolFile.value) return
|
||||||
|
aiRecognizing.value = true
|
||||||
|
await new Promise(r => setTimeout(r, 1200))
|
||||||
|
clinicalForm.value.projectCode = 'CT-2025-' + Math.floor(1000 + Math.random() * 9000)
|
||||||
|
clinicalForm.value.projectTitle = protocolFile.value.name.replace(/\.[^.]+$/, '') || '临床试验方案'
|
||||||
|
clinicalForm.value.policyholder = clinicalForm.value.policyholder || '示例投保人'
|
||||||
|
clinicalForm.value.sponsor = clinicalForm.value.sponsor || '示例申办者'
|
||||||
|
clinicalForm.value.drugName = clinicalForm.value.drugName || '示例药物'
|
||||||
|
clinicalForm.value.phase = clinicalForm.value.phase || 'I'
|
||||||
|
clinicalForm.value.subjectCount = clinicalForm.value.subjectCount || 100
|
||||||
|
clinicalForm.value.quotePurpose = clinicalForm.value.quotePurpose || 'budget'
|
||||||
|
clinicalForm.value.schemes[0].perPersonLimit = '20'
|
||||||
|
clinicalForm.value.schemes[0].aggregateLimit = '500'
|
||||||
|
clinicalForm.value.schemes[0].insuredCount = clinicalForm.value.subjectCount
|
||||||
|
aiRecognized.value = true
|
||||||
|
aiRecognizing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleManualFill() {
|
||||||
|
manualFillMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClinicalGenerate() {
|
||||||
|
if (!canGenerateClinical.value) return
|
||||||
|
generating.value = true
|
||||||
|
aiQuoteResult.value = ''
|
||||||
|
quoteTaskCreated.value = false
|
||||||
|
await new Promise(r => setTimeout(r, 1500))
|
||||||
|
aiQuoteResult.value =
|
||||||
|
`基于当前项目信息(${clinicalForm.value.projectTitle},${clinicalForm.value.phase}期)的预估报价:\n` +
|
||||||
|
'· 建议每人保额:80–120 万\n· 累计责任限额:400–600 万\n· 预估年保费区间:约 1.1 万–1.4 万元\n' +
|
||||||
|
'(实际以各保司精准报价为准,报价任务已创建并传输至 Smart-OPS)'
|
||||||
|
quoteTaskCreated.value = true
|
||||||
|
generating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function generateCaptcha() {
|
function generateCaptcha() {
|
||||||
captchaCode.value = Math.random().toString(36).slice(2, 6).toUpperCase()
|
captchaCode.value = Math.random().toString(36).slice(2, 6).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
@ -249,12 +423,10 @@ function refreshCaptcha() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateCaptcha()
|
generateCaptcha()
|
||||||
|
if (route.query.tab === 'clinical') activeTab.value = 'clinical'
|
||||||
|
if (route.query.tab === 'product') activeTab.value = 'product'
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleCard(card: ExpandedCard) {
|
|
||||||
expandedCard.value = expandedCard.value === card ? null : card
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePvSubmit() {
|
async function handlePvSubmit() {
|
||||||
if (pvForm.value.captcha.toUpperCase() !== captchaCode.value) {
|
if (pvForm.value.captcha.toUpperCase() !== captchaCode.value) {
|
||||||
alert('验证码错误,请重新输入')
|
alert('验证码错误,请重新输入')
|
||||||
|
|
@ -273,18 +445,6 @@ async function handlePvSubmit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClinicalSubmit() {
|
|
||||||
clinicalSubmitting.value = true
|
|
||||||
try {
|
|
||||||
await new Promise(r => setTimeout(r, 800))
|
|
||||||
alert('临床试验保险报价提交成功!我们会尽快与您联系。')
|
|
||||||
clinicalForm.value = { projectType: '', riskLevel: '', coverageAmount: '', participantCount: '', duration: '', remark: '' }
|
|
||||||
clinicalPrivacyAgreed.value = false
|
|
||||||
} finally {
|
|
||||||
clinicalSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleProductSubmit() {
|
async function handleProductSubmit() {
|
||||||
productSubmitting.value = true
|
productSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
<template>
|
||||||
|
<PageContainer>
|
||||||
|
<div class="quote-compare-page">
|
||||||
|
<PageHeader
|
||||||
|
title="报价对比"
|
||||||
|
description="各保司报价结果汇总,方便投保人对比选择。报价信息由保司回复至 RMO@vdano.com 后经临研安整理呈现。"
|
||||||
|
/>
|
||||||
|
<div class="page-body">
|
||||||
|
<section class="section">
|
||||||
|
<!-- 项目筛选 -->
|
||||||
|
<div class="filter-card card">
|
||||||
|
<div class="filter-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>项目</label>
|
||||||
|
<select v-model="selectedProject" @change="loadQuotes">
|
||||||
|
<option value="">请选择项目</option>
|
||||||
|
<option
|
||||||
|
v-for="p in mockProjects"
|
||||||
|
:key="p.id"
|
||||||
|
:value="p.id"
|
||||||
|
>
|
||||||
|
{{ p.code }} - {{ p.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 报价方案选择 -->
|
||||||
|
<div v-if="selectedProject" class="scheme-card card">
|
||||||
|
<h3 class="scheme-title">报价方案</h3>
|
||||||
|
<div class="scheme-tabs">
|
||||||
|
<button
|
||||||
|
v-for="(s, idx) in mockSchemes"
|
||||||
|
:key="idx"
|
||||||
|
:class="['scheme-tab', { active: selectedSchemeIdx === idx }]"
|
||||||
|
@click="selectedSchemeIdx = idx"
|
||||||
|
>
|
||||||
|
方案 {{ idx + 1 }}:每人 {{ s.perPerson }}万 / 累计 {{ s.aggregate }}万
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对比表格 -->
|
||||||
|
<div v-if="selectedProject && quotes.length > 0" class="compare-card card">
|
||||||
|
<h3 class="compare-title">保司报价对比</h3>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="compare-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>保司</th>
|
||||||
|
<th>报价状态</th>
|
||||||
|
<th>年保费</th>
|
||||||
|
<th>每人责任限额</th>
|
||||||
|
<th>累计责任限额</th>
|
||||||
|
<th>免赔额</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="q in quotes" :key="q.insurer">
|
||||||
|
<td class="insurer-name">{{ q.insurer }}</td>
|
||||||
|
<td>
|
||||||
|
<span :class="['status-badge', q.status]">
|
||||||
|
{{ statusText[q.status] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="premium">{{ q.premium || '--' }}</td>
|
||||||
|
<td>{{ q.perPersonLimit || '--' }}</td>
|
||||||
|
<td>{{ q.aggregateLimit || '--' }}</td>
|
||||||
|
<td>{{ q.deductible || '--' }}</td>
|
||||||
|
<td class="note">{{ q.note || '--' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="contact-tip">
|
||||||
|
如需调整报价,请联系经纪人进行线下沟通。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="selectedProject && quotes.length === 0" class="empty-state card">
|
||||||
|
<p>暂无报价数据,保司回复至 RMO@vdano.com 后经整理将在此展示。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import PageContainer from '@/components/PageContainer.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
|
||||||
|
const selectedProject = ref('')
|
||||||
|
const selectedSchemeIdx = ref(0)
|
||||||
|
|
||||||
|
const mockProjects = [
|
||||||
|
{ id: '1', code: 'CT-2025-1001', title: 'XXX药物III期临床试验' },
|
||||||
|
{ id: '2', code: 'CT-2025-1002', title: 'YYY疫苗I期临床试验' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockSchemes = [
|
||||||
|
{ perPerson: '100', aggregate: '500' },
|
||||||
|
{ perPerson: '80', aggregate: '400' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusText: Record<string, string> = {
|
||||||
|
pending: '待报价',
|
||||||
|
received: '已报价'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuoteItem {
|
||||||
|
insurer: string
|
||||||
|
status: 'pending' | 'received'
|
||||||
|
premium?: string
|
||||||
|
perPersonLimit?: string
|
||||||
|
aggregateLimit?: string
|
||||||
|
deductible?: string
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotes = ref<QuoteItem[]>([])
|
||||||
|
|
||||||
|
function loadQuotes() {
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
quotes.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 模拟数据
|
||||||
|
quotes.value = [
|
||||||
|
{ insurer: '太平洋', status: 'received', premium: '¥12,800/年', perPersonLimit: '100万', aggregateLimit: '500万', deductible: '0', note: '3个工作日内出单' },
|
||||||
|
{ insurer: '太平', status: 'received', premium: '¥11,500/年', perPersonLimit: '80万', aggregateLimit: '400万', deductible: '0', note: '支持医疗直付' },
|
||||||
|
{ insurer: '大地', status: 'received', premium: '¥13,200/年', perPersonLimit: '100万', aggregateLimit: '600万', deductible: '0', note: '含SUSAR/SAE专项' },
|
||||||
|
{ insurer: '平安', status: 'received', premium: '¥14,000/年', perPersonLimit: '120万', aggregateLimit: '800万', deductible: '0', note: '全国网点理赔' },
|
||||||
|
{ insurer: '华泰财', status: 'received', premium: '¥12,000/年', perPersonLimit: '100万', aggregateLimit: '500万', deductible: '0', note: '与经纪服务联动' },
|
||||||
|
{ insurer: '亚太', status: 'pending', premium: undefined, perPersonLimit: undefined, aggregateLimit: undefined, deductible: undefined, note: undefined }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedProject, loadQuotes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quote-compare-page {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card,
|
||||||
|
.scheme-card,
|
||||||
|
.compare-card,
|
||||||
|
.empty-state {
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--white);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-title,
|
||||||
|
.compare-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-tab {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-tab.active {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
border-color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-table th,
|
||||||
|
.compare-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-table tbody tr:hover {
|
||||||
|
background: rgba(14, 165, 233, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurer-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
max-width: 180px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.pending {
|
||||||
|
background: rgba(234, 179, 8, 0.2);
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.received {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,872 @@
|
||||||
|
<template>
|
||||||
|
<PageContainer>
|
||||||
|
<div class="quote-task-detail-page">
|
||||||
|
<PageHeader
|
||||||
|
:title="`报价任务详情 - ${task?.projectCode || ''}`"
|
||||||
|
description="查看/编辑报价任务,包含收集到的全部信息及与保司的邮件往来记录。"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<div class="header-actions-row">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showQuoteInfoModal = true">报价信息</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="showRiskModal = true">风险评估</button>
|
||||||
|
<button
|
||||||
|
v-if="task && ['created', 'preliminary'].includes(task.status)"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline"
|
||||||
|
@click="showApplyModal = true"
|
||||||
|
>
|
||||||
|
申请报价
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="task && (task.status === 'formal' || task.status === 'completed')"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline"
|
||||||
|
@click="openOrganizeModal"
|
||||||
|
>
|
||||||
|
整理报价
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="task && task.status === 'completed'"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="showReturnModal = true"
|
||||||
|
>
|
||||||
|
返回报价
|
||||||
|
</button>
|
||||||
|
<RouterLink to="/dashboard/smart-ops/quote-tasks" class="btn btn-secondary">
|
||||||
|
返回列表
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
<div class="page-body" v-if="task">
|
||||||
|
<section class="section">
|
||||||
|
<!-- Tab 切换 -->
|
||||||
|
<div class="detail-tabs">
|
||||||
|
<button
|
||||||
|
:class="['detail-tab', { active: detailTab === 'collected' }]"
|
||||||
|
@click="detailTab = 'collected'"
|
||||||
|
>
|
||||||
|
收集到的报价信息
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['detail-tab', { active: detailTab === 'standard' }]"
|
||||||
|
@click="detailTab = 'standard'"
|
||||||
|
>
|
||||||
|
标准报价信息
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['detail-tab', { active: detailTab === 'organized' }]"
|
||||||
|
@click="detailTab = 'organized'"
|
||||||
|
>
|
||||||
|
整理后的报价信息
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 1: 收集到的报价信息 -->
|
||||||
|
<div v-show="detailTab === 'collected'" class="tab-panel">
|
||||||
|
<div class="info-card card">
|
||||||
|
<h3 class="card-title">客户提供的原始信息</h3>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">投保人</span>
|
||||||
|
<span class="info-value">{{ task.policyholder }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">被保险人/申办者</span>
|
||||||
|
<span class="info-value">{{ task.sponsor }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">承保试验</span>
|
||||||
|
<span class="info-value">{{ task.projectTitle }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案号</span>
|
||||||
|
<span class="info-value">{{ task.projectCode }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">研究分期</span>
|
||||||
|
<span class="info-value">{{ task.phase }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">受试者人数</span>
|
||||||
|
<span class="info-value">{{ task.subjectCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">保单期限</span>
|
||||||
|
<span class="info-value">{{ task.policyDuration || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">累计责任限额</span>
|
||||||
|
<span class="info-value">{{ task.aggregateLimit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">每人责任限额</span>
|
||||||
|
<span class="info-value">{{ task.perPersonLimit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">每次事故免赔额</span>
|
||||||
|
<span class="info-value">{{ task.deductible || '0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item info-item-full">
|
||||||
|
<span class="info-label">特别约定</span>
|
||||||
|
<span class="info-value">{{ task.specialRequirements || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保司报价任务 & 邮件记录 -->
|
||||||
|
<div class="info-card card">
|
||||||
|
<h3 class="card-title">当前保司报价任务</h3>
|
||||||
|
<div class="insurer-info">
|
||||||
|
<div class="insurer-row">
|
||||||
|
<span class="insurer-name">{{ task.insurer }}</span>
|
||||||
|
<span :class="['status-badge', task.status]">{{ statusText[task.status] }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="task.premium" class="insurer-quote">
|
||||||
|
报价金额:<strong>{{ task.premium }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件记录 -->
|
||||||
|
<div class="info-card card">
|
||||||
|
<h3 class="card-title">邮件往来记录</h3>
|
||||||
|
<div class="mail-list">
|
||||||
|
<div v-for="(m, idx) in mockMails" :key="idx" class="mail-item">
|
||||||
|
<div class="mail-header">
|
||||||
|
<span class="mail-dir">{{ m.direction === 'sent' ? '发往保司' : '保司回复' }}</span>
|
||||||
|
<span class="mail-date">{{ m.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mail-body">
|
||||||
|
<p><strong>主题:</strong>{{ m.subject }}</p>
|
||||||
|
<p v-if="m.content" class="mail-content">{{ m.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="mockMails.length === 0" class="empty-hint">暂无邮件记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 2: 标准报价信息 -->
|
||||||
|
<div v-show="detailTab === 'standard'" class="tab-panel">
|
||||||
|
<div class="info-card card">
|
||||||
|
<h3 class="card-title">标准报价信息表</h3>
|
||||||
|
<p class="form-desc">根据收集到的信息生成的标准格式报价表,可导出 Excel/PDF 用于与保司、项目组沟通。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">承保险种</span>
|
||||||
|
<span class="info-value">临床试验责任保险</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">投保人</span>
|
||||||
|
<span class="info-value">{{ task.policyholder }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">被保险人</span>
|
||||||
|
<span class="info-value">{{ task.sponsor }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">承保试验</span>
|
||||||
|
<span class="info-value">{{ task.projectTitle }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案号</span>
|
||||||
|
<span class="info-value">{{ task.projectCode }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">研究分期</span>
|
||||||
|
<span class="info-value">{{ task.phase }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">受试者人数</span>
|
||||||
|
<span class="info-value">{{ task.subjectCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">保单期限</span>
|
||||||
|
<span class="info-value">{{ task.policyDuration || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">保单限额</span>
|
||||||
|
<span class="info-value">{{ task.aggregateLimit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">每人责任限额</span>
|
||||||
|
<span class="info-value">{{ task.perPersonLimit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">每次事故免赔额</span>
|
||||||
|
<span class="info-value">{{ task.deductible || '0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item info-item-full">
|
||||||
|
<span class="info-label">特别约定</span>
|
||||||
|
<span class="info-value">{{ task.specialRequirements || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-actions">
|
||||||
|
<button type="button" class="btn btn-outline" @click="exportQuoteInfo('excel')">导出 Excel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="exportQuoteInfo('pdf')">导出 PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 3: 整理后的报价信息(各保司报价回复) -->
|
||||||
|
<div v-show="detailTab === 'organized'" class="tab-panel">
|
||||||
|
<div class="info-card card">
|
||||||
|
<h3 class="card-title">各保司报价回复汇总</h3>
|
||||||
|
<p class="form-desc">归集各保司返回的正式报价,便于客户/投保人对比选定。</p>
|
||||||
|
<div class="organize-table-wrap">
|
||||||
|
<table class="organize-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>保司</th>
|
||||||
|
<th>报价状态</th>
|
||||||
|
<th>年保费</th>
|
||||||
|
<th>每人责任限额</th>
|
||||||
|
<th>累计责任限额</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="q in organizedQuotes" :key="q.insurer">
|
||||||
|
<td>{{ q.insurer }}</td>
|
||||||
|
<td><span :class="['status-badge', q.status]">{{ statusText[q.status] }}</span></td>
|
||||||
|
<td>{{ q.premium || '--' }}</td>
|
||||||
|
<td>{{ q.perPersonLimit || '--' }}</td>
|
||||||
|
<td>{{ q.aggregateLimit || '--' }}</td>
|
||||||
|
<td>{{ q.note || '--' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p v-if="organizedQuotes.length === 0" class="empty-hint">暂无整理后的报价数据</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div v-else class="loading-state">加载中…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 报价信息(生成标准报价信息)弹窗 -->
|
||||||
|
<div v-if="showQuoteInfoModal" class="modal-overlay" @click.self="showQuoteInfoModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>生成标准报价信息</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showQuoteInfoModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>根据当前方案信息生成符合行业/平台模板的报价文件,可导出 Excel 或 PDF。</p>
|
||||||
|
<div class="modal-preview">
|
||||||
|
<p>承保险种、投保人、被保险人、承保试验、方案号、研究分期、受试者人数、保单期限、保单限额等</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showQuoteInfoModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="exportQuoteInfo('excel')">导出 Excel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="exportQuoteInfo('pdf')">导出 PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 风险评估 弹窗 -->
|
||||||
|
<div v-if="showRiskModal" class="modal-overlay" @click.self="showRiskModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>风险评估</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showRiskModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>根据受试药物安全特征、试验设计、受试者状态对项目风险进行定量/定性评估。</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>受试药物安全特征</label>
|
||||||
|
<select v-model="riskForm.drugSafety">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="low">低风险</option>
|
||||||
|
<option value="medium">中风险</option>
|
||||||
|
<option value="high">高风险</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>试验设计风险</label>
|
||||||
|
<select v-model="riskForm.trialDesign">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="low">低</option>
|
||||||
|
<option value="medium">中</option>
|
||||||
|
<option value="high">高</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>受试者状态风险</label>
|
||||||
|
<select v-model="riskForm.subjectStatus">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="low">低</option>
|
||||||
|
<option value="medium">中</option>
|
||||||
|
<option value="high">高</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>综合评估说明</label>
|
||||||
|
<textarea v-model="riskForm.remark" rows="3" placeholder="补充说明"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showRiskModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="submitRiskAssessment">提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 申请报价 弹窗 -->
|
||||||
|
<div v-if="showApplyModal" class="modal-overlay" @click.self="showApplyModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>申请报价</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showApplyModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>将方案信息发起给 {{ task?.insurer }} 申请正式报价。</p>
|
||||||
|
<p class="form-hint">确认发送内容及模板后,系统将记录申请时间并更新任务状态。</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showApplyModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="confirmApplyQuote">确认申请</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 整理报价 弹窗 -->
|
||||||
|
<div v-if="showOrganizeModal" class="modal-overlay" @click.self="showOrganizeModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>整理报价</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showOrganizeModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>填写 {{ task?.insurer }} 返回的正式报价数据。</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>年保费</label>
|
||||||
|
<input v-model="organizeForm.premium" type="text" placeholder="如:¥12,800/年" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>每人责任限额</label>
|
||||||
|
<input v-model="organizeForm.perPersonLimit" type="text" placeholder="如:100万" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>累计责任限额</label>
|
||||||
|
<input v-model="organizeForm.aggregateLimit" type="text" placeholder="如:500万" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showOrganizeModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="confirmOrganize">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 返回报价 弹窗 -->
|
||||||
|
<div v-if="showReturnModal" class="modal-overlay" @click.self="showReturnModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>返回报价</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showReturnModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>确认最终方案和保司,将报价结果返回给投保人,并同步到项目报价页面。</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>补充说明</label>
|
||||||
|
<textarea v-model="returnForm.remark" rows="3" placeholder="可选"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showReturnModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="confirmReturnQuote">确认返回</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import PageContainer from '@/components/PageContainer.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
|
||||||
|
interface QuoteTaskDetail {
|
||||||
|
id: string
|
||||||
|
policyholder: string
|
||||||
|
sponsor: string
|
||||||
|
projectTitle: string
|
||||||
|
projectCode: string
|
||||||
|
phase: string
|
||||||
|
subjectCount: number
|
||||||
|
policyDuration?: string
|
||||||
|
aggregateLimit: string
|
||||||
|
perPersonLimit: string
|
||||||
|
deductible?: string
|
||||||
|
specialRequirements?: string
|
||||||
|
insurer: string
|
||||||
|
status: 'created' | 'preliminary' | 'formal' | 'completed'
|
||||||
|
premium?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const task = ref<QuoteTaskDetail | null>(null)
|
||||||
|
const showQuoteInfoModal = ref(false)
|
||||||
|
const showRiskModal = ref(false)
|
||||||
|
const showApplyModal = ref(false)
|
||||||
|
const showOrganizeModal = ref(false)
|
||||||
|
const showReturnModal = ref(false)
|
||||||
|
const detailTab = ref<'collected' | 'standard' | 'organized'>('collected')
|
||||||
|
const riskForm = ref({ drugSafety: '', trialDesign: '', subjectStatus: '', remark: '' })
|
||||||
|
const organizeForm = ref({ premium: '', perPersonLimit: '', aggregateLimit: '' })
|
||||||
|
const returnForm = ref({ remark: '' })
|
||||||
|
|
||||||
|
interface OrganizedQuote {
|
||||||
|
insurer: string
|
||||||
|
status: string
|
||||||
|
premium?: string
|
||||||
|
perPersonLimit?: string
|
||||||
|
aggregateLimit?: string
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
const organizedQuotes = ref<OrganizedQuote[]>([
|
||||||
|
{ insurer: '太平洋', status: 'completed', premium: '¥12,800/年', perPersonLimit: '100万', aggregateLimit: '500万', note: '3个工作日内出单' },
|
||||||
|
{ insurer: '大地', status: 'formal', premium: '¥13,200/年', perPersonLimit: '100万', aggregateLimit: '600万', note: '含SUSAR/SAE专项' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusText: Record<string, string> = {
|
||||||
|
created: '已创建',
|
||||||
|
preliminary: '初步评估',
|
||||||
|
formal: '正式报价',
|
||||||
|
completed: '报价完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockMails = ref([
|
||||||
|
{ direction: 'sent' as const, date: '2025-02-15 10:00', subject: '临床试验责任保险询价 - CT-2025-1001', content: '请见附件报价资料,请于5个工作日内回复。' },
|
||||||
|
{ direction: 'received' as const, date: '2025-02-16 14:30', subject: 'Re: 临床试验责任保险询价 - CT-2025-1001', content: '报价已出,详见附件。' }
|
||||||
|
])
|
||||||
|
|
||||||
|
function loadTask() {
|
||||||
|
const id = route.params.id as string
|
||||||
|
// 模拟数据
|
||||||
|
task.value = {
|
||||||
|
id,
|
||||||
|
policyholder: '示例制药',
|
||||||
|
sponsor: '示例制药有限公司',
|
||||||
|
projectTitle: 'XXX药物III期临床试验',
|
||||||
|
projectCode: 'CT-2025-1001',
|
||||||
|
phase: 'III期',
|
||||||
|
subjectCount: 300,
|
||||||
|
policyDuration: '(12,24] 月',
|
||||||
|
aggregateLimit: '500万',
|
||||||
|
perPersonLimit: '100万',
|
||||||
|
deductible: '0',
|
||||||
|
specialRequirements: '含SUSAR/SAE专项保障',
|
||||||
|
insurer: '太平洋',
|
||||||
|
status: 'completed',
|
||||||
|
premium: '¥12,800/年'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportQuoteInfo(format: string) {
|
||||||
|
alert(`导出标准报价信息(${format})成功`)
|
||||||
|
if (showQuoteInfoModal.value) showQuoteInfoModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitRiskAssessment() {
|
||||||
|
alert('风险评估已提交,评估结果已关联当前方案')
|
||||||
|
showRiskModal.value = false
|
||||||
|
riskForm.value = { drugSafety: '', trialDesign: '', subjectStatus: '', remark: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmApplyQuote() {
|
||||||
|
alert(`已向 ${task.value?.insurer} 发起报价申请`)
|
||||||
|
showApplyModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrganizeModal() {
|
||||||
|
if (task.value) {
|
||||||
|
const existing = organizedQuotes.value.find(q => q.insurer === task.value!.insurer)
|
||||||
|
organizeForm.value = existing
|
||||||
|
? { premium: existing.premium || '', perPersonLimit: existing.perPersonLimit || '', aggregateLimit: existing.aggregateLimit || '' }
|
||||||
|
: { premium: task.value.premium || '', perPersonLimit: task.value.perPersonLimit || '', aggregateLimit: task.value.aggregateLimit || '' }
|
||||||
|
}
|
||||||
|
showOrganizeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmOrganize() {
|
||||||
|
if (task.value) {
|
||||||
|
const existing = organizedQuotes.value.find(q => q.insurer === task.value!.insurer)
|
||||||
|
if (existing) {
|
||||||
|
existing.premium = organizeForm.value.premium
|
||||||
|
existing.perPersonLimit = organizeForm.value.perPersonLimit
|
||||||
|
existing.aggregateLimit = organizeForm.value.aggregateLimit
|
||||||
|
} else {
|
||||||
|
organizedQuotes.value.push({
|
||||||
|
insurer: task.value.insurer,
|
||||||
|
status: task.value.status,
|
||||||
|
premium: organizeForm.value.premium,
|
||||||
|
perPersonLimit: organizeForm.value.perPersonLimit,
|
||||||
|
aggregateLimit: organizeForm.value.aggregateLimit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert('报价数据已保存')
|
||||||
|
showOrganizeModal.value = false
|
||||||
|
organizeForm.value = { premium: '', perPersonLimit: '', aggregateLimit: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmReturnQuote() {
|
||||||
|
alert('报价已返回投保人,已同步到项目报价页面')
|
||||||
|
showReturnModal.value = false
|
||||||
|
returnForm.value = { remark: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadTask)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quote-task-detail-page {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tab.active {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
border-color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table th,
|
||||||
|
.organize-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table th {
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--white);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurer-info {
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurer-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.created { background: rgba(107, 114, 128, 0.2); color: #4b5563; }
|
||||||
|
.status-badge.preliminary { background: rgba(234, 179, 8, 0.2); color: #b45309; }
|
||||||
|
.status-badge.formal { background: rgba(59, 130, 246, 0.2); color: #1d4ed8; }
|
||||||
|
.status-badge.completed { background: rgba(34, 197, 94, 0.2); color: #15803d; }
|
||||||
|
|
||||||
|
.insurer-quote {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-color, #f9fafb);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-dir {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-body p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-content {
|
||||||
|
margin-top: 8px !important;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
padding: 48px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
border: 1px solid var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 480px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-preview {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-color, #f9fafb);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .form-group input,
|
||||||
|
.modal-body .form-group select,
|
||||||
|
.modal-body .form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,584 @@
|
||||||
|
<template>
|
||||||
|
<PageContainer>
|
||||||
|
<div class="quote-task-list-page">
|
||||||
|
<PageHeader
|
||||||
|
title="保险报价(Smart-OPS)"
|
||||||
|
description="临研安/华泰经纪工作台:管理报价任务,发送保司询价,整理报价结果。保司回复至 RMO@vdano.com。"
|
||||||
|
/>
|
||||||
|
<div class="page-body">
|
||||||
|
<section class="section">
|
||||||
|
<div class="list-card card">
|
||||||
|
<div class="list-toolbar">
|
||||||
|
<div class="filter-row">
|
||||||
|
<input
|
||||||
|
v-model="keyword"
|
||||||
|
type="text"
|
||||||
|
placeholder="项目编号 / 项目标题"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<select v-model="statusFilter" class="status-select">
|
||||||
|
<option value="">全部状态</option>
|
||||||
|
<option value="created">已创建</option>
|
||||||
|
<option value="preliminary">初步评估</option>
|
||||||
|
<option value="formal">正式报价</option>
|
||||||
|
<option value="completed">报价完成</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary" @click="loadTasks">查询</button>
|
||||||
|
</div>
|
||||||
|
<div class="action-toolbar">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showQuoteInfoModal = true">
|
||||||
|
生成标准报价信息
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="showApplyQuoteModal = true">
|
||||||
|
申请报价
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="openOrganizeModal">
|
||||||
|
整理报价
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="task-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>投保人</th>
|
||||||
|
<th>项目编号</th>
|
||||||
|
<th>项目标题</th>
|
||||||
|
<th>主险限额</th>
|
||||||
|
<th>附加险限额</th>
|
||||||
|
<th>免赔额</th>
|
||||||
|
<th>保司</th>
|
||||||
|
<th>报价状态</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="t in filteredTasks" :key="t.id">
|
||||||
|
<td>{{ t.policyholder }}</td>
|
||||||
|
<td>{{ t.projectCode }}</td>
|
||||||
|
<td class="project-title">{{ t.projectTitle }}</td>
|
||||||
|
<td>{{ t.mainLimit }}</td>
|
||||||
|
<td>{{ t.addOnLimit || '--' }}</td>
|
||||||
|
<td>{{ t.deductible || '--' }}</td>
|
||||||
|
<td>{{ t.insurer }}</td>
|
||||||
|
<td>
|
||||||
|
<span :class="['status-badge', t.status]">
|
||||||
|
{{ statusText[t.status] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-btns">
|
||||||
|
<RouterLink :to="`/dashboard/smart-ops/quote-tasks/${t.id}`" class="btn-link">
|
||||||
|
编辑
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink :to="`/dashboard/smart-ops/quote-tasks/${t.id}`" class="btn-link">
|
||||||
|
查看
|
||||||
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
v-if="['created', 'preliminary'].includes(t.status)"
|
||||||
|
type="button"
|
||||||
|
class="btn-link"
|
||||||
|
@click="handleApplyQuote(t)"
|
||||||
|
>
|
||||||
|
申请报价
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-link"
|
||||||
|
@click="handleQuoteInfo(t)"
|
||||||
|
>
|
||||||
|
报价信息
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="t.status === 'formal' || t.status === 'completed'"
|
||||||
|
type="button"
|
||||||
|
class="btn-link"
|
||||||
|
@click="handleOrganizeQuote(t)"
|
||||||
|
>
|
||||||
|
整理报价
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-if="filteredTasks.length === 0" class="empty-state">
|
||||||
|
暂无报价任务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成标准报价信息 弹窗 -->
|
||||||
|
<div v-if="showQuoteInfoModal" class="modal-overlay" @click.self="showQuoteInfoModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>生成标准报价信息</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showQuoteInfoModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>根据已收集的基础信息生成符合行业/平台模板的报价文件,可预览及导出。</p>
|
||||||
|
<div class="modal-preview">
|
||||||
|
<p>预览:标准报价信息表(承保险种、投保人、被保险人、承保试验、方案号、研究分期、受试者人数、保单期限、保单限额等)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showQuoteInfoModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="exportQuoteInfo('excel')">导出 Excel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="exportQuoteInfo('pdf')">导出 PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 申请报价 弹窗 -->
|
||||||
|
<div v-if="showApplyQuoteModal" class="modal-overlay" @click.self="showApplyQuoteModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>申请报价</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showApplyQuoteModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>选择需申请的保司,确认发送内容及模板,支持批量申请。</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选择保司</label>
|
||||||
|
<div class="insurer-checkboxes">
|
||||||
|
<label v-for="ins in ['太平洋', '太平', '大地', '平安', '华泰财', '亚太']" :key="ins" class="checkbox-label">
|
||||||
|
<input type="checkbox" :value="ins" v-model="selectedInsurers" />
|
||||||
|
{{ ins }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showApplyQuoteModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="confirmApplyQuote">确认申请</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 整理报价 弹窗 -->
|
||||||
|
<div v-if="showOrganizeModal" class="modal-overlay" @click.self="showOrganizeModal = false">
|
||||||
|
<div class="modal-content modal-wide">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>整理报价</h3>
|
||||||
|
<button type="button" class="modal-close" @click="showOrganizeModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>归集各保司返回的报价,生成汇总对比表,可导出 Excel/PDF。</p>
|
||||||
|
<div class="organize-table-wrap">
|
||||||
|
<p v-if="filteredTasks.length === 0" class="empty-hint">暂无报价任务可整理</p>
|
||||||
|
<table v-else class="organize-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>保司</th>
|
||||||
|
<th>报价状态</th>
|
||||||
|
<th>年保费</th>
|
||||||
|
<th>每人限额</th>
|
||||||
|
<th>累计限额</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="t in filteredTasks" :key="t.id">
|
||||||
|
<td>{{ t.insurer }}</td>
|
||||||
|
<td><span :class="['status-badge', t.status]">{{ statusText[t.status] }}</span></td>
|
||||||
|
<td><input v-model="organizeData[t.id].premium" type="text" placeholder="填写" class="organize-input" /></td>
|
||||||
|
<td><input v-model="organizeData[t.id].perPerson" type="text" placeholder="填写" class="organize-input" /></td>
|
||||||
|
<td><input v-model="organizeData[t.id].aggregate" type="text" placeholder="填写" class="organize-input" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" @click="showOrganizeModal = false">取消</button>
|
||||||
|
<button type="button" class="btn btn-outline" @click="exportOrganize('excel')">导出 Excel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="confirmOrganize">保存并生成对比表</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import PageContainer from '@/components/PageContainer.vue'
|
||||||
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
|
||||||
|
interface QuoteTask {
|
||||||
|
id: string
|
||||||
|
policyholder: string
|
||||||
|
projectCode: string
|
||||||
|
projectTitle: string
|
||||||
|
mainLimit: string
|
||||||
|
addOnLimit?: string
|
||||||
|
deductible?: string
|
||||||
|
insurer: string
|
||||||
|
status: 'created' | 'preliminary' | 'formal' | 'completed'
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const statusFilter = ref('')
|
||||||
|
const showQuoteInfoModal = ref(false)
|
||||||
|
const showApplyQuoteModal = ref(false)
|
||||||
|
const showOrganizeModal = ref(false)
|
||||||
|
const selectedInsurers = ref<string[]>([])
|
||||||
|
const organizeData = ref<Record<string, { premium?: string; perPerson?: string; aggregate?: string }>>({})
|
||||||
|
|
||||||
|
const statusText: Record<string, string> = {
|
||||||
|
created: '已创建',
|
||||||
|
preliminary: '初步评估',
|
||||||
|
formal: '正式报价',
|
||||||
|
completed: '报价完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = ref<QuoteTask[]>([])
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
let list = tasks.value
|
||||||
|
if (keyword.value.trim()) {
|
||||||
|
const k = keyword.value.toLowerCase()
|
||||||
|
list = list.filter(t =>
|
||||||
|
t.projectCode.toLowerCase().includes(k) ||
|
||||||
|
t.projectTitle.toLowerCase().includes(k)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (statusFilter.value) {
|
||||||
|
list = list.filter(t => t.status === statusFilter.value)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadTasks() {
|
||||||
|
// 模拟数据
|
||||||
|
tasks.value = [
|
||||||
|
{ id: '1', policyholder: '示例制药', projectCode: 'CT-2025-1001', projectTitle: 'XXX药物III期临床试验', mainLimit: '100万/人', addOnLimit: '500万', deductible: '0', insurer: '太平洋', status: 'completed' },
|
||||||
|
{ id: '2', policyholder: '示例制药', projectCode: 'CT-2025-1001', projectTitle: 'XXX药物III期临床试验', mainLimit: '100万/人', addOnLimit: '500万', deductible: '0', insurer: '大地', status: 'formal' },
|
||||||
|
{ id: '3', policyholder: '示例制药', projectCode: 'CT-2025-1001', projectTitle: 'XXX药物III期临床试验', mainLimit: '80万/人', addOnLimit: '400万', deductible: '0', insurer: '太平', status: 'preliminary' },
|
||||||
|
{ id: '4', policyholder: '示例生物', projectCode: 'CT-2025-1002', projectTitle: 'YYY疫苗I期临床试验', mainLimit: '50万/人', addOnLimit: '200万', deductible: '0', insurer: '太保', status: 'created' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApplyQuote(t: QuoteTask) {
|
||||||
|
selectedInsurers.value = [t.insurer]
|
||||||
|
showApplyQuoteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuoteInfo(t: QuoteTask) {
|
||||||
|
showQuoteInfoModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrganizeModal() {
|
||||||
|
const list = filteredTasks.value
|
||||||
|
organizeData.value = list.length ? list.reduce((acc, x) => ({ ...acc, [x.id]: {} }), {} as Record<string, { premium?: string; perPerson?: string; aggregate?: string }>) : {}
|
||||||
|
showOrganizeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOrganizeQuote(t: QuoteTask) {
|
||||||
|
openOrganizeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportQuoteInfo(format: string) {
|
||||||
|
alert(`导出标准报价信息(${format})成功`)
|
||||||
|
showQuoteInfoModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmApplyQuote() {
|
||||||
|
if (selectedInsurers.value.length === 0) {
|
||||||
|
alert('请至少选择一家保司')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert(`已向 ${selectedInsurers.value.join('、')} 发起报价申请`)
|
||||||
|
showApplyQuoteModal.value = false
|
||||||
|
selectedInsurers.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmOrganize() {
|
||||||
|
alert('报价已整理,对比表已生成')
|
||||||
|
showOrganizeModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportOrganize(format: string) {
|
||||||
|
alert(`导出报价对比表(${format})成功`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadTasks)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quote-task-list-page {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
padding: 24px 28px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--white);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-toolbar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 220px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
width: 140px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--brand-primary, #0ea5e9);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-table th,
|
||||||
|
.task-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand-text-default);
|
||||||
|
background: rgba(14, 165, 233, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.created {
|
||||||
|
background: rgba(107, 114, 128, 0.2);
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.preliminary {
|
||||||
|
background: rgba(234, 179, 8, 0.2);
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.formal {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.completed {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 48px 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--brand-primary, #0ea5e9);
|
||||||
|
border: 1px solid var(--brand-primary, #0ea5e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 480px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.modal-wide {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-preview {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-color, #f9fafb);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurer-checkboxes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table th,
|
||||||
|
.organize-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-table th {
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organize-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue