feat(comparison): 添加产品对比功能

- 在路由中添加 /comparison 路径
- 创建产品对比页面和相关组件
- 实现产品对比数据展示和滚动处理
- 集成 Element UI 表格组件
- 优化页面样式和交互
This commit is contained in:
陈昱达
2025-06-12 17:27:54 +08:00
parent 9769036058
commit 1a6b0fc3b4
8 changed files with 580 additions and 3 deletions

View File

@@ -9,6 +9,13 @@ module.exports = {
style: true
},
'vant'
],
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
]
]
}

View File

@@ -18,9 +18,11 @@
},
"dependencies": {
"@better-scroll/core": "^2.5.1",
"@isaacs/cliui": "^8.0.2",
"axios": "^0.19.0",
"core-js": "^2.6.5",
"core-js": "^3.43.0",
"dingtalk-jsapi": "^3.0.38",
"element-ui": "^2.15.14",
"eruda": "^2.11.3",
"fastclick": "^1.0.6",
"markdown-it": "^12.3.2",
@@ -33,7 +35,7 @@
"swiper": "^5.4.5",
"v-viewer": "^1.6.4",
"vant": "^2.12.54",
"vue": "^2.6.10",
"vue": "^2.7.16",
"vue-pdf": "^4.3.0",
"vue-quill-editor": "^3.0.6",
"vue-router": "^3.5.2",
@@ -51,6 +53,7 @@
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-import": "^1.12.0",
"browserslist": "^4.25.0",
"caniuse-lite": "^1.0.30001721",

View File

@@ -20,6 +20,10 @@ import { Toast, Form, Loading, Lazyload, Notify, Image, Button, Dialog } from 'v
import generatedFormat from '@/assets/js/generatedFormat/index'
import generatedApi from '@/api/generatedApi/index'
import generatedComponents from './generatedComponents'
import {Table,TableColumn} from 'element-ui'
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
for (let item in generatedComponents) {
Vue.component(item, generatedComponents[item])
}

View File

@@ -15,4 +15,12 @@ export default [
title: '客服助手',
},
},
{
path: '/comparison',
name: 'comparison',
component: () => import('@/views/comparison/index.vue'),
meta: {
title: '产品对比',
},
},
]

View File

