RMO-Front/src/views/dashboard/QuoteCompare.vue

411 lines
11 KiB
Vue

<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>
<p class="compare-desc">归集各保司返回的正式报价,按多维度对比呈现,便于选定。</p>
<div class="table-wrap">
<table class="compare-table insurer-compare-table transposed">
<thead>
<tr>
<th class="th-sticky">维度</th>
<th v-for="q in quotes" :key="q.insurer">{{ q.insurer }}</th>
</tr>
</thead>
<tbody>
<template v-for="(group, gIdx) in compareDimensionGroups" :key="gIdx">
<tr v-if="group.groupName" class="group-header">
<td :colspan="quotes.length + 1" class="group-cell">{{ group.groupName }}</td>
</tr>
<tr v-for="dim in group.dimensions" :key="dim.key">
<td class="td-sticky dim-label">{{ dim.label }}</td>
<td v-for="q in quotes" :key="q.insurer">
<template v-if="dim.key === 'status'">
<span v-if="getQuoteValue(q, dim.key)" :class="['status-badge', getQuoteValue(q, dim.key)]">{{ statusText[getQuoteValue(q, dim.key)] || getQuoteValue(q, dim.key) }}</span>
<span v-else>待填</span>
</template>
<template v-else>
{{ getQuoteValue(q, dim.key) || '待填' }}
</template>
</td>
</tr>
</template>
</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: '已报价',
completed: '报价完成',
formal: '正式报价',
created: '已创建',
preliminary: '初步评估'
}
interface QuoteItem {
insurer: string
status: string
premium?: string
perPersonLimit?: string
aggregateLimit?: string
deductible?: string
validity?: string
addOnSupported?: string
insuranceClause?: string
coveredEvents?: string
relatedFactors?: string
med100Percent?: string
possibleRelatedPercent?: string
certainRelatedPercent?: string
fullProcessCommitment?: string
icfReview?: string
crcTraining?: string
serviceHotline?: string
expenseSettlement?: string
claimsCommunication?: string
claimsTimeCommit?: string
disputeCoordination?: string
refundForUnenrolled?: string
paymentCycle?: string
note?: string
}
const compareDimensionGroups = [
{
groupName: '基础报价与保障限额',
dimensions: [
{ key: 'status', label: '报价状态' },
{ key: 'premium', label: '保费(元)' },
{ key: 'perPersonLimit', label: '每人限额(万)' },
{ key: 'aggregateLimit', label: '累计限额(万)' },
{ key: 'deductible', label: '免赔额(万)' },
{ key: 'validity', label: '有效期' },
{ key: 'addOnSupported', label: '附加险' }
]
},
{
groupName: '保障范围',
dimensions: [
{ key: 'insuranceClause', label: '保险条款' },
{ key: 'coveredEvents', label: '承保事件' },
{ key: 'relatedFactors', label: '相关因素' },
{ key: 'med100Percent', label: '医药费100%' },
{ key: 'possibleRelatedPercent', label: '可能相关%' },
{ key: 'certainRelatedPercent', label: '肯定相关%' },
{ key: 'fullProcessCommitment', label: '全流程承诺' }
]
},
{
groupName: '风险管理服务',
dimensions: [
{ key: 'icfReview', label: 'ICF审阅' },
{ key: 'crcTraining', label: 'CRC培训' }
]
},
{
groupName: '理赔服务',
dimensions: [
{ key: 'serviceHotline', label: '服务专线' },
{ key: 'expenseSettlement', label: '费用理算' },
{ key: 'claimsCommunication', label: '理赔沟通' },
{ key: 'claimsTimeCommit', label: '理赔时效' },
{ key: 'disputeCoordination', label: '纠纷协调' }
]
},
{
groupName: '价格与退费',
dimensions: [
{ key: 'refundForUnenrolled', label: '未入组退费' },
{ key: 'paymentCycle', label: '支付周期' },
{ key: 'note', label: '备注' }
]
}
]
function getQuoteValue(q: QuoteItem, key: string): string {
const v = (q as Record<string, unknown>)[key]
if (v === undefined || v === null) return ''
return String(v)
}
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', note: '—' }
]
}
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: 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);
}
.compare-desc {
font-size: 14px;
color: var(--text-color);
margin: -8px 0 16px 0;
line-height: 1.5;
}
.insurer-compare-table.transposed {
font-size: 13px;
}
.insurer-compare-table .th-sticky,
.insurer-compare-table .td-sticky {
position: sticky;
left: 0;
background: var(--white);
z-index: 1;
box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.08);
}
.insurer-compare-table thead .th-sticky {
background: rgba(14, 165, 233, 0.08);
}
.insurer-compare-table .group-header .group-cell {
background: rgba(14, 165, 233, 0.06);
font-weight: 600;
font-size: 13px;
padding: 10px 16px;
border-bottom: 1px solid var(--border-color);
}
.insurer-compare-table .dim-label {
font-weight: 500;
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,
.status-badge.completed {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
.status-badge.formal {
background: rgba(59, 130, 246, 0.2);
color: #1d4ed8;
}
.status-badge.preliminary {
background: rgba(234, 179, 8, 0.2);
color: #b45309;
}
.status-badge.created {
background: rgba(107, 114, 128, 0.2);
color: #4b5563;
}
.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>