feat(docs): add interactive cloud services demo components

Add 10 new Vue components for cloud services documentation with interactive demos including:
- Cloud services overview and provider comparison
- Pricing calculator and region latency visualization
- Compute instance configurator and storage type explorer
- API call workflow and deployment process steps
- IAM structure and policy editor demos
This commit is contained in:
sanbuphy
2026-02-11 09:45:45 +08:00
parent a1198630be
commit 99d608d2a0
13 changed files with 2529 additions and 877 deletions
@@ -0,0 +1,224 @@
<template>
<div class="iam-structure">
<div class="structure-layers">
<div
v-for="(layer, index) in layers"
:key="index"
class="layer"
:class="{ active: selectedLayer === index }"
@click="selectLayer(index)"
>
<div class="layer-icon">{{ layer.icon }}</div>
<div class="layer-content">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-desc">{{ layer.shortDesc }}</div>
</div>
</div>
</div>
<div v-if="selectedLayerData" class="layer-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
<span class="detail-name">{{ selectedLayerData.name }}</span>
</div>
<div class="detail-desc">{{ selectedLayerData.description }}</div>
<div class="detail-examples">
<div class="example-title">示例</div>
<ul>
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
{{ example }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedLayer = ref(0)
const layers = [
{
icon: '👑',
name: '根账号',
shortDesc: '最高权限',
description: '云账号的所有者,拥有全部资源的完全控制权限。建议仅用于初始设置,日常操作使用子账号。',
examples: [
'创建/删除 IAM 用户',
'管理账单和支付方式',
'关闭账号',
'恢复已删除资源'
]
},
{
icon: '👤',
name: 'IAM 用户',
shortDesc: '个人身份',
description: '为具体人员(如员工)创建的长期凭证,用于日常登录和操作云服务。',
examples: [
'开发人员账号',
'运维人员账号',
'只读审计账号',
'API 调用账号'
]
},
{
icon: '👥',
name: '用户组',
shortDesc: '批量管理',
description: '将多个用户归为一组,统一分配权限,简化管理。',
examples: [
'开发组(开发权限)',
'运维组(运维权限)',
'财务组(账单权限)',
'审计组(只读权限)'
]
},
{
icon: '🎭',
name: '角色',
shortDesc: '临时授权',
description: '一种临时身份,可以被切换或赋予其他账号/服务,具有时效性更安全。',
examples: [
'跨账号访问角色',
'服务角色(如 Lambda',
'临时运维角色',
'第三方登录角色'
]
},
{
icon: '📋',
name: '策略',
shortDesc: '权限规则',
description: '定义"谁可以对什么资源执行什么操作"的规则文档,以 JSON 格式编写。',
examples: [
'允许访问 S3 存储桶',
'禁止删除 EC2 实例',
'只允许查看 RDS',
'允许特定时间段访问'
]
}
]
const selectedLayerData = computed(() => layers[selectedLayer.value])
function selectLayer(index) {
selectedLayer.value = index
}
</script>
<style scoped>
.iam-structure {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.structure-layers {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.layer {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.layer:hover {
border-color: var(--vp-c-brand);
}
.layer.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.layer-icon {
font-size: 1.25rem;
width: 32px;
text-align: center;
}
.layer-content {
flex: 1;
}
.layer-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.15rem;
}
.layer-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.layer-detail {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.detail-icon {
font-size: 1.25rem;
}
.detail-name {
font-weight: 600;
font-size: 1rem;
}
.detail-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
line-height: 1.5;
}
.detail-examples {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
}
.example-title {
font-weight: 500;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.detail-examples ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-examples li {
margin-bottom: 0.25rem;
}
.detail-examples li:last-child {
margin-bottom: 0;
}
</style>
@@ -0,0 +1,211 @@
<template>
<div class="policy-editor-demo">
<div class="editor-layout">
<div class="editor-panel">
<div class="panel-title">策略编辑器</div>
<div class="action-list">
<div
v-for="action in actions"
:key="action.id"
class="action-item"
>
<label class="checkbox">
<input
type="checkbox"
v-model="selectedActions"
:value="action.id"
>
<span>{{ action.name }}</span>
</label>
<span class="action-desc">{{ action.desc }}</span>
</div>
</div>
</div>
<div class="preview-panel">
<div class="panel-title">生成的策略</div>
<pre><code>{{ generatedPolicy }}</code></pre>
</div>
</div>
<div class="effect-preview">
<div class="effect-title">权限效果预览</div>
<div class="effect-list">
<div
v-for="effect in effectList"
:key="effect.action"
class="effect-item"
:class="effect.allowed ? 'allowed' : 'denied'"
>
<span class="effect-icon">{{ effect.allowed ? '✓' : '✗' }}</span>
<span class="effect-text">{{ effect.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedActions = ref(['describe', 'start'])
const actions = [
{ id: 'describe', name: '查看实例', desc: 'DescribeInstances', resource: 'ecs:Describe*' },
{ id: 'start', name: '启动实例', desc: 'StartInstance', resource: 'ecs:StartInstance' },
{ id: 'stop', name: '停止实例', desc: 'StopInstance', resource: 'ecs:StopInstance' },
{ id: 'reboot', name: '重启实例', desc: 'RebootInstance', resource: 'ecs:RebootInstance' },
{ id: 'create', name: '创建实例', desc: 'CreateInstance', resource: 'ecs:CreateInstance' },
{ id: 'delete', name: '删除实例', desc: 'DeleteInstance', resource: 'ecs:DeleteInstance' }
]
const generatedPolicy = computed(() => {
const selected = actions.filter(a => selectedActions.value.includes(a.id))
const actionList = selected.map(a => a.resource)
return JSON.stringify({
Version: "1",
Statement: [
{
Effect: "Allow",
Action: actionList,
Resource: "*"
}
]
}, null, 2)
})
const effectList = computed(() => {
return actions.map(action => ({
name: action.name,
action: action.id,
allowed: selectedActions.value.includes(action.id)
}))
})
</script>
<style scoped>
.policy-editor-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.editor-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.editor-panel,
.preview-panel {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
}
.panel-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.action-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.action-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0;
}
.checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.85rem;
}
.checkbox input {
cursor: pointer;
}
.action-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.preview-panel pre {
margin: 0;
font-size: 0.75rem;
line-height: 1.5;
overflow-x: auto;
}
.preview-panel code {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1);
}
.effect-preview {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
}
.effect-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.effect-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.effect-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
border-radius: 4px;
font-size: 0.8rem;
}
.effect-item.allowed {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
.effect-item.denied {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.effect-icon {
font-weight: 600;
}
@media (max-width: 640px) {
.editor-layout {
grid-template-columns: 1fr;
}
.effect-list {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,230 @@
<template>
<div class="api-call-demo">
<div class="flow-steps">
<div
v-for="(step, index) in steps"
:key="index"
class="step"
:class="{ active: currentStep >= index, completed: currentStep > index }"
>
<div class="step-num">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
<div class="action-panel">
<button
class="action-btn"
@click="nextStep"
:disabled="currentStep >= steps.length"
>
{{ currentStep >= steps.length ? '已完成' : '下一步' }}
</button>
<button class="action-btn outline" @click="reset">
重置
</button>
</div>
<div v-if="currentStep > 0" class="code-preview">
<div class="code-title">{{ steps[currentStep - 1].codeTitle }}</div>
<pre><code>{{ steps[currentStep - 1].code }}</code></pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{
title: '获取 AccessKey',
desc: '在控制台创建 AccessKey ID 和 Secret',
codeTitle: '配置凭证',
code: `// 环境变量设置
export ALIYUN_ACCESS_KEY_ID=your_key_id
export ALIYUN_ACCESS_KEY_SECRET=your_secret`
},
{
title: '安装 SDK',
desc: '安装对应语言的云服务 SDK',
codeTitle: '安装依赖',
code: `# Python
pip install alibabacloud-ecs20140526
# Node.js
npm install @alicloud/ecs20140526`
},
{
title: '编写调用代码',
desc: '使用 SDK 调用云服务 API',
codeTitle: '调用示例',
code: `from alibabacloud_ecs20140526 import models as ecs_models
# 创建客户端
client = create_client()
# 调用 API
response = client.describe_instances(
ecs_models.DescribeInstancesRequest()
)
print(response.body)`
},
{
title: '处理响应',
desc: '解析 API 返回的数据',
codeTitle: '处理结果',
code: `// 解析响应
instances = response.body.instances.instance
for inst in instances:
print(f"ID: {inst.instance_id}")
print(f"状态: {inst.status}")
print(f"IP: {inst.public_ip_address}")`
}
]
function nextStep() {
if (currentStep.value < steps.length) {
currentStep.value++
}
}
function reset() {
currentStep.value = 0
}
</script>
<style scoped>
.api-call-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.step {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 6px;
opacity: 0.5;
transition: all 0.2s;
}
.step.active {
opacity: 1;
background: var(--vp-c-bg);
}
.step.completed {
opacity: 0.7;
}
.step-num {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step.active .step-num {
background: var(--vp-c-brand);
color: white;
}
.step.completed .step-num {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.15rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.action-panel {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.action-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-brand);
border-radius: 4px;
background: var(--vp-c-brand);
color: white;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.outline {
background: transparent;
color: var(--vp-c-brand);
}
.code-preview {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
}
.code-title {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
pre {
margin: 0;
font-size: 0.75rem;
line-height: 1.5;
overflow-x: auto;
}
code {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,183 @@
<template>
<div class="cloud-history-demo">
<div class="timeline">
<div
v-for="(event, index) in events"
:key="index"
class="timeline-item"
:class="{ active: selectedEvent === index }"
@click="selectedEvent = index"
>
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="timeline-year">{{ event.year }}</div>
<div class="timeline-title">{{ event.title }}</div>
</div>
</div>
</div>
<div v-if="selectedEventData" class="event-detail">
<div class="detail-year">{{ selectedEventData.year }}</div>
<div class="detail-title">{{ selectedEventData.title }}</div>
<div class="detail-desc">{{ selectedEventData.description }}</div>
<div class="detail-impact">
<span class="impact-label">影响:</span>
<span class="impact-text">{{ selectedEventData.impact }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedEvent = ref(3)
const events = [
{
year: '1960s',
title: '概念萌芽',
description: 'J.C.R. Licklider 提出"星际计算机网络"设想,是云计算概念的最早雏形。',
impact: '奠定了分布式计算的理论基础'
},
{
year: '1990s',
title: '虚拟化技术',
description: 'VMware 推出 x86 虚拟化技术,允许在一台物理机上运行多个虚拟机。',
impact: '为云计算的资源池化提供了技术基础'
},
{
year: '2006',
title: 'AWS 诞生',
description: 'Amazon 推出 EC2 和 S3,标志着现代云计算服务的正式诞生。',
impact: '开创了公有云服务的商业模式'
},
{
year: '2009',
title: '阿里云成立',
description: '阿里巴巴成立阿里云,成为中国最早的云计算服务商。',
impact: '推动了中国云计算市场的发展'
},
{
year: '2010s',
title: '云原生时代',
description: 'Docker、Kubernetes 等技术兴起,微服务架构成为主流。',
impact: '改变了应用开发和部署的方式'
},
{
year: '2020s',
title: 'AI 云时代',
description: '大模型和 AI 服务成为云厂商的核心竞争力,Serverless 普及。',
impact: '云计算进入智能化新阶段'
}
]
const selectedEventData = computed(() => events[selectedEvent.value])
</script>
<style scoped>
.cloud-history-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.timeline {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.timeline-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
min-width: 80px;
}
.timeline-item:hover {
background: var(--vp-c-bg);
}
.timeline-item.active {
background: var(--vp-c-brand-soft);
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--vp-c-divider);
transition: all 0.2s;
}
.timeline-item.active .timeline-dot {
background: var(--vp-c-brand);
}
.timeline-content {
text-align: center;
}
.timeline-year {
font-weight: 600;
font-size: 0.85rem;
}
.timeline-title {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.event-detail {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 1rem;
}
.detail-year {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-weight: 600;
margin-bottom: 0.25rem;
}
.detail-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.detail-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 0.75rem;
}
.detail-impact {
font-size: 0.8rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.impact-label {
color: var(--vp-c-text-2);
margin-right: 0.5rem;
}
.impact-text {
font-weight: 500;
}
</style>
@@ -0,0 +1,203 @@
<template>
<div class="cloud-services-overview">
<div class="services-grid">
<div
v-for="service in services"
:key="service.id"
class="service-card"
:class="{ active: selectedService === service.id }"
@click="selectService(service.id)"
>
<div class="service-icon">{{ service.icon }}</div>
<div class="service-name">{{ service.name }}</div>
<div class="service-examples">{{ service.examples }}</div>
</div>
</div>
<div v-if="selectedServiceData" class="service-detail">
<div class="detail-title">{{ selectedServiceData.name }}</div>
<div class="detail-desc">{{ selectedServiceData.description }}</div>
<div class="detail-compare">
<div class="compare-item">
<span class="label">AWS:</span>
<span class="value">{{ selectedServiceData.aws }}</span>
</div>
<div class="compare-item">
<span class="label">阿里云:</span>
<span class="value">{{ selectedServiceData.aliyun }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedService = ref(null)
const services = [
{
id: 'compute',
icon: '⚙️',
name: '计算',
examples: 'EC2 / ECS',
description: '提供虚拟服务器和计算能力,是云服务的基础',
aws: 'Amazon EC2',
aliyun: 'ECS 云服务器'
},
{
id: 'storage',
icon: '💾',
name: '存储',
examples: 'S3 / OSS',
description: '对象存储服务,用于存放图片、文档等文件',
aws: 'Amazon S3',
aliyun: 'OSS 对象存储'
},
{
id: 'network',
icon: '🌐',
name: '网络',
examples: 'VPC / 专有网络',
description: '构建隔离的虚拟网络环境',
aws: 'Amazon VPC',
aliyun: '专有网络 VPC'
},
{
id: 'database',
icon: '🗄️',
name: '数据库',
examples: 'RDS / PolarDB',
description: '托管的关系型数据库服务',
aws: 'Amazon RDS',
aliyun: 'RDS 关系型数据库'
},
{
id: 'security',
icon: '🔒',
name: '安全',
examples: 'IAM / RAM',
description: '身份认证和访问控制服务',
aws: 'AWS IAM',
aliyun: 'RAM 访问控制'
},
{
id: 'middleware',
icon: '🔧',
name: '中间件',
examples: 'MQ / RocketMQ',
description: '消息队列和缓存服务',
aws: 'Amazon MQ',
aliyun: 'RocketMQ'
}
]
const selectedServiceData = computed(() =>
services.find(s => s.id === selectedService.value)
)
function selectService(id) {
selectedService.value = selectedService.value === id ? null : id
}
</script>
<style scoped>
.cloud-services-overview {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.service-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.service-card:hover {
border-color: var(--vp-c-brand);
}
.service-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.service-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.service-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.service-examples {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.service-detail {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.detail-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.detail-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.compare-item {
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
}
.compare-item .label {
color: var(--vp-c-text-2);
margin-right: 0.5rem;
}
.compare-item .value {
font-weight: 500;
}
@media (max-width: 640px) {
.services-grid {
grid-template-columns: repeat(2, 1fr);
}
.detail-compare {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,206 @@
<template>
<div class="compute-instance-demo">
<div class="config-panel">
<div class="config-row">
<label>地域</label>
<div class="options">
<button
v-for="region in regions"
:key="region.id"
:class="{ active: config.region === region.id }"
@click="config.region = region.id"
>
{{ region.name }}
</button>
</div>
</div>
<div class="config-row">
<label>规格</label>
<div class="options">
<button
v-for="spec in specs"
:key="spec.id"
:class="{ active: config.spec === spec.id }"
@click="config.spec = spec.id"
>
{{ spec.name }}
</button>
</div>
</div>
<div class="config-row">
<label>镜像</label>
<div class="options">
<button
v-for="image in images"
:key="image.id"
:class="{ active: config.image === image.id }"
@click="config.image = image.id"
>
{{ image.name }}
</button>
</div>
</div>
</div>
<div class="result-panel">
<div class="result-title">配置结果</div>
<div class="result-grid">
<div class="result-item">
<span class="label">配置</span>
<span class="value">{{ selectedSpec?.name }} / {{ selectedImage?.name }}</span>
</div>
<div class="result-item">
<span class="label">预估价格</span>
<span class="value price">¥{{ price }}/</span>
</div>
<div class="result-item">
<span class="label">适用场景</span>
<span class="value">{{ selectedSpec?.scene }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const config = ref({
region: 'hangzhou',
spec: 'medium',
image: 'ubuntu'
})
const regions = [
{ id: 'hangzhou', name: '华东-杭州' },
{ id: 'beijing', name: '华北-北京' },
{ id: 'shenzhen', name: '华南-深圳' },
{ id: 'singapore', name: '亚太-新加坡' }
]
const specs = [
{ id: 'small', name: '1核2G', scene: '测试环境、个人博客', price: 89 },
{ id: 'medium', name: '2核4G', scene: '中小型应用、开发环境', price: 199 },
{ id: 'large', name: '4核8G', scene: '生产环境、中型网站', price: 399 },
{ id: 'xlarge', name: '8核16G', scene: '大型应用、数据库', price: 799 }
]
const images = [
{ id: 'ubuntu', name: 'Ubuntu 22.04' },
{ id: 'centos', name: 'CentOS 7.9' },
{ id: 'windows', name: 'Windows Server' },
{ id: 'alpine', name: 'Alpine Linux' }
]
const selectedSpec = computed(() => specs.find(s => s.id === config.value.spec))
const selectedImage = computed(() => images.find(i => i.id === config.value.image))
const price = computed(() => selectedSpec.value?.price || 0)
</script>
<style scoped>
.compute-instance-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.config-panel {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.config-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.config-row label {
width: 50px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
flex: 1;
}
.options button {
padding: 0.35rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.options button:hover {
border-color: var(--vp-c-brand);
}
.options button.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
}
.result-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.result-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.result-item {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.result-item .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.result-item .value {
font-size: 0.9rem;
font-weight: 500;
}
.result-item .price {
color: var(--vp-c-brand);
}
@media (max-width: 640px) {
.result-grid {
grid-template-columns: 1fr;
}
.config-row {
flex-direction: column;
align-items: flex-start;
}
.config-row label {
width: auto;
}
}
</style>
@@ -0,0 +1,286 @@
<template>
<div class="deploy-workflow-demo">
<div class="workflow-steps">
<div
v-for="(step, index) in steps"
:key="index"
class="step-card"
:class="{ active: currentStep === index, completed: currentStep > index }"
@click="currentStep = index"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-time">{{ step.time }}</div>
</div>
</div>
</div>
<div v-if="currentStepData" class="step-detail">
<div class="detail-header">
<span class="detail-step">步骤 {{ currentStep + 1 }}</span>
<span class="detail-name">{{ currentStepData.name }}</span>
</div>
<div class="detail-content">
<div class="detail-desc">{{ currentStepData.description }}</div>
<div class="detail-tasks">
<div class="tasks-title">具体操作</div>
<ul>
<li v-for="(task, i) in currentStepData.tasks" :key="i">{{ task }}</li>
</ul>
</div>
</div>
</div>
<div class="workflow-actions">
<button class="action-btn" @click="prevStep" :disabled="currentStep === 0">
上一步
</button>
<button class="action-btn primary" @click="nextStep" :disabled="currentStep >= steps.length - 1">
{{ currentStep >= steps.length - 1 ? '完成' : '下一步' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const steps = [
{
name: '准备代码',
time: '5分钟',
description: '将网站代码打包成可部署的格式',
tasks: [
'整理 HTML/CSS/JS 文件',
'压缩图片和静态资源',
'检查文件路径是否正确'
]
},
{
name: '创建存储桶',
time: '2分钟',
description: '在对象存储服务中创建存储空间',
tasks: [
'登录云控制台',
'进入对象存储 OSS/S3',
'点击"创建 Bucket"',
'设置 Bucket 名称和地域'
]
},
{
name: '上传文件',
time: '3分钟',
description: '将网站文件上传到存储桶',
tasks: [
'进入 Bucket 管理页面',
'点击"上传文件"',
'选择本地网站文件',
'等待上传完成'
]
},
{
name: '配置 CDN',
time: '5分钟',
description: '配置内容分发网络加速访问',
tasks: [
'进入 CDN 控制台',
'添加加速域名',
'配置源站为存储桶',
'等待 CDN 部署完成'
]
},
{
name: '域名绑定',
time: '10分钟',
description: '将自定义域名绑定到 CDN',
tasks: [
'添加域名解析记录',
'配置 CNAME 到 CDN',
'申请 SSL 证书',
'测试 HTTPS 访问'
]
}
]
const currentStepData = computed(() => steps[currentStep.value])
function nextStep() {
if (currentStep.value < steps.length - 1) {
currentStep.value++
}
}
function prevStep() {
if (currentStep.value > 0) {
currentStep.value--
}
}
</script>
<style scoped>
.deploy-workflow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.workflow-steps {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.step-card {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
min-width: 120px;
}
.step-card:hover {
border-color: var(--vp-c-brand);
}
.step-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.step-card.completed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.step-number {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step-card.active .step-number {
background: var(--vp-c-brand);
color: white;
}
.step-card.completed .step-number {
background: #22c55e;
color: white;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 500;
font-size: 0.85rem;
}
.step-time {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.step-detail {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-step {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 600;
}
.detail-name {
font-weight: 600;
}
.detail-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.tasks-title {
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 0.4rem;
}
.detail-tasks ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-tasks li {
margin-bottom: 0.2rem;
}
.workflow-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.primary {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.action-btn.primary:hover:not(:disabled) {
opacity: 0.9;
}
</style>
@@ -0,0 +1,223 @@
<template>
<div class="pricing-calculator">
<div class="config-section">
<div class="config-row">
<span class="label">实例规格</span>
<select v-model="config.spec">
<option value="small">1核2G (入门)</option>
<option value="medium">2核4G (标准)</option>
<option value="large">4核8G (高性能)</option>
</select>
</div>
<div class="config-row">
<span class="label">运行时长</span>
<input type="range" v-model.number="config.hours" min="1" max="24" />
<span class="value">{{ config.hours }} 小时/</span>
</div>
<div class="config-row">
<span class="label">运行天数</span>
<input type="range" v-model.number="config.days" min="1" max="31" />
<span class="value">{{ config.days }} /</span>
</div>
</div>
<div class="result-section">
<div class="result-header">月度成本对比</div>
<div class="result-cards">
<div class="result-card">
<div class="model">按需付费</div>
<div class="price">${{ costs.ondemand }}/</div>
</div>
<div class="result-card recommended">
<div class="model">预留实例</div>
<div class="price">${{ costs.reserved }}/</div>
<div class="saving"> {{ savings }}%</div>
</div>
<div class="result-card">
<div class="model">抢占式</div>
<div class="price">${{ costs.spot }}/</div>
</div>
</div>
</div>
<div class="tip-box">
<span class="tip-icon">💡</span>
<span class="tip-text">{{ recommendation }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const config = ref({
spec: 'medium',
hours: 12,
days: 22
})
const specPrices = {
small: { ondemand: 0.08, reserved: 45, spot: 0.024 },
medium: { ondemand: 0.16, reserved: 89, spot: 0.048 },
large: { ondemand: 0.32, reserved: 179, spot: 0.096 }
}
const costs = computed(() => {
const price = specPrices[config.value.spec]
const monthlyHours = config.value.hours * config.value.days
return {
ondemand: Math.round(price.ondemand * monthlyHours),
reserved: price.reserved,
spot: Math.round(price.spot * monthlyHours)
}
})
const savings = computed(() => {
const save = costs.value.ondemand - costs.value.reserved
return Math.round((save / costs.value.ondemand) * 100)
})
const recommendation = computed(() => {
if (config.value.days < 15) {
return '当前使用频率较低,建议选择按需付费'
} else if (savings.value > 30) {
return `当前使用负载稳定,切换预留实例可省 ${savings.value}%`
} else {
return '根据当前配置,预留实例更具成本优势'
}
})
</script>
<style scoped>
.pricing-calculator {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background: var(--vp-c-bg-soft);
}
.config-section {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.config-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.85rem;
}
.config-row:last-child {
margin-bottom: 0;
}
.config-row .label {
width: 70px;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.config-row select {
padding: 0.25rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 0.85rem;
}
.config-row input[type="range"] {
flex: 1;
min-width: 80px;
}
.config-row .value {
width: 85px;
text-align: right;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.result-header {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.result-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.result-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
}
.result-card.recommended {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.result-card .model {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.result-card .price {
font-size: 1.1rem;
font-weight: 600;
}
.result-card .saving {
font-size: 0.75rem;
color: var(--vp-c-brand);
margin-top: 0.25rem;
}
.tip-box {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
}
.tip-icon {
font-size: 1rem;
}
.tip-text {
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.result-cards {
grid-template-columns: 1fr;
}
.config-row {
flex-wrap: wrap;
}
.config-row .label {
width: 100%;
}
.config-row .value {
width: auto;
}
}
</style>
@@ -0,0 +1,193 @@
<template>
<div class="provider-comparison">
<div class="compare-table">
<div class="table-header">
<div class="col feature">对比项</div>
<div class="col provider">AWS</div>
<div class="col provider">阿里云</div>
<div class="col provider">腾讯云</div>
</div>
<div
v-for="row in compareData"
:key="row.feature"
class="table-row"
>
<div class="col feature">{{ row.feature }}</div>
<div class="col provider" :class="{ highlight: row.awsHighlight }">
{{ row.aws }}
</div>
<div class="col provider" :class="{ highlight: row.aliyunHighlight }">
{{ row.aliyun }}
</div>
<div class="col provider" :class="{ highlight: row.tencentHighlight }">
{{ row.tencent }}
</div>
</div>
</div>
<div class="selection-guide">
<div class="guide-title">💡 选择建议</div>
<div class="guide-items">
<div class="guide-item">
<span class="scenario">出海业务</span>
<span class="recommend"> AWS</span>
</div>
<div class="guide-item">
<span class="scenario">国内电商</span>
<span class="recommend"> 阿里云</span>
</div>
<div class="guide-item">
<span class="scenario">游戏/社交</span>
<span class="recommend"> 腾讯云</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
const compareData = [
{
feature: '全球覆盖',
aws: '⭐⭐⭐⭐⭐',
aliyun: '⭐⭐⭐',
tencent: '⭐⭐⭐',
awsHighlight: true
},
{
feature: '国内速度',
aws: '⭐⭐⭐',
aliyun: '⭐⭐⭐⭐⭐',
tencent: '⭐⭐⭐⭐⭐',
aliyunHighlight: true,
tencentHighlight: true
},
{
feature: '文档中文',
aws: '⭐⭐⭐',
aliyun: '⭐⭐⭐⭐⭐',
tencent: '⭐⭐⭐⭐⭐',
aliyunHighlight: true,
tencentHighlight: true
},
{
feature: '价格优势',
aws: '⭐⭐⭐',
aliyun: '⭐⭐⭐⭐',
tencent: '⭐⭐⭐⭐⭐',
tencentHighlight: true
},
{
feature: '生态丰富',
aws: '⭐⭐⭐⭐⭐',
aliyun: '⭐⭐⭐⭐',
tencent: '⭐⭐⭐⭐',
awsHighlight: true
}
]
</script>
<style scoped>
.provider-comparison {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.compare-table {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.table-header,
.table-row {
display: grid;
grid-template-columns: 100px 1fr 1fr 1fr;
gap: 0.5rem;
align-items: center;
}
.table-header {
font-weight: 600;
font-size: 0.85rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.table-row {
font-size: 0.85rem;
padding: 0.4rem 0;
}
.col {
padding: 0.4rem 0.6rem;
border-radius: 4px;
}
.col.feature {
font-weight: 500;
color: var(--vp-c-text-1);
}
.col.provider {
text-align: center;
background: var(--vp-c-bg);
}
.col.provider.highlight {
background: var(--vp-c-brand-soft);
font-weight: 500;
}
.selection-guide {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.guide-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.guide-items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.guide-item {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
}
.guide-item .scenario {
color: var(--vp-c-text-2);
}
.guide-item .recommend {
font-weight: 500;
color: var(--vp-c-brand);
}
@media (max-width: 640px) {
.table-header,
.table-row {
grid-template-columns: 80px 1fr 1fr 1fr;
font-size: 0.75rem;
}
.col {
padding: 0.3rem 0.4rem;
}
}
</style>
@@ -0,0 +1,241 @@
<template>
<div class="region-latency-demo">
<div class="user-location">
<label>你的位置:</label>
<div class="location-options">
<button
v-for="loc in locations"
:key="loc.id"
:class="{ active: userLocation === loc.id }"
@click="userLocation = loc.id"
>
{{ loc.name }}
</button>
</div>
</div>
<div class="latency-table">
<div class="table-header">
<div class="col region">云厂商地域</div>
<div class="col latency">延迟</div>
<div class="col rating">推荐度</div>
</div>
<div
v-for="item in latencyData"
:key="item.region"
class="table-row"
:class="{ best: item.rating === '⭐⭐⭐' }"
>
<div class="col region">{{ item.region }}</div>
<div class="col latency">
<div class="latency-bar">
<div class="bar-fill" :style="{ width: item.percent + '%' }"></div>
<span class="latency-value">{{ item.latency }}ms</span>
</div>
</div>
<div class="col rating">{{ item.rating }}</div>
</div>
</div>
<div class="recommendation">
<span class="rec-icon">💡</span>
<span class="rec-text">{{ recommendation }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const userLocation = ref('beijing')
const locations = [
{ id: 'beijing', name: '北京' },
{ id: 'shanghai', name: '上海' },
{ id: 'guangzhou', name: '广州' },
{ id: 'chengdu', name: '成都' }
]
const latencyMap = {
beijing: [
{ region: '华北-北京', latency: 15, rating: '⭐⭐⭐' },
{ region: '华东-上海', latency: 35, rating: '⭐⭐' },
{ region: '华南-广州', latency: 55, rating: '⭐' },
{ region: '亚太-新加坡', latency: 85, rating: '⭐' }
],
shanghai: [
{ region: '华东-上海', latency: 12, rating: '⭐⭐⭐' },
{ region: '华北-北京', latency: 38, rating: '⭐⭐' },
{ region: '华南-广州', latency: 45, rating: '⭐⭐' },
{ region: '亚太-新加坡', latency: 75, rating: '⭐' }
],
guangzhou: [
{ region: '华南-广州', latency: 10, rating: '⭐⭐⭐' },
{ region: '华东-上海', latency: 42, rating: '⭐⭐' },
{ region: '华北-北京', latency: 58, rating: '⭐' },
{ region: '亚太-新加坡', latency: 45, rating: '⭐⭐' }
],
chengdu: [
{ region: '华东-上海', latency: 40, rating: '⭐⭐' },
{ region: '华北-北京', latency: 48, rating: '⭐⭐' },
{ region: '华南-广州', latency: 52, rating: '⭐' },
{ region: '西南-成都', latency: 8, rating: '⭐⭐⭐' }
]
}
const latencyData = computed(() => {
const data = latencyMap[userLocation.value] || latencyMap.beijing
const maxLatency = Math.max(...data.map(d => d.latency))
return data.map(d => ({
...d,
percent: (d.latency / maxLatency) * 100
}))
})
const recommendation = computed(() => {
const best = latencyData.value.find(d => d.rating === '⭐⭐⭐')
return `建议选择 ${best?.region},延迟最低 (${best?.latency}ms)`
})
</script>
<style scoped>
.region-latency-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.user-location {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.user-location label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.location-options {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.location-options button {
padding: 0.35rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.location-options button:hover {
border-color: var(--vp-c-brand);
}
.location-options button.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
}
.latency-table {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 1rem;
}
.table-header,
.table-row {
display: grid;
grid-template-columns: 100px 1fr 60px;
gap: 0.75rem;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 4px;
}
.table-header {
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
}
.table-row {
font-size: 0.85rem;
background: var(--vp-c-bg);
}
.table-row.best {
background: var(--vp-c-brand-soft);
}
.col.region {
font-weight: 500;
}
.latency-bar {
display: flex;
align-items: center;
gap: 0.5rem;
height: 20px;
background: var(--vp-c-bg-soft);
border-radius: 10px;
overflow: hidden;
padding: 2px;
}
.bar-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 8px;
transition: width 0.3s;
}
.latency-value {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 45px;
}
.recommendation {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
}
.rec-icon {
font-size: 1rem;
}
.rec-text {
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.user-location {
flex-direction: column;
align-items: flex-start;
}
.table-header,
.table-row {
grid-template-columns: 80px 1fr 50px;
font-size: 0.75rem;
}
}
</style>
@@ -0,0 +1,163 @@
<template>
<div class="storage-type-demo">
<div class="type-cards">
<div
v-for="type in storageTypes"
:key="type.id"
class="type-card"
:class="{ active: selectedType === type.id }"
@click="selectedType = type.id"
>
<div class="type-icon">{{ type.icon }}</div>
<div class="type-name">{{ type.name }}</div>
<div class="type-example">{{ type.example }}</div>
</div>
</div>
<div v-if="selectedTypeData" class="type-detail">
<div class="detail-row">
<span class="label">特点</span>
<span class="value">{{ selectedTypeData.features }}</span>
</div>
<div class="detail-row">
<span class="label">适用场景</span>
<span class="value">{{ selectedTypeData.scenarios }}</span>
</div>
<div class="detail-row">
<span class="label">计费方式</span>
<span class="value">{{ selectedTypeData.pricing }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedType = ref('object')
const storageTypes = [
{
id: 'object',
icon: '📦',
name: '对象存储',
example: 'S3 / OSS',
features: '海量存储、高可靠、低成本',
scenarios: '图片、视频、备份、静态网站',
pricing: '按存储容量 + 请求次数'
},
{
id: 'block',
icon: '💽',
name: '块存储',
example: 'EBS / 云盘',
features: '低延迟、高性能、可挂载',
scenarios: '数据库、文件系统、操作系统',
pricing: '按容量 + IOPS'
},
{
id: 'file',
icon: '📁',
name: '文件存储',
example: 'EFS / NAS',
features: '共享访问、POSIX 兼容',
scenarios: '共享文件、内容管理、HPC',
pricing: '按容量 + 吞吐'
},
{
id: 'archive',
icon: '🗃️',
name: '归档存储',
example: 'Glacier / 归档',
features: '极低成本、取回慢',
scenarios: '冷数据、合规备份、长期归档',
pricing: '按容量,取回额外收费'
}
]
const selectedTypeData = computed(() =>
storageTypes.find(t => t.id === selectedType.value)
)
</script>
<style scoped>
.storage-type-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.type-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.type-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.type-card:hover {
border-color: var(--vp-c-brand);
}
.type-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.type-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.type-name {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.15rem;
}
.type-example {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.type-detail {
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-row {
display: flex;
gap: 0.75rem;
font-size: 0.85rem;
}
.detail-row .label {
color: var(--vp-c-text-2);
width: 80px;
flex-shrink: 0;
}
.detail-row .value {
flex: 1;
}
@media (max-width: 640px) {
.type-cards {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
+30
View File
@@ -350,6 +350,21 @@ import ServiceSelectionDemo from './components/appendix/cloud-services/ServiceSe
import DatabaseServicesDemo from './components/appendix/cloud-services/DatabaseServicesDemo.vue'
import K8sServicesDemo from './components/appendix/cloud-services/K8sServicesDemo.vue'
// Cloud Services Simple Components (new)
import CloudServicesOverview from './components/appendix/cloud-services/CloudServicesOverview.vue'
import ProviderComparison from './components/appendix/cloud-services/ProviderComparison.vue'
import PricingCalculator from './components/appendix/cloud-services/PricingCalculator.vue'
import ComputeInstanceDemo from './components/appendix/cloud-services/ComputeInstanceDemo.vue'
import StorageTypeDemo from './components/appendix/cloud-services/StorageTypeDemo.vue'
import ApiCallDemo from './components/appendix/cloud-services/ApiCallDemo.vue'
import CloudHistoryDemo from './components/appendix/cloud-services/CloudHistoryDemo.vue'
import DeployWorkflowDemo from './components/appendix/cloud-services/DeployWorkflowDemo.vue'
import RegionLatencyDemo from './components/appendix/cloud-services/RegionLatencyDemo.vue'
// Cloud IAM Simple Components (new)
import IAMStructure from './components/appendix/cloud-iam/IAMStructure.vue'
import PolicyEditorDemo from './components/appendix/cloud-iam/PolicyEditorDemo.vue'
// Gateway Proxy Components
import ReverseProxyDemo from './components/appendix/gateway-proxy/ReverseProxyDemo.vue'
import ApiGatewayDemo from './components/appendix/gateway-proxy/ApiGatewayDemo.vue'
@@ -784,6 +799,21 @@ export default {
app.component('DatabaseServicesDemo', DatabaseServicesDemo)
app.component('K8sServicesDemo', K8sServicesDemo)
// Cloud Services Simple Components Registration (new)
app.component('CloudServicesOverview', CloudServicesOverview)
app.component('ProviderComparison', ProviderComparison)
app.component('PricingCalculator', PricingCalculator)
app.component('ComputeInstanceDemo', ComputeInstanceDemo)
app.component('StorageTypeDemo', StorageTypeDemo)
app.component('ApiCallDemo', ApiCallDemo)
app.component('CloudHistoryDemo', CloudHistoryDemo)
app.component('DeployWorkflowDemo', DeployWorkflowDemo)
app.component('RegionLatencyDemo', RegionLatencyDemo)
// Cloud IAM Simple Components Registration (new)
app.component('IAMStructure', IAMStructure)
app.component('PolicyEditorDemo', PolicyEditorDemo)
// Cloud IAM Components Registration
app.component('IamRamComparisonDemo', IamRamComparisonDemo)
app.component('IdentityProviderDemo', IdentityProviderDemo)
File diff suppressed because it is too large Load Diff