2025-03-26 12:48:43 +08:00
|
|
|
|
<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="1"
|
|
|
|
|
|
: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.5"
|
|
|
|
|
|
: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="区域">
|
2025-04-02 21:51:36 +08:00
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="form.region"
|
|
|
|
|
|
placeholder="请选择区域"
|
|
|
|
|
|
class="full-width"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
:filter-method="filterRegions"
|
|
|
|
|
|
:remote-method="filterRegions">
|
2025-03-26 12:48:43 +08:00
|
|
|
|
<el-option
|
2025-04-02 21:51:36 +08:00
|
|
|
|
v-for="region in filteredRegions"
|
2025-03-26 12:48:43 +08:00
|
|
|
|
: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">
|
2025-04-02 22:37:29 +08:00
|
|
|
|
<span>{{ getRegionName(scope.row.region) }}</span>
|
2025-03-26 12:48:43 +08:00
|
|
|
|
</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: [],
|
2025-04-02 21:51:36 +08:00
|
|
|
|
filteredRegions: [], // 添加筛选后的区域列表
|
2025-03-26 12:48:43 +08:00
|
|
|
|
searchResults: [],
|
|
|
|
|
|
selectedInstance: null,
|
|
|
|
|
|
comparisonList: [], // 用于存储要比较的实例
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
searched: false,
|
|
|
|
|
|
quoteInfo: {
|
|
|
|
|
|
contact: '林先生',
|
|
|
|
|
|
phone: '18626324958'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async created() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用API服务获取区域列表
|
|
|
|
|
|
this.regions = await apiService.getRegions()
|
2025-04-02 21:51:36 +08:00
|
|
|
|
this.filteredRegions = [...this.regions] // 初始化筛选后的区域列表
|
2025-03-26 12:48:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果数据加载成功,默认选择第一个区域
|
|
|
|
|
|
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: {
|
2025-04-02 21:51:36 +08:00
|
|
|
|
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]
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-03-26 12:48:43 +08:00
|
|
|
|
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),
|
2025-04-02 22:37:29 +08:00
|
|
|
|
this.getRegionName(instance.region),
|
2025-03-26 12:48:43 +08:00
|
|
|
|
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列
|
2025-04-02 22:37:29 +08:00
|
|
|
|
{ wch: 25 }, // E列
|
2025-03-26 12:48:43 +08:00
|
|
|
|
{ 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>
|