@@ -11,16 +11,25 @@
</van-swipe-item>
<!-- <van-swipe-item>2</van-swipe-item>-->
</van-swipe>
<Announcement class="mt10"></Announcement>
<van-swipe>
<van-swipe-item v-for="item in list">
<!-- <img :src="item.img" alt="" />-->
</van-swipe-item>
</van-swipe>
</div>
</template>
<script>
import { Swipe, SwipeItem } from 'vant'
import SvgIcon from '@/components/svg-icon/index.vue'
import Announcement from '@/views/AI/components/Announcement.vue'
export default {
name: 'NavigationList',
components: {
Announcement,
SvgIcon,
[Swipe.name]: Swipe,
[SwipeItem.name]: SwipeItem,
@@ -30,7 +39,13 @@ export default {
navigationItems: [
{ title: 'AI智能助手', icon: 'product', path: '/chatPage' },
{ title: 'AI客服助手', icon: 'sale', path: '/customer' },
{ title: '产品对比', icon: 'earth', path: '' },
{ title: '产品对比', icon: 'earth', path: '/comparison' },
],
list: [
{
img: 'https://mmecoa.qpic.cn/mmecoa_png/5e6A2zKzZuWfRtPicpicFia0mA02116ia24HYKmSVPlcO6IDqMNMpqGeVbciblmGX1iajMGgT1JtUibqjyfDBmibcFibO7Q/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=10005&wx_lazy=1',
text: '',
},
],
}
},

View File

@@ -0,0 +1,239 @@
<template>
<div class="table-container" ref="container" @scroll="handleScroll">
<!-- 固定表头 -->
<table class="header-table">
<colgroup>
<col v-for="(width, index) in columnWidths" :key="index" :style="{ width: width + 'px' }" />
</colgroup>
<thead>
<tr>
<th v-if="hasKeyColumn">字段</th>
<th v-for="(item, index) in list" :key="index">{{ item.title }}</th>
</tr>
</thead>
</table>
<!-- 可滚动内容区域 -->
<div class="body-wrapper" ref="bodyWrapper">
<table class="content-table">
<colgroup>
<col v-for="(width, index) in columnWidths" :key="index" :style="{ width: width + 'px' }" />
</colgroup>
<tbody>
<tr v-for="(row, rowIndex) in processedTableData" :key="rowIndex" v-show="!row.hideRow" :class="{ 'group-title-row': row.isGroupTitle }">
<!-- 字段名 -->
<td v-if="row.hasKey && !row.hasValue" :colspan="list.length + 1" class="field-column single-line">
{{ row.key }}
</td>
<td v-else-if="row.hasKey" class="field-column">
{{ row.key }}
</td>
<!-- 值渲染 -->
<td v-for="(valObj, valIndex) in row.renderValues" :key="valIndex" :colspan="valObj.colspan" v-if="!valObj.isHidden">
{{ formatValue(valObj.value) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: 'ComparisonTable',
props: {
list: {
type: Array,
default: () => [{ title: '保司A' }, { title: '保司B' }, { title: '保司C' }],
},
tableData: {
type: Array,
default: () => [
{ value: ['这是第一个分组标题'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ key: '公司介绍', value: ['安联人寿保险有限公司(简...'] },
{ value: ['这是第二个分组标题'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '产品名称', value: ['产品A', '产品B', '产品C'] },
{ key: '客服电话', value: ['95342', '95512', '95511'] },
],
},
},
computed: {
hasKeyColumn() {
return this.tableData.some((row) => {
const key = row.key !== undefined ? row.key : ''
return key !== null && key !== ''
})
},
columnWidths() {
const fieldColWidth = 120 // 字段列宽度
const otherColWidth = 150 // 其他列宽度
return [fieldColWidth, ...Array(this.list.length).fill(otherColWidth)]
},
processedTableData() {
const headerCount = this.list.length
return this.tableData.map((row) => {
const key = row.key !== undefined ? row.key : ''
const value = (row.value || []).map((v) => (v === null || v === undefined ? '' : v))
const hasKey = key !== null && key !== ''
const hasValue = value.some((v) => v !== '')
const hideRow = !hasKey && !hasValue
const isGroupTitle = !hasKey && hasValue
let renderValues = []
if (hasValue) {
let i = 0
while (i < value.length) {
if (value[i] === '') {
i++
continue
}
let span = 1
for (let j = i + 1; j < value.length; j++) {
if (value[j] !== '') break
span++
}
renderValues.push({
value: value[i],
colspan: span,
isHidden: false,
})
i += span
}
}
const renderedColCount = renderValues.reduce((sum, v) => sum + v.colspan, 0)
if (!hasValue) {
renderValues = [
{
value: '-',
colspan: headerCount,
isHidden: true,
},
]
} else if (renderedColCount < headerCount) {
if (renderValues.length > 0) {
renderValues[renderValues.length - 1].colspan += headerCount - renderedColCount
} else {
renderValues.push({
value: '-',
colspan: headerCount,
isHidden: false,
})
}
}
return {
...row,
key,
value,
hasKey,
hasValue,
hideRow,
isGroupTitle,
renderValues,
}
})
},
},
methods: {
formatValue(val) {
if (val === '-' || val === '') return ''
return val
},
handleScroll(e) {
const scrollLeft = e.target.scrollLeft
const headerTable = this.$el.querySelector('.header-table')
if (headerTable) {
headerTable.style.transform = `translateX(${scrollLeft}px)`
}
},
},
}
</script>
<style scoped lang="scss">
$table-border: #b6ccd9;
.table-container {
background-color: #fff;
border: 1px solid #e8e8e8;
padding: 8px;
overflow: auto;
max-height: 600px;
}
.header-table,
.content-table {
border-collapse: collapse;
font-size: 14px;
touch-action: unset;
}
.header-table {
position: sticky;
top: 0;
z-index: 10;
background: white;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
.field-column {
background: #2976e8;
color: #fff;
text-align: center;
}
.single-line {
background: #f0f0f0;
color: #333;
text-align: center;
}
.group-title-row td {
background: #f9f9f9;
font-weight: bold;
color: #555;
position: sticky;
top: 40px;
z-index: 8;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.header-table th,
.content-table td {
padding: 8px 12px;
border: 1px solid $table-border;
word-break: break-word;
white-space: normal;
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="comparison-container">
<div class="main" v-if="!showComparison">
<main>
<div style="height: 100%">
<van-tabs v-model="active" color="#2E5CA9" title-active-color="#2E5CA9">
<van-tab :title="item.title" v-for="(item, index) in list" :key="index">
<div class="tab-container">
<!-- 横向滚动区域 -->
<div class="comparison-list" v-if="comparisonMap.length > 0">
<div v-for="compareItem in comparisonMap" :key="compareItem.id" class="comparison-item fs12">
{{ compareItem.title }}
<van-icon name="close" class="close-item" @click="removeItem(compareItem)" />
</div>
</div>
<!-- 可点击添加对比的列表 -->
<van-cell-group inset style="margin: 0 !important">
<van-cell v-for="it in item.list" :key="it.id" @click="cellClick(it)">
<template #title>
<div class="cell-title-right-title">{{ it.title }}</div>
</template>
<template #right-icon>
<button
class="cell-title-right-btn fs12"
style="color: #2e5ca9"
:disabled="comparisonMap.find((compareItem) => it.id === compareItem.id)"
>
+ 添加对比
</button>
</template>
</van-cell>
</van-cell-group>
</div>
</van-tab>
</van-tabs>
</div>
</main>
<div class="button-group">
<van-button size="large" class="fs14 fw600" style="color: #fff; background: #2e5ca9" @click="showComparison = true">开始比对</van-button>
</div>
</div>
<div v-else>
<comparsion :list="comparisonMap"></comparsion>
</div>
</div>
</template>
<script>
import { Tab, Tabs, Cell, CellGroup, Icon } from 'vant'
import Comparsion from '@/views/comparison/table.vue'
export default {
name: 'ComparisonTab',
components: {
Comparsion,
[Tab.name]: Tab,
[Tabs.name]: Tabs,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[Icon.name]: Icon,
},
data() {
return {
active: 0,
list: [
{
title: '重疾',
list: [
{ id: 1, title: '恒安标准一生无忧庆典版重度恶性肿瘤疾病保险' },
{ id: 2, title: '恒安标准中老年恶性肿瘤疾病保险6.0版)' },
{ id: 3, title: '恒安标准恒鑫世家终身寿险(分红型)' },
{ id: 4, title: '恒安标准幸福到老长寿2.0养老年金保险(分红型)' },
{ id: 5, title: '恒安标准恒盈慧选年金保险(分红型)' },
],
},
{
title: '年金',
list: [],
},
],
// 按 tab 存储对比数据
comparisonMap: [],
showComparison: false,
}
},
methods: {
cellClick(item) {
const exists = this.comparisonMap.some((it) => it.id === item.id)
if (!exists) {
this.$set(this, 'comparisonMap', [...this.comparisonMap, item])
}
},
removeItem(item) {
this.comparisonMap = this.comparisonMap.filter((it) => it.id !== item.id)
},
},
}
</script>
<style scoped lang="scss">
$primary-color: #2e5ca9;
$primary-text-color: #f6aa21;
$primary-trans-color: #87a2d0;
.comparison-container {
height: 100vh;
background: #f5f5f9;
//padding: 10px;
.main {
display: flex;
flex-direction: column;
height: 100%;
main {
flex: 1;
}
}
}
.cell-title-right-btn {
outline: none;
border: none;
background: #fff;
&:disabled {
color: #ccc !important;
}
}
.tab-container {
display: flex;
flex-direction: column;
overflow-y: scroll;
height: 100%;
}
.comparison-list {
display: flex;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch; /* 移动端滚动优化 */
//border: 1px solid #eee;
border-radius: 8px;
//margin-bottom: 10px;
touch-action: unset;
//padding: 8px 0;
&::-webkit-scrollbar {
display: none; /* 隐藏滚动条 */
}
}
.placeholder {
//padding: 20px 15px;
color: #999;
font-size: 14px;
min-width: 100px;
text-align: center;
}
.comparison-item {
touch-action: unset;
flex: 0 0 auto;
margin-top: 6px;
margin-bottom: 14px;
width: 120px;
height: 70px;
border: 2px solid #fff;
margin-right: 10px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border-radius: 6px;
position: relative;
//文字自动折行
white-space: normal;
padding: 0 5px;
.close-item {
position: absolute;
background: #fff;
color: $primary-trans-color;
border-radius: 50%;
right: -7px;
top: -8px;
font-size: 14px;
}
}
::v-deep .van-tabs__content {
height: 100%;
}
::v-deep .van-tab__pane {
height: calc(100vh - 95px);
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div style="touch-action: unset" class="comparsion-container">
<el-table :data="tableData" style="width: 100%" border :height="getTableHeight()">
<!-- 左侧行标题 -->
<el-table-column fixed prop="rowTitle" label="指标" width="100"></el-table-column>
<!-- 动态列根据 list 生成 -->
<el-table-column v-for="(item, index) in list" :key="index" :label="item.title" width="150">
<template #default="scope">
<!-- 根据 item.id 映射对应的字段名比如 b1b2b3 -->
{{ formatValue(scope.row['b' + item.id]) }}
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: () => [],
},
},
data() {
return {
// 表格数据(每行是一个指标,列是各个产品值)
tableData: [
{
rowTitle: '保费',
b1: 1200,
b2: 1500,
b3: 900,
b4: 1800,
b5: 1300,
},
{
rowTitle: '保额',
b1: 100000,
b2: 80000,
b3: 500000,
b4: 300000,
b5: 200000,
},
{
rowTitle: '保障期限',
b1: '终身',
b2: '1年',
b3: '终身',
b4: '至80岁',
b5: '至70岁',
},
],
}
},
methods: {
formatValue(val) {
if (typeof val === 'number') {
return val.toFixed(2)
}
return val || '-'
},
getTableHeight() {
return window.innerHeight - 20
},
},
}
</script>
<style>
.el-table {
width: 100%;
}
.el-table,
.el-table * {
touch-action: auto !important;
}
</style>
<style lang="scss" scoped>
.comparsion-container {
height: calc(100vh - 20px);
padding: 10px;
}
::v-deep .el-table {
& th {
&.el-table__cell {
background: #1989fa;
color: #fff;
font-weight: 500;
}
}
}
::v-deep .el-table__body {
tbody {
td:nth-child(1) {
background: #1989fa !important;
color: #fff;
font-weight: 500;
}
}
}
.el-table {
}
</style>