411 lines
11 KiB
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>
|