calc/frontend/src/views/InstanceSearch.vue

1084 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="instance-search">
<el-card class="search-card">
<template #header>
<div class="card-header">
<span><i class="el-icon-search"></i> EC2 实例配置搜索</span>
<div class="card-subtitle">根据您的需求规格找到最合适的实例类型</div>
</div>
</template>
<div class="form-container">
<el-form :model="form" label-width="120px">
<el-row :gutter="20">
<el-col :md="8" :sm="24">
<el-form-item label="CPU 核心数">
<el-input-number
v-model="form.cpu_cores"
:min="0"
:max="64"
:step="1"
placeholder="所需CPU核心数"
class="full-width">
</el-input-number>
</el-form-item>
</el-col>
<el-col :md="8" :sm="24">
<el-form-item label="内存(GB)">
<el-input-number
v-model="form.memory_gb"
:min="0"
:max="256"
:step="0.5"
placeholder="所需内存容量(GB)"
class="full-width">
</el-input-number>
</el-form-item>
</el-col>
<el-col :md="8" :sm="24">
<el-form-item label="磁盘(GB)">
<el-input-number
v-model="form.disk_gb"
:min="8"
:max="16000"
:step="1"
placeholder="GP3卷存储容量(GB)"
class="full-width">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :md="6" :sm="24">
<el-form-item label="区域">
<el-select
v-model="form.region"
placeholder="请选择区域"
class="full-width"
filterable
clearable
:filter-method="filterRegions"
:remote-method="filterRegions">
<el-option
v-for="region in filteredRegions"
:key="region.code"
:label="region.name"
:value="region.code">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="6" :sm="24">
<el-form-item label="操作系统">
<el-select v-model="form.operating_system" placeholder="选择操作系统" class="full-width">
<el-option label="Linux" value="Linux"></el-option>
<el-option label="Windows" value="Windows"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="6" :sm="24">
<el-form-item label="磁盘类型">
<el-select v-model="form.disk_type" disabled placeholder="磁盘类型" class="full-width">
<el-option label="GP3 (通用SSD)" value="gp3"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="6" :sm="24" class="flexible-col">
<el-form-item>
<el-button type="primary" @click="searchInstances" icon="el-icon-search" class="search-button">搜索实例</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="searchResults.length" class="search-results animated fadeIn">
<h3><i class="el-icon-s-data"></i> 符合条件的实例</h3>
<el-table :data="searchResults" border style="width: 100%" :stripe="true" class="result-table">
<el-table-column prop="instance_type" label="实例类型" width="120">
<template #default="scope">
<div class="instance-type">
<el-tag type="info">{{ scope.row.instance_type }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="180"></el-table-column>
<el-table-column prop="cpu" label="CPU核心" width="100" align="center">
<template #default="scope">
<div class="spec-value">
<i class="el-icon-cpu"></i> {{ scope.row.cpu }}
</div>
</template>
</el-table-column>
<el-table-column prop="memory" label="内存(GB)" width="100" align="center">
<template #default="scope">
<div class="spec-value">
<i class="el-icon-coin"></i> {{ scope.row.memory }}
</div>
</template>
</el-table-column>
<el-table-column prop="disk_gb" label="磁盘(GB)" width="100" align="center">
<template #default="scope">
<div class="spec-value">
<i class="el-icon-folder"></i> {{ scope.row.disk_gb }}
</div>
</template>
</el-table-column>
<el-table-column label="每月价格" width="260">
<template #default="scope">
<div class="price-breakdown">
<div class="price-item">
<span class="price-label">实例:</span>
<span class="price-value">${{ scope.row.monthly_price.toFixed(2) }}</span>
</div>
<div class="price-item">
<span class="price-label">磁盘:</span>
<span class="price-value">${{ scope.row.disk_monthly_price.toFixed(2) }}</span>
</div>
<div class="price-item total">
<span class="price-label">总计:</span>
<span class="price-value highlight">${{ scope.row.total_monthly_price.toFixed(2) }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="scope">
<div class="action-buttons-cell">
<el-button
type="primary"
size="small"
@click="selectInstance(scope.row)"
icon="el-icon-select">
查看详情
</el-button>
<el-button
type="success"
size="small"
@click="addToComparison(scope.row)"
icon="el-icon-plus">
加入对比
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="searched" class="no-results">
<el-empty description="没有找到符合条件的实例,请尝试降低配置要求"></el-empty>
</div>
<div v-if="selectedInstance" class="selected-instance animated fadeIn">
<h3><i class="el-icon-s-check"></i> 已选择实例</h3>
<el-card class="instance-card">
<el-row :gutter="20">
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">实例类型</div>
<div class="info-value">{{ selectedInstance.instance_type }}</div>
</div>
</el-col>
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">CPU核心</div>
<div class="info-value">{{ selectedInstance.cpu }} 核</div>
</div>
</el-col>
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">内存</div>
<div class="info-value">{{ selectedInstance.memory }} GB</div>
</div>
</el-col>
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">磁盘</div>
<div class="info-value">{{ selectedInstance.disk_gb }} GB (GP3)</div>
</div>
</el-col>
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">区域</div>
<div class="info-value">{{ getRegionName(form.region) }}</div>
</div>
</el-col>
<el-col :md="4" :sm="24">
<div class="instance-info-item">
<div class="info-label">操作系统</div>
<div class="info-value">{{ form.operating_system }}</div>
</div>
</el-col>
</el-row>
<el-divider content-position="center">价格信息</el-divider>
<el-row :gutter="20">
<el-col :md="8" :sm="24">
<div class="price-info-item">
<div class="price-icon">⏱️</div>
<div class="price-title">每小时价格</div>
<div class="price-amount">${{ selectedInstance.hourly_price.toFixed(4) }}</div>
<div class="price-note">实例每小时费用</div>
</div>
</el-col>
<el-col :md="8" :sm="24">
<div class="price-info-item">
<div class="price-icon">💾</div>
<div class="price-title">存储价格</div>
<div class="price-amount">${{ (selectedInstance.disk_monthly_price / 30).toFixed(4) }}/天</div>
<div class="price-note">{{ selectedInstance.disk_gb }}GB GP3卷每天费用</div>
</div>
</el-col>
<el-col :md="8" :sm="24">
<div class="price-info-item total">
<div class="price-icon">💰</div>
<div class="price-title">每月总费用</div>
<div class="price-amount">${{ selectedInstance.total_monthly_price.toFixed(2) }}</div>
<div class="price-note">预计30天使用总费用</div>
</div>
</el-col>
</el-row>
<div class="action-buttons">
<el-button type="success" icon="el-icon-shopping-cart-full" @click="addToComparison(selectedInstance)">添加到对比</el-button>
<el-button type="primary" icon="el-icon-s-finance" @click="goToCalculator">详细价格计算</el-button>
</div>
</el-card>
</div>
<!-- 价格对比部分 -->
<div v-if="comparisonList.length > 0" class="comparison-section animated fadeIn">
<h3><i class="el-icon-s-data"></i> AWS亚马逊报价单</h3>
<el-card class="comparison-card">
<!-- 报价单头部 -->
<div class="quote-header">
<div class="quote-title">AWS亚马逊报价单</div>
<div class="quote-info">
<table class="info-table">
<tr>
<td class="info-label">联系人:</td>
<td class="info-value">
<el-input v-model="quoteInfo.contact" placeholder="请输入联系人"></el-input>
</td>
<td></td>
<td class="info-label">签发日期:</td>
<td class="info-value">{{ getCurrentDate() }}</td>
</tr>
<tr>
<td class="info-label">电话:</td>
<td class="info-value">
<el-input v-model="quoteInfo.phone" placeholder="请输入电话"></el-input>
</td>
<td></td>
<td class="info-label">电话:</td>
<td class="info-value"></td>
</tr>
</table>
</div>
</div>
<!-- 报价单内容 -->
<el-table :data="comparisonList" border style="width: 100%" :stripe="true" class="comparison-table">
<el-table-column label="产品名称" width="80" align="center">
<template #default>
<span>EC2</span>
</template>
</el-table-column>
<el-table-column prop="instance_type" label="规格型号" width="150" align="center">
<template #default="scope">
<div>{{ formatSpecDescription(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="磁盘" width="120" align="center">
<template #default="scope">
<span>{{ scope.row.disk_gb }}G GP3</span>
</template>
</el-table-column>
<el-table-column label="操作系统" width="120" align="center">
<template #default="scope">
<span>{{ formatOS(scope.row.operating_system) }}</span>
</template>
</el-table-column>
<el-table-column label="区域" width="120" align="center">
<template #default="scope">
<span>{{ getRegionName(scope.row.region) }}</span>
</template>
</el-table-column>
<el-table-column label="官方月付全额 美元USD" width="180" align="center">
<template #default="scope">
<span class="price-value highlight">${{ scope.row.total_monthly_price.toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="官方年付全额 美元USD" width="180" align="center">
<template #default="scope">
<span class="price-value highlight">${{ (scope.row.total_monthly_price * 12).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="scope">
<el-button
type="danger"
size="mini"
@click="removeFromComparison(scope.$index)"
icon="el-icon-delete">
移除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 说明事项 -->
<div class="quote-notes">
<div class="note-title">说明事项:</div>
<div class="note-items">
<div class="note-item">1. 以上价格仅包服务器和磁盘的费用, 以上价格仅包服务器和磁盘的费用, 公共带宽流量按官网价格 美国$0.12USD/GB</div>
</div>
</div>
<div class="quote-actions">
<div class="summary-item">
<div class="summary-label">对比实例数量:</div>
<div class="summary-value">{{ comparisonList.length }}</div>
</div>
<div class="summary-buttons">
<el-button type="primary" icon="el-icon-download" @click="exportComparison">导出报价单</el-button>
<el-button type="danger" icon="el-icon-delete" @click="clearComparison">清空报价单</el-button>
</div>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<script>
import apiService from '../api'
import XLSX from 'xlsx-js-style'
import { saveAs } from 'file-saver'
export default {
name: 'InstanceSearch',
data() {
return {
form: {
cpu_cores: null,
memory_gb: null,
disk_gb: 30, // 默认30GB
region: 'us-east-1',
disk_type: 'gp3',
operating_system: 'Linux' // 默认Linux
},
regions: [],
filteredRegions: [], // 添加筛选后的区域列表
searchResults: [],
selectedInstance: null,
comparisonList: [], // 用于存储要比较的实例
loading: false,
searched: false,
quoteInfo: {
contact: '林先生',
phone: '18626324958'
}
}
},
async created() {
try {
// 使用API服务获取区域列表
this.regions = await apiService.getRegions()
this.filteredRegions = [...this.regions] // 初始化筛选后的区域列表
// 如果数据加载成功,默认选择第一个区域
if (this.regions && this.regions.length > 0) {
this.form.region = this.regions[0].code
}
// 从本地存储加载之前保存的对比列表
const savedComparison = localStorage.getItem('instance_comparison')
if (savedComparison) {
this.comparisonList = JSON.parse(savedComparison)
}
} catch (error) {
console.error('Error fetching data:', error)
this.$message.error('获取数据失败')
}
},
methods: {
filterRegions(query) {
if (query) {
this.filteredRegions = this.regions.filter(region => {
return region.name.toLowerCase().includes(query.toLowerCase()) ||
region.code.toLowerCase().includes(query.toLowerCase())
})
} else {
this.filteredRegions = [...this.regions]
}
},
async searchInstances() {
if (!this.form.cpu_cores && !this.form.memory_gb && !this.form.disk_gb) {
this.$message.warning('请至少指定一项配置要求')
return
}
this.loading = true
try {
// 使用API服务搜索实例
const data = await apiService.searchInstances({
cpu_cores: this.form.cpu_cores,
memory_gb: this.form.memory_gb,
disk_gb: this.form.disk_gb,
region: this.form.region,
operating_system: this.form.operating_system
})
// 为每个结果添加操作系统信息
this.searchResults = data.map(item => ({
...item,
operating_system: this.form.operating_system,
region: this.form.region
}))
this.searched = true
this.selectedInstance = null
if (this.searchResults.length === 0) {
this.$message.info('没有找到符合条件的实例')
}
} catch (error) {
console.error('Error searching instances:', error)
this.$message.error('搜索实例失败')
} finally {
this.loading = false
}
},
selectInstance(instance) {
this.selectedInstance = instance
// 平滑滚动到选定实例区域
setTimeout(() => {
const element = document.querySelector('.selected-instance')
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, 100)
},
addToComparison(instance) {
if (!instance) return
// 检查是否已经添加过相同实例
const exists = this.comparisonList.find(
item => item.instance_type === instance.instance_type &&
item.region === this.form.region &&
item.operating_system === this.form.operating_system
)
if (exists) {
this.$message.warning('该实例已在对比列表中')
return
}
// 添加区域和操作系统信息到实例
const comparisonInstance = {
...instance,
region: this.form.region,
operating_system: this.form.operating_system
}
this.comparisonList.push(comparisonInstance)
this.$message.success('已添加到对比列表')
// 保存到本地存储
localStorage.setItem('instance_comparison', JSON.stringify(this.comparisonList))
// 滚动到对比列表区域
setTimeout(() => {
const element = document.querySelector('.comparison-section')
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, 100)
},
removeFromComparison(index) {
this.comparisonList.splice(index, 1)
localStorage.setItem('instance_comparison', JSON.stringify(this.comparisonList))
this.$message.success('已从对比列表中移除')
},
clearComparison() {
this.comparisonList = []
localStorage.removeItem('instance_comparison')
this.$message.success('已清空对比列表')
},
exportComparison() {
if (this.comparisonList.length === 0) {
this.$message.warning('报价单为空')
return
}
// 创建工作簿
const wb = XLSX.utils.book_new()
// 创建标题行
const titleRow = [['AWS亚马逊报价单']]
// 创建联系人信息行
const infoRows = [
['联系人:', this.quoteInfo.contact, '', '签发日期:', this.getCurrentDate()],
['电话:', this.quoteInfo.phone, '', '电话:', '']
]
// 创建空行
const emptyRow = ['', '', '', '', '', '', '']
// 创建表头行
const headerRow = ['产品名称', '规格型号', '磁盘', '操作系统', '区域', '官方月付全额 美元USD', '官方年付全额 美元USD']
// 创建数据行
const dataRows = this.comparisonList.map(instance => [
'EC2',
`${instance.cpu}${instance.memory}G ${instance.instance_type}`,
`${instance.disk_gb}G GP3`,
this.formatOS(instance.operating_system),
this.getRegionName(instance.region),
instance.total_monthly_price.toFixed(2),
(instance.total_monthly_price * 12).toFixed(2)
])
// 创建说明事项行
const noteRows = [
emptyRow,
['说明事项:'],
['1. 以上价格仅包服务器和磁盘的费用, 以上价格仅包服务器和磁盘的费用, 公共带宽流量按官网价格 均价$0.12USD/GB']
]
// 合并所有行
const allRows = [...titleRow, ...infoRows, emptyRow, headerRow, ...dataRows, ...noteRows]
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(allRows)
// 设置列宽
const colWidths = [
{ wch: 15 }, // A列
{ wch: 25 }, // B列
{ wch: 15 }, // C列
{ wch: 15 }, // D列
{ wch: 25 }, // E列
{ wch: 25 }, // F列
{ wch: 25 } // G列
]
ws['!cols'] = colWidths
// 设置行高
const rowHeights = Array(allRows.length).fill({ hpt: 25 })
rowHeights[0] = { hpt: 35 } // 标题行
rowHeights[4] = { hpt: 30 } // 表头行
ws['!rows'] = rowHeights
// 设置单元格合并
const headerRowIndex = 4 // 表头行的索引
const noteStartRow = headerRowIndex + dataRows.length + 2 // 说明事项开始的行索引
ws['!merges'] = [
// 合并标题行 (A1:G1)
{ s: { r: 0, c: 0 }, e: { r: 0, c: 6 } },
// 合并说明事项行
{ s: { r: noteStartRow, c: 0 }, e: { r: noteStartRow, c: 6 } },
{ s: { r: noteStartRow + 1, c: 0 }, e: { r: noteStartRow + 1, c: 6 } },
{ s: { r: noteStartRow + 2, c: 0 }, e: { r: noteStartRow + 2, c: 6 } }
]
// 设置说明事项样式
for (let r = noteStartRow; r < noteStartRow + 3; r++) {
const cellRef = XLSX.utils.encode_cell({ r, c: 0 })
if (ws[cellRef]) {
ws[cellRef].s = {
...ws[cellRef].s,
fill: { patternType: 'solid', fgColor: { rgb: 'FFFBEA' } },
alignment: { horizontal: 'left', vertical: 'center' }
}
}
}
// 设置标题行样式
ws['A1'].s = {
font: { sz: 16, bold: true, color: { rgb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'center' },
fill: { patternType: 'solid', fgColor: { rgb: '4472C4' } },
border: {
top: { style: 'thin' },
bottom: { style: 'thin' },
left: { style: 'thin' },
right: { style: 'thin' }
}
}
// 设置表头行样式
const cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
cols.forEach(col => {
const cellRef = `${col}${headerRowIndex + 1}`
if (!ws[cellRef]) ws[cellRef] = { v: '', t: 's' }
ws[cellRef].s = {
font: { bold: true },
alignment: { horizontal: 'center', vertical: 'center' },
fill: { patternType: 'solid', fgColor: { rgb: 'E0E0E0' } },
border: {
top: { style: 'thin' },
bottom: { style: 'thin' },
left: { style: 'thin' },
right: { style: 'thin' }
}
}
})
// 设置所有单元格的边框和对齐方式
for (let r = 0; r < allRows.length; r++) {
for (let c = 0; c < 7; c++) {
const cellRef = XLSX.utils.encode_cell({ r, c })
if (!ws[cellRef]) ws[cellRef] = { v: '', t: 's' }
if (!ws[cellRef].s) ws[cellRef].s = {}
ws[cellRef].s.border = {
top: { style: 'thin' },
bottom: { style: 'thin' },
left: { style: 'thin' },
right: { style: 'thin' }
}
ws[cellRef].s.alignment = { horizontal: 'center', vertical: 'center' }
}
}
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, 'AWS报价单')
// 导出工作簿
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array', bookSST: true, cellStyles: true })
const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
saveAs(blob, 'AWS亚马逊报价单.xlsx')
this.$message.success('报价单已导出为Excel文件')
},
goToCalculator() {
if (!this.selectedInstance) return
// 创建计算器请求参数
const params = {
instance_type: this.selectedInstance.instance_type,
region: this.form.region,
disk_gb: this.form.disk_gb,
operating_system: this.form.operating_system
}
// 保存到本地存储并跳转到计算器页面
localStorage.setItem('calculator_params', JSON.stringify(params))
this.$router.push('/')
},
// 获取区域的中文名称
getRegionName(regionCode) {
const region = this.regions.find(r => r.code === regionCode)
return region ? region.name : regionCode
},
// 获取当前日期 格式YYYY/MM/DD
getCurrentDate() {
const date = new Date()
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}/${month}/${day}`
},
// 格式化规格描述
formatSpecDescription(instance) {
return `${instance.cpu}${instance.memory}G ${instance.instance_type}`
},
// 格式化操作系统显示
formatOS(os) {
if (os === 'Linux') return 'Linux'
if (os === 'Windows') return 'Windows'
return os
}
}
}
</script>
<style scoped>
.instance-search {
max-width: 1200px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.search-card {
margin-bottom: 20px;
transition: all 0.3s ease;
}
.search-card:hover {
transform: translateY(-5px);
}
.card-header {
display: flex;
flex-direction: column;
padding: 5px 0;
}
.card-header span {
font-size: 20px;
font-weight: 600;
}
.card-header i {
margin-right: 8px;
}
.card-subtitle {
margin-top: 5px;
font-size: 14px;
opacity: 0.8;
}
.form-container {
padding: 20px 10px;
}
.full-width {
width: 100%;
}
.flexible-col {
display: flex;
align-items: center;
}
.search-button {
width: 100%;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.search-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.loading-container {
padding: 20px 0;
}
.search-results {
margin-top: 30px;
}
.search-results h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 18px;
text-align: center;
}
.search-results h3 i {
margin-right: 8px;
color: var(--secondary-color);
}
.result-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.05);
}
.instance-type {
display: flex;
justify-content: center;
}
.spec-value {
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
}
.spec-value i {
margin-right: 5px;
color: #409EFF;
}
.price-breakdown {
display: flex;
flex-direction: column;
gap: 5px;
padding: 5px;
}
.price-item {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.price-item.total {
margin-top: 5px;
padding-top: 5px;
border-top: 1px dashed #ddd;
font-weight: 600;
}
.price-label {
color: #7f8c8d;
}
.price-value {
font-weight: 500;
}
.price-value.highlight {
color: #2ecc71;
font-weight: 700;
}
.no-results {
padding: 40px 0;
}
.selected-instance {
margin-top: 40px;
padding-top: 20px;
border-top: 1px dashed #e0e0e0;
}
.selected-instance h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 18px;
text-align: center;
}
.selected-instance h3 i {
margin-right: 8px;
color: var(--accent-color);
}
.instance-card {
border-left: 4px solid var(--accent-color);
}
.instance-info-item {
text-align: center;
padding: 15px;
height: 100%;
}
.info-label {
color: #7f8c8d;
font-size: 14px;
margin-bottom: 8px;
}
.info-value {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
.price-info-item {
text-align: center;
padding: 20px 15px;
border-radius: 8px;
background-color: #f8f9fa;
margin-bottom: 15px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.3s ease;
}
.price-info-item:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.05);
}
.price-info-item.total {
background-color: #e8f7f0;
}
.price-icon {
font-size: 24px;
margin-bottom: 10px;
}
.price-title {
color: #7f8c8d;
font-size: 14px;
margin-bottom: 10px;
}
.price-amount {
font-size: 22px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 5px;
}
.price-note {
font-size: 12px;
color: #95a5a6;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.animated {
animation-duration: 0.5s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fadeIn {
animation-name: fadeIn;
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.instance-info-item,
.price-info-item {
margin-bottom: 15px;
}
}
.action-buttons-cell {
display: flex;
flex-direction: column;
gap: 5px;
}
.comparison-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px dashed #e0e0e0;
}
.comparison-section h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 18px;
text-align: center;
}
.comparison-card {
margin-bottom: 20px;
position: relative;
}
.comparison-table {
margin-bottom: 15px;
}
.quote-header {
margin-bottom: 20px;
}
.quote-title {
background-color: #00bcd4;
color: white;
text-align: center;
padding: 10px;
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.quote-info {
border: 1px solid #e0e0e0;
}
.info-table {
width: 100%;
border-collapse: collapse;
}
.info-table td {
padding: 8px;
border: 1px solid #e0e0e0;
}
.info-label {
width: 80px;
font-weight: bold;
background-color: #f5f5f5;
}
.info-value {
width: 200px;
}
.quote-notes {
margin-top: 15px;
border: 1px solid #ffcc00;
background-color: #fffbea;
padding: 10px;
}
.note-title {
font-weight: bold;
margin-bottom: 10px;
}
.note-items {
color: #333;
}
.note-item {
margin-bottom: 5px;
}
.quote-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
}
</style>