feat(docs): add interactive demo components for technical appendices

Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is **Easy-Vibe**, an educational curriculum for learning AI Vibe Coding from zero to advanced levels. It's a documentation-based project using **VitePress** to serve educational content about AI-assisted software development.
**Easy-Vibe** is an educational curriculum for learning AI Vibe Coding from zero to advanced levels. It's a documentation-based project using **VitePress** to serve educational content about AI-assisted software development.
The curriculum follows a progressive four-stage structure:
+48
View File
@@ -606,6 +606,22 @@ export default defineConfig({
{
text: '浏览器调试器',
link: '/zh-cn/appendix/browser-devtools'
},
{
text: '浏览器渲染原理',
link: '/zh-cn/appendix/browser-rendering-pipeline'
},
{
text: '前端路由原理',
link: '/zh-cn/appendix/frontend-routing'
},
{
text: '组件状态管理',
link: '/zh-cn/appendix/component-state-management'
},
{
text: '前端工程化',
link: '/zh-cn/appendix/frontend-engineering'
}
]
},
@@ -617,18 +633,50 @@ export default defineConfig({
text: '后端进化史',
link: '/zh-cn/appendix/backend-evolution'
},
{
text: '后端分层架构',
link: '/zh-cn/appendix/backend-layered-architecture'
},
{
text: '后端编程语言',
link: '/zh-cn/appendix/backend-languages'
},
{
text: '并发编程模型',
link: '/zh-cn/appendix/concurrency-models'
},
{
text: '接口设计规范',
link: '/zh-cn/appendix/api-design'
},
{ text: '数据库原理', link: '/zh-cn/appendix/database-intro' },
{ text: '系统缓存设计', link: '/zh-cn/appendix/cache-design' },
{ text: '消息队列设计', link: '/zh-cn/appendix/queue-design' },
{ text: '鉴权原理与实战', link: '/zh-cn/appendix/auth-design' },
{ text: '网关与反向代理', link: '/zh-cn/appendix/gateway-proxy' },
{ text: '负载均衡策略', link: '/zh-cn/appendix/load-balancing' },
{ text: '埋点设计', link: '/zh-cn/appendix/tracking-design' },
{ text: '线上运维', link: '/zh-cn/appendix/operations' }
]
},
{
text: '云计算与服务',
collapsed: false,
items: [
{
text: '云服务基础',
link: '/zh-cn/appendix/cloud-services'
},
{
text: 'IAM 权限管理',
link: '/zh-cn/appendix/cloud-iam'
},
{
text: '对象存储与 CDN',
link: '/zh-cn/appendix/cloud-storage-cdn'
}
]
},
{
text: '通用技能',
collapsed: false,
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
文档演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('API文档演示')
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,97 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>错误处理演示</h4>
<p class="hint">展示RESTful API中的错误处理机制</p>
</div>
<div class="demo-content">
<div class="error-types">
<div class="error-item">
<span class="code">400</span>
<span class="name">Bad Request</span>
<span class="desc">请求参数错误</span>
</div>
<div class="error-item">
<span class="code">401</span>
<span class="name">Unauthorized</span>
<span class="desc">未授权访问</span>
</div>
<div class="error-item">
<span class="code">404</span>
<span class="name">Not Found</span>
<span class="desc">资源不存在</span>
</div>
<div class="error-item">
<span class="code">500</span>
<span class="name">Server Error</span>
<span class="desc">服务器内部错误</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.error-types {
display: flex;
flex-direction: column;
gap: 12px;
}
.error-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid #f56c6c;
}
.error-item .code {
font-family: monospace;
font-weight: 600;
color: #f56c6c;
font-size: 16px;
min-width: 50px;
}
.error-item .name {
font-weight: 500;
color: var(--vp-c-text-1);
min-width: 120px;
}
.error-item .desc {
color: var(--vp-c-text-2);
font-size: 14px;
}
</style>
@@ -0,0 +1,428 @@
<!--
HttpMethodsDemo.vue - HTTP 方法对比演示组件
展示 GET/POST/PUT/PATCH/DELETE 的区别和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">🎯</span>
<span class="title">HTTP 方法用正确的姿势操作资源</span>
</div>
<div class="content">
<!-- HTTP 方法选择器 -->
<div class="method-selector">
<button
v-for="method in methods"
:key="method.name"
class="method-btn"
:class="[method.name, { active: selectedMethod === method.name }]"
@click="selectedMethod = method.name"
>
{{ method.name }}
</button>
</div>
<!-- 当前方法详情 -->
<div class="method-detail" v-if="currentMethod">
<div class="detail-header">
<span class="http-badge" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="method-desc">{{ currentMethod.description }}</span>
</div>
<div class="properties">
<div class="property" :class="{ yes: currentMethod.idempotent }">
<span class="prop-icon">{{ currentMethod.idempotent ? '✅' : '❌' }}</span>
<span class="prop-label">幂等性</span>
<span class="prop-hint">{{ currentMethod.idempotent ? '多次执行结果相同' : '每次执行可能产生不同结果' }}</span>
</div>
<div class="property" :class="{ yes: currentMethod.safe }">
<span class="prop-icon">{{ currentMethod.safe ? '✅' : '❌' }}</span>
<span class="prop-label">安全性</span>
<span class="prop-hint">{{ currentMethod.safe ? '不修改服务器状态' : '可能会修改服务器状态' }}</span>
</div>
<div class="property has-body">
<span class="prop-icon">{{ currentMethod.hasBody ? '✅' : '❌' }}</span>
<span class="prop-label">请求体</span>
<span class="prop-hint">{{ currentMethod.hasBody ? '可以携带请求体数据' : '通常不携带请求体' }}</span>
</div>
</div>
<div class="example-section">
<div class="example-title">📝 使用示例</div>
<div class="example-content">
<div class="example-item" v-for="(example, idx) in currentMethod.examples" :key="idx">
<div class="example-scenario">{{ example.scenario }}</div>
<div class="example-request">
<span class="http-method" :class="currentMethod.name">{{ currentMethod.name }}</span>
<span class="request-url">{{ example.url }}</span>
</div>
<div class="example-body" v-if="example.body">
<pre><code>{{ JSON.stringify(example.body, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const methods = [
{
name: 'GET',
description: '获取资源',
idempotent: true,
safe: true,
hasBody: false,
examples: [
{
scenario: '获取用户列表',
url: '/api/v1/users?page=1&page_size=20'
},
{
scenario: '获取单个用户详情',
url: '/api/v1/users/123'
}
]
},
{
name: 'POST',
description: '创建资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '创建新用户',
url: '/api/v1/users',
body: {
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000'
}
},
{
scenario: '提交订单',
url: '/api/v1/orders',
body: {
user_id: 123,
items: [
{ product_id: 'P001', quantity: 2 },
{ product_id: 'P002', quantity: 1 }
],
shipping_address: {...}
}
}
]
},
{
name: 'PUT',
description: '全量更新资源',
idempotent: true,
safe: false,
hasBody: true,
examples: [
{
scenario: '更新用户全部信息(替换)',
url: '/api/v1/users/123',
body: {
id: 123,
name: '张三(已更新)',
email: 'zhangsan_new@example.com',
phone: '13900139000',
avatar: 'https://...',
status: 'active'
}
}
]
},
{
name: 'PATCH',
description: '部分更新资源',
idempotent: false,
safe: false,
hasBody: true,
examples: [
{
scenario: '仅修改用户邮箱',
url: '/api/v1/users/123',
body: {
email: 'newemail@example.com'
}
},
{
scenario: '更新多个字段',
url: '/api/v1/users/123',
body: {
phone: '13800138000',
avatar: 'https://...'
}
}
]
},
{
name: 'DELETE',
description: '删除资源',
idempotent: true,
safe: false,
hasBody: false,
examples: [
{
scenario: '删除单个用户',
url: '/api/v1/users/123'
},
{
scenario: '批量删除(通过查询参数)',
url: '/api/v1/users?ids=1,2,3,4,5'
}
]
}
]
const selectedMethod = ref('GET')
const currentMethod = computed(() =>
methods.find(m => m.name === selectedMethod.value)
)
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.method-selector {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--vp-c-divider);
}
.method-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.method-btn:hover {
transform: translateY(-1px);
}
.method-btn.active {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* HTTP 方法颜色 */
.method-btn.GET, .http-method.GET { border-color: #22c55e; color: #16a34a; }
.method-btn.GET.active, .http-badge.GET { background: #22c55e; color: white; }
.method-btn.POST, .http-method.POST { border-color: #3b82f6; color: #2563eb; }
.method-btn.POST.active, .http-badge.POST { background: #3b82f6; color: white; }
.method-btn.PUT, .http-method.PUT { border-color: #f59e0b; color: #d97706; }
.method-btn.PUT.active, .http-badge.PUT { background: #f59e0b; color: white; }
.method-btn.PATCH, .http-method.PATCH { border-color: #8b5cf6; color: #7c3aed; }
.method-btn.PATCH.active, .http-badge.PATCH { background: #8b5cf6; color: white; }
.method-btn.DELETE, .http-method.DELETE { border-color: #ef4444; color: #dc2626; }
.method-btn.DELETE.active, .http-badge.DELETE { background: #ef4444; color: white; }
.method-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.http-badge {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
}
.method-desc {
font-size: 15px;
color: var(--vp-c-text-2);
}
.properties {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.property {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
opacity: 0.6;
transition: all 0.2s ease;
}
.property.yes {
opacity: 1;
border-color: #22c55e;
background: #f0fdf4;
}
.property.has-body {
opacity: 1;
}
.property.has-body:not(.yes) {
border-color: #f59e0b;
background: #fffbeb;
}
.prop-icon {
font-size: 20px;
margin-bottom: 4px;
}
.prop-label {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.prop-hint {
font-size: 11px;
color: var(--vp-c-text-3);
text-align: center;
}
.example-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--vp-c-divider);
}
.example-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.example-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.example-item:last-child {
margin-bottom: 0;
}
.example-scenario {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.example-request {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.http-method {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
}
.request-url {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.example-body {
background: var(--vp-c-bg);
border-radius: 4px;
padding: 8px;
}
.example-body pre {
margin: 0;
font-size: 11px;
line-height: 1.4;
overflow-x: auto;
}
.example-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.properties {
grid-template-columns: 1fr;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,145 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>HTTP 请求结构解析</h4>
<p class="hint">详解 HTTP 请求的组成部分</p>
</div>
<div class="demo-content">
<div class="structure-box">
<div class="section request-line">
<div class="label">请求行</div>
<div class="content">
<code>GET /api/users/123 HTTP/1.1</code>
</div>
<div class="explain">
<span>方法</span> + <span>路径</span> + <span>协议版本</span>
</div>
</div>
<div class="section headers">
<div class="label">请求头</div>
<div class="content header-list">
<div><code>Host: api.example.com</code></div>
<div><code>Content-Type: application/json</code></div>
<div><code>Authorization: Bearer token123</code></div>
</div>
<div class="explain">元信息域名数据格式认证等</div>
</div>
<div class="section body">
<div class="label">请求体 (可选)</div>
<div class="content">
<pre>{
"name": "张三",
"email": "zhangsan@example.com"
}</pre>
</div>
<div class="explain">POST/PUT 请求携带的数据</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 纯静态展示组件
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.structure-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border-left: 4px solid var(--vp-c-brand);
}
.section.headers {
border-left-color: #67c23a;
}
.section.body {
border-left-color: #e6a23c;
}
.label {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-size: 14px;
}
.content {
background: #1e1e1e;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
overflow-x: auto;
}
.content code {
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
}
.content pre {
margin: 0;
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 1.5;
}
.header-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.explain {
font-size: 12px;
color: var(--vp-c-text-2);
}
.explain span {
display: inline-block;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
margin: 0 2px;
}
</style>
@@ -0,0 +1,103 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>RESTful 资源类比</h4>
<p class="hint">通过生活中的类比理解 RESTful 资源概念</p>
</div>
<div class="demo-content">
<div class="analogy-box">
<div class="analogy-item">
<div class="icon">📚</div>
<div class="text">
<strong>资源 = 图书</strong>
<p>每本书有唯一的 ISBN资源标识</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">🏪</div>
<div class="text">
<strong>URL = 书架位置</strong>
<p>/library/books/123 表示第 123 号书</p>
</div>
</div>
<div class="analogy-item">
<div class="icon">📝</div>
<div class="text">
<strong>HTTP 方法 = 操作</strong>
<p>GET(查看)POST(借书)PUT(修改)DELETE(还书)</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 静态组件,无需逻辑
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.analogy-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 8px;
}
.icon {
font-size: 24px;
flex-shrink: 0;
}
.text {
flex: 1;
}
.text strong {
display: block;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.text p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
响应结构演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('HTTP响应结构演示')
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应体')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,365 @@
<template>
<div class="restful-design-demo">
<div class="header">
<div class="title">RESTful API 设计核心原则</div>
<div class="subtitle">
RESTRepresentational State Transfer是一种架构风格让接口设计像自然资源一样直观
</div>
</div>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
:class="{ active: selectedPrinciple === principle.id }"
@click="selectedPrinciple = principle.id"
>
<div class="principle-icon">{{ principle.icon }}</div>
<div class="principle-name">{{ principle.name }}</div>
<div class="principle-brief">{{ principle.brief }}</div>
</div>
</div>
<div class="detail-panel">
<div class="detail-header">
<span class="detail-title">{{ activePrinciple.name }}</span>
<span class="detail-tag">{{ activePrinciple.tag }}</span>
</div>
<div class="detail-content">
<div class="explanation">
<h4>核心概念</h4>
<p>{{ activePrinciple.explanation }}</p>
</div>
<div class="comparison">
<h4>对比示例</h4>
<div class="code-comparison">
<div class="code-block bad">
<div class="code-label">传统方式不推荐</div>
<pre><code>{{ activePrinciple.badExample }}</code></pre>
</div>
<div class="code-block good">
<div class="code-label">RESTful 方式推荐</div>
<pre><code>{{ activePrinciple.goodExample }}</code></pre>
</div>
</div>
</div>
<div class="tips">
<h4>设计要点</h4>
<ul>
<li v-for="(tip, index) in activePrinciple.tips" :key="index">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const principles = [
{
id: 'resource',
name: '资源导向',
icon: '📦',
brief: 'URL 表示资源,而非动作',
tag: '核心原则',
explanation: '将系统中的实体抽象为资源(Resource),每个资源对应唯一的 URL。资源是名词,而不是动词或动作。',
badExample: `GET /getUserById?id=123
GET /deleteOrder?orderId=456
POST /createProduct`,
goodExample: `GET /users/123
DELETE /orders/456
POST /products`,
tips: [
'使用名词复数形式(/users 而非 /user',
'避免在 URL 中出现动词(get、create、delete 等)',
'资源层级用路径表示(/users/123/orders',
'资源名使用小写字母,多个单词用连字符(/order-items'
]
},
{
id: 'method',
name: 'HTTP 方法',
icon: '🎯',
brief: '用 HTTP 方法表达操作语义',
tag: '动作表达',
explanation: '使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)来表示对资源的操作类型,让接口语义更加清晰。',
badExample: `POST /users/query // 查询用户
POST /users/create // 创建用户
POST /users/update // 更新用户
POST /users/delete // 删除用户`,
goodExample: `GET /users // 查询用户列表
POST /users // 创建用户
GET /users/123 // 查询单个用户
PUT /users/123 // 全量更新用户
PATCH /users/123 // 部分更新用户
DELETE /users/123 // 删除用户`,
tips: [
'GET 用于获取资源,是幂等且安全的',
'POST 用于创建资源,返回 201 和新资源 URI',
'PUT 用于全量更新,替换整个资源',
'PATCH 用于部分更新,只修改指定字段',
'DELETE 用于删除资源,返回 204 或 200'
]
},
{
id: 'stateless',
name: '无状态',
icon: '🔄',
brief: '每个请求独立,服务端不保存会话',
tag: '可扩展性',
explanation: '服务端不保存客户端的上下文状态,每个请求都必须包含服务端处理该请求所需的全部信息。这让系统更容易水平扩展。',
badExample: `// 服务端维护会话状态
POST /login
→ 服务端创建 session,返回 session_id cookie
GET /profile
→ 服务端根据 session_id 查找用户
→ 如果会话过期,需要重新登录`,
goodExample: `// 无状态认证
POST /auth/token
→ 验证凭证,返回 JWT token
GET /profile
Authorization: Bearer <token>
→ 服务端验证 token,提取用户信息
→ 请求独立,可随时扩展到多台服务器`,
tips: [
'使用 JWT 或 API Key 进行无状态认证',
'避免在服务端存储会话状态',
'每个请求包含完整的认证信息',
'便于负载均衡和水平扩展',
'使用 Redis 等缓存共享必要的状态数据'
]
},
{
id: 'representation',
name: '统一表现',
icon: '📋',
brief: '使用标准数据格式',
tag: '数据交换',
explanation: '资源的表示(Representation)应该使用标准的数据格式,通常是 JSON。客户端可以通过 Accept 头部请求不同的表示格式。',
badExample: `// 混合格式,字段不一致
GET /users
{
"user_list": [...],
"total_count": 100
}
GET /orders
{
"data": [...],
"pagination": {
"total": 100,
"page": 1
}
}`,
goodExample: `// 统一的响应结构
GET /users
{
"code": 200,
"message": "success",
"data": {
"items": [...],
"pagination": {
"total": 100,
"page": 1,
"page_size": 20
}
},
"timestamp": "2024-01-15T10:30:00Z"
}`,
tips: [
'使用 JSON 作为默认数据格式',
'统一的响应结构(code、message、data',
'支持字段过滤(fields=id,name,email',
'日期使用 ISO 8601 格式',
'字段命名使用 camelCase 或 snake_case,保持一致'
]
}
]
const selectedPrinciple = ref('resource')
const activePrinciple = computed(() =>
principles.find(p => p.id === selectedPrinciple.value) || principles[0]
)
</script>
<style scoped>
.restful-design-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.principle-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.principle-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.principle-card.active {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.principle-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.principle-name {
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.principle-brief {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1.25rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
font-weight: 600;
}
.detail-content h4 {
font-weight: 700;
color: var(--vp-c-text-1);
margin: 1rem 0 0.5rem;
}
.explanation p {
color: var(--vp-c-text-2);
line-height: 1.7;
}
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.code-block {
border-radius: 6px;
overflow: hidden;
}
.code-block.bad {
border: 1px solid #ef4444;
}
.code-block.good {
border: 1px solid #22c55e;
}
.code-label {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.code-block.bad .code-label {
background: #ef4444;
}
.code-block.good .code-label {
background: #22c55e;
}
.code-block pre {
margin: 0;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
font-size: 0.8rem;
line-height: 1.5;
overflow-x: auto;
}
.tips ul {
margin: 0;
padding-left: 1.25rem;
color: var(--vp-c-text-2);
}
.tips li {
margin: 0.4rem 0;
line-height: 1.6;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: repeat(2, 1fr);
}
.code-comparison {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,618 @@
<!--
StatusCodeDemo.vue - HTTP 状态码演示组件
展示常见 HTTP 状态码的含义和使用场景
-->
<template>
<div class="demo">
<div class="header">
<span class="icon">📡</span>
<span class="title">HTTP 状态码服务器的"情绪表达"</span>
</div>
<div class="content">
<div class="category-tabs">
<button
v-for="category in categories"
:key="category.code"
class="category-btn"
:class="[category.class, { active: selectedCategory === category.code }]"
@click="selectedCategory = category.code"
>
<span class="category-code">{{ category.code }}xx</span>
<span class="category-name">{{ category.name }}</span>
</button>
</div>
<div class="status-codes" v-if="filteredCodes.length > 0">
<div
v-for="code in filteredCodes"
:key="code.number"
class="status-card"
:class="{ expanded: expandedCode === code.number }"
@click="toggleExpand(code.number)"
>
<div class="status-header">
<span class="status-number" :class="getCategoryClass(code.number)">{{ code.number }}</span>
<span class="status-name">{{ code.name }}</span>
<span class="expand-icon">{{ expandedCode === code.number ? '▼' : '▶' }}</span>
</div>
<div class="status-detail" v-show="expandedCode === code.number">
<div class="detail-section">
<h4>💡 含义解释</h4>
<p>{{ code.description }}</p>
</div>
<div class="detail-section">
<h4>📝 使用场景</h4>
<ul>
<li v-for="(scenario, idx) in code.scenarios" :key="idx">{{ scenario }}</li>
</ul>
</div>
<div class="detail-section" v-if="code.example">
<h4>💻 示例代码</h4>
<div class="code-example">
<div class="code-request">
<span class="method-badge" :class="getCategoryClass(code.number)">{{ code.example.method }}</span>
<code>{{ code.example.path }}</code>
</div>
<div class="code-response">
<pre><code>{{ JSON.stringify(code.example.response, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const categories = [
{ code: '2', name: '成功', class: 'success' },
{ code: '3', name: '重定向', class: 'redirect' },
{ code: '4', name: '客户端错误', class: 'client-error' },
{ code: '5', name: '服务器错误', class: 'server-error' }
]
const statusCodes = [
{
number: 200,
name: 'OK',
description: '请求已成功处理。这是最常用的成功状态码。',
scenarios: [
'GET 请求成功返回数据',
'POST 请求成功处理但未创建新资源',
'PUT/PATCH 更新成功'
],
example: {
method: 'GET',
path: '/api/v1/users/123',
response: {
code: 0,
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com'
}
}
}
},
{
number: 201,
name: 'Created',
description: '请求成功处理并创建了新的资源。通常用于 POST 请求。',
scenarios: [
'成功创建用户账号',
'成功创建订单',
'成功上传文件'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 0,
data: {
id: 124,
name: '李四',
created_at: '2024-01-15T10:30:00Z'
}
}
}
},
{
number: 204,
name: 'No Content',
description: '请求成功处理,但响应中没有返回内容。',
scenarios: [
'DELETE 删除成功',
'PUT/PATCH 更新成功但无需返回数据',
'预检请求(OPTIONS)响应'
],
example: {
method: 'DELETE',
path: '/api/v1/users/123',
response: null
}
},
{
number: 301,
name: 'Moved Permanently',
description: '请求的资源已永久移动到新的 URL。',
scenarios: [
'API 版本升级,旧版本废弃',
'网站重构,URL 结构变更',
'资源合并或重命名'
]
},
{
number: 304,
name: 'Not Modified',
description: '资源自上次请求以来未被修改,客户端可使用缓存版本。',
scenarios: [
'客户端带有 If-None-Match 或 If-Modified-Since 头部',
'静态资源缓存优化',
'减少不必要的数据传输'
]
},
{
number: 400,
name: 'Bad Request',
description: '请求语法错误或参数无效,服务器无法理解请求。',
scenarios: [
'请求体格式不正确(如 JSON 语法错误)',
'缺少必填参数',
'参数类型不匹配(字符串传数字)'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10001,
message: '参数校验失败',
errors: [
{ field: 'email', message: '邮箱格式不正确' }
]
}
}
},
{
number: 401,
name: 'Unauthorized',
description: '请求需要用户身份验证,但未提供或凭证无效。',
scenarios: [
'未登录就访问受保护资源',
'Token 过期或无效',
'缺少 Authorization 头部'
],
example: {
method: 'GET',
path: '/api/v1/user/profile',
response: {
code: 10018,
message: '认证令牌已过期,请重新登录'
}
}
},
{
number: 403,
name: 'Forbidden',
description: '服务器理解请求,但拒绝执行(权限不足)。',
scenarios: [
'已登录但访问了没有权限的资源',
'普通用户尝试访问管理员功能',
'账号被禁用或权限被撤销'
],
example: {
method: 'DELETE',
path: '/api/v1/users/456',
response: {
code: 10021,
message: '权限不足,需要管理员权限才能删除用户'
}
}
},
{
number: 404,
name: 'Not Found',
description: '服务器找不到请求的资源。',
scenarios: [
'URL 拼写错误',
'资源已被删除或不存在',
'API 版本已废弃'
],
example: {
method: 'GET',
path: '/api/v1/users/99999',
response: {
code: 10002,
message: '用户不存在'
}
}
},
{
number: 409,
name: 'Conflict',
description: '请求与服务器当前状态冲突(如资源重复)。',
scenarios: [
'尝试创建已存在的用户(唯一约束冲突)',
'乐观锁版本号不匹配',
'并发修改导致的状态冲突'
],
example: {
method: 'POST',
path: '/api/v1/users',
response: {
code: 10011,
message: '邮箱已被注册'
}
}
},
{
number: 422,
name: 'Unprocessable Entity',
description: '请求格式正确,但语义上有错误(验证失败)。',
scenarios: [
'请求体 JSON 格式正确,但字段值不符合业务规则',
'密码强度不足',
'余额不足无法完成支付'
],
example: {
method: 'POST',
path: '/api/v1/orders',
response: {
code: 10014,
message: '订单金额不能为负数'
}
}
},
{
number: 429,
name: 'Too Many Requests',
description: '客户端发送请求过多,触发了限流。',
scenarios: [
'短时间内大量请求',
'超出 API 配额限制',
'触发防刷机制'
],
example: {
method: 'GET',
path: '/api/v1/data',
response: {
code: 10005,
message: '请求过于频繁,请 60 秒后重试'
}
}
},
{
number: 500,
name: 'Internal Server Error',
description: '服务器内部错误,无法完成请求。',
scenarios: [
'代码抛出未捕获的异常',
'数据库连接失败',
'依赖服务不可用'
],
example: {
method: 'GET',
path: '/api/v1/users',
response: {
code: 10000,
message: '服务器内部错误,请联系管理员'
}
}
},
{
number: 502,
name: 'Bad Gateway',
description: '网关或代理从上游服务器收到无效响应。',
scenarios: [
'反向代理(Nginx)无法连接到后端服务',
'后端服务崩溃或重启中',
'网关配置错误'
]
},
{
number: 503,
name: 'Service Unavailable',
description: '服务器暂时无法处理请求(维护或过载)。',
scenarios: [
'服务器正在进行维护',
'服务器过载,触发熔断',
'依赖服务大面积故障'
],
example: {
method: 'GET',
path: '/api/v1/status',
response: {
code: 10007,
message: '服务维护中,预计 10 分钟后恢复'
}
}
},
{
number: 504,
name: 'Gateway Timeout',
description: '网关或代理等待上游服务器响应超时。',
scenarios: [
'后端处理时间过长',
'网络延迟或丢包',
'数据库查询超时'
]
}
]
const selectedCategory = ref('2')
const expandedCode = ref(null)
const filteredCodes = computed(() => {
const prefix = selectedCategory.value
return statusCodes.filter(code => {
const codePrefix = Math.floor(code.number / 100).toString()
return codePrefix === prefix
})
})
function getCategoryClass(number) {
const prefix = Math.floor(number / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
}
}
function toggleExpand(number) {
expandedCode.value = expandedCode.value === number ? null : number
}
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.category-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.category-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
}
.category-btn:hover {
transform: translateY(-2px);
}
.category-btn.active {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 分类颜色 */
.category-btn.success, .status-number.success { border-color: #22c55e; color: #16a34a; }
.category-btn.success.active { background: #22c55e; color: white; }
.category-btn.redirect, .status-number.redirect { border-color: #3b82f6; color: #2563eb; }
.category-btn.redirect.active { background: #3b82f6; color: white; }
.category-btn.client-error, .status-number.client-error { border-color: #f59e0b; color: #d97706; }
.category-btn.client-error.active { background: #f59e0b; color: white; }
.category-btn.server-error, .status-number.server-error { border-color: #ef4444; color: #dc2626; }
.category-btn.server-error.active { background: #ef4444; color: white; }
.category-code {
font-size: 18px;
font-weight: 700;
}
.category-name {
font-size: 12px;
margin-top: 4px;
}
.status-codes {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
}
.status-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.status-card.expanded {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.1);
}
.status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.status-number {
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
background: var(--vp-c-bg-soft);
border: 1px solid;
}
.status-name {
flex: 1;
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.expand-icon {
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-detail {
padding: 16px;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.detail-section {
margin-bottom: 16px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h4 {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 8px 0;
}
.detail-section p {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0;
}
.detail-section ul {
margin: 0;
padding-left: 16px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 4px 0;
}
.code-example {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-request {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.method-badge {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 11px;
background: var(--vp-c-bg);
border: 1px solid;
}
.code-request code {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.code-response {
padding: 12px;
background: var(--vp-c-bg);
}
.code-response pre {
margin: 0;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-response code {
font-family: monospace;
color: var(--vp-c-text-1);
}
@media (max-width: 640px) {
.category-tabs {
flex-direction: column;
}
.category-btn {
flex-direction: row;
justify-content: space-between;
}
.status-header {
flex-wrap: wrap;
}
.code-request {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
版本策略演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('版本策略演示')
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,283 @@
<template>
<div class="architecture-comparison-demo">
<div class="demo-header">
<h4>🏗 架构演进对比</h4>
<p>四个时代的核心架构特征对比</p>
</div>
<div class="comparison-grid">
<div class="era-card" v-for="era in eras" :key="era.name" :class="{ active: selectedEra === era.name }" @click="selectedEra = era.name">
<div class="era-icon">{{ era.icon }}</div>
<div class="era-name">{{ era.name }}</div>
<div class="era-year">{{ era.year }}</div>
<div class="era-tag">{{ era.tag }}</div>
</div>
</div>
<div class="detail-panel" v-if="selectedEra">
<div class="detail-header">
<span class="detail-icon">{{ currentEra.icon }}</span>
<h5>{{ currentEra.name }} ({{ currentEra.year }})</h5>
</div>
<div class="detail-content">
<div class="feature-section">
<h6>🏗 架构特征</h6>
<ul>
<li v-for="(feat, i) in currentEra.features" :key="i">{{ feat }}</li>
</ul>
</div>
<div class="feature-section">
<h6> 优点</h6>
<ul>
<li v-for="(pro, i) in currentEra.pros" :key="i">{{ pro }}</li>
</ul>
</div>
<div class="feature-section">
<h6> 痛点</h6>
<ul>
<li v-for="(con, i) in currentEra.cons" :key="i">{{ con }}</li>
</ul>
</div>
<div class="tech-stack">
<h6>🔧 典型技术</h6>
<div class="tech-tags">
<span v-for="(tech, i) in currentEra.techs" :key="i" class="tech-tag">{{ tech }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedEra = ref('单体')
const eras = [
{ name: '物理机', icon: '🖥️', year: '1990s', tag: '单机' },
{ name: '单体', icon: '🏢', year: '2000s', tag: '集中' },
{ name: '微服务', icon: '🏭', year: '2010s', tag: '分布' },
{ name: 'Serverless', icon: '☁️', year: '2020s+', tag: '无服' }
]
const eraDetails = {
'物理机': {
features: ['单机部署,无冗余', 'FTP 手动上传代码', '垂直扩展(买更强的机器)', '无服务治理概念'],
pros: ['部署简单,无需复杂配置', '单机性能好,无网络延迟', '易于调试和排查问题'],
cons: ['单点故障,服务不可用', '扩展困难,只能垂直扩容', '手动运维,效率低下'],
techs: ['Apache/Nginx', 'CGI/Perl', 'FTP/SFTP', '物理服务器']
},
'单体': {
features: ['单一代码库,统一技术栈', '共享数据库,事务一致性', '统一部署,整体发布', '进程内通信,无网络开销'],
pros: ['开发简单,易于上手', '测试方便,本地启动即可', '部署简单,一个包搞定'],
cons: ['代码耦合,牵一发而动全身', '技术栈单一,难以引入新技术', '团队扩张后协作困难'],
techs: ['Spring/Django/Rails', 'Tomcat/Gunicorn', 'MySQL/PostgreSQL', 'Maven/Gradle']
},
'微服务': {
features: ['服务拆分,独立部署', '技术栈异构,自由选择', '数据库独立,最终一致性', '服务间网络通信'],
pros: ['服务独立,团队自治', '技术栈灵活,选择最适合的', '故障隔离,不影响全局'],
cons: ['分布式复杂度,调试困难', '网络延迟,性能损耗', '运维成本激增'],
techs: ['Docker/Kubernetes', 'gRPC/REST', 'Kafka/RabbitMQ', 'Prometheus/Grafana']
},
'Serverless': {
features: ['函数粒度,事件驱动', '自动扩缩容,按需计费', '无服务器管理,平台托管', '冷启动,有延迟'],
pros: ['无需运维,专注业务', '自动扩展,应对流量高峰', '按调用付费,成本低'],
cons: ['冷启动延迟', '平台锁定,迁移困难', '调试困难,本地难复现'],
techs: ['AWS Lambda', 'Vercel/Cloudflare', 'Supabase/Firebase', 'EventBridge']
}
}
const currentEra = computed(() => {
const name = selectedEra.value
return {
icon: eras.find(e => e.name === name)?.icon || '🏗️',
name,
year: eras.find(e => e.name === name)?.year || '',
...eraDetails[name]
}
})
</script>
<style scoped>
.architecture-comparison-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.era-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.era-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.era-card.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.era-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.era-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.era-year {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.era-tag {
display: inline-block;
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 10px;
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.detail-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1.5rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 1.5rem;
}
.detail-header h5 {
margin: 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.feature-section {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 1rem;
}
.feature-section h6 {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--vp-c-brand);
}
.feature-section ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.feature-section li {
margin-bottom: 0.4rem;
line-height: 1.5;
}
.feature-section li:last-child {
margin-bottom: 0;
}
.tech-stack {
grid-column: 1 / -1;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 1rem;
}
.tech-stack h6 {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--vp-c-brand);
}
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tech-tag {
padding: 0.25rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: repeat(2, 1fr);
}
.detail-content {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,271 @@
<template>
<div class="container-docker-demo">
<div class="demo-header">
<h4>🐳 Docker 容器化演示</h4>
<p>理解容器如何让应用"一次打包,到处运行"</p>
</div>
<div class="docker-visualization">
<div class="layer traditional" :class="{ active: showTraditional }" @click="showTraditional = true; showDocker = false">
<h5>传统部署</h5>
<div class="server-stack">
<div class="layer-item app">应用 A</div>
<div class="layer-item conflict" v-if="showConflict">依赖冲突!</div>
<div class="layer-item deps">依赖库 v1.0</div>
<div class="layer-item os">操作系统</div>
<div class="layer-item hardware">物理服务器</div>
</div>
</div>
<div class="vs-divider">VS</div>
<div class="layer docker" :class="{ active: showDocker }" @click="showDocker = true; showTraditional = false">
<h5>Docker 容器</h5>
<div class="docker-stack">
<div class="containers">
<div class="container-box">
<div class="container-app">应用 A</div>
<div class="container-deps">依赖 v1.0</div>
</div>
<div class="container-box">
<div class="container-app">应用 B</div>
<div class="container-deps">依赖 v2.0</div>
</div>
</div>
<div class="docker-engine">Docker Engine</div>
<div class="host-os">宿主机操作系统</div>
<div class="hardware">物理服务器</div>
</div>
</div>
</div>
<div class="benefits-grid">
<div class="benefit-card" v-for="benefit in benefits" :key="benefit.title">
<div class="benefit-icon">{{ benefit.icon }}</div>
<div class="benefit-title">{{ benefit.title }}</div>
<div class="benefit-desc">{{ benefit.desc }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showTraditional = ref(true)
const showDocker = ref(false)
const showConflict = ref(false)
const benefits = [
{ icon: '📦', title: '环境一致性', desc: '开发、测试、生产环境完全一致,告别"在我机器上能跑"' },
{ icon: '🚀', title: '快速部署', desc: '秒级启动,镜像分发,滚动更新无停机' },
{ icon: '📊', title: '资源隔离', desc: 'CPU/内存限制,互不干扰,一台机器跑多个应用' },
{ icon: '🔄', title: '版本管理', desc: '镜像版本化,随时回滚,灰度发布' }
]
</script>
<style scoped>
.container-docker-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.docker-visualization {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: stretch;
}
.layer {
flex: 1;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s;
}
.layer:hover,
.layer.active {
border-color: var(--vp-c-brand);
}
.layer h5 {
margin: 0 0 1rem 0;
text-align: center;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.server-stack,
.docker-stack {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.layer-item {
padding: 0.6rem;
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
}
.layer-item.app {
background: rgba(102, 126, 234, 0.2);
color: var(--vp-c-brand);
font-weight: 600;
}
.layer-item.deps {
background: var(--vp-c-bg-soft);
border: 1px dashed var(--vp-c-divider);
}
.layer-item.os,
.layer-item.hardware {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.layer-item.conflict {
background: rgba(239, 68, 68, 0.2);
color: var(--vp-c-danger);
font-weight: 600;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.containers {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.container-box {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 6px;
padding: 0.5rem;
text-align: center;
}
.container-app {
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-brand);
margin-bottom: 0.2rem;
}
.container-deps {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.docker-engine {
padding: 0.6rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
font-weight: 600;
color: #059669;
}
.host-os,
.hardware {
padding: 0.6rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.vs-divider {
display: flex;
align-items: center;
font-weight: 700;
color: var(--vp-c-text-3);
font-size: 0.9rem;
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.benefit-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: all 0.2s;
}
.benefit-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.benefit-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.benefit-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.benefit-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
@media (max-width: 768px) {
.docker-visualization {
flex-direction: column;
}
.vs-divider {
justify-content: center;
padding: 0.5rem 0;
}
.benefits-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,255 @@
<template>
<div class="deployment-flow-demo">
<div class="demo-header">
<h4>🚀 部署方式演进</h4>
<p>从手工部署到自动化流水线的变化</p>
</div>
<div class="flow-timeline">
<div
v-for="(step, idx) in steps"
:key="idx"
class="flow-step"
:class="{ active: currentStep === idx }"
@click="currentStep = idx"
>
<div class="step-connector" v-if="idx > 0">
<div class="connector-line"></div>
</div>
<div class="step-content">
<div class="step-icon">{{ step.icon }}</div>
<div class="step-era">{{ step.era }}</div>
<div class="step-title">{{ step.title }}</div>
</div>
</div>
</div>
<div class="step-detail" v-if="currentStep !== null">
<h5>{{ steps[currentStep].title }}</h5>
<div class="detail-grid">
<div class="detail-item">
<span class="label">部署方式:</span>
<span class="value">{{ steps[currentStep].deploy }}</span>
</div>
<div class="detail-item">
<span class="label">耗时:</span>
<span class="value">{{ steps[currentStep].time }}</span>
</div>
<div class="detail-item">
<span class="label">风险:</span>
<span class="value">{{ steps[currentStep].risk }}</span>
</div>
</div>
<div class="tools-list">
<span class="tools-label">代表工具:</span>
<span v-for="tool in steps[currentStep].tools" :key="tool" class="tool-tag">{{ tool }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(1)
const steps = [
{
icon: '👤',
era: '1990s',
title: '手工部署',
deploy: 'FTP 上传文件',
time: '30分钟-2小时',
risk: '人为错误率高',
tools: ['FTP', 'SSH', 'SCP']
},
{
icon: '📦',
era: '2000s',
title: '脚本部署',
deploy: '自动化脚本',
time: '10-30分钟',
risk: '脚本维护成本',
tools: ['Shell', 'Ansible', 'Puppet']
},
{
icon: '🔄',
era: '2010s',
title: 'CI/CD 流水线',
deploy: '自动化流水线',
time: '5-15分钟',
risk: '流水线配置复杂',
tools: ['Jenkins', 'GitLab CI', 'GitHub Actions']
},
{
icon: '🚀',
era: '2020s+',
title: 'GitOps',
deploy: '声明式部署',
time: '秒级',
risk: '学习曲线陡峭',
tools: ['ArgoCD', 'Flux', 'Kubernetes']
}
]
</script>
<style scoped>
.deployment-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.flow-timeline {
display: flex;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 1rem;
}
.flow-step {
flex: 1;
display: flex;
align-items: center;
cursor: pointer;
position: relative;
}
.step-connector {
position: absolute;
left: -0.5rem;
top: 50%;
transform: translateY(-50%);
width: 0.5rem;
height: 2px;
}
.connector-line {
width: 100%;
height: 100%;
background: var(--vp-c-divider);
}
.step-content {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem;
text-align: center;
transition: all 0.2s;
}
.flow-step:hover .step-content,
.flow-step.active .step-content {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.05);
}
.step-icon {
font-size: 1.25rem;
margin-bottom: 0.25rem;
}
.step-era {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.125rem;
}
.step-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.step-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
}
.step-detail h5 {
margin: 0 0 0.75rem 0;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.detail-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.value {
font-size: 0.85rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.tools-list {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.tools-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.tool-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.flow-timeline {
flex-wrap: wrap;
}
.flow-step {
flex: 0 0 calc(50% - 0.25rem);
}
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,282 @@
<template>
<div class="evolution-intro-demo">
<div class="intro-header">
<h3>后端架构进化之旅</h3>
<p>用一个餐厅的成长历程理解后端架构的 30 年变迁</p>
</div>
<div class="timeline-cards">
<div
v-for="(stage, idx) in stages"
:key="idx"
class="stage-card"
:class="{ active: currentStage === idx }"
@click="currentStage = idx"
>
<div class="stage-era">{{ stage.era }}</div>
<div class="stage-icon">{{ stage.icon }}</div>
<div class="stage-name">{{ stage.name }}</div>
<div class="stage-arch">{{ stage.arch }}</div>
</div>
</div>
<div class="stage-detail">
<Transition name="fade" mode="out-in">
<div :key="currentStage" class="detail-panel">
<div class="detail-header">
<span class="detail-icon">{{ stages[currentStage].icon }}</span>
<h4>{{ stages[currentStage].restaurant }}</h4>
</div>
<div class="detail-content">
<div class="detail-section">
<h5>🍽 餐厅场景</h5>
<p>{{ stages[currentStage].scenario }}</p>
</div>
<div class="detail-section">
<h5>💻 后端映射</h5>
<p>{{ stages[currentStage].mapping }}</p>
</div>
<div class="detail-section">
<h5> 核心痛点</h5>
<ul>
<li v-for="(pain, i) in stages[currentStage].pains" :key="i">{{ pain }}</li>
</ul>
</div>
</div>
</div>
</Transition>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStage = ref(0)
const stages = [
{
era: '1990s',
icon: '🏠',
name: '家庭小作坊',
arch: '物理服务器',
restaurant: '家庭小厨房',
scenario: '一位厨师在一间小厨房里,亲自去菜市场买菜、洗菜、切菜、炒菜、上菜。客人多了就忙不过来,只能让客人排队等。',
mapping: '一台物理服务器,处理所有请求:接收HTTP请求、读取文件、执行CGI脚本、返回响应。CPU和内存有限,请求多了只能排队。',
pains: [
'单机性能瓶颈:客人太多时,厨师根本忙不过来',
'垂直扩展成本高:买更贵的机器就像换更大的厨房,治标不治本',
'单点故障:厨师生病了,整个餐馆必须关门'
]
},
{
era: '2000s',
icon: '🏢',
name: '大型中央厨房',
arch: '单体架构',
restaurant: '连锁餐厅中央厨房',
scenario: '建立了一个大型中央厨房,分工明确:有人专门洗菜、有人专门切菜、有人专门炒菜。但所有人都在一个大空间里工作,互相依赖。',
mapping: '单体应用架构:所有功能模块(用户、订单、支付)都在同一个进程中运行,共享同一个数据库,部署在一个大应用服务器上。',
pains: [
'牵一发而动全身:切菜师傅切到手,整个厨房都要停下来',
'技术债务累积:老员工(老代码)越来越多,新人很难接手',
'部署风险高:更新一个菜品(功能)可能影响整个菜单(系统)'
]
},
{
era: '2010s',
icon: '🏭',
name: '专业化分工',
arch: '微服务架构',
restaurant: '餐饮集团多厨房',
scenario: '把中央厨房拆分成多个专业厨房:一个专门做中餐、一个专门做西餐、一个专门做甜点。每个厨房独立运营,通过标准化流程协作。',
mapping: '微服务架构:每个业务功能(用户服务、订单服务、支付服务)都是独立的进程,有自己的数据库,通过HTTP/gRPC通信。',
pains: [
'分布式复杂度:协调多个厨房比管理一个厨房难得多',
'网络依赖:中餐厨房需要西餐厨房的原料时,可能网络延迟或故障',
'运维成本激增:需要更多人手(运维工程师)来管理这么多厨房'
]
},
{
era: '2020s+',
icon: '🍽️',
name: '外卖平台',
arch: 'Serverless',
restaurant: '外卖/云厨房',
scenario: '你不再自己开厨房,而是在外卖平台上注册。有订单时,平台调度附近的厨房为你制作食物。你只管设计菜品和推广,不用关心厨房在哪、有多少厨师。',
mapping: 'Serverless架构:开发者只写业务代码(函数),不关心服务器在哪、有多少台、怎么扩容。云平台自动调度资源,按实际执行时间付费。',
pains: [
'冷启动延迟:第一家店接单时可能需要热身(冷启动),客人要等',
'平台依赖:完全依赖外卖平台(云厂商),迁移成本高',
'资源限制:不能做太复杂的菜品(函数有时长和内存限制)'
]
}
]
</script>
<style scoped>
.evolution-intro-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 32px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.intro-header {
text-align: center;
margin-bottom: 32px;
}
.intro-header h3 {
font-size: 24px;
font-weight: 700;
margin: 0 0 8px 0;
}
.intro-header p {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
.timeline-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stage-card {
background: rgba(255, 255, 255, 0.1);
border: 2px solid transparent;
border-radius: 12px;
padding: 16px 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.stage-card:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.stage-card.active {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.stage-era {
font-size: 11px;
opacity: 0.7;
margin-bottom: 4px;
}
.stage-icon {
font-size: 32px;
margin-bottom: 8px;
}
.stage-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
}
.stage-arch {
font-size: 11px;
opacity: 0.7;
}
.stage-detail {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 24px;
}
.detail-panel {
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-icon {
font-size: 32px;
}
.detail-header h4 {
font-size: 20px;
font-weight: 600;
margin: 0;
}
.detail-content {
display: grid;
gap: 16px;
}
.detail-section h5 {
font-size: 14px;
font-weight: 600;
margin: 0 0 8px 0;
color: #ffd700;
}
.detail-section p {
font-size: 13px;
line-height: 1.6;
margin: 0;
opacity: 0.9;
}
.detail-section ul {
margin: 0;
padding-left: 18px;
}
.detail-section li {
font-size: 13px;
line-height: 1.6;
margin-bottom: 4px;
opacity: 0.9;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s ease;
}
.fade-enter-from {
opacity: 0;
transform: translateX(20px);
}
.fade-leave-to {
opacity: 0;
transform: translateX(-20px);
}
@media (max-width: 768px) {
.timeline-cards {
grid-template-columns: repeat(2, 1fr);
}
.stage-detail {
padding: 16px;
}
}
</style>
@@ -0,0 +1,444 @@
<template>
<div class="kubernetes-demo">
<div class="demo-header">
<h4> Kubernetes 编排演示</h4>
<p>观察 K8s 如何自动调度容器实现负载均衡和故障恢复</p>
</div>
<div class="k8s-architecture">
<div class="control-plane">
<div class="plane-title">控制平面 (Control Plane)</div>
<div class="components">
<div class="component" v-for="comp in controlPlane" :key="comp.name"
:class="{ active: activeComponent === comp.name }"
@click="activeComponent = comp.name">
<div class="comp-icon">{{ comp.icon }}</div>
<div class="comp-name">{{ comp.name }}</div>
<div class="comp-desc">{{ comp.desc }}</div>
</div>
</div>
</div>
<div class="worker-nodes">
<div class="plane-title">工作节点 (Worker Nodes)</div>
<div class="nodes-container">
<div class="node" v-for="node in workerNodes" :key="node.name"
:class="{
active: node.status === 'active',
failed: node.status === 'failed',
selected: selectedNode === node.name
}"
@click="selectNode(node.name)">
<div class="node-header">
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
<span class="node-status" :class="node.status">{{ node.statusText }}</span>
</div>
<div class="node-resources">
<div class="resource">
<span class="res-label">CPU:</span>
<div class="res-bar">
<div class="res-fill" :style="{ width: node.cpu + '%' }" :class="{ high: node.cpu > 80 }"></div>
</div>
<span class="res-value">{{ node.cpu }}%</span>
</div>
<div class="resource">
<span class="res-label">内存:</span>
<div class="res-bar">
<div class="res-fill" :style="{ width: node.memory + '%' }" :class="{ high: node.memory > 80 }"></div>
</div>
<span class="res-value">{{ node.memory }}%</span>
</div>
</div>
<div class="node-pods">
<div class="pods-label">运行 Pod: {{ node.pods }} </div>
<div class="pods-grid">
<div v-for="n in Math.min(node.pods, 8)" :key="n" class="pod-dot" :class="{
running: node.status === 'active',
pending: node.status === 'pending',
failed: node.status === 'failed'
}"></div>
<div v-if="node.pods > 8" class="pod-more">+{{ node.pods - 8 }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="k8s-controls">
<button class="control-btn" @click="simulateScheduling" :disabled="isScheduling">{{ isScheduling ? '调度中...' : '🚀 模拟 Pod 调度' }}</button>
<button class="control-btn" @click="simulateScaling" :disabled="isScaling">{{ isScaling ? '扩容中...' : '📈 自动扩容' }}</button>
<button class="control-btn danger" @click="simulateFailure" :disabled="isFailing">{{ isFailing ? '故障注入中...' : '💥 模拟节点故障' }}</button>
<button class="control-btn" @click="resetCluster">🔄 重置集群</button>
</div>
<div class="k8s-logs" v-if="logs.length > 0">
<div class="log-entry" v-for="(log, idx) in logs.slice(-5)" :key="idx" :class="log.level">
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
<div class="demo-explanation">
<h5>💡 Kubernetes 核心概念</h5>
<ul>
<li><strong>Pod</strong>最小的部署单元一个 Pod 可以包含一个或多个容器</li>
<li><strong>Deployment</strong>管理 Pod 的副本数量和滚动更新</li>
<li><strong>Service</strong>提供稳定的网络访问入口实现负载均衡</li>
<li><strong>Scheduler</strong>根据资源需求和策略自动将 Pod 调度到合适的节点</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const controlPlane = [
{ name: 'API Server', icon: '🌐', desc: '集群的统一入口' },
{ name: 'etcd', icon: '🗄️', desc: '分布式键值存储' },
{ name: 'Scheduler', icon: '📋', desc: 'Pod 调度器' },
{ name: 'Controller', icon: '🎮', desc: '控制器管理器' }
]
const workerNodes = reactive([
{
name: 'Node-1',
icon: '🖥️',
status: 'active',
statusText: '运行中',
cpu: 45,
memory: 60,
pods: 5
},
{
name: 'Node-2',
icon: '🖥️',
status: 'active',
statusText: '运行中',
cpu: 30,
memory: 40,
pods: 3
},
{
name: 'Node-3',
icon: '🖥️',
status: 'pending',
statusText: '准备中',
cpu: 0,
memory: 0,
pods: 0
}
])
const activeComponent = ref(null)
const selectedNode = ref(null)
const isScheduling = ref(false)
const isScaling = ref(false)
const isFailing = ref(false)
const logs = ref([])
const addLog = (message, level = 'info') => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
logs.value.push({ time, message, level })
if (logs.value.length > 20) logs.value.shift()
}
const selectNode = (name) => {
selectedNode.value = selectedNode.value === name ? null : name
}
const simulateScheduling = async () => {
isScheduling.value = true
addLog('开始调度新 Pod...', 'info')
await new Promise(r => setTimeout(r, 800))
addLog('Scheduler: 评估节点资源...', 'info')
await new Promise(r => setTimeout(r, 800))
const targetNode = workerNodes.find(n => n.status === 'active' && n.cpu < 70)
if (targetNode) {
targetNode.pods++
targetNode.cpu += 10
addLog(`Pod 已调度到 ${targetNode.name}`, 'success')
} else {
addLog('警告: 没有合适的节点可调度', 'warning')
}
isScheduling.value = false
}
const simulateScaling = async () => {
isScaling.value = true
addLog('检测到高负载,开始水平扩容...', 'info')
const pendingNode = workerNodes.find(n => n.status === 'pending')
if (pendingNode) {
await new Promise(r => setTimeout(r, 1500))
pendingNode.status = 'active'
pendingNode.statusText = '运行中'
pendingNode.cpu = 20
pendingNode.memory = 30
addLog(`${pendingNode.name} 已启动并加入集群`, 'success')
} else {
addLog('已达到最大节点数', 'warning')
}
isScaling.value = false
}
const simulateFailure = async () => {
isFailing.value = true
const targetNode = workerNodes.find(n => n.status === 'active')
if (targetNode) {
addLog(`警告: ${targetNode.name} 失去连接!`, 'error')
targetNode.status = 'failed'
targetNode.statusText = '故障'
await new Promise(r => setTimeout(r, 1000))
addLog('Controller: 开始重新调度 Pod...', 'info')
await new Promise(r => setTimeout(r, 1500))
const healthyNode = workerNodes.find(n => n.status === 'active' && n.name !== targetNode.name)
if (healthyNode) {
healthyNode.pods += targetNode.pods
addLog(`Pod 已成功迁移到 ${healthyNode.name}`, 'success')
}
targetNode.pods = 0
targetNode.cpu = 0
targetNode.memory = 0
}
isFailing.value = false
}
const resetCluster = () => {
workerNodes.forEach((node, index) => {
if (index < 2) {
node.status = 'active'
node.statusText = '运行中'
node.cpu = 30 + index * 15
node.memory = 40 + index * 20
node.pods = 3 + index * 2
} else {
node.status = 'pending'
node.statusText = '准备中'
node.cpu = 0
node.memory = 0
node.pods = 0
}
})
logs.value = []
addLog('集群已重置', 'info')
}
</script>
<style scoped>
.container-docker-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.docker-visualization {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: stretch;
}
.layer {
flex: 1;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s;
}
.layer:hover,
.layer.active {
border-color: var(--vp-c-brand);
}
.layer h5 {
margin: 0 0 1rem 0;
text-align: center;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.server-stack,
.docker-stack {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.layer-item {
padding: 0.6rem;
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
}
.layer-item.app {
background: rgba(102, 126, 234, 0.2);
color: var(--vp-c-brand);
font-weight: 600;
}
.layer-item.deps {
background: var(--vp-c-bg-soft);
border: 1px dashed var(--vp-c-divider);
}
.layer-item.os,
.layer-item.hardware {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.layer-item.conflict {
background: rgba(239, 68, 68, 0.2);
color: var(--vp-c-danger);
font-weight: 600;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.containers {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.container-box {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 6px;
padding: 0.5rem;
text-align: center;
}
.container-app {
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-brand);
margin-bottom: 0.2rem;
}
.container-deps {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.docker-engine {
padding: 0.6rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
font-weight: 600;
color: #059669;
}
.host-os,
.hardware {
padding: 0.6rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.vs-divider {
display: flex;
align-items: center;
font-weight: 700;
color: var(--vp-c-text-3);
font-size: 0.9rem;
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.benefit-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: all 0.2s;
}
.benefit-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.benefit-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.benefit-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.benefit-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
@media (max-width: 768px) {
.docker-visualization {
flex-direction: column;
}
.vs-divider {
justify-content: center;
padding: 0.5rem 0;
}
.benefits-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,362 @@
<template>
<div class="microservices-demo">
<div class="demo-header">
<h4>🏭 微服务架构演示</h4>
<p>观察多个独立服务如何协作以及服务间通信方式</p>
</div>
<div class="services-grid">
<div
v-for="service in services"
:key="service.name"
class="service-card"
:class="{ active: activeService === service.name, failed: service.status === 'failed' }"
@click="selectService(service.name)"
>
<div class="service-header">
<span class="service-icon">{{ service.icon }}</span>
<span class="service-name">{{ service.name }}</span>
<span class="service-status" :class="service.status">{{ service.statusText }}</span>
</div>
<div class="service-details">
<div class="detail-row">
<span class="label">端口:</span>
<span class="value">{{ service.port }}</span>
</div>
<div class="detail-row">
<span class="label">数据库:</span>
<span class="value">{{ service.database }}</span>
</div>
<div class="detail-row">
<span class="label">依赖:</span>
<span class="value deps">{{ service.dependencies.join(', ') || '无' }}</span>
</div>
</div>
</div>
</div>
<div class="communication-flow">
<h5>服务间通信链路</h5>
<div class="flow-visualization">
<div class="flow-step" v-for="(step, idx) in flowSteps" :key="idx"
:class="{ active: currentFlowStep === idx, completed: currentFlowStep > idx }">
<div class="step-number">{{ idx + 1 }}</div>
<div class="step-content">
<div class="step-service">{{ step.service }}</div>
<div class="step-action">{{ step.action }}</div>
</div>
</div>
</div>
<div class="flow-controls">
<button class="flow-btn" @click="startFlow" :disabled="isFlowRunning">开始流程</button>
<button class="flow-btn" @click="resetFlow">重置</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const services = ref([
{
name: '用户服务',
icon: '👤',
status: 'healthy',
statusText: '健康',
port: '8081',
database: 'MySQL',
dependencies: []
},
{
name: '订单服务',
icon: '📦',
status: 'healthy',
statusText: '健康',
port: '8082',
database: 'PostgreSQL',
dependencies: ['用户服务']
},
{
name: '支付服务',
icon: '💳',
status: 'healthy',
statusText: '健康',
port: '8083',
database: 'MongoDB',
dependencies: ['用户服务', '订单服务']
},
{
name: '库存服务',
icon: '🏭',
status: 'healthy',
statusText: '健康',
port: '8084',
database: 'Redis',
dependencies: ['订单服务']
}
])
const activeService = ref(null)
const currentFlowStep = ref(-1)
const isFlowRunning = ref(false)
const flowSteps = [
{ service: '用户服务', action: '验证用户身份' },
{ service: '订单服务', action: '创建订单记录' },
{ service: '库存服务', action: '检查库存数量' },
{ service: '支付服务', action: '处理支付请求' },
{ service: '订单服务', action: '更新订单状态' }
]
const selectService = (name) => {
activeService.value = activeService.value === name ? null : name
}
const startFlow = async () => {
isFlowRunning.value = true
currentFlowStep.value = 0
for (let i = 0; i < flowSteps.length; i++) {
currentFlowStep.value = i
await new Promise(resolve => setTimeout(resolve, 1500))
}
isFlowRunning.value = false
}
const resetFlow = () => {
currentFlowStep.value = -1
isFlowRunning.value = false
}
</script>
<style scoped>
.microservices-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.services-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.service-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.service-card:hover {
border-color: var(--vp-c-brand);
}
.service-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.service-card.failed {
border-color: var(--vp-c-danger);
background: rgba(239, 68, 68, 0.05);
}
.service-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.service-icon {
font-size: 1.25rem;
}
.service-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
flex: 1;
}
.service-status {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
border-radius: 10px;
}
.service-status.healthy {
background: rgba(34, 197, 94, 0.2);
color: #16a34a;
}
.service-status.failed {
background: rgba(239, 68, 68, 0.2);
color: #dc2626;
}
.service-details {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.detail-row {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
}
.label {
color: var(--vp-c-text-3);
}
.value {
color: var(--vp-c-text-1);
font-weight: 500;
}
.value.deps {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.communication-flow {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.communication-flow h5 {
margin: 0 0 1rem 0;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.flow-visualization {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid transparent;
transition: all 0.3s;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
}
.flow-step.completed {
border-color: var(--vp-c-success);
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;
color: var(--vp-c-text-2);
}
.flow-step.active .step-number {
background: var(--vp-c-brand);
color: white;
}
.flow-step.completed .step-number {
background: var(--vp-c-success);
color: white;
}
.step-content {
flex: 1;
}
.step-service {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.step-action {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.flow-controls {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.flow-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.flow-btn:hover {
border-color: var(--vp-c-brand);
}
.flow-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.services-grid {
grid-template-columns: 1fr;
}
.service-header {
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,381 @@
<template>
<div class="monolith-demo">
<div class="demo-header">
<h4>🏢 单体架构演示</h4>
<p>观察单体应用如何处理请求以及模块间的依赖关系</p>
</div>
<div class="monolith-diagram">
<div class="monolith-box" :class="{ crashed: hasCrashed }">
<div class="monolith-header">单体应用进程</div>
<div class="modules-container">
<div
v-for="module in modules"
:key="module.name"
class="module-box"
:class="{ active: activeModule === module.name, crashed: crashedModule === module.name }"
@click="triggerModule(module.name)"
>
<div class="module-icon">{{ module.icon }}</div>
<div class="module-name">{{ module.name }}</div>
<div class="module-status" :class="module.status">{{ module.statusText }}</div>
</div>
</div>
<div class="shared-db">
<div class="db-icon">🗄</div>
<div class="db-label">共享数据库</div>
</div>
</div>
<div class="request-flow">
<div
v-for="req in requests"
:key="req.id"
class="flow-request"
:class="req.status"
>
<span class="req-type">{{ req.type }}</span>
<span class="req-arrow"></span>
<span class="req-target">{{ req.target }}</span>
</div>
</div>
</div>
<div class="controls">
<button class="control-btn" @click="simulateNormalRequest">正常请求</button>
<button class="control-btn danger" @click="simulateCrash">模拟模块故障</button>
<button class="control-btn" @click="reset">重置</button>
</div>
<div class="demo-explanation">
<h5>💡 单体架构的特点</h5>
<ul>
<li><strong>共享进程空间</strong>所有模块在同一个进程中运行内存共享</li>
<li><strong>数据库耦合</strong>所有模块共享同一个数据库Schema变更影响全局</li>
<li><strong>级联故障</strong>一个模块崩溃可能导致整个进程挂掉雪崩效应</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const modules = ref([
{ name: '用户模块', icon: '👤', status: 'healthy', statusText: '健康' },
{ name: '订单模块', icon: '📦', status: 'healthy', statusText: '健康' },
{ name: '支付模块', icon: '💳', status: 'healthy', statusText: '健康' },
{ name: '库存模块', icon: '🏭', status: 'healthy', statusText: '健康' }
])
const requests = ref([])
const hasCrashed = ref(false)
const crashedModule = ref(null)
const activeModule = ref(null)
const requestId = ref(0)
const simulateNormalRequest = () => {
const targets = ['用户模块', '订单模块', '支付模块', '库存模块']
const target = targets[Math.floor(Math.random() * targets.length)]
activeModule.value = target
requestId.value++
requests.value.push({
id: requestId.value,
type: 'GET',
target: target,
status: 'active'
})
setTimeout(() => {
activeModule.value = null
if (requests.value.length > 5) {
requests.value.shift()
}
}, 1500)
}
const simulateCrash = () => {
const targetModule = '订单模块'
hasCrashed.value = true
crashedModule.value = targetModule
const module = modules.value.find(m => m.name === targetModule)
if (module) {
module.status = 'crashed'
module.statusText = '已崩溃'
}
// Cascade effect - other modules become unavailable
setTimeout(() => {
modules.value.forEach(m => {
if (m.name !== targetModule) {
m.status = 'affected'
m.statusText = '受影响'
}
})
}, 500)
}
const reset = () => {
hasCrashed.value = false
crashedModule.value = null
activeModule.value = null
requests.value = []
modules.value.forEach(m => {
m.status = 'healthy'
m.statusText = '健康'
})
}
</script>
<style scoped>
.monolith-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.monolith-diagram {
display: flex;
gap: 1.5rem;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.monolith-box {
flex: 1;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 1rem;
transition: all 0.3s;
}
.monolith-box.crashed {
border-color: var(--vp-c-danger);
background: rgba(239, 68, 68, 0.05);
}
.monolith-header {
text-align: center;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.modules-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.module-box {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.module-box:hover {
border-color: var(--vp-c-brand);
}
.module-box.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
}
.module-box.crashed {
border-color: var(--vp-c-danger);
background: rgba(239, 68, 68, 0.1);
}
.module-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.module-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.module-status {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 10px;
display: inline-block;
}
.module-status.healthy {
background: rgba(34, 197, 94, 0.2);
color: #16a34a;
}
.module-status.crashed {
background: rgba(239, 68, 68, 0.2);
color: #dc2626;
}
.module-status.affected {
background: rgba(245, 158, 11, 0.2);
color: #d97706;
}
.shared-db {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 1px dashed var(--vp-c-divider);
border-radius: 6px;
}
.db-icon {
font-size: 1.25rem;
}
.db-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.request-flow {
width: 150px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-request {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.75rem;
}
.flow-request.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
}
.req-type {
font-weight: 600;
color: var(--vp-c-brand);
}
.req-arrow {
color: var(--vp-c-text-3);
}
.req-target {
color: var(--vp-c-text-2);
}
.controls {
display: flex;
gap: 0.75rem;
justify-content: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.control-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
border-color: var(--vp-c-brand);
}
.control-btn.danger {
border-color: var(--vp-c-danger);
color: var(--vp-c-danger);
}
.control-btn.danger:hover {
background: rgba(239, 68, 68, 0.1);
}
.demo-explanation {
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.demo-explanation h5 {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 0.75rem 0;
}
.demo-explanation ul {
margin: 0 0 1rem 0;
padding-left: 1.25rem;
}
.demo-explanation li {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.4rem;
}
.demo-explanation li strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.monolith-diagram {
flex-direction: column;
}
.request-flow {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,475 @@
<template>
<div class="physical-server-demo">
<div class="demo-header">
<h4>🖥 物理服务器时代演示</h4>
<p>点击"发送请求"观察早期 CGI 服务器的处理瓶颈</p>
</div>
<div class="demo-stage">
<div class="client-zone">
<div class="zone-title">👤 用户浏览器</div>
<div class="request-queue">
<div
v-for="(req, idx) in pendingRequests"
:key="req.id"
class="request-card"
:style="{ animationDelay: idx * 0.1 + 's' }"
>
<span class="req-method">{{ req.method }}</span>
<span class="req-path">{{ req.path }}</span>
</div>
</div>
<button
class="send-btn"
:disabled="isProcessing"
@click="sendRequest"
>
{{ isProcessing ? '处理中...' : '🚀 发起请求' }}
</button>
</div>
<div class="connection-zone">
<div class="network-line" :class="{ busy: isProcessing }">
<div class="packets">
<div
v-for="pkt in packets"
:key="pkt.id"
class="packet"
:class="pkt.type"
:style="{ top: pkt.top + 'px' }"
>
{{ pkt.type === 'req' ? '📤' : '📥' }}
</div>
</div>
</div>
<div class="latency-display" v-if="currentLatency > 0">
{{ currentLatency }}ms
</div>
</div>
<div class="server-zone">
<div class="zone-title">🖥 CGI 服务器</div>
<div class="server-status">
<div
class="status-indicator"
:class="{ processing: isProcessing }"
>
<span class="status-dot"></span>
<span class="status-text">{{ serverStatus }}</span>
</div>
<div class="cpu-usage" v-if="isProcessing">
<div class="cpu-bar">
<div
class="cpu-fill"
:style="{ width: cpuUsage + '%' }"
></div>
</div>
<span class="cpu-text">CPU: {{ cpuUsage }}%</span>
</div>
</div>
<div class="process-queue">
<div
v-for="proc in processQueue"
:key="proc.id"
class="process-item"
>
<span class="proc-name">{{ proc.name }}</span>
<div class="proc-progress">
<div
class="proc-bar"
:style="{ width: proc.progress + '%' }"
></div>
</div>
</div>
</div>
</div>
</div>
<div class="demo-explanation">
<h5>💡 早期的痛点在哪里</h5>
<ul>
<li>
<strong>进程启动开销</strong>每个请求都要启动新的 CGI
进程就像每来一个客人都要重新搭一个厨房
</li>
<li>
<strong>资源无法复用</strong>数据库连接每次都要重新建立CPU
频繁在进程间切换
</li>
<li>
<strong>扩展困难</strong>只能买更强的单机垂直扩展无法通过增加机器分担压力
</li>
</ul>
<p class="demo-conclusion">
这就是<strong>物理服务器 + CGI</strong>时代的核心问题<span
class="highlight"
>进程级隔离带来了稳定性但也带来了巨大的性能开销</span
>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isProcessing = ref(false)
const currentLatency = ref(0)
const cpuUsage = ref(0)
const packets = ref([])
const pendingRequests = ref([])
const processQueue = ref([])
const requestCounter = ref(0)
const packetCounter = ref(0)
const serverStatus = computed(() => {
if (isProcessing.value) return '处理中...'
return '等待请求'
})
const sendRequest = async () => {
if (isProcessing.value) return
isProcessing.value = true
requestCounter.value++
const requestId = requestCounter.value
// Add request to queue
pendingRequests.value.push({
id: requestId,
method: 'GET',
path: '/index.cgi'
})
// Simulate network latency
currentLatency.value = 0
const latencyInterval = setInterval(() => {
currentLatency.value += Math.floor(Math.random() * 50) + 20
}, 100)
// Simulate packet
const packetId = ++packetCounter.value
packets.value.push({
id: packetId,
type: 'req',
top: 20
})
// Add process to queue
processQueue.value.push({
id: requestId,
name: `CGI Process #${requestId}`,
progress: 0
})
// Simulate CPU usage fluctuation
const cpuInterval = setInterval(() => {
cpuUsage.value = Math.min(100, cpuUsage.value + Math.random() * 20 + 10)
processQueue.value.forEach(p => {
p.progress = Math.min(100, p.progress + Math.random() * 15 + 5)
})
}, 100)
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 2000))
clearInterval(latencyInterval)
clearInterval(cpuInterval)
// Cleanup
pendingRequests.value = pendingRequests.value.filter(r => r.id !== requestId)
packets.value = packets.value.filter(p => p.id !== packetId)
processQueue.value = processQueue.value.filter(p => p.id !== requestId)
cpuUsage.value = 0
currentLatency.value = 0
isProcessing.value = false
}
</script>
<style scoped>
.physical-server-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.demo-stage {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.client-zone,
.server-zone {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.zone-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 0.75rem;
text-align: center;
}
.request-queue {
min-height: 60px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.5rem;
margin-bottom: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.request-card {
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
padding: 0.4rem 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
}
.req-method {
background: rgba(255, 255, 255, 0.2);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-weight: 600;
}
.send-btn {
width: 100%;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
padding: 0.6rem;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.send-btn:hover:not(:disabled) {
opacity: 0.9;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.connection-zone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 60px;
}
.network-line {
width: 3px;
height: 120px;
background: var(--vp-c-divider);
border-radius: 2px;
position: relative;
opacity: 0.5;
transition: opacity 0.3s;
}
.network-line.busy {
opacity: 1;
background: var(--vp-c-brand);
}
.latency-display {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 600;
}
.server-status {
margin-bottom: 0.75rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
margin-bottom: 0.5rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vp-c-success);
}
.status-indicator.processing .status-dot {
background: var(--vp-c-danger);
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.status-text {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.cpu-usage {
display: flex;
align-items: center;
gap: 0.5rem;
}
.cpu-bar {
flex: 1;
height: 6px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
overflow: hidden;
}
.cpu-fill {
height: 100%;
background: var(--vp-c-danger);
border-radius: 3px;
transition: width 0.1s ease;
}
.cpu-text {
font-size: 0.7rem;
color: var(--vp-c-text-2);
min-width: 60px;
text-align: right;
}
.process-queue {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.process-item {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.5rem;
}
.proc-name {
display: block;
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
}
.proc-progress {
height: 4px;
background: var(--vp-c-bg);
border-radius: 2px;
overflow: hidden;
}
.proc-bar {
height: 100%;
background: var(--vp-c-brand);
border-radius: 2px;
transition: width 0.1s linear;
}
.demo-explanation {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.demo-explanation h5 {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 0.75rem 0;
}
.demo-explanation ul {
margin: 0 0 1rem 0;
padding-left: 1.25rem;
}
.demo-explanation li {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.4rem;
}
.demo-explanation li strong {
color: var(--vp-c-text-1);
}
.demo-conclusion {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.highlight {
color: var(--vp-c-brand);
font-weight: 600;
}
@media (max-width: 768px) {
.demo-stage {
grid-template-columns: 1fr;
gap: 1rem;
}
.connection-zone {
flex-direction: row;
height: 60px;
}
.network-line {
width: 100%;
height: 3px;
}
}
</style>
@@ -0,0 +1,238 @@
<template>
<div class="scaling-strategy-demo">
<div class="demo-header">
<h4>📈 扩展策略对比</h4>
<p>垂直扩展 vs 水平扩展</p>
</div>
<div class="strategies">
<div class="strategy-card" :class="{ active: activeStrategy === 'vertical' }" @click="activeStrategy = 'vertical'">
<div class="strategy-icon">📦</div>
<div class="strategy-name">垂直扩展</div>
<div class="strategy-desc">买更强的机器</div>
<div class="visual-vertical">
<div class="server" :class="{ scale: activeStrategy === 'vertical' }">
<div class="cpu">CPU</div>
<div class="memory">内存</div>
</div>
</div>
</div>
<div class="strategy-card" :class="{ active: activeStrategy === 'horizontal' }" @click="activeStrategy = 'horizontal'">
<div class="strategy-icon">🔄</div>
<div class="strategy-name">水平扩展</div>
<div class="strategy-desc">加更多机器</div>
<div class="visual-horizontal">
<div class="servers">
<div class="server-mini" v-for="n in 4" :key="n" :class="{ active: activeStrategy === 'horizontal' && n <= serverCount }" :style="{ animationDelay: (n * 0.1) + 's' }"></div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-row header">
<span>维度</span>
<span>垂直扩展</span>
<span>水平扩展</span>
</div>
<div class="table-row" v-for="item in comparisonData" :key="item.dim">
<span>{{ item.dim }}</span>
<span :class="{ better: item.verticalBetter }">{{ item.vertical }}</span>
<span :class="{ better: item.horizontalBetter }">{{ item.horizontal }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeStrategy = ref('horizontal')
const serverCount = ref(3)
const comparisonData = [
{ dim: '成本', vertical: '硬件贵', horizontal: '机器多', verticalBetter: false, horizontalBetter: true },
{ dim: '上限', vertical: '有瓶颈', horizontal: '理论上无限', verticalBetter: false, horizontalBetter: true },
{ dim: '复杂度', vertical: '简单', horizontal: '需要分布式', verticalBetter: true, horizontalBetter: false },
{ dim: '数据', vertical: '一致性好', horizontal: '需要同步', verticalBetter: true, horizontalBetter: false }
]
</script>
<style scoped>
.scaling-strategy-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.strategies {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
.strategy-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;
}
.strategy-card:hover {
border-color: var(--vp-c-brand);
}
.strategy-card.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.05);
}
.strategy-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.strategy-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 0.125rem;
}
.strategy-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.visual-vertical,
.visual-horizontal {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.server {
width: 50px;
height: 40px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
transition: all 0.3s;
}
.server.scale {
transform: scale(1.2);
border-color: var(--vp-c-brand);
}
.cpu, .memory {
font-size: 0.5rem;
padding: 1px 3px;
background: var(--vp-c-bg);
border-radius: 2px;
color: var(--vp-c-text-2);
}
.servers {
display: flex;
gap: 4px;
}
.server-mini {
width: 20px;
height: 30px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 2px;
opacity: 0.3;
transition: all 0.3s;
}
.server-mini.active {
opacity: 1;
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
animation: popIn 0.3s ease;
}
@keyframes popIn {
0% { transform: scale(0.8); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.comparison-table {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.table-row:not(.header):not(:last-child) {
border-bottom: 1px solid var(--vp-c-divider);
}
.table-row.header {
background: var(--vp-c-bg-soft);
font-weight: 600;
color: var(--vp-c-text-1);
}
.table-row span:first-child {
color: var(--vp-c-text-2);
}
.better {
color: var(--vp-c-success);
font-weight: 500;
}
@media (max-width: 768px) {
.strategies {
grid-template-columns: 1fr;
}
.comparison-table .table-row {
font-size: 0.75rem;
padding: 0.4rem 0.5rem;
}
}
</style>
@@ -0,0 +1,435 @@
<template>
<div class="serverless-demo">
<div class="demo-header">
<h4> Serverless 架构演示</h4>
<p>观察 Serverless 如何按需执行函数自动扩缩容</p>
</div>
<div class="serverless-visualization">
<div class="function-grid">
<div v-for="func in functions" :key="func.name" class="function-card" :class="{ active: func.state === 'running', cold: func.state === 'cold', warming: func.state === 'warming' }" @click="triggerFunction(func.name)">
<div class="function-icon">{{ func.icon }}</div>
<div class="function-name">{{ func.name }}</div>
<div class="function-state" :class="func.state">{{ stateText(func.state) }}</div>
<div class="function-metrics" v-if="func.invocations > 0">
<span>调用: {{ func.invocations }}</span>
<span>平均: {{ func.avgDuration }}ms</span>
</div>
</div>
</div>
<div class="auto-scaling-panel">
<div class="scaling-title">自动扩缩容状态</div>
<div class="scaling-metrics">
<div class="metric">
<span class="metric-label">并发请求:</span>
<span class="metric-value">{{ concurrentRequests }}</span>
</div>
<div class="metric">
<span class="metric-label">运行实例:</span>
<span class="metric-value">{{ runningInstances }}</span>
</div>
<div class="metric">
<span class="metric-label">冷启动:</span>
<span class="metric-value">{{ coldStarts }}</span>
</div>
</div>
<div class="scaling-chart">
<div v-for="(point, idx) in scalingHistory" :key="idx" class="chart-bar" :style="{ height: point + '%' }" :class="{ high: point > 70 }"></div>
</div>
</div>
</div>
<div class="traffic-simulator">
<div class="simulator-title">流量模拟器</div>
<div class="traffic-patterns">
<button v-for="pattern in trafficPatterns" :key="pattern.name" class="pattern-btn" :class="{ active: currentPattern === pattern.name }" @click="applyPattern(pattern)">
<span class="pattern-icon">{{ pattern.icon }}</span>
<span class="pattern-name">{{ pattern.name }}</span>
<span class="pattern-desc">{{ pattern.desc }}</span>
</button>
</div>
</div>
<div class="demo-explanation">
<h5>💡 Serverless 核心特性</h5>
<ul>
<li><strong>按需执行</strong>函数只在被调用时运行不调用不产生费用</li>
<li><strong>自动扩缩容</strong> 0 到数千实例自动扩展无需人工干预</li>
<li><strong>冷启动</strong>长时间未调用后首次调用会有延迟需要预热策略</li>
<li><strong>事件驱动</strong>响应 HTTP 请求消息队列定时任务等多种事件源</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
const functions = reactive([
{ name: '用户登录', icon: '🔐', state: 'cold', invocations: 0, avgDuration: 0 },
{ name: '订单处理', icon: '📦', state: 'cold', invocations: 0, avgDuration: 0 },
{ name: '图片处理', icon: '🖼️', state: 'cold', invocations: 0, avgDuration: 0 },
{ name: '数据备份', icon: '💾', state: 'cold', invocations: 0, avgDuration: 0 }
])
const concurrentRequests = ref(0)
const runningInstances = ref(0)
const coldStarts = ref(0)
const scalingHistory = ref([10, 15, 20, 25, 30, 35, 40, 35, 30, 25, 20, 15])
const currentPattern = ref(null)
const isFlowRunning = ref(false)
const trafficPatterns = [
{ name: '正常流量', icon: '📊', desc: '平稳的请求速率' },
{ name: '突发流量', icon: '🚀', desc: '突然的流量激增' },
{ name: '潮汐流量', icon: '🌊', desc: '周期性的高低峰' }
]
const stateText = (state) => {
const map = { cold: '冷状态', warming: '预热中', running: '运行中' }
return map[state] || state
}
const triggerFunction = async (name) => {
const fn = functions.find(f => f.name === name)
if (!fn) return
if (fn.state === 'cold') {
fn.state = 'warming'
coldStarts.value++
await new Promise(r => setTimeout(r, 800))
}
fn.state = 'running'
fn.invocations++
concurrentRequests.value++
runningInstances.value++
const duration = Math.floor(Math.random() * 150) + 50
fn.avgDuration = Math.floor((fn.avgDuration * (fn.invocations - 1) + duration) / fn.invocations)
await new Promise(r => setTimeout(r, duration))
concurrentRequests.value--
if (concurrentRequests.value === 0) {
runningInstances.value = 0
}
setTimeout(() => {
if (fn.invocations > 0) {
fn.state = 'cold'
}
}, 3000)
}
const applyPattern = (pattern) => {
currentPattern.value = pattern.name
// 模拟流量模式
if (pattern.name === '突发流量') {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const fn = functions[Math.floor(Math.random() * functions.length)]
triggerFunction(fn.name)
}, i * 200)
}
} else if (pattern.name === '潮汐流量') {
const interval = setInterval(() => {
const fn = functions[Math.floor(Math.random() * functions.length)]
triggerFunction(fn.name)
}, 500)
setTimeout(() => clearInterval(interval), 3000)
}
}
let interval
onMounted(() => {
interval = setInterval(() => {
scalingHistory.value.shift()
const last = scalingHistory.value[scalingHistory.value.length - 1]
const variation = Math.floor(Math.random() * 20) - 10
const next = Math.max(10, Math.min(90, last + variation))
scalingHistory.value.push(next)
}, 2000)
})
onUnmounted(() => {
clearInterval(interval)
})
</script>
<style scoped>
.serverless-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.serverless-visualization {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.function-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.function-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.function-card:hover {
border-color: var(--vp-c-brand);
}
.function-card.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.function-card.cold {
opacity: 0.7;
}
.function-card.warming {
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.function-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.function-name {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.function-state {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
border-radius: 10px;
display: inline-block;
margin-bottom: 0.5rem;
}
.function-state.cold {
background: rgba(156, 163, 175, 0.2);
color: var(--vp-c-text-2);
}
.function-state.warming {
background: rgba(245, 158, 11, 0.2);
color: #d97706;
}
.function-state.running {
background: rgba(34, 197, 94, 0.2);
color: #16a34a;
}
.function-metrics {
display: flex;
justify-content: space-around;
font-size: 0.7rem;
color: var(--vp-c-text-2);
border-top: 1px solid var(--vp-c-divider);
padding-top: 0.5rem;
}
.auto-scaling-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.scaling-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
text-align: center;
}
.scaling-metrics {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.metric {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
}
.metric-label {
color: var(--vp-c-text-2);
}
.metric-value {
font-weight: 600;
color: var(--vp-c-brand);
}
.scaling-chart {
display: flex;
align-items: flex-end;
gap: 2px;
height: 60px;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.chart-bar {
flex: 1;
background: var(--vp-c-brand);
border-radius: 1px;
transition: height 0.3s;
min-height: 2px;
}
.chart-bar.high {
background: var(--vp-c-warning);
}
.traffic-simulator {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.simulator-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
text-align: center;
}
.traffic-patterns {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.pattern-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.pattern-btn:hover {
border-color: var(--vp-c-brand);
}
.pattern-btn.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
}
.pattern-icon {
font-size: 1.5rem;
}
.pattern-name {
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.pattern-desc {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.demo-explanation {
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.demo-explanation h5 {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 0.75rem 0;
}
.demo-explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.demo-explanation li {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.4rem;
}
.demo-explanation li strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.serverless-visualization {
grid-template-columns: 1fr;
}
.function-grid {
grid-template-columns: 1fr;
}
.traffic-patterns {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,250 @@
<template>
<div class="tech-stack-timeline-demo">
<div class="demo-header">
<h4>📚 技术栈演进时间线</h4>
<p>每个时代的主流技术栈</p>
</div>
<div class="timeline">
<div
v-for="(era, idx) in eras"
:key="idx"
class="era-section"
:class="{ active: activeEra === idx }"
@click="activeEra = idx"
>
<div class="era-marker">
<div class="era-dot"></div>
<div class="era-line"></div>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-icon">{{ era.icon }}</span>
<span class="era-name">{{ era.name }}</span>
<span class="era-period">{{ era.period }}</span>
</div>
<div class="tech-categories">
<div class="category" v-for="(cat, cIdx) in era.categories" :key="cIdx">
<div class="category-name">{{ cat.name }}</div>
<div class="tech-tags">
<span
v-for="(tech, tIdx) in cat.techs"
:key="tIdx"
class="tech-tag"
:class="{ highlight: tIdx === 0 }"
>
{{ tech }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeEra = ref(0)
const eras = [
{
icon: '🖥️',
name: '物理机时代',
period: '1990s',
categories: [
{ name: 'Web服务器', techs: ['Apache', 'Nginx', 'IIS'] },
{ name: '后端语言', techs: ['Perl', 'PHP', 'ASP'] },
{ name: '数据库', techs: ['MySQL', 'PostgreSQL', 'Oracle'] },
{ name: '部署方式', techs: ['FTP', 'SSH', '手动'] }
]
},
{
icon: '🏢',
name: '单体架构',
period: '2000s',
categories: [
{ name: '后端框架', techs: ['Spring', 'Django', 'Rails', 'Laravel'] },
{ name: '前端技术', techs: ['jQuery', 'Bootstrap', 'JSP'] },
{ name: '数据库', techs: ['MySQL', 'Redis', 'MongoDB'] },
{ name: '构建工具', techs: ['Maven', 'Gradle', 'Ant'] }
]
},
{
icon: '🏭',
name: '微服务',
period: '2010s',
categories: [
{ name: '容器化', techs: ['Docker', 'Kubernetes', 'Helm'] },
{ name: '服务框架', techs: ['Spring Cloud', 'gRPC', 'Dubbo'] },
{ name: '数据存储', techs: ['Redis', 'MongoDB', 'Kafka', 'ES'] },
{ name: '可观测', techs: ['Prometheus', 'Grafana', 'Jaeger'] }
]
},
{
icon: '☁️',
name: 'Serverless',
period: '2020s+',
categories: [
{ name: '函数计算', techs: ['Lambda', 'Vercel', 'Cloudflare'] },
{ name: 'BaaS', techs: ['Supabase', 'Firebase', 'Auth0'] },
{ name: '前端框架', techs: ['Next.js', 'Nuxt', 'SvelteKit'] },
{ name: '数据库', techs: ['PlanetScale', 'Neon', 'Turso'] }
]
}
]
</script>
<style scoped>
.tech-stack-timeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.timeline {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.era-section {
display: flex;
gap: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.era-section:hover,
.era-section.active {
background: var(--vp-c-bg);
border-radius: 6px;
}
.era-marker {
display: flex;
flex-direction: column;
align-items: center;
width: 24px;
}
.era-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--vp-c-divider);
border: 2px solid var(--vp-c-bg);
transition: all 0.2s;
}
.era-section.active .era-dot {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.era-line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
margin: 4px 0;
}
.era-content {
flex: 1;
padding: 0.5rem 0.5rem 0.5rem 0;
}
.era-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.era-icon {
font-size: 1.25rem;
}
.era-name {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.era-period {
font-size: 0.75rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
.tech-categories {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.category {
background: var(--vp-c-bg-soft);
border-radius: 4px;
padding: 0.5rem;
}
.category-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tech-tag {
font-size: 0.7rem;
padding: 0.1rem 0.35rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.tech-tag.highlight {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.05);
}
@media (max-width: 768px) {
.tech-categories {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,83 @@
<template>
<div class="memory-management-demo">
<div class="demo-header">
<h4>内存管理机制可视化</h4>
</div>
<div class="memory-models">
<div class="model-card" v-for="model in models" :key="model.name">
<div class="model-icon">{{ model.icon }}</div>
<div class="model-name">{{ model.name }}</div>
<div class="model-desc">{{ model.desc }}</div>
<div class="model-languages">
<span v-for="lang in model.languages" :key="lang" class="lang-tag">{{ lang }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
const models = [
{
name: '垃圾回收 (GC)',
icon: '♻️',
desc: '运行时自动回收不再使用的内存',
languages: ['Java', 'Go', 'Python', 'Node.js']
},
{
name: '手动管理',
icon: '🔧',
desc: '开发者显式申请和释放内存',
languages: ['C', 'C++']
},
{
name: '所有权系统',
icon: '🔒',
desc: '编译时通过规则保证内存安全',
languages: ['Rust']
}
]
</script>
<style scoped>
.memory-management-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.memory-models {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 20px;
}
.model-card {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.model-icon {
font-size: 2em;
margin-bottom: 8px;
}
.model-name {
font-weight: bold;
margin-bottom: 4px;
}
.model-desc {
font-size: 0.9em;
color: var(--vp-c-text-2);
margin-bottom: 12px;
}
.lang-tag {
display: inline-block;
padding: 2px 6px;
margin: 2px;
background: var(--vp-c-bg-mute);
border-radius: 4px;
font-size: 0.8em;
}
</style>
@@ -0,0 +1,534 @@
<template>
<div class="clean-architecture-demo">
<div class="demo-header">
<h4>🏗 整洁架构Clean Architecture与分层架构</h4>
<p class="subtitle">分层架构是整洁架构的基础理解两者的关系有助于构建更灵活的系统</p>
</div>
<!-- 架构对比 -->
<div class="architecture-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="comparison-content">
<!-- 传统分层架构 -->
<div v-if="currentTab === 'layered'" class="tab-panel">
<div class="arch-diagram layered">
<div class="layer-box controller">
<div class="layer-title">Controller </div>
<div class="layer-desc">接收请求参数校验</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box service">
<div class="layer-title">Service </div>
<div class="layer-desc">业务逻辑事务管理</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box repository">
<div class="layer-title">Repository </div>
<div class="layer-desc">数据访问ORM 映射</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box domain">
<div class="layer-title">Domain </div>
<div class="layer-desc">实体定义业务规则</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 传统分层架构特点</h5>
<ul>
<li><strong>垂直依赖</strong>上层直接依赖下层依赖方向从上到下</li>
<li><strong>简单直观</strong>结构清晰易于理解和实现</li>
<li><strong>适合中小型项目</strong>快速开发上手简单</li>
<li><strong>潜在问题</strong>底层变更可能影响上层循环依赖风险</li>
</ul>
</div>
</div>
<!-- 整洁架构 -->
<div v-else-if="currentTab === 'clean'" class="tab-panel">
<div class="arch-diagram clean">
<div class="clean-layers">
<div class="clean-layer framework">
<div class="layer-name">框架与驱动层</div>
<div class="layer-items">Web / DB / UI / 外部接口</div>
</div>
<div class="clean-layer interface">
<div class="layer-name">接口适配层</div>
<div class="layer-items">Controller / Gateway / Presenter</div>
</div>
<div class="clean-layer application">
<div class="layer-name">应用层</div>
<div class="layer-items">Service / UseCase / DTO</div>
</div>
<div class="clean-layer domain">
<div class="layer-name">领域层核心</div>
<div class="layer-items">Entity / ValueObject / DomainService</div>
</div>
</div>
<div class="dependency-rule">
<div class="rule-arrow">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖方向</span>
</div>
<div class="rule-text">
外层依赖内层内层不依赖外层
</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 整洁架构特点</h5>
<ul>
<li><strong>依赖倒置</strong>依赖方向从外到内通过接口隔离</li>
<li><strong>领域为核心</strong>业务逻辑位于中心独立于框架</li>
<li><strong>可测试性强</strong>核心业务可脱离框架进行单元测试</li>
<li><strong>技术无关</strong>可轻松切换数据库框架等外部技术</li>
</ul>
</div>
</div>
<!-- 对比总结 -->
<div v-else class="tab-panel">
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>传统分层架构</th>
<th>整洁架构</th>
</tr>
</thead>
<tbody>
<tr>
<td>依赖方向</td>
<td>从上到下</td>
<td>从外到内</td>
</tr>
<tr>
<td>核心业务位置</td>
<td>Service </td>
<td>Domain 中心</td>
</tr>
<tr>
<td>框架依赖</td>
<td>较深 Spring</td>
<td>较浅通过接口隔离</td>
</tr>
<tr>
<td>可测试性</td>
<td>需要集成测试</td>
<td>核心可单元测试</td>
</tr>
<tr>
<td>学习曲线</td>
<td>平缓</td>
<td>较陡</td>
</tr>
<tr>
<td>适用场景</td>
<td>中小型项目快速迭代</td>
<td>大型复杂业务长期维护</td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation">
<h5>💡 选型建议</h5>
<div class="rec-grid">
<div class="rec-card">
<div class="rec-title">选择传统分层架构当...</div>
<ul>
<li>项目规模较小业务相对简单</li>
<li>团队对 DDD 不熟悉</li>
<li>需要快速上线验证市场</li>
<li>技术栈相对固定</li>
</ul>
</div>
<div class="rec-card recommended">
<div class="rec-title">选择整洁架构当...</div>
<ul>
<li>业务复杂领域模型丰富</li>
<li>需要长期维护和演进</li>
<li>需要频繁切换技术栈</li>
<li>团队有较强的设计能力</li>
</ul>
<div class="rec-badge">推荐</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTab = ref('layered')
const tabs = [
{ id: 'layered', name: '传统分层' },
{ id: 'clean', name: '整洁架构' },
{ id: 'comparison', name: '对比总结' }
]
</script>
<style scoped>
.clean-architecture-demo {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.architecture-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Layered Architecture */
.arch-diagram.layered {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.layer-box {
width: 100%;
max-width: 400px;
padding: 16px;
border-radius: 8px;
text-align: center;
border-left: 4px solid;
}
.layer-box.controller {
background: #f0f9ff;
border-left-color: #52c41a;
}
.layer-box.service {
background: #fff7e6;
border-left-color: #fa8c16;
}
.layer-box.repository {
background: #e6f7ff;
border-left-color: #1890ff;
}
.layer-box.domain {
background: #f6ffed;
border-left-color: #73d13d;
}
.layer-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.layer-desc {
font-size: 12px;
color: #666;
}
.arrow {
color: #909399;
font-size: 12px;
text-align: center;
}
.arch-characteristics {
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.arch-characteristics h5 {
margin: 0 0 12px 0;
color: #1a1a2e;
font-size: 14px;
}
.arch-characteristics ul {
margin: 0;
padding-left: 20px;
}
.arch-characteristics li {
margin: 8px 0;
color: #595959;
font-size: 13px;
line-height: 1.5;
}
/* Clean Architecture */
.arch-diagram.clean {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.clean-layers {
display: flex;
flex-direction: column-reverse;
gap: 8px;
}
.clean-layer {
padding: 12px 16px;
border-radius: 8px;
border-left: 4px solid;
}
.clean-layer.framework {
background: #f0f0f0;
border-left-color: #8c8c8c;
}
.clean-layer.interface {
background: #e6f7ff;
border-left-color: #1890ff;
}
.clean-layer.application {
background: #fff7e6;
border-left-color: #fa8c16;
}
.clean-layer.domain {
background: #f6ffed;
border-left-color: #52c41a;
}
.clean-layer .layer-name {
font-weight: 600;
color: #1a1a2e;
font-size: 14px;
margin-bottom: 4px;
}
.clean-layer .layer-items {
font-size: 12px;
color: #666;
}
.dependency-rule {
background: white;
border-radius: 8px;
padding: 16px;
text-align: center;
border: 2px dashed #1890ff;
}
.rule-arrow {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
}
.rule-arrow .arrow-line {
width: 2px;
height: 20px;
background: #1890ff;
}
.rule-arrow .arrow-head {
color: #1890ff;
font-weight: 600;
font-size: 14px;
}
.rule-text {
color: #595959;
font-size: 13px;
}
/* Comparison Table */
.comparison-table {
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.comparison-table th,
.comparison-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.comparison-table th {
background: #f5f7fa;
font-weight: 600;
color: #1a1a2e;
}
.comparison-table tr:hover {
background: #fafafa;
}
/* Recommendation */
.recommendation {
margin-top: 24px;
}
.recommendation h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.rec-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.rec-card {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
position: relative;
}
.rec-card.recommended {
background: #f6ffed;
border: 2px solid #52c41a;
}
.rec-badge {
position: absolute;
top: -10px;
right: 16px;
background: #52c41a;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.rec-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
font-size: 14px;
}
.rec-card ul {
margin: 0;
padding-left: 18px;
}
.rec-card li {
margin: 6px 0;
color: #595959;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 768px) {
.rec-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,340 @@
<template>
<div class="controller-layer-demo">
<div class="demo-header">
<h4>🎮 Controller 请求的"接待员"</h4>
<p class="subtitle">点击流程节点查看 Controller 如何接收和处理请求</p>
</div>
<div class="flow-container">
<!-- 请求发起 -->
<div class="flow-step">
<div class="step-icon">🌐</div>
<div class="step-content">
<div class="step-title">客户端发起请求</div>
<div class="step-code">
POST /api/users/register
Content-Type: application/json
{
"username": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}
</div>
</div>
</div>
<div class="arrow-connector"> 请求到达</div>
<!-- Controller 接收 -->
<div
class="flow-step controller-step"
:class="{ active: showDetails === 'controller' }"
@click="toggleDetails('controller')"
>
<div class="step-icon">🎮</div>
<div class="step-content">
<div class="step-title">Controller 接收并解析请求</div>
<div class="step-code">
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping("/register")
public ResponseEntity&lt;UserDTO&gt; register(
@RequestBody @Valid UserRegisterRequest request
) {
// 调用 Service 处理业务
UserDTO user = userService.register(request);
return ResponseEntity.ok(user);
}
}
</div>
</div>
</div>
<div class="arrow-connector"> 参数校验 + 调用</div>
<!-- 校验逻辑 -->
<div
class="flow-step validation-step"
:class="{ active: showDetails === 'validation' }"
@click="toggleDetails('validation')"
>
<div class="step-icon"></div>
<div class="step-content">
<div class="step-title">参数校验Controller 的职责之一</div>
<div class="step-code">
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Size(min = 6, message = "密码至少6位")
private String password;
}
</div>
<div v-if="showDetails === 'validation'" class="detail-panel">
<h5>为什么校验要放在 Controller</h5>
<ul>
<li>🛡 第一道防线尽早拦截非法请求</li>
<li>📦 减轻下游压力Service 层可以假设数据已清洗</li>
<li>🔧 关注点分离Service 专注于业务不处理格式验证</li>
</ul>
</div>
</div>
</div>
<div class="arrow-connector"> 返回结果</div>
<!-- 响应返回 -->
<div class="flow-step">
<div class="step-icon">📤</div>
<div class="step-content">
<div class="step-title">Controller 封装响应返回给客户端</div>
<div class="step-code">
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"message": "注册成功",
"data": {
"id": 10001,
"username": "张三",
"email": "zhangsan@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
}
</div>
</div>
</div>
</div>
<!-- Controller 职责总结 -->
<div class="controller-summary">
<h5>🎯 Controller 的核心职责</h5>
<div class="duty-grid">
<div class="duty-item">
<div class="duty-icon">📡</div>
<div class="duty-title">接收请求</div>
<div class="duty-desc">映射 HTTP 请求到方法</div>
</div>
<div class="duty-item">
<div class="duty-icon"></div>
<div class="duty-title">参数校验</div>
<div class="duty-desc">基础格式和必填校验</div>
</div>
<div class="duty-item">
<div class="duty-icon">🔄</div>
<div class="duty-title">调用 Service</div>
<div class="duty-desc">将请求转发给业务层</div>
</div>
<div class="duty-item">
<div class="duty-icon">📦</div>
<div class="duty-title">封装响应</div>
<div class="duty-desc">统一响应格式返回</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showDetails = ref('')
const toggleDetails = (section) => {
showDetails.value = showDetails.value === section ? '' : section
}
</script>
<style scoped>
.controller-layer-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.flow-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.flow-step {
display: flex;
gap: 16px;
padding: 16px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
}
.flow-step:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.flow-step.active {
border: 2px solid #409eff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
.controller-step {
border-left: 4px solid #67c23a;
}
.validation-step {
border-left: 4px solid #e6a23c;
}
.step-icon {
font-size: 24px;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
color: #303133;
margin-bottom: 8px;
font-size: 14px;
}
.step-code {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
color: #333;
white-space: pre-wrap;
line-height: 1.5;
}
.arrow-connector {
text-align: center;
padding: 8px;
font-size: 12px;
color: #909399;
font-weight: 500;
}
.detail-panel {
margin-top: 12px;
padding: 16px;
background: #f0f7ff;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.detail-panel h5 {
margin: 0 0 12px 0;
color: #1a1a2e;
font-size: 14px;
}
.detail-panel ul {
margin: 0;
padding-left: 20px;
}
.detail-panel li {
margin: 6px 0;
color: #606266;
font-size: 12px;
line-height: 1.6;
}
.controller-summary {
margin-top: 24px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.controller-summary h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.duty-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.duty-item {
text-align: center;
padding: 16px 12px;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s ease;
}
.duty-item:hover {
background: #e6f7ff;
transform: translateY(-2px);
}
.duty-icon {
font-size: 28px;
margin-bottom: 8px;
}
.duty-title {
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.duty-desc {
color: #909399;
font-size: 11px;
}
@media (max-width: 768px) {
.duty-grid {
grid-template-columns: repeat(2, 1fr);
}
.flow-step {
flex-direction: column;
gap: 8px;
}
.step-content {
width: 100%;
}
}
</style>
@@ -0,0 +1,319 @@
<template>
<div class="dependency-direction-demo">
<div class="demo-header">
<h4>🔄 依赖方向分层架构的核心规则</h4>
<p class="subtitle">理解依赖方向才能真正掌握分层架构</p>
</div>
<!-- 依赖方向可视化 -->
<div class="direction-visualization">
<div class="arch-diagram">
<!-- 外层 -->
<div class="layer outer">
<div class="layer-label">外层UI / 外部系统</div>
<div class="layer-box">Controller</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖</span>
</div>
<!-- 中层 -->
<div class="layer middle">
<div class="layer-label">中层应用层</div>
<div class="layer-box">Service</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖</span>
</div>
<!-- 内层 -->
<div class="layer inner">
<div class="layer-label">内层领域层</div>
<div class="layer-box">Domain / Repository</div>
</div>
</div>
<!-- 核心原则说明 -->
<div class="principle-box">
<div class="principle-title">🎯 核心原则依赖倒置DIP</div>
<div class="principle-content">
<p><strong>上层模块不应该依赖下层模块的具体实现而应该依赖于抽象</strong></p>
<div class="rule-list">
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Controller Service 接口</strong>
<div class="rule-desc">Controller 只依赖 Service 的接口不依赖实现类</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Service Repository 接口</strong>
<div class="rule-desc">Service 只依赖 Repository 接口不关心数据怎么存</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>所有层依赖 Domain</strong>
<div class="rule-desc">Domain 是核心被所有上层依赖 Domain 不依赖任何层</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 依赖方向示意图 -->
<div class="direction-diagram">
<h5>📊 依赖方向示意图</h5>
<div class="diagram-content">
<pre class="diagram-code">
Controller Layer
UserController
- @Autowired private IUserService userService;
依赖接口不依赖实现
依赖Dependency
Service Layer
UserServiceImpl
- @Autowired private UserRepository repository;
依赖 Repository 接口
依赖
Repository Layer
UserRepository
- extends JpaRepository&lt;User, Long&gt;
依赖
Domain Layer (核心领域)
User (Entity)
- 不包含任何层依赖
- 被所有层依赖
</pre>
</div>
</div>
</div>
</template>
<script setup>
// Component logic can be added here if needed
</script>
<style scoped>
.dependency-direction-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.direction-visualization {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.arch-diagram {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.layer {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-label {
font-size: 11px;
color: #909399;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.layer-box {
padding: 16px 20px;
background: #f5f7fa;
border-radius: 8px;
font-weight: 500;
color: #303133;
text-align: center;
border-left: 4px solid #409eff;
}
.layer.outer .layer-box {
border-left-color: #67c23a;
}
.layer.middle .layer-box {
border-left-color: #e6a23c;
}
.layer.inner .layer-box {
border-left-color: #409eff;
}
.dependency-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 0;
}
.arrow-line {
width: 2px;
height: 12px;
background: #dcdfe6;
}
.arrow-head {
color: #909399;
font-size: 11px;
margin-top: 2px;
}
.principle-box {
background: linear-gradient(135deg, #e6f7ff 0%, #f0f7ff 100%);
border-radius: 10px;
padding: 20px;
border-left: 4px solid #1890ff;
}
.principle-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
}
.principle-content p {
margin: 0 0 12px 0;
color: #595959;
font-size: 13px;
line-height: 1.6;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rule-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
background: white;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.rule-icon {
font-size: 16px;
flex-shrink: 0;
}
.rule-text {
flex: 1;
}
.rule-text strong {
color: #1a1a2e;
font-size: 13px;
}
.rule-desc {
color: #8c8c8c;
font-size: 12px;
margin-top: 2px;
}
.direction-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.direction-diagram h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.diagram-content {
overflow-x: auto;
}
.diagram-code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
color: #595959;
margin: 0;
white-space: pre;
}
@media (max-width: 768px) {
.model-cards {
grid-template-columns: 1fr;
}
.comparison-tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
text-align: center;
}
}
</style>
@@ -0,0 +1,501 @@
<template>
<div class="domain-model-demo">
<div class="demo-header">
<h4>📦 Domain 领域模型设计</h4>
<p class="subtitle">Domain 是业务概念的载体所有层的依赖基础</p>
</div>
<!-- 领域模型对比 -->
<div class="model-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="comparison-content">
<!-- 贫血模型 vs 充血模型 -->
<div v-if="currentTab === 'comparison'" class="tab-panel">
<div class="model-cards">
<div class="model-card anemic">
<div class="card-header">
<span class="card-icon">📄</span>
<span class="card-title">贫血模型 (Anemic)</span>
<span class="card-badge">传统做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity只有 getter/setter</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 只有 getter/setter,没有业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 其他 getter/setter
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service所有业务逻辑都在这里</div>
<pre><code>@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
// 贫血模型:业务逻辑散落在 Service 里
if (order.getStatus() == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (order.getStatus() == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
// 修改状态
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}</code></pre>
</div>
<div class="problems">
<div class="problem-title">😫 贫血模型的问题</div>
<ul>
<li><strong>违背面向对象</strong>对象只有数据没有行为变成了 "数据结构"</li>
<li><strong>逻辑分散</strong>同样的业务规则可能在多个 Service 重复</li>
<li><strong>难以维护</strong>改一个规则要找所有用到的地方</li>
</ul>
</div>
</div>
</div>
<div class="model-card rich">
<div class="card-header">
<span class="card-icon">🧠</span>
<span class="card-title">充血模型 (Rich Domain)</span>
<span class="card-badge tag-green">推荐做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity包含业务逻辑</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 🎯 业务行为封装在实体里
/**
* 取消订单
*/
public void cancel() {
// 状态校验内聚在方法里
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (this.status == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
this.status = OrderStatus.CANCELLED;
// 可以触发领域事件
registerEvent(new OrderCancelledEvent(this.id));
}
/**
* 支付订单
*/
public void pay(Payment payment) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不正确");
}
if (!payment.getAmount().equals(this.totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
this.paymentTime = LocalDateTime.now();
}
// ... 其他业务方法
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service只做协调不做业务判断</div>
<pre><code>@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public void cancelOrder(Long orderId) {
// 1. 加载聚合根
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 2. 💡 调用领域对象的业务方法
// 业务规则封装在 Order 里,Service 只做协调
order.cancel();
// 3. 保存变更
orderRepository.save(order);
// 4. 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}</code></pre>
</div>
<div class="benefits">
<div class="benefit-title">😊 充血模型的优势</div>
<ul>
<li><strong>符合面向对象</strong>数据和行为封装在一起是真正的 "对象"</li>
<li><strong>业务内聚</strong>规则跟着对象走改一处处处生效</li>
<li><strong>可复用可测试</strong>领域对象是纯内存对象单元测试不需要数据库</li>
<li><strong>表达力强</strong>order.cancel() orderService.cancel(order) 更自然</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 值对象 -->
<div v-else-if="currentTab === 'valueobject'" class="tab-panel">
<div class="value-object-content">
<div class="concept-intro">
<h5>💎 什么是值对象Value Object</h5>
<p>值对象是没有唯一标识不可变的对象它描述了某种特征或属性两个值对象如果所有属性相等就被认为是同一个对象</p>
</div>
<div class="vo-examples">
<div class="example-card">
<div class="example-title">📍 地址 Address</div>
<pre><code>// 值对象:不可变、无 ID
public record Address(
String province, // 省
String city, // 市
String district, // 区
String street, // 街道
String zipCode // 邮编
) {
// 值对象的方法通常是转换或计算
public String toDisplayString() {
return String.format("%s%s%s%s",
province, city, district, street);
}
// 校验逻辑
public boolean isValid() {
return StringUtils.isNotBlank(province)
&& StringUtils.isNotBlank(city);
}
}
// 使用:地址相等只要属性相同
Address addr1 = new Address("广东", "深圳", "南山", "科技园", "518000");
Address addr2 = new Address("广东", "深圳", "南山", "科技园", "518000");
System.out.println(addr1.equals(addr2)); // true - 值对象比较的是值</code></pre>
</div>
<div class="example-card">
<div class="example-title">💰 金钱 Money</div>
<pre><code>// 金钱是经典的值对象
public record Money(
BigDecimal amount,
Currency currency
) {
// 工厂方法
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, Currency.getInstance(currencyCode));
}
public static Money yuan(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
// 值对象的核心:运算返回新的值对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), currency);
}
public boolean isGreaterThan(Money other) {
return this.amount.compareTo(other.amount) > 0;
}
// 格式化显示
public String toDisplayString() {
return currency.getSymbol() + amount.setScale(2, RoundingMode.HALF_UP);
}
}
// 使用示例
Money price = Money.yuan(new BigDecimal("199.99"));
Money shipping = Money.yuan(new BigDecimal("10.00"));
Money discount = Money.yuan(new BigDecimal("20.00"));
Money total = price.add(shipping).add(discount.negate());
System.out.println(total.toDisplayString()); // ¥189.99</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTab = ref('comparison')
const tabs = [
{ id: 'comparison', name: '贫血 vs 充血' },
{ id: 'valueobject', name: '值对象设计' }
]
</script>
<style scoped>
.domain-model-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.model-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.model-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.model-card {
background: #f8f9fa;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e4e7ed;
}
.model-card.anemic {
border-left: 4px solid #ff4d4f;
}
.model-card.rich {
border-left: 4px solid #52c41a;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.card-badge {
margin-left: auto;
padding: 2px 8px;
background: #ff4d4f;
color: white;
border-radius: 10px;
font-size: 11px;
}
.card-badge.tag-green {
background: #52c41a;
}
.card-content {
padding: 16px;
}
.code-section {
margin-bottom: 16px;
}
.code-label {
font-size: 11px;
color: #909399;
margin-bottom: 6px;
font-weight: 500;
}
.code-section pre {
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 0;
}
.code-section code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.5;
}
.problems, .benefits {
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.problems {
background: #fff1f0;
border-left: 3px solid #ff4d4f;
}
.benefits {
background: #f6ffed;
border-left: 3px solid #52c41a;
}
.problem-title, .benefit-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.problem-title {
color: #cf1322;
}
.benefit-title {
color: #389e0d;
}
.problems ul, .benefits ul {
margin: 0;
padding-left: 16px;
}
.problems li, .benefits li {
margin: 6px 0;
font-size: 12px;
line-height: 1.5;
color: #595959;
}
@media (max-width: 1024px) {
.model-cards {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,338 @@
<template>
<div class="dto-flow-demo">
<div class="demo-header">
<h4>🔄 DTO 流转数据在不同层之间的转换</h4>
<p class="subtitle">DTOData Transfer Object是层与层之间传递数据的载体</p>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-step">
<div class="step-title">Controller </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 接收 Request DTO</span>
</div>
<div class="code-line">
<span class="keyword">public</span> ResponseEntity&lt;UserDTO&gt; createUser(
</div>
<div class="code-line">
&nbsp;&nbsp;@RequestBody <span class="highlight">@Valid UserCreateRequest request</span>
</div>
<div class="code-line">
) { ... }
</div>
</div>
</div>
<div class="flow-arrow"> 转换为 Service 需要的参数</div>
<div class="flow-step">
<div class="step-title">Service </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 业务处理</span>
</div>
<div class="code-line">
<span class="keyword">public</span> UserDTO createUser(UserCreateParam param) {
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="comment">// 转换为 Entity</span>
</div>
<div class="code-line">
&nbsp;&nbsp;User user = <span class="highlight">param.toEntity()</span>;
</div>
<div class="code-line">
&nbsp;&nbsp;userRepository.save(user);
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">return</span> <span class="highlight">UserDTO.from(user)</span>;
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow"> 转换为 Repository 需要的 Entity</div>
<div class="flow-step">
<div class="step-title">Repository </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 数据持久化</span>
</div>
<div class="code-line">
<span class="keyword">public interface</span> UserRepository
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">extends</span> JpaRepository&lt;<span class="highlight">User</span>, Long&gt; {
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow"> 返回 Entity转换为 DTO</div>
<div class="flow-step">
<div class="step-title">返回给客户端</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// Response DTO</span>
</div>
<div class="code-line">
{
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"id"</span>: 10001,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"username"</span>: <span class="string">"张三"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"email"</span>: <span class="string">"zhangsan@example.com"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"createdAt"</span>: <span class="string">"2024-01-15T10:30:00Z"</span>
</div>
<div class="code-line">
}
</div>
</div>
</div>
</div>
<!-- 不同层 DTO 对比 -->
<div class="dto-comparison">
<h5>📋 不同层的 DTO 职责</h5>
<div class="comparison-table">
<div class="table-header">
<div class="col-layer">层级</div>
<div class="col-dto">DTO 类型</div>
<div class="col-purpose">职责</div>
<div class="col-example">示例</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag controller">Controller</span>
</div>
<div class="col-dto">
Request / Response DTO
</div>
<div class="col-purpose">
定义 API 契约参数校验序列化
</div>
<div class="col-example">
<code>UserCreateRequest</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag service">Service</span>
</div>
<div class="col-dto">
Param / Result DTO
</div>
<div class="col-purpose">
封装业务方法参数解耦 Controller Service
</div>
<div class="col-example">
<code>UserCreateParam</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag repository">Repository</span>
</div>
<div class="col-dto">
Entity / DO
</div>
<div class="col-purpose">
映射数据库表结构ORM 映射
</div>
<div class="col-example">
<code>UserEntity</code>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('conversion')
</script>
<style scoped>
.dto-flow-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-step {
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
border-left: 4px solid #409eff;
}
.flow-step:nth-child(odd) {
border-left-color: #67c23a;
}
.step-title {
padding: 12px 16px;
background: white;
font-weight: 600;
color: #303133;
font-size: 14px;
border-bottom: 1px solid #ebeef5;
}
.step-code {
padding: 16px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.6;
}
.code-line {
padding: 2px 0;
}
.comment {
color: #6a9955;
}
.keyword {
color: #569cd6;
}
.highlight {
background: #fff3cd;
padding: 2px 4px;
border-radius: 3px;
color: #856404;
}
.string {
color: #ce9178;
}
.flow-arrow {
text-align: center;
padding: 12px;
color: #909399;
font-size: 13px;
font-weight: 500;
}
.dto-comparison {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.dto-comparison h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.comparison-table {
overflow-x: auto;
}
.table-header, .table-row {
display: grid;
grid-template-columns: 100px 150px 1fr 120px;
gap: 12px;
padding: 12px;
align-items: center;
}
.table-header {
background: #f5f7fa;
border-radius: 8px;
font-weight: 600;
color: #303133;
font-size: 13px;
}
.table-row {
border-bottom: 1px solid #ebeef5;
font-size: 12px;
color: #606266;
}
.table-row:last-child {
border-bottom: none;
}
.layer-tag {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
.layer-tag.controller {
background: #f0f9ff;
color: #1890ff;
}
.layer-tag.service {
background: #fff7e6;
color: #fa8c16;
}
.layer-tag.repository {
background: #f6ffed;
color: #52c41a;
}
@media (max-width: 768px) {
.table-header, .table-row {
grid-template-columns: 1fr;
gap: 8px;
}
.table-header {
display: none;
}
}
</style>
@@ -0,0 +1,352 @@
<template>
<div class="layered-architecture-demo">
<div class="architecture-container">
<!-- 客户端 -->
<div class="client-layer">
<div class="layer-box client">
<div class="layer-icon">🌐</div>
<div class="layer-title">客户端</div>
<div class="layer-desc">Web / App / 小程序</div>
</div>
<div class="arrow-down"> HTTP/HTTPS</div>
</div>
<!-- 后端分层 -->
<div class="backend-layers">
<!-- Controller -->
<div
class="layer-box controller"
:class="{ active: activeLayer === 'controller' }"
@click="setActiveLayer('controller')"
>
<div class="layer-header">
<span class="layer-icon">🎮</span>
<span class="layer-name">Controller</span>
<span class="layer-badge">入口</span>
</div>
<div class="layer-content">
<div class="duty">职责接收请求参数校验调用 Service</div>
<div class="tech">技术Spring MVC / Gin / Echo</div>
</div>
</div>
<div class="arrow-down"> 调用</div>
<!-- Service -->
<div
class="layer-box service"
:class="{ active: activeLayer === 'service' }"
@click="setActiveLayer('service')"
>
<div class="layer-header">
<span class="layer-icon"></span>
<span class="layer-name">Service</span>
<span class="layer-badge">业务核心</span>
</div>
<div class="layer-content">
<div class="duty">职责业务逻辑编排事务管理跨模块协调</div>
<div class="tech">技术纯代码逻辑 / 无框架依赖</div>
</div>
</div>
<div class="arrow-down"> 调用</div>
<!-- Repository -->
<div
class="layer-box repository"
:class="{ active: activeLayer === 'repository' }"
@click="setActiveLayer('repository')"
>
<div class="layer-header">
<span class="layer-icon">🗄</span>
<span class="layer-name">Repository</span>
<span class="layer-badge">数据访问</span>
</div>
<div class="layer-content">
<div class="duty">职责数据持久化查询封装ORM 映射</div>
<div class="tech">技术MyBatis / GORM / Hibernate</div>
</div>
</div>
<div class="arrow-down"> SQL</div>
<!-- Domain -->
<div
class="layer-box domain"
:class="{ active: activeLayer === 'domain' }"
@click="setActiveLayer('domain')"
>
<div class="layer-header">
<span class="layer-icon">📦</span>
<span class="layer-name">Domain / Model</span>
<span class="layer-badge">领域模型</span>
</div>
<div class="layer-content">
<div class="duty">职责实体定义业务规则值对象</div>
<div class="tech">技术POJO / Struct / Class</div>
</div>
</div>
<div class="arrow-down"> 持久化</div>
<!-- 数据库 -->
<div class="layer-box database">
<div class="layer-icon">💾</div>
<div class="layer-title">数据库</div>
<div class="layer-desc">MySQL / PostgreSQL / MongoDB</div>
</div>
</div>
<!-- 右侧说明面板 -->
<div class="info-panel" v-if="activeLayer">
<h4>{{ layerInfo.title }}</h4>
<p>{{ layerInfo.description }}</p>
<div class="analogy">
<strong>💡 类比</strong>{{ layerInfo.analogy }}
</div>
<div class="common-mistakes">
<strong> 常见错误</strong>
<ul>
<li v-for="mistake in layerInfo.mistakes" :key="mistake">{{ mistake }}</li>
</ul>
</div>
</div>
</div>
<!-- 底部交互提示 -->
<div class="interaction-hint">
💡 点击各层查看详细说明 | 实际调用流向从上到下依赖从下到上
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLayer = ref('')
const setActiveLayer = (layer) => {
activeLayer.value = activeLayer.value === layer ? '' : layer
}
const layerInfo = computed(() => {
const infoMap = {
controller: {
title: 'Controller 层 - 请求的"门童"',
description: 'Controller 是系统的入口,负责接收 HTTP 请求、解析参数、进行基础校验,然后调用 Service 层处理业务。',
analogy: '就像餐厅的门童,负责迎接客人(接收请求)、检查预约(参数校验)、引导入座(路由到对应服务),但不负责做菜。',
mistakes: [
'在 Controller 里写业务逻辑(应该放在 Service)',
'直接操作数据库(应该调用 Repository',
'不做参数校验,导致脏数据流入系统'
]
},
service: {
title: 'Service 层 - 业务逻辑的"厨师"',
description: 'Service 是系统的核心,负责编排业务逻辑、管理事务、协调多个 Repository。这一层应该包含所有的业务规则和流程。',
analogy: '就像餐厅的厨师,负责按照菜谱(业务规则)做菜,需要协调各种食材(数据),把控菜品质量(业务正确性)。',
mistakes: [
'Service 之间互相调用,形成循环依赖',
'在 Service 里直接写 SQL(应该放在 Repository',
'一个 Service 方法过长,包含多个业务场景(应该拆分成多个方法)'
]
},
repository: {
title: 'Repository 层 - 数据的"仓管"',
description: 'Repository 负责与数据库交互,封装所有的 CRUD 操作。上层不需要关心具体的数据库类型和 SQL 语句。',
analogy: '就像餐厅的仓管员,负责从仓库(数据库)取食材、存放剩余食材,厨师(Service)只需要告诉他要什么,不需要知道仓库在哪。',
mistakes: [
'在 Repository 里写业务逻辑(应该只负责数据访问)',
'直接返回数据库实体给前端(应该转换为 DTO)',
'一个 Repository 操作多个表(应该拆分到不同 Repository'
]
},
domain: {
title: 'Domain 层 - 业务概念的"蓝图"',
description: 'Domain 定义了系统中的实体、值对象、业务规则。它是所有层的依赖基础,但不依赖任何其他层。',
analogy: '就像餐厅的菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。所有厨师都要按照这个标准来做。',
mistakes: [
'Domain 对象里包含持久化注解(应该保持纯净)',
'在 Domain 里写业务逻辑(业务逻辑应该在 Service)',
'Domain 对象之间循环依赖'
]
}
}
return infoMap[activeLayer.value] || {}
})
</script>
<style scoped>
.layered-architecture-demo {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.architecture-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.client-layer {
display: flex;
flex-direction: column;
align-items: center;
}
.backend-layers {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-box {
padding: 16px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.layer-box:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.layer-box.active {
border-color: #409eff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
/* 各层颜色主题 */
.controller { border-left: 4px solid #67c23a; }
.service { border-left: 4px solid #e6a23c; }
.repository { border-left: 4px solid #409eff; }
.domain { border-left: 4px solid #909399; }
.client { border-left: 4px solid #f56c6c; }
.database { border-left: 4px solid #8e44ad; }
.layer-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.layer-icon {
font-size: 20px;
}
.layer-name {
font-weight: 600;
font-size: 15px;
color: #303133;
}
.layer-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
background: #f0f2f5;
color: #606266;
}
.layer-content {
padding-left: 30px;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.duty, .tech {
margin: 4px 0;
}
.arrow-down {
text-align: center;
padding: 6px;
font-size: 12px;
color: #909399;
}
.info-panel {
width: 320px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: sticky;
top: 20px;
}
.info-panel h4 {
margin: 0 0 12px 0;
color: #303133;
font-size: 16px;
padding-bottom: 10px;
border-bottom: 2px solid #409eff;
}
.info-panel p {
margin: 0 0 16px 0;
color: #606266;
font-size: 13px;
line-height: 1.7;
}
.analogy, .common-mistakes {
margin: 12px 0;
padding: 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.6;
}
.analogy {
background: #f0f9ff;
border-left: 3px solid #409eff;
color: #1d4ed8;
}
.common-mistakes {
background: #fff2f0;
border-left: 3px solid #ff4d4f;
color: #cf1322;
}
.common-mistakes ul {
margin: 6px 0 0 0;
padding-left: 16px;
}
.common-mistakes li {
margin: 4px 0;
}
.interaction-hint {
text-align: center;
padding: 16px;
margin-top: 16px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 8px;
color: #389e0d;
font-size: 13px;
}
@media (max-width: 1024px) {
.architecture-container {
flex-direction: column;
}
.info-panel {
width: 100%;
position: static;
}
}
</style>
@@ -0,0 +1,698 @@
<template>
<div class="service-layer-demo">
<div class="demo-header">
<h4> Service 业务逻辑的"指挥家"</h4>
<p class="subtitle">Service 层编排业务逻辑协调多个 Repository管理事务边界</p>
</div>
<!-- 场景选择器 -->
<div class="scenario-selector">
<div class="selector-label">选择业务场景</div>
<div class="scenario-buttons">
<button
v-for="scenario in scenarios"
:key="scenario.id"
:class="['scenario-btn', { active: currentScenario === scenario.id }]"
@click="currentScenario = scenario.id"
>
{{ scenario.name }}
</button>
</div>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-header">
<span class="flow-title">{{ currentScenarioData.title }}</span>
<span class="flow-desc">{{ currentScenarioData.description }}</span>
</div>
<div class="flow-steps">
<div
v-for="(step, index) in currentScenarioData.steps"
:key="index"
class="flow-step"
:class="{ 'has-sub-steps': step.subSteps }"
@click="toggleStep(index)"
>
<div class="step-header">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-layer">{{ step.layer }}</div>
</div>
<div v-if="step.subSteps" class="expand-icon">
{{ expandedSteps.includes(index) ? '' : '' }}
</div>
</div>
<div v-if="step.code" class="step-code">
<pre><code>{{ step.code }}</code></pre>
</div>
<!-- 子步骤事务管理 -->
<div v-if="step.subSteps && expandedSteps.includes(index)" class="sub-steps">
<div
v-for="(subStep, subIndex) in step.subSteps"
:key="subIndex"
class="sub-step"
:class="subStep.status"
>
<div class="sub-step-icon">{{ subStep.icon }}</div>
<div class="sub-step-content">
<div class="sub-step-name">{{ subStep.name }}</div>
<div class="sub-step-desc">{{ subStep.desc }}</div>
</div>
<div class="sub-step-status">{{ subStep.statusText }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Service 设计原则 -->
<div class="design-principles">
<h5>🎯 Service 层设计原则</h5>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
>
<div class="principle-icon">{{ principle.icon }}</div>
<div class="principle-title">{{ principle.title }}</div>
<div class="principle-desc">{{ principle.desc }}</div>
<div class="principle-example">
<code>{{ principle.example }}</code>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentScenario = ref('order')
const expandedSteps = ref([])
const scenarios = [
{ id: 'order', name: '下单流程' },
{ id: 'refund', name: '退款处理' },
{ id: 'report', name: '报表生成' }
]
const scenarioData = {
order: {
title: '🛒 电商下单流程',
description: '用户下单涉及库存扣减、订单创建、支付记录等多个操作,需要保证事务一致性',
steps: [
{
name: '参数校验与DTO转换',
layer: 'Controller',
code: `@PostMapping("/orders")
public ResponseEntity&lt;OrderDTO&gt; createOrder(
@RequestBody @Valid CreateOrderRequest request
) {
// 调用 Service
OrderDTO order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}`
},
{
name: '业务逻辑编排(事务管理)',
layer: 'Service',
code: `@Service
@Transactional // 关键:事务管理
public class OrderService {
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 检查库存
inventoryService.checkAndDeduct(request.getSkuId(),
request.getQuantity());
// 2. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(calculateTotal(request));
orderRepository.save(order);
// 3. 创建支付记录
Payment payment = createPayment(order);
paymentRepository.save(payment);
// 任一失败都会回滚
return convertToDTO(order);
}
}`,
subSteps: [
{
icon: '✅',
name: '检查并扣减库存',
desc: '确保库存充足,预先锁定',
status: 'success',
statusText: '成功'
},
{
icon: '📝',
name: '创建订单记录',
desc: '生成订单主表数据',
status: 'success',
statusText: '成功'
},
{
icon: '💳',
name: '创建支付记录',
desc: '初始化待支付状态',
status: 'success',
statusText: '成功'
},
{
icon: '🔄',
name: '事务提交',
desc: '所有操作原子性提交',
status: 'success',
statusText: '已提交'
}
]
},
{
name: '数据持久化',
layer: 'Repository',
code: `public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
// 基本的 CRUD 已内置
}
// 实际执行:INSERT INTO orders (...) VALUES (...)`
}
]
},
refund: {
title: '💰 退款处理流程',
description: '退款涉及订单状态变更、支付原路返回、库存回滚等操作',
steps: [
{
name: '接收退款申请',
layer: 'Controller',
code: `@PostMapping("/orders/{orderId}/refund")
public ResponseEntity&lt;RefundDTO&gt; applyRefund(
@PathVariable Long orderId,
@RequestBody @Valid RefundRequest request
) {
RefundDTO refund = refundService.processRefund(orderId, request);
return ResponseEntity.ok(refund);
}`
},
{
name: '退款业务处理',
layer: 'Service',
code: `@Service
@Transactional
public class RefundService {
public RefundDTO processRefund(Long orderId, RefundRequest request) {
// 1. 验证订单状态
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("订单不存在"));
if (order.getStatus() != OrderStatus.PAID) {
throw new InvalidOrderStateException("订单状态不允许退款");
}
// 2. 计算退款金额
BigDecimal refundAmount = calculateRefundAmount(order, request);
// 3. 调用支付渠道退款
PaymentRefundResult result = paymentService.refund(
order.getPaymentNo(),
refundAmount,
request.getReason()
);
// 4. 更新订单状态
order.setStatus(OrderStatus.REFUNDING);
orderRepository.save(order);
// 5. 保存退款记录
RefundRecord record = new RefundRecord();
record.setOrderId(orderId);
record.setAmount(refundAmount);
record.setReason(request.getReason());
record.setStatus(RefundStatus.PROCESSING);
refundRecordRepository.save(record);
// 6. 异步恢复库存
inventoryService.restoreStockAsync(order.getItems());
return convertToDTO(record);
}
}`,
subSteps: [
{ icon: '🔍', name: '验证订单状态', desc: '检查订单是否存在且可退款', status: 'success', statusText: '通过' },
{ icon: '💰', name: '计算退款金额', desc: '根据规则计算应退金额', status: 'success', statusText: '完成' },
{ icon: '🏦', name: '调用支付渠道', desc: '请求第三方支付退款', status: 'success', statusText: '处理中' },
{ icon: '📝', name: '更新订单状态', desc: '标记为退款中', status: 'success', statusText: '已更新' },
{ icon: '📊', name: '保存退款记录', desc: '记录退款流水', status: 'success', statusText: '已保存' },
{ icon: '🔄', name: '异步恢复库存', desc: '后台恢复商品库存', status: 'success', statusText: '已提交' }
]
}
]
},
report: {
title: '📊 报表生成流程',
description: '复杂的报表通常涉及多个数据源查询、数据聚合计算、异步导出等',
steps: [
{
name: '接收报表请求',
layer: 'Controller',
code: `@GetMapping("/reports/sales")
public ResponseEntity<ReportTaskDTO> generateSalesReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate endDate,
@RequestParam(required = false) List<Long> regionIds
) {
// 大报表采用异步生成
ReportTaskDTO task = reportService.createReportTask(
ReportType.SALES, startDate, endDate, regionIds
);
return ResponseEntity.accepted().body(task);
}`
},
{
name: '复杂报表业务编排',
layer: 'Service',
code: `@Service
public class ReportService {
@Async("reportExecutor")
public void generateReportAsync(Long taskId) {
ReportTask task = reportTaskRepository.findById(taskId)
.orElseThrow();
try {
task.setStatus(TaskStatus.RUNNING);
reportTaskRepository.save(task);
// 1. 从多个数据源聚合数据
SalesReportData data = aggregateSalesData(task);
// 2. 计算各项指标
calculateMetrics(data);
// 3. 生成图表数据
generateChartData(data);
// 4. 导出为 Excel
String fileUrl = exportToExcel(data, task);
task.setStatus(TaskStatus.COMPLETED);
task.setFileUrl(fileUrl);
task.setCompletedAt(LocalDateTime.now());
} catch (Exception e) {
task.setStatus(TaskStatus.FAILED);
task.setErrorMessage(e.getMessage());
}
reportTaskRepository.save(task);
}
private SalesReportData aggregateSalesData(ReportTask task) {
// 协调多个 Repository 查询数据
List<Order> orders = orderRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
List<Payment> payments = paymentRepository
.findByPaidAtBetween(task.getStartDate(), task.getEndDate());
List<RefundRecord> refunds = refundRecordRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
// 数据聚合逻辑...
return new SalesReportData(orders, payments, refunds);
}
}`,
subSteps: [
{ icon: '📥', name: '从多个数据源查询', desc: 'Orders/Payments/Refunds', status: 'success', statusText: '已查询' },
{ icon: '🔄', name: '数据聚合与清洗', desc: '关联数据、处理缺失值', status: 'success', statusText: '已完成' },
{ icon: '📊', name: '计算业务指标', desc: 'GMV、订单数、客单价等', status: 'success', statusText: '已计算' },
{ icon: '📈', name: '生成图表数据', desc: '趋势图、占比图数据结构', status: 'success', statusText: '已生成' },
{ icon: '📄', name: '导出 Excel 文件', desc: '生成并上传至 OSS', status: 'success', statusText: '已完成' }
]
}
]
}
}
const currentScenarioData = computed(() => scenarioData[currentScenario.value])
const toggleStep = (index) => {
const i = expandedSteps.value.indexOf(index)
if (i > -1) {
expandedSteps.value.splice(i, 1)
} else {
expandedSteps.value.push(index)
}
}
const principles = [
{
id: 1,
icon: '🎯',
title: '单一职责',
desc: '一个 Service 类只负责一块业务领域',
example: 'UserService 只管用户,OrderService 只管订单'
},
{
id: 2,
icon: '🔄',
title: '事务边界',
desc: '在 Service 层声明式管理事务',
example: '@Transactional 放在 Service 方法上'
},
{
id: 3,
icon: '🔗',
title: '避免循环依赖',
desc: 'Service 之间不要互相调用',
example: 'A 调用 BB 又调用 A 会导致循环'
},
{
id: 4,
icon: '📦',
title: 'DTO 转换',
desc: '返回前转换为 DTO,不暴露实体',
example: 'return new UserDTO(user)'
}
]
</script>
<style scoped>
.service-layer-demo {
padding: 24px;
background: linear-gradient(135deg, #fff9f0 0%, #fff0e6 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.scenario-selector {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.selector-label {
font-weight: 600;
color: #303133;
font-size: 14px;
white-space: nowrap;
}
.scenario-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.scenario-btn {
padding: 8px 16px;
border: 1px solid #dcdfe6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #606266;
transition: all 0.3s ease;
}
.scenario-btn:hover {
border-color: #e6a23c;
color: #e6a23c;
}
.scenario-btn.active {
background: #e6a23c;
border-color: #e6a23c;
color: white;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.flow-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.flow-desc {
font-size: 13px;
color: #909399;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.flow-step {
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid #e6a23c;
}
.flow-step:hover {
background: #fff8f0;
transform: translateX(4px);
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.step-number {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e6a23c;
color: white;
border-radius: 50%;
font-size: 13px;
font-weight: 600;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.step-layer {
font-size: 12px;
color: #e6a23c;
margin-top: 2px;
}
.expand-icon {
color: #909399;
font-size: 12px;
}
.step-code {
padding: 0 16px 16px 56px;
}
.step-code pre {
margin: 0;
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
}
.step-code code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
}
.sub-steps {
padding: 0 16px 16px 56px;
}
.sub-step {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
border-left: 3px solid #dcdfe6;
}
.sub-step.success {
border-left-color: #67c23a;
background: #f6ffed;
}
.sub-step-icon {
font-size: 16px;
}
.sub-step-content {
flex: 1;
}
.sub-step-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.sub-step-desc {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
.sub-step-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: #f0f9ff;
color: #1890ff;
}
.sub-step.success .sub-step-status {
background: #f6ffed;
color: #52c41a;
}
.design-principles {
margin-top: 24px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.design-principles h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.principle-card {
padding: 14px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #e6a23c;
}
.principle-icon {
font-size: 20px;
margin-bottom: 6px;
}
.principle-title {
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.principle-desc {
color: #606266;
font-size: 11px;
margin-bottom: 8px;
line-height: 1.5;
}
.principle-example {
background: #2d2d2d;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
}
.principle-example code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.4;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: 1fr;
}
.scenario-selector {
flex-direction: column;
align-items: flex-start;
}
.step-code {
padding-left: 16px;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
合成层演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('合成层演示')
const description = ref('展示浏览器渲染的最后阶段,将各层合成最终页面')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
DOM到渲染树演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('DOM到渲染树演示')
const description = ref('展示浏览器如何将DOM树和CSSOM树组合成渲染树的过程')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
布局重排演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('布局重排演示')
const description = ref('展示浏览器渲染过程中的布局计算和重排机制')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
宏任务微任务演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('宏任务微任务演示')
const description = ref('展示JavaScript事件循环中的宏任务和微任务执行顺序')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
绘制层演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('绘制层演示')
const description = ref('展示浏览器渲染过程中的绘制层合成机制')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
渲染性能演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('渲染性能演示')
const description = ref('展示浏览器渲染性能优化技巧和最佳实践')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,403 @@
<template>
<div class="demo-container">
<h4>浏览器渲染管线全景图</h4>
<p class="demo-description">点击每个阶段查看详情观察数据如何在管线中流动</p>
<div class="pipeline-container">
<div class="pipeline-flow">
<div
v-for="(stage, index) in stages"
:key="stage.id"
class="stage-card"
:class="{
active: activeStage === index,
completed: activeStage > index,
pending: activeStage < index
}"
@click="selectStage(index)"
>
<div class="stage-icon">{{ stage.icon }}</div>
<div class="stage-name">{{ stage.name }}</div>
<div class="stage-time">{{ stage.time }}</div>
</div>
<div class="flow-arrows">
<div v-for="i in stages.length - 1" :key="i" class="flow-arrow">
<span class="arrow-line"></span>
<span class="arrow-head"></span>
</div>
</div>
</div>
<div class="stage-detail" v-if="activeStage >= 0">
<div class="detail-header">
<span class="detail-icon">{{ stages[activeStage].icon }}</span>
<span class="detail-title">{{ stages[activeStage].name }}</span>
</div>
<div class="detail-content">
<p>{{ stages[activeStage].description }}</p>
<div class="detail-meta">
<div class="meta-item">
<span class="meta-label">输入:</span>
<span class="meta-value">{{ stages[activeStage].input }}</span>
</div>
<div class="meta-item">
<span class="meta-label">输出:</span>
<span class="meta-value">{{ stages[activeStage].output }}</span>
</div>
<div class="meta-item">
<span class="meta-label">耗时:</span>
<span class="meta-value">{{ stages[activeStage].time }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="simulation-controls">
<el-button type="primary" @click="startSimulation" :disabled="isSimulating">
{{ isSimulating ? '模拟中...' : '开始模拟' }}
</el-button>
<el-button @click="resetSimulation">重置</el-button>
<el-slider v-model="simulationSpeed" :min="1" :max="5" :step="1" style="width: 150px;" />
</div>
<div class="pipeline-stats" v-if="showStats">
<div class="stat-card">
<div class="stat-value">{{ totalTime }}ms</div>
<div class="stat-label">总耗时</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ bottleneckStage }}</div>
<div class="stat-label">瓶颈阶段</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ optimizationTip }}</div>
<div class="stat-label">优化建议</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const stages = [
{
id: 'html',
icon: '📄',
name: 'HTML解析',
time: '15ms',
description: '浏览器接收HTML字节流,进行词法分析和语法分析,构建DOM树。这是渲染管线的起点。',
input: 'HTML字节流',
output: 'DOM树'
},
{
id: 'css',
icon: '🎨',
name: 'CSS解析',
time: '12ms',
description: '解析CSS样式表,处理选择器优先级,构建CSSOM树。CSSOM与DOM是并行构建的。',
input: 'CSS字节流',
output: 'CSSOM树'
},
{
id: 'render',
icon: '🌳',
name: '构建渲染树',
time: '8ms',
description: '将DOM树和CSSOM树合并,生成渲染树。只包含可见节点,并计算每个节点的样式。',
input: 'DOM + CSSOM',
output: '渲染树'
},
{
id: 'layout',
icon: '📐',
name: '布局 (Reflow)',
time: '25ms',
description: '计算每个节点在视口中的精确位置和大小。这是最耗时的阶段之一,牵一发而动全身。',
input: '渲染树',
output: '几何信息'
},
{
id: 'paint',
icon: '✏️',
name: '绘制 (Paint)',
time: '18ms',
description: '将每个节点转换为屏幕上的实际像素。包括文本、颜色、图像、边框等视觉内容。',
input: '几何信息',
output: '绘制记录'
},
{
id: 'composite',
icon: '🔮',
name: '合成 (Composite)',
time: '5ms',
description: '将多个图层按照正确的层级顺序合并为最终图像。利用GPU加速,是现代浏览器的优化重点。',
input: '绘制记录',
output: '屏幕像素'
}
]
const activeStage = ref(0)
const isSimulating = ref(false)
const simulationSpeed = ref(3)
const showStats = ref(false)
const totalTime = computed(() => {
return stages.reduce((sum, stage) => sum + parseInt(stage.time), 0)
})
const bottleneckStage = computed(() => '布局阶段')
const optimizationTip = computed(() => '减少DOM操作')
function selectStage(index) {
activeStage.value = index
}
function startSimulation() {
isSimulating.value = true
showStats.value = true
activeStage.value = 0
const interval = setInterval(() => {
if (activeStage.value < stages.length - 1) {
activeStage.value++
} else {
clearInterval(interval)
isSimulating.value = false
}
}, (6 - simulationSpeed.value) * 800)
}
function resetSimulation() {
isSimulating.value = false
activeStage.value = 0
showStats.value = false
}
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 8px 0;
color: #303133;
}
.demo-description {
color: #606266;
font-size: 14px;
margin-bottom: 20px;
}
.pipeline-container {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.pipeline-flow {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-bottom: 20px;
overflow-x: auto;
padding-bottom: 10px;
}
.stage-card {
flex-shrink: 0;
width: 100px;
padding: 12px 8px;
border: 2px solid #e4e7ed;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: white;
position: relative;
z-index: 1;
}
.stage-card:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.stage-card.active {
border-color: #409eff;
background: #ecf5ff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
.stage-card.completed {
border-color: #67c23a;
background: #f0f9eb;
}
.stage-icon {
font-size: 24px;
margin-bottom: 4px;
}
.stage-name {
font-size: 12px;
font-weight: 500;
color: #303133;
margin-bottom: 2px;
}
.stage-time {
font-size: 11px;
color: #909399;
}
.flow-arrows {
position: absolute;
top: 50%;
left: 50px;
right: 50px;
height: 2px;
display: flex;
justify-content: space-around;
align-items: center;
pointer-events: none;
z-index: 0;
}
.flow-arrow {
display: flex;
align-items: center;
color: #c0c4cc;
font-size: 12px;
}
.arrow-line {
width: 30px;
height: 2px;
background: #dcdfe6;
}
.arrow-head {
margin-left: -5px;
}
.stage-detail {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.detail-icon {
font-size: 20px;
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.detail-content {
color: #606266;
font-size: 14px;
line-height: 1.6;
}
.detail-content p {
margin: 0 0 12px 0;
}
.detail-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.meta-item {
display: flex;
gap: 6px;
font-size: 13px;
}
.meta-label {
color: #909399;
}
.meta-value {
color: #409eff;
font-weight: 500;
}
.simulation-controls {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.pipeline-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #409eff;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #909399;
}
@media (max-width: 768px) {
.pipeline-flow {
flex-direction: column;
gap: 12px;
}
.flow-arrows {
display: none;
}
.stage-card {
width: 100%;
max-width: 200px;
}
.detail-meta {
flex-direction: column;
gap: 8px;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存架构概览演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('缓存架构概览')
const description = ref('展示缓存系统的整体架构,包括本地缓存、分布式缓存、CDN缓存等多级缓存架构')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存层级演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('缓存层级演示')
const description = ref('展示多级缓存的层级结构和数据流动过程')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存监控面板演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('缓存监控面板')
const description = ref('可视化展示缓存系统的监控指标,包括命中率、响应时间、缓存大小等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存模式对比演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('缓存模式对比')
const description = ref('对比展示Cache-Aside、Read-Through、Write-Through、Write-Behind等不同缓存模式')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
电商缓存架构演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('电商缓存架构演示')
const description = ref('展示电商系统中的多级缓存架构设计,包括商品缓存、库存缓存、用户缓存等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,505 @@
<template>
<div class="access-key-management-demo">
<div class="demo-header">
<h4>访问密钥AK/SK生命周期管理</h4>
<p class="demo-desc">模拟 AK/SK 的创建使用和轮换流程</p>
</div>
<div class="lifecycle-container">
<!-- AK/SK Card -->
<div class="aksk-card">
<div class="card-header">
<span class="status-badge" :class="akStatus">{{ statusText }}</span>
<span class="age-indicator">已创建 {{ akAge }} </span>
</div>
<div class="credentials-display">
<div class="credential-row">
<span class="label">Access Key ID:</span>
<div class="value-container">
<span class="value">{{ maskedAK }}</span>
<button class="icon-btn" @click="toggleAKVisibility">
{{ showAK ? '🙈' : '👁' }}
</button>
</div>
</div>
<div class="credential-row">
<span class="label">Secret Access Key:</span>
<div class="value-container">
<span class="value">{{ maskedSK }}</span>
<button class="icon-btn" @click="toggleSKVisibility">
{{ showSK ? '🙈' : '👁' }}
</button>
</div>
</div>
</div>
<div class="usage-stats">
<div class="stat-item">
<span class="stat-value">{{ apiCalls }}</span>
<span class="stat-label">API 调用</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ lastUsed }}</span>
<span class="stat-label">最后使用</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-panel">
<button
class="action-btn primary"
@click="rotateKey"
:disabled="isRotating"
>
<span class="btn-icon">🔄</span>
<span class="btn-text">轮换密钥</span>
</button>
<button
class="action-btn warning"
@click="deactivateKey"
:disabled="akStatus === 'inactive'"
>
<span class="btn-icon"></span>
<span class="btn-text">{{ akStatus === 'inactive' ? '已禁用' : '禁用密钥' }}</span>
</button>
<button
class="action-btn danger"
@click="deleteKey"
>
<span class="btn-icon">🗑</span>
<span class="btn-text">删除密钥</span>
</button>
</div>
</div>
<!-- Rotation Progress -->
<div class="rotation-progress" v-if="isRotating">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: rotationProgress + '%' }"></div>
</div>
<span class="progress-text">{{ rotationStatus }}</span>
</div>
<!-- Best Practices -->
<div class="best-practices">
<h5>🔒 AK/SK 安全管理最佳实践</h5>
<ul>
<li v-for="(tip, i) in securityTips" :key="i">
<span class="tip-icon">{{ tip.icon }}</span>
<span class="tip-text">{{ tip.text }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// AK/SK Data
const akId = ref('AKIAIOSFODNN7EXAMPLE')
const skId = ref('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY')
const akStatus = ref('active')
const akAge = ref(45)
const apiCalls = ref(123456)
const lastUsed = ref('2 小时前')
// Visibility
const showAK = ref(false)
const showSK = ref(false)
// Rotation
const isRotating = ref(false)
const rotationProgress = ref(0)
const rotationStatus = ref('')
// Computed
const maskedAK = computed(() => {
if (showAK.value) return akId.value
return akId.value.substring(0, 8) + '...'
})
const maskedSK = computed(() => {
if (showSK.value) return skId.value
return '************************************'
})
const statusText = computed(() => {
const map = {
active: '活跃',
inactive: '已禁用',
rotating: '轮换中'
}
return map[akStatus.value] || akStatus.value
})
// Methods
function toggleAKVisibility() {
showAK.value = !showAK.value
}
function toggleSKVisibility() {
showSK.value = !showSK.value
}
async function rotateKey() {
isRotating.value = true
rotationProgress.value = 0
rotationStatus.value = '生成新密钥对...'
// Step 1: Generate new key
await simulateProgress(30, '创建新 Access Key...')
const newAK = 'AKIA' + Math.random().toString(36).substring(2, 14).toUpperCase()
// Step 2: Update applications
await simulateProgress(60, '更新应用配置...')
// Step 3: Test new key
await simulateProgress(85, '验证新密钥...')
// Step 4: Disable old key
await simulateProgress(100, '禁用旧密钥...')
// Update data
akId.value = newAK
akAge.value = 0
apiCalls.value = 0
lastUsed.value = '刚刚'
isRotating.value = false
akStatus.value = 'active'
}
function simulateProgress(target, status) {
return new Promise((resolve) => {
rotationStatus.value = status
const interval = setInterval(() => {
rotationProgress.value += 1
if (rotationProgress.value >= target) {
clearInterval(interval)
resolve()
}
}, 20)
})
}
function deactivateKey() {
if (confirm('确定要禁用这个访问密钥吗?禁用后使用该密钥的应用将无法访问云服务。')) {
akStatus.value = 'inactive'
}
}
function deleteKey() {
if (confirm('警告:删除访问密钥是不可逆的操作!\n\n确定要删除这个密钥吗?')) {
alert('密钥已删除(演示模式)')
}
}
// Security Tips
const securityTips = [
{ icon: '🔄', text: '每 90 天轮换一次访问密钥' },
{ icon: '🔒', text: '绝不将 AK/SK 硬编码在代码中' },
{ icon: '👁️', text: '定期审计和监控密钥使用情况' },
{ icon: '🗑️', text: '及时删除不再使用的访问密钥' },
{ icon: '🛡️', text: '优先使用 IAM 角色替代访问密钥' }
]
</script>
<style scoped>
.access-key-management-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.lifecycle-container {
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
margin-bottom: 20px;
}
/* AK/SK Card */
.aksk-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.active {
background: #4caf50;
color: white;
}
.status-badge.inactive {
background: #f44336;
color: white;
}
.age-indicator {
font-size: 0.75rem;
opacity: 0.7;
}
.credentials-display {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.credential-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.credential-row .label {
font-size: 0.7rem;
opacity: 0.7;
text-transform: uppercase;
}
.value-container {
display: flex;
align-items: center;
gap: 8px;
}
.value {
flex: 1;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
font-family: monospace;
font-size: 0.8rem;
word-break: break-all;
}
.icon-btn {
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.icon-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.usage-stats {
display: flex;
gap: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-value {
font-size: 1.2rem;
font-weight: 700;
color: #4caf50;
}
.stat-label {
font-size: 0.7rem;
opacity: 0.7;
}
/* Action Panel */
.action-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.action-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.primary {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
}
.action-btn.primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(76, 175, 80, 0.3);
}
.action-btn.warning {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
}
.action-btn.warning:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 152, 0, 0.3);
}
.action-btn.danger {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
color: white;
}
.action-btn.danger:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(244, 67, 54, 0.3);
}
.btn-icon {
font-size: 1.2rem;
}
/* Rotation Progress */
.rotation-progress {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
margin-top: 20px;
}
.progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 12px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-text {
display: block;
text-align: center;
font-size: 0.9rem;
opacity: 0.9;
}
/* Best Practices */
.best-practices {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
margin-top: 20px;
}
.best-practices h5 {
margin: 0 0 16px 0;
font-size: 1rem;
font-weight: 600;
}
.best-practices ul {
list-style: none;
padding: 0;
margin: 0;
}
.best-practices li {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.best-practices li:last-child {
border-bottom: none;
}
.tip-icon {
font-size: 1.2rem;
}
.tip-text {
font-size: 0.85rem;
opacity: 0.9;
}
@media (max-width: 768px) {
.lifecycle-container {
grid-template-columns: 1fr;
}
.action-panel {
flex-direction: row;
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 140px;
}
}
</style>
@@ -0,0 +1,461 @@
<template>
<div class="best-practices-demo">
<div class="demo-header">
<h4>云账号权限管理最佳实践清单</h4>
<p class="demo-desc">点击查看详细的实施指南和代码示例</p>
</div>
<div class="practices-grid">
<div
v-for="(practice, index) in bestPractices"
:key="index"
class="practice-card"
:class="{ expanded: expandedCard === index }"
@click="toggleCard(index)"
>
<div class="card-header">
<div class="icon-wrapper" :style="{ background: practice.color }">
<span class="icon">{{ practice.icon }}</span>
</div>
<div class="title-wrapper">
<h5>{{ practice.title }}</h5>
<span class="priority" :class="practice.priority">{{ practice.priorityText }}</span>
</div>
<div class="expand-icon">{{ expandedCard === index ? '' : '+' }}</div>
</div>
<div class="card-body" v-if="expandedCard === index">
<p class="description">{{ practice.description }}</p>
<div class="checklist">
<h6> 检查清单</h6>
<ul>
<li v-for="(item, i) in practice.checklist" :key="i">{{ item }}</li>
</ul>
</div>
<div class="code-example" v-if="practice.code">
<h6>代码示例</h6>
<pre><code>{{ practice.code }}</code></pre>
</div>
<div class="tools" v-if="practice.tools">
<h6>推荐工具</h6>
<div class="tool-tags">
<span v-for="(tool, i) in practice.tools" :key="i" class="tool-tag">{{ tool }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const expandedCard = ref(0)
const bestPractices = [
{
icon: '👑',
title: '根账号保护',
priority: 'p0',
priorityText: 'P0 - 最高优先级',
color: '#f44336',
description: '根账号是云服务的所有者,拥有所有权限。必须实施最高级别的保护措施。',
checklist: [
'启用 MFA(推荐硬件 MFA 设备)',
'创建 IAM 管理员用户用于日常操作',
'删除或锁定根账号的访问密钥',
'配置根账号使用告警',
'设置账号恢复联系信息'
],
code: `# AWS CLI - 创建管理员用户并禁用根账号 AK
aws iam create-user --user-name AdminUser
aws iam attach-user-policy --user-name AdminUser \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# 删除根账号访问密钥(必须使用根账号登录控制台操作)`,
tools: ['硬件 MFA (YubiKey)', '虚拟 MFA (Google Authenticator)', 'AWS IAM', '阿里云 RAM']
},
{
icon: '👤',
title: '用户权限最小化',
priority: 'p0',
priorityText: 'P0 - 最高优先级',
color: '#ff9800',
description: '遵循最小权限原则,只授予用户完成工作所需的最低权限。',
checklist: [
'避免使用 AdministratorAccess 等全权限策略',
'使用 IAM 用户组批量管理权限',
'定期审查和删除未使用的 IAM 用户',
'为不同角色创建细粒度的自定义策略',
'使用 IAM Access Analyzer 识别过度宽松的权限'
],
code: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "ap-northeast-1"
}
}
}
]
}`,
tools: ['IAM Policy Simulator', 'IAM Access Analyzer', 'AWS CloudTrail', 'AWS Config']
},
{
icon: '🎭',
title: '优先使用 IAM 角色',
priority: 'p1',
priorityText: 'P1 - 高优先级',
color: '#4caf50',
description: 'IAM 角色没有长期凭证,通过临时凭证访问,大大降低凭证泄露风险。',
checklist: [
'EC2 实例使用实例角色(Instance Profile)',
'Lambda 函数使用执行角色',
'ECS 任务使用任务角色',
'跨账号访问使用角色扮演(AssumeRole)',
'CI/CD 流水线使用 OIDC 联邦身份'
],
code: `import boto3
# EC2 实例自动使用附加的实例角色
# 无需提供任何凭证
s3 = boto3.client('s3')
# 跨账号角色扮演
sts = boto3.client('sts')
assumed_role = sts.assume_role(
RoleArn='arn:aws:iam::123456789012:role/CrossAccountRole',
RoleSessionName='MyApplication',
DurationSeconds=3600
)
# 使用临时凭证
temp_creds = assumed_role['Credentials']
s3_cross = boto3.client(
's3',
aws_access_key_id=temp_creds['AccessKeyId'],
aws_secret_access_key=temp_creds['SecretAccessKey'],
aws_session_token=temp_creds['SessionToken']
)`,
tools: ['IAM Roles', 'AWS STS', 'EC2 Instance Profiles', 'Lambda Execution Roles']
},
{
icon: '🔑',
title: '访问密钥安全管理',
priority: 'p1',
priorityText: 'P1 - 高优先级',
color: '#2196f3',
description: '如果必须使用访问密钥(AK/SK),需要实施严格的安全管理措施。',
checklist: [
'绝不将 AK/SK 硬编码在代码或配置文件中',
'使用环境变量或密钥管理服务(如 AWS Secrets Manager)',
'每 90 天轮换一次访问密钥',
'定期审查和删除未使用的访问密钥',
'启用 CloudTrail 记录所有 AK/SK 的使用情况'
],
code: `# ❌ 错误做法 - 硬编码凭证
import boto3
s3 = boto3.client(
's3',
aws_access_key_id='AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
)
# ✅ 正确做法 - 使用环境变量
import boto3
import os
s3 = boto3.client(
's3',
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY')
)
# ✅ 正确做法 - 使用 AWS Secrets Manager
import boto3
import json
secrets_client = boto3.client('secretsmanager')
secret_value = secrets_client.get_secret_value(SecretId='my-app/credentials')
credentials = json.loads(secret_value['SecretString'])
s3 = boto3.client(
's3',
aws_access_key_id=credentials['access_key_id'],
aws_secret_access_key=credentials['secret_access_key']
)`,
tools: ['AWS Secrets Manager', 'HashiCorp Vault', 'Azure Key Vault', 'GCP Secret Manager']
},
{
icon: '📊',
title: '监控与审计',
priority: 'p2',
priorityText: 'P2 - 中优先级',
color: '#9c27b0',
description: '建立全面的监控和审计机制,及时发现和响应安全事件。',
checklist: [
'启用 CloudTrail 记录所有 API 调用',
'配置关键操作的实时告警(根账号使用、策略变更等)',
'使用 IAM Access Analyzer 持续分析权限',
'定期审查 IAM 用户和权限配置',
'将日志存储到独立的审计账号,防止篡改'
],
code: `# AWS CloudTrail 配置示例
aws cloudtrail create-trail \
--name OrganizationTrail \
--s3-bucket-name my-cloudtrail-bucket \
--is-organization-trail \
--enable-log-file-validation \
--is-multi-region-trail
# CloudWatch 告警配置 - 根账号使用
aws cloudwatch put-metric-alarm \
--alarm-name RootAccountUsageAlarm \
--alarm-description "Alert when root account is used" \
--metric-name RootAccountUsage \
--namespace CloudTrailMetrics \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold
# IAM Access Analyzer 创建分析器
aws accessanalyzer create-analyzer \
--analyzer-name MyOrgAnalyzer \
--type ORGANIZATION`,
tools: ['AWS CloudTrail', 'AWS CloudWatch', 'IAM Access Analyzer', 'AWS Config', 'AWS Security Hub']
}
]
function toggleCard(index) {
expandedCard.value = expandedCard.value === index ? null : index
}
</script>
<style scoped>
.best-practices-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.practices-grid {
display: grid;
gap: 16px;
}
.practice-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.practice-card:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.practice-card.expanded {
background: rgba(255, 255, 255, 0.95);
color: #333;
}
.card-header {
display: flex;
align-items: center;
gap: 16px;
}
.icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.title-wrapper {
flex: 1;
}
.title-wrapper h5 {
margin: 0 0 4px 0;
font-size: 1.1rem;
}
.priority {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.priority.p0 {
background: #f44336;
color: white;
}
.priority.p1 {
background: #ff9800;
color: white;
}
.priority.p2 {
background: #2196f3;
color: white;
}
.expand-icon {
font-size: 1.5rem;
font-weight: 300;
opacity: 0.7;
}
.card-body {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.description {
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 16px;
}
.checklist {
margin-bottom: 20px;
}
.checklist h6 {
margin: 0 0 12px 0;
font-size: 0.9rem;
color: #667eea;
}
.checklist ul {
list-style: none;
padding: 0;
margin: 0;
}
.checklist li {
padding: 6px 0;
padding-left: 24px;
position: relative;
font-size: 0.9rem;
}
.checklist li:before {
content: '☐';
position: absolute;
left: 0;
color: #667eea;
}
.code-example {
margin-bottom: 20px;
}
.code-example h6 {
margin: 0 0 12px 0;
font-size: 0.9rem;
color: #667eea;
}
.code-example pre {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 0;
}
.code-example code {
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.8rem;
line-height: 1.5;
}
.tools h6 {
margin: 0 0 12px 0;
font-size: 0.9rem;
color: #667eea;
}
.tool-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tool-tag {
padding: 4px 12px;
background: #e3f2fd;
color: #1565c0;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
@media (max-width: 768px) {
.card-header {
flex-wrap: wrap;
}
.icon-wrapper {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
.title-wrapper h5 {
font-size: 1rem;
}
}
</style>
@@ -0,0 +1,196 @@
<template>
<div class="cross-account-access-demo">
<div class="demo-header">
<h4>跨账号访问流程演示</h4>
<p class="demo-desc">角色扮演AssumeRole获取临时凭证</p>
</div>
<div class="flow-diagram">
<div class="account-box source">
<div class="account-header">账号 A源账号</div>
<div class="account-content">
<div class="entity">IAM User / Application</div>
<div class="action">调用 sts:AssumeRole</div>
</div>
</div>
<div class="arrow"></div>
<div class="account-box sts">
<div class="account-header">STS 服务</div>
<div class="account-content">
<div class="step">1. 验证源身份</div>
<div class="step">2. 检查信任策略</div>
<div class="step">3. 生成临时凭证</div>
</div>
</div>
<div class="arrow"></div>
<div class="account-box target">
<div class="account-header">账号 B目标账号</div>
<div class="account-content">
<div class="entity">CrossAccountRole</div>
<div class="resource">访问 S3 / EC2 等资源</div>
</div>
</div>
</div>
<div class="code-example">
<h5>Python 代码示例</h5>
<pre><code>import boto3
# 在账号 A 中使用 IAM 用户凭证
sts_client = boto3.client('sts')
# 扮演账号 B 的角色
assumed_role = sts_client.assume_role(
RoleArn='arn:aws:iam::123456789012:role/CrossAccountRole',
RoleSessionName='MySession',
DurationSeconds=3600
)
# 获取临时凭证
credentials = assumed_role['Credentials']
# 使用临时凭证访问账号 B 的资源
s3_client = boto3.client(
's3',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)</code></pre>
</div>
</div>
</template>
<style scoped>
.cross-account-access-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.account-box {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
min-width: 180px;
color: #333;
}
.account-header {
font-weight: 700;
font-size: 0.85rem;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
}
.account-content {
font-size: 0.8rem;
}
.entity {
background: #e3f2fd;
padding: 6px 10px;
border-radius: 4px;
margin-bottom: 8px;
color: #1565c0;
font-weight: 500;
}
.action {
color: #666;
font-style: italic;
}
.step {
padding: 4px 0;
color: #666;
border-bottom: 1px solid #f0f0f0;
}
.step:last-child {
border-bottom: none;
}
.resource {
background: #e8f5e9;
padding: 6px 10px;
border-radius: 4px;
margin-top: 8px;
color: #2e7d32;
}
.arrow {
font-size: 2rem;
color: rgba(255, 255, 255, 0.8);
}
.code-example {
background: #1e1e1e;
border-radius: 12px;
padding: 20px;
}
.code-example h5 {
margin: 0 0 12px 0;
color: #fff;
font-size: 0.9rem;
}
.code-example pre {
margin: 0;
overflow-x: auto;
}
.code-example code {
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.8rem;
line-height: 1.5;
}
@media (max-width: 768px) {
.flow-diagram {
flex-direction: column;
}
.arrow {
transform: rotate(90deg);
}
.account-box {
min-width: auto;
width: 100%;
}
}
</style>
@@ -0,0 +1,368 @@
<template>
<div class="iam-ram-comparison-demo">
<div class="demo-header">
<h4>AWS IAM vs 阿里云 RAM 对比</h4>
<p class="demo-desc">点击各个模块查看详细对比</p>
</div>
<div class="comparison-container">
<!-- AWS IAM Column -->
<div class="platform-column aws-column">
<div class="platform-header aws">
<div class="logo">AWS</div>
<h5>IAM</h5>
<span class="subtitle">Identity and Access Management</span>
</div>
<div class="features-list">
<div
v-for="(feature, index) in awsFeatures"
:key="index"
class="feature-item"
:class="{ active: selectedFeature === `aws-${index}` }"
@click="selectFeature('aws', index)"
>
<div class="feature-icon">{{ feature.icon }}</div>
<div class="feature-content">
<span class="feature-name">{{ feature.name }}</span>
<span class="feature-desc">{{ feature.desc }}</span>
</div>
</div>
</div>
</div>
<!-- Comparison Details -->
<div class="comparison-details" v-if="selectedFeatureData">
<div class="detail-card">
<h6>{{ selectedFeatureData.name }}</h6>
<div class="comparison-row">
<div class="aws-detail">
<span class="label">AWS IAM</span>
<p>{{ selectedFeatureData.awsDetail }}</p>
<code v-if="selectedFeatureData.awsExample">{{ selectedFeatureData.awsExample }}</code>
</div>
<div class="vs-divider">VS</div>
<div class="ram-detail">
<span class="label">阿里云 RAM</span>
<p>{{ selectedFeatureData.ramDetail }}</p>
<code v-if="selectedFeatureData.ramExample">{{ selectedFeatureData.ramExample }}</code>
</div>
</div>
</div>
</div>
<!-- Alibaba Cloud RAM Column -->
<div class="platform-column ram-column">
<div class="platform-header ram">
<div class="logo">阿里云</div>
<h5>RAM</h5>
<span class="subtitle">Resource Access Management</span>
</div>
<div class="features-list">
<div
v-for="(feature, index) in ramFeatures"
:key="index"
class="feature-item"
:class="{ active: selectedFeature === `ram-${index}` }"
@click="selectFeature('ram', index)"
>
<div class="feature-icon">{{ feature.icon }}</div>
<div class="feature-content">
<span class="feature-name">{{ feature.name }}</span>
<span class="feature-desc">{{ feature.desc }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedFeature = ref(null)
const featureDetails = [
{
name: '用户管理',
awsDetail: '使用 IAM User,支持编程访问和控制台访问,可分配独立 AK/SK',
ramDetail: '使用 RAM 用户,功能与 IAM User 类似,支持子账号登录控制台',
awsExample: 'arn:aws:iam::123456789012:user/alice',
ramExample: 'acs:ram::123456789012:user/alice'
},
{
name: '用户组管理',
awsDetail: 'IAM Group 用于批量管理用户权限,一个用户可属于多个组',
ramDetail: 'RAM 用户组功能类似,支持按部门或项目分组管理',
awsExample: 'arn:aws:iam::123456789012:group/Developers',
ramExample: 'acs:ram::123456789012:group/Developers'
},
{
name: '角色与扮演',
awsDetail: 'IAM Role 支持跨账号访问和服务角色,使用 STS AssumeRole',
ramDetail: 'RAM 角色支持跨云账号访问和临时授权,使用 STS AssumeRole',
awsExample: 'arn:aws:iam::123456789012:role/CrossAccountRole',
ramExample: 'acs:ram::123456789012:role/CrossAccountRole'
},
{
name: '权限策略',
awsDetail: 'IAM Policy 使用 JSON 格式,支持 Action/Resource/Condition',
ramDetail: 'RAM Policy 语法类似,支持阿里云服务特定的 Action',
awsExample: '"Action": "s3:GetObject"',
ramExample: '"Action": "oss:GetObject"'
},
{
name: '身份联合',
awsDetail: '支持 SAML 2.0 和 OIDC,可与 AD、Okta 等 IdP 集成',
ramDetail: '支持 SAML 2.0 和企业 AD/LDAP,支持钉钉等国内 IdP',
awsExample: 'SAML Provider: arn:aws:iam::123:saml-provider/Okta',
ramExample: 'SAML Provider: acs:ram::123:saml-provider/DingTalk'
},
{
name: '访问密钥',
awsDetail: 'IAM User 可创建 AK/SK,支持定期轮换和访问分析',
ramDetail: 'RAM 用户支持 AccessKey,提供密钥使用分析和安全建议',
awsExample: 'AKIAIOSFODNN7EXAMPLE',
ramExample: 'LTAI...'
}
]
const awsFeatures = featureDetails.map((f, i) => ({
icon: ['👤', '👥', '🎭', '📋', '🔗', '🔑'][i],
name: f.name,
desc: f.awsDetail.slice(0, 30) + '...'
}))
const ramFeatures = featureDetails.map((f, i) => ({
icon: ['👤', '👥', '🎭', '📋', '🔗', '🔑'][i],
name: f.name,
desc: f.ramDetail.slice(0, 30) + '...'
}))
const selectedFeatureData = computed(() => {
if (!selectedFeature.value) return null
const [platform, index] = selectedFeature.value.split('-')
return featureDetails[parseInt(index)]
})
function selectFeature(platform, index) {
selectedFeature.value = `${platform}-${index}`
}
</script>
<style scoped>
.iam-ram-comparison-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.5rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 16px;
align-items: start;
}
.platform-column {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
overflow: hidden;
}
.platform-header {
padding: 16px;
text-align: center;
}
.platform-header.aws {
background: linear-gradient(135deg, #ff9900 0%, #ff6600 100%);
}
.platform-header.ram {
background: linear-gradient(135deg, #ff6a00 0%, #ee0979 100%);
}
.platform-header .logo {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 4px;
}
.platform-header h5 {
margin: 0;
font-size: 1.1rem;
}
.platform-header .subtitle {
font-size: 0.7rem;
opacity: 0.9;
}
.features-list {
padding: 12px;
}
.feature-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.feature-item:hover,
.feature-item.active {
background: rgba(255, 255, 255, 0.2);
transform: translateX(4px);
}
.feature-icon {
font-size: 1.2rem;
}
.feature-content {
display: flex;
flex-direction: column;
flex: 1;
}
.feature-name {
font-weight: 600;
font-size: 0.85rem;
}
.feature-desc {
font-size: 0.7rem;
opacity: 0.8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comparison-details {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
color: #333;
}
.detail-card {
text-align: center;
}
.detail-card h6 {
margin: 0 0 16px 0;
font-size: 1.1rem;
color: #667eea;
}
.comparison-row {
display: flex;
align-items: stretch;
gap: 16px;
}
.aws-detail,
.ram-detail {
flex: 1;
padding: 12px;
border-radius: 8px;
text-align: left;
}
.aws-detail {
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
}
.ram-detail {
background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%);
}
.aws-detail .label,
.ram-detail .label {
display: block;
font-weight: 700;
font-size: 0.8rem;
margin-bottom: 6px;
}
.aws-detail .label {
color: #ff6f00;
}
.ram-detail .label {
color: #c2185b;
}
.aws-detail p,
.ram-detail p {
margin: 0 0 8px 0;
font-size: 0.8rem;
line-height: 1.4;
}
.aws-detail code,
.ram-detail code {
display: block;
padding: 6px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
font-size: 0.65rem;
word-break: break-all;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9rem;
color: #999;
padding: 0 8px;
}
@media (max-width: 1024px) {
.comparison-container {
grid-template-columns: 1fr;
gap: 20px;
}
.comparison-details {
order: -1;
}
.comparison-row {
flex-direction: column;
}
.vs-divider {
padding: 8px 0;
}
}
</style>
@@ -0,0 +1,362 @@
<template>
<div class="identity-provider-demo">
<div class="demo-header">
<h4>身份提供商(IdP)集成流程</h4>
<p class="demo-desc">点击步骤查看 SSO 单点登录流程</p>
</div>
<div class="sso-flow">
<div class="flow-step" v-for="(step, index) in steps" :key="index"
:class="{ active: currentStep === index, completed: currentStep > index }"
@click="goToStep(index)">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<span class="step-title">{{ step.title }}</span>
<span class="step-desc">{{ step.desc }}</span>
</div>
<div class="step-arrow" v-if="index < steps.length - 1"></div>
</div>
</div>
<div class="detail-panel" v-if="currentStepData">
<h5>{{ currentStepData.title }}</h5>
<p>{{ currentStepData.detail }}</p>
<div class="code-block" v-if="currentStepData.code">
<pre><code>{{ currentStepData.code }}</code></pre>
</div>
<div class="entity-flow" v-if="currentStepData.flow">
<div class="flow-row" v-for="(row, i) in currentStepData.flow" :key="i">
<span class="entity" :class="row.from.type">{{ row.from.name }}</span>
<span class="action">{{ row.action }}</span>
<span class="entity" :class="row.to.type">{{ row.to.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const steps = [
{ title: '用户访问应用', desc: '用户尝试访问企业应用' },
{ title: '重定向到 IdP', desc: '应用将用户重定向到身份提供商' },
{ title: '用户登录认证', desc: '用户在 IdP 输入企业账号密码' },
{ title: '颁发 SAML 令牌', desc: 'IdP 验证成功后颁发 SAML Assertion' },
{ title: '返回应用', desc: '携带令牌返回企业应用' },
{ title: '换取云凭证', desc: '应用使用令牌换取云临时凭证' },
{ title: '访问云资源', desc: '使用临时凭证访问云资源' }
]
const stepDetails = [
{
title: '用户访问企业应用',
detail: '用户打开浏览器,访问企业内部的业务系统(如 CRM、ERP 等)。此时用户尚未登录,应用检测到用户没有有效的会话。',
flow: [
{ from: { name: '用户', type: 'user' }, action: '访问 →', to: { name: '企业应用', type: 'app' } }
]
},
{
title: '应用重定向到 IdP',
detail: '应用发现用户未登录,生成 SAML Request,将用户浏览器重定向到企业的身份提供商(IdP,如 Azure AD、Okta 等)。',
code: `// SAML Request 示例
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
Destination="https://idp.example.com/saml/sso"
ID="_1234567890"
IssueInstant="2024-01-15T10:00:00Z">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
https://app.example.com
</saml:Issuer>
</samlp:AuthnRequest>`,
flow: [
{ from: { name: '企业应用', type: 'app' }, action: '重定向 →', to: { name: 'IdP', type: 'idp' } }
]
},
{
title: '用户在 IdP 登录',
detail: '用户在 IdP 的登录页面输入企业账号和密码。IdP 验证用户身份,可能还需要进行 MFA 多因素认证。',
flow: [
{ from: { name: '用户', type: 'user' }, action: '登录 →', to: { name: 'IdP', type: 'idp' } }
]
},
{
title: 'IdP 颁发 SAML Assertion',
detail: '用户认证成功后,IdP 生成包含用户身份和属性的 SAML Assertion(断言),并使用 IdP 的私钥签名。',
code: `<!-- SAML Response 示例 -->
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="_response123"
InResponseTo="_1234567890"
IssueInstant="2024-01-15T10:01:00Z">
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_assertion456"
IssueInstant="2024-01-15T10:01:00Z">
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
user@example.com
</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="Role">
<saml:AttributeValue>Admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>`,
flow: [
{ from: { name: 'IdP', type: 'idp' }, action: '颁发令牌 →', to: { name: '用户浏览器', type: 'user' } }
]
},
{
title: '返回企业应用',
detail: 'IdP 通过浏览器将 SAML Response POST 到企业应用。应用验证 SAML 断言的签名,确认用户身份,建立用户会话。',
flow: [
{ from: { name: '用户浏览器', type: 'user' }, action: 'POST 令牌 →', to: { name: '企业应用', type: 'app' } }
]
},
{
title: '换取云临时凭证',
detail: '企业应用使用 SAML Assertion 向云厂商的 STS 服务请求临时安全凭证。云服务验证 SAML 断言后,颁发临时 AK/SK/Token。',
code: `# Python 示例:使用 SAML 换取 AWS 临时凭证
import boto3
# 创建 STS 客户端
sts = boto3.client('sts')
# 使用 SAML 断言请求临时凭证
response = sts.assume_role_with_saml(
RoleArn='arn:aws:iam::123456789012:role/SAML-Role',
PrincipalArn='arn:aws:iam::123456789012:saml-provider/Okta',
SAMLAssertion='VGhpcyBpcyBhIHRlc3QgU0FNTCBhc3NlcnRpb24=',
DurationSeconds=3600
)
# 获取临时凭证
credentials = response['Credentials']
print(f"Access Key: {credentials['AccessKeyId']}")
print(f"Secret Key: {credentials['SecretAccessKey']}")
print(f"Session Token: {credentials['SessionToken']}")`,
flow: [
{ from: { name: '企业应用', type: 'app' }, action: 'AssumeRole →', to: { name: '云 STS', type: 'cloud' } }
]
},
{
title: '访问云资源',
detail: '企业应用使用获取到的临时凭证,调用云服务的 API 访问资源(如 S3、EC2、数据库等)。临时凭证有过期时间,到期后需要重新获取。',
flow: [
{ from: { name: '企业应用', type: 'app' }, action: '访问资源 →', to: { name: '云服务', type: 'cloud' } }
]
}
]
const currentStepData = computed(() => {
if (currentStep.value === null) return null
return stepDetails[currentStep.value]
})
function goToStep(index) {
currentStep.value = index
}
</script>
<style scoped>
.identity-provider-demo {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.sso-flow {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-bottom: 24px;
}
.flow-step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 120px;
}
.flow-step:hover,
.flow-step.active {
background: rgba(255, 255, 255, 0.25);
transform: scale(1.02);
}
.flow-step.completed {
background: rgba(76, 175, 80, 0.3);
}
.step-number {
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
}
.step-content {
display: flex;
flex-direction: column;
flex: 1;
}
.step-title {
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.step-desc {
font-size: 0.6rem;
opacity: 0.8;
white-space: nowrap;
}
.step-arrow {
font-size: 1.2rem;
opacity: 0.6;
}
.detail-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
color: #333;
}
.detail-panel h5 {
margin: 0 0 12px 0;
font-size: 1.1rem;
color: #1e3c72;
}
.detail-panel p {
margin: 0 0 16px 0;
font-size: 0.9rem;
line-height: 1.5;
}
.code-block {
background: #1e1e1e;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
overflow-x: auto;
}
.code-block pre {
margin: 0;
}
.code-block code {
color: #d4d4d4;
font-size: 0.75rem;
line-height: 1.4;
font-family: 'Consolas', 'Monaco', monospace;
}
.entity-flow {
display: flex;
flex-direction: column;
gap: 8px;
}
.flow-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 8px;
background: #f5f5f5;
border-radius: 6px;
}
.entity {
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.entity.user {
background: #e3f2fd;
color: #1565c0;
}
.entity.app {
background: #f3e5f5;
color: #7b1fa2;
}
.entity.idp {
background: #e8f5e9;
color: #2e7d32;
}
.entity.cloud {
background: #fff3e0;
color: #ef6c00;
}
.action {
font-size: 0.75rem;
color: #666;
font-weight: 500;
}
@media (max-width: 768px) {
.sso-flow {
flex-direction: column;
align-items: stretch;
}
.flow-step {
min-width: auto;
}
.step-arrow {
display: none;
}
.flow-row {
flex-direction: column;
gap: 4px;
}
}
</style>
@@ -0,0 +1,350 @@
<template>
<div class="mfa-security-demo">
<div class="demo-header">
<h4>MFA 多因素认证模拟</h4>
<p class="demo-desc">体验 MFA 双因素认证流程</p>
</div>
<div class="mfa-flow">
<div class="auth-step" :class="{ active: step >= 1, completed: step > 1 }">
<div class="step-icon">🔐</div>
<div class="step-label">密码验证</div>
</div>
<div class="step-arrow"></div>
<div class="auth-step" :class="{ active: step >= 2, completed: step > 2 }">
<div class="step-icon">📱</div>
<div class="step-label">MFA 验证</div>
</div>
<div class="step-arrow"></div>
<div class="auth-step" :class="{ active: step >= 3 }">
<div class="step-icon"></div>
<div class="step-label">登录成功</div>
</div>
</div>
<div class="auth-panel" v-if="step === 1">
<h5>请输入密码</h5>
<input type="password" v-model="password" placeholder="输入密码" @keyup.enter="verifyPassword" />
<button @click="verifyPassword" :disabled="!password">验证密码</button>
</div>
<div class="auth-panel" v-if="step === 2">
<h5>MFA 验证</h5>
<div class="totp-display">
<span class="totp-code">{{ totpCode }}</span>
<div class="totp-timer">
<div class="timer-bar" :style="{ width: timerWidth + '%' }"></div>
</div>
</div>
<input type="text" v-model="userCode" placeholder="输入6位验证码" maxlength="6" @keyup.enter="verifyMFA" />
<button @click="verifyMFA" :disabled="userCode.length !== 6">验证</button>
</div>
<div class="success-message" v-if="step === 3">
<div class="success-icon">🎉</div>
<h5>登录成功</h5>
<p>已通过 MFA 双因素认证</p>
<button @click="reset">重新演示</button>
</div>
<div class="security-tips">
<h5>💡 MFA 安全提示</h5>
<ul>
<li>启用 MFA 可降低 99.9% 的账号被盗风险</li>
<li>推荐使用 TOTP 应用Google AuthenticatorMicrosoft Authenticator</li>
<li>硬件安全密钥 YubiKey提供最高级别的安全性</li>
<li>务必备份 MFA 恢复码防止设备丢失无法登录</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const step = ref(1)
const password = ref('')
const userCode = ref('')
const totpCode = ref('123456')
const timerWidth = ref(100)
let timerInterval = null
function generateTOTP() {
return Math.floor(100000 + Math.random() * 900000).toString()
}
function startTimer() {
timerWidth.value = 100
if (timerInterval) clearInterval(timerInterval)
timerInterval = setInterval(() => {
timerWidth.value -= 1.67
if (timerWidth.value <= 0) {
totpCode.value = generateTOTP()
timerWidth.value = 100
}
}, 100)
}
function verifyPassword() {
if (password.value) {
step.value = 2
totpCode.value = generateTOTP()
startTimer()
}
}
function verifyMFA() {
if (userCode.value.length === 6) {
step.value = 3
if (timerInterval) clearInterval(timerInterval)
}
}
function reset() {
step.value = 1
password.value = ''
userCode.value = ''
if (timerInterval) clearInterval(timerInterval)
}
onMounted(() => {
if (step.value === 2) startTimer()
})
onUnmounted(() => {
if (timerInterval) clearInterval(timerInterval)
})
</script>
<style scoped>
.mfa-security-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.mfa-flow {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.auth-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
opacity: 0.5;
transition: all 0.3s ease;
}
.auth-step.active {
opacity: 1;
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.5);
}
.auth-step.completed {
opacity: 1;
background: rgba(76, 175, 80, 0.3);
}
.step-icon {
font-size: 2rem;
}
.step-label {
font-size: 0.8rem;
font-weight: 500;
}
.step-arrow {
font-size: 1.5rem;
opacity: 0.6;
}
.auth-panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
}
.auth-panel h5 {
margin: 0 0 16px 0;
font-size: 1.1rem;
}
.auth-panel input {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(0, 0, 0, 0.2);
color: white;
font-size: 1rem;
margin-bottom: 12px;
box-sizing: border-box;
}
.auth-panel input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.auth-panel button {
width: 100%;
padding: 12px 24px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.auth-panel button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.auth-panel button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(76, 175, 80, 0.3);
}
.totp-display {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 16px;
text-align: center;
margin-bottom: 16px;
}
.totp-code {
display: block;
font-size: 2.5rem;
font-weight: 700;
font-family: monospace;
letter-spacing: 0.2em;
margin-bottom: 8px;
}
.totp-timer {
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
overflow: hidden;
}
.timer-bar {
height: 100%;
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
transition: width 0.1s linear;
}
.success-message {
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.5);
border-radius: 12px;
padding: 32px;
text-align: center;
margin-bottom: 20px;
}
.success-icon {
font-size: 4rem;
margin-bottom: 16px;
}
.success-message h5 {
margin: 0 0 8px 0;
font-size: 1.5rem;
}
.success-message p {
margin: 0 0 20px 0;
opacity: 0.8;
}
.success-message button {
padding: 12px 32px;
border: none;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.success-message button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.security-tips {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
}
.security-tips h5 {
margin: 0 0 12px 0;
font-size: 1rem;
}
.security-tips ul {
list-style: none;
padding: 0;
margin: 0;
}
.security-tips li {
padding: 6px 0;
font-size: 0.85rem;
opacity: 0.9;
}
@media (max-width: 768px) {
.mfa-flow {
flex-direction: column;
}
.step-arrow {
transform: rotate(90deg);
}
.lifecycle-container {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,359 @@
<template>
<div class="permission-hierarchy-demo">
<div class="demo-header">
<h4>权限层级结构</h4>
<p class="demo-desc">点击层级查看详细权限范围</p>
</div>
<div class="hierarchy-container">
<div
v-for="(level, index) in hierarchyLevels"
:key="index"
class="level-row"
:class="{ active: selectedLevel === index }"
@click="selectLevel(index)"
>
<div class="level-icon">{{ level.icon }}</div>
<div class="level-content">
<span class="level-name">{{ level.name }}</span>
<span class="level-scope">{{ level.scope }}</span>
</div>
<div class="permission-badges">
<span
v-for="(perm, i) in level.permissions.slice(0, 3)"
:key="i"
class="badge"
>
{{ perm }}
</span>
<span v-if="level.permissions.length > 3" class="badge more">
+{{ level.permissions.length - 3 }}
</span>
</div>
</div>
</div>
<div class="detail-panel" v-if="selectedLevelData">
<h5>{{ selectedLevelData.name }} 详情</h5>
<div class="detail-section">
<span class="label">权限范围:</span>
<span class="value">{{ selectedLevelData.scope }}</span>
</div>
<div class="detail-section">
<span class="label">典型场景:</span>
<span class="value">{{ selectedLevelData.scenario }}</span>
</div>
<div class="detail-section permissions-list">
<span class="label">拥有权限:</span>
<div class="permissions-grid">
<span
v-for="(perm, i) in selectedLevelData.permissions"
:key="i"
class="perm-tag"
:class="perm.type"
>
{{ perm.name }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedLevel = ref(0)
const hierarchyLevels = [
{
icon: '👑',
name: '根账号 (Root)',
scope: '全账号最高权限',
scenario: '账号所有者,拥有云服务的所有权限',
permissions: [
{ name: '完全管理权限', type: 'full' },
{ name: '账单管理', type: 'billing' },
{ name: '组织架构管理', type: 'org' },
{ name: '关闭账号', type: 'critical' },
{ name: '恢复已删除资源', type: 'admin' }
]
},
{
icon: '👤',
name: 'IAM 管理员',
scope: 'IAM 全权限',
scenario: '管理所有 IAM 用户、角色、策略',
permissions: [
{ name: '创建/删除用户', type: 'user' },
{ name: '创建/删除角色', type: 'role' },
{ name: '管理策略', type: 'policy' },
{ name: '查看凭证报告', type: 'audit' }
]
},
{
icon: '👥',
name: '普通 IAM 用户',
scope: '受限权限',
scenario: '日常开发人员,只能访问特定资源',
permissions: [
{ name: '只读访问 EC2', type: 'read' },
{ name: '读写指定 S3 桶', type: 'limited' },
{ name: '查看 CloudWatch 日志', type: 'read' },
{ name: '无法创建 IAM 资源', type: 'deny' }
]
},
{
icon: '🎭',
name: '临时角色 (Role)',
scope: '按策略定义',
scenario: '跨账号访问、服务角色、临时授权',
permissions: [
{ name: '临时凭证 (1-12小时)', type: 'temp' },
{ name: '按信任策略授权', type: 'conditional' },
{ name: '可跨账号使用', type: 'cross' },
{ name: '无长期凭证', type: 'secure' }
]
},
{
icon: '🔑',
name: '服务账号 / 应用',
scope: 'API 访问权限',
scenario: '应用程序、CI/CD 流水线、自动化脚本',
permissions: [
{ name: 'AK/SK 或临时凭证', type: 'api' },
{ name: '特定服务 API 权限', type: 'service' },
{ name: '无控制台访问', type: 'programmatic' },
{ name: '建议定期轮换密钥', type: 'security' }
]
}
]
const selectedLevelData = computed(() => {
if (selectedLevel.value === null) return null
return hierarchyLevels[selectedLevel.value]
})
function selectLevel(index) {
selectedLevel.value = index
}
</script>
<style scoped>
.permission-hierarchy-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.hierarchy-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
}
.level-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.level-row:hover,
.level-row.active {
background: rgba(255, 255, 255, 0.2);
transform: translateX(8px);
}
.level-icon {
font-size: 1.6rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
}
.level-content {
display: flex;
flex-direction: column;
flex: 1;
}
.level-name {
font-weight: 600;
font-size: 0.95rem;
}
.level-scope {
font-size: 0.75rem;
opacity: 0.8;
}
.permission-badges {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
max-width: 150px;
}
.badge {
padding: 2px 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
font-size: 0.65rem;
white-space: nowrap;
}
.badge.more {
background: rgba(255, 255, 255, 0.4);
}
.detail-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
color: #333;
}
.detail-panel h5 {
margin: 0 0 16px 0;
font-size: 1.1rem;
color: #667eea;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
}
.detail-section {
margin-bottom: 12px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.detail-section .label {
font-weight: 600;
color: #666;
min-width: 80px;
font-size: 0.85rem;
}
.detail-section .value {
color: #333;
font-size: 0.9rem;
flex: 1;
}
.permissions-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
}
.perm-tag {
padding: 4px 10px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 500;
}
.perm-tag.full {
background: #f44336;
color: white;
}
.perm-tag.read,
.perm-tag.user,
.perm-tag.readonly {
background: #4caf50;
color: white;
}
.perm-tag.limited,
.perm-tag.role,
.perm-tag.limited {
background: #ff9800;
color: white;
}
.perm-tag.deny,
.perm-tag.critical {
background: #9c27b0;
color: white;
}
.perm-tag.temp,
.perm-tag.conditional,
.perm-tag.service {
background: #2196f3;
color: white;
}
.perm-tag.admin,
.perm-tag.org,
.perm-tag.billing {
background: #673ab7;
color: white;
}
.perm-tag.api,
.perm-tag.programmatic,
.perm-tag.security {
background: #607d8b;
color: white;
}
.perm-tag.cross,
.perm-tag.secure,
.perm-tag.audit,
.perm-tag.policy {
background: #795548;
color: white;
}
@media (max-width: 768px) {
.level-row {
flex-wrap: wrap;
}
.permission-badges {
width: 100%;
justify-content: flex-start;
max-width: none;
margin-top: 8px;
}
.detail-section {
flex-direction: column;
gap: 4px;
}
}
</style>
@@ -0,0 +1,470 @@
<template>
<div class="role-policy-demo">
<div class="demo-header">
<h4>角色与策略关系可视化</h4>
<p class="demo-desc">拖动查看角色如何关联多个策略</p>
</div>
<div class="visualization-container">
<!-- Central Role -->
<div class="central-role">
<div class="role-core" @click="toggleRoleDetails"
:class="{ expanded: showRoleDetails }">
<div class="role-icon">🎭</div>
<div class="role-info">
<span class="role-name">{{ roleName }}</span>
<span class="role-type">{{ roleType }}</span>
</div>
<div class="expand-icon">{{ showRoleDetails ? '▼' : '▶' }}</div>
</div>
<!-- Trust Policy -->
<div class="trust-policy" v-if="showRoleDetails">
<div class="policy-header">
<span class="policy-icon">🔐</span>
<span class="policy-title">信任策略 (Trust Policy)</span>
</div>
<div class="policy-content">
<div class="policy-item" v-for="(trust, i) in trustPolicy" :key="i">
<span class="principal">{{ trust.principal }}</span>
<span class="action">可执行: {{ trust.action }}</span>
<span class="condition" v-if="trust.condition">条件: {{ trust.condition }}</span>
</div>
</div>
</div>
</div>
<!-- Connection Lines (SVG) -->
<svg class="connection-lines" v-if="mounted">
<line
v-for="(line, index) in connectionLines"
:key="index"
:x1="line.x1"
:y1="line.y1"
:x2="line.x2"
:y2="line.y2"
:class="['connection-line', line.type, { active: hoveredPolicy === line.policyIndex }]"
@mouseenter="hoveredPolicy = line.policyIndex"
@mouseleave="hoveredPolicy = null"
/>
</svg>
<!-- Attached Policies -->
<div class="attached-policies">
<div
v-for="(policy, index) in attachedPolicies"
:key="index"
class="policy-card"
:class="{ active: hoveredPolicy === index, selected: selectedPolicy === index }"
:style="getPolicyPosition(index)"
@mouseenter="hoveredPolicy = index"
@mouseleave="hoveredPolicy = null"
@click="selectPolicy(index)"
>
<div class="policy-header">
<span class="policy-icon">{{ policy.icon }}</span>
<span class="policy-name">{{ policy.name }}</span>
</div>
<div class="policy-permissions" v-if="selectedPolicy === index">
<div class="permission-item" v-for="(perm, i) in policy.permissions" :key="i">
<span class="perm-effect" :class="perm.effect">{{ perm.effect }}</span>
<span class="perm-action">{{ perm.action }}</span>
<span class="perm-resource">{{ perm.resource }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
// Role Data
const roleName = ref('CrossAccountS3AccessRole')
const roleType = ref('跨账号访问角色')
const showRoleDetails = ref(false)
const trustPolicy = ref([
{ principal: '账号 A (123456789012)', action: 'sts:AssumeRole', condition: 'ExternalId 匹配' },
{ principal: '特定 IAM 用户', action: 'sts:AssumeRole', condition: 'IP 白名单' }
])
// Policies Data
const attachedPolicies = ref([
{
name: 'S3ReadWritePolicy',
icon: '📦',
permissions: [
{ effect: 'Allow', action: 's3:GetObject', resource: 'arn:aws:s3:::bucket/*' },
{ effect: 'Allow', action: 's3:PutObject', resource: 'arn:aws:s3:::bucket/*' },
{ effect: 'Allow', action: 's3:ListBucket', resource: 'arn:aws:s3:::bucket' }
]
},
{
name: 'CloudWatchLogsPolicy',
icon: '📊',
permissions: [
{ effect: 'Allow', action: 'logs:CreateLogGroup', resource: '*' },
{ effect: 'Allow', action: 'logs:CreateLogStream', resource: '*' },
{ effect: 'Allow', action: 'logs:PutLogEvents', resource: '*' }
]
},
{
name: 'DenySensitiveData',
icon: '🚫',
permissions: [
{ effect: 'Deny', action: 's3:GetObject', resource: 'arn:aws:s3:::bucket/sensitive/*' },
{ effect: 'Deny', action: 's3:DeleteObject', resource: 'arn:aws:s3:::bucket/*' }
]
}
])
// State
const hoveredPolicy = ref(null)
const selectedPolicy = ref(0)
const mounted = ref(false)
const connectionLines = ref([])
// Methods
function toggleRoleDetails() {
showRoleDetails.value = !showRoleDetails.value
}
function selectPolicy(index) {
selectedPolicy.value = index
}
function selectFeature(platform, index) {
// For compatibility with other demos
}
function getPolicyPosition(index) {
const positions = [
{ top: '0%', right: '0%' },
{ top: '35%', right: '5%' },
{ top: '70%', right: '0%' }
]
return positions[index] || positions[0]
}
function calculateConnections() {
// Simplified connection calculation
connectionLines.value = attachedPolicies.value.map((_, index) => ({
x1: 50,
y1: 50,
x2: 80 + (index * 5),
y2: 20 + (index * 30),
type: index === 2 ? 'deny' : 'allow',
policyIndex: index
}))
}
// Lifecycle
onMounted(() => {
nextTick(() => {
mounted.value = true
calculateConnections()
})
})
onUnmounted(() => {
mounted.value = false
})
</script>
<style scoped>
.role-policy-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 600px;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.4rem;
}
.demo-desc {
margin: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.visualization-container {
position: relative;
min-height: 500px;
}
/* Central Role */
.central-role {
position: absolute;
left: 5%;
top: 50%;
transform: translateY(-50%);
width: 280px;
z-index: 10;
}
.role-core {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.role-core:hover {
transform: scale(1.02);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.role-core.expanded {
border-radius: 16px 16px 0 0;
}
.role-icon {
font-size: 2.5rem;
text-align: center;
margin-bottom: 8px;
}
.role-info {
text-align: center;
}
.role-name {
display: block;
color: #333;
font-weight: 700;
font-size: 1rem;
margin-bottom: 4px;
}
.role-type {
display: block;
color: #666;
font-size: 0.8rem;
}
.expand-icon {
text-align: center;
margin-top: 8px;
color: #999;
font-size: 0.8rem;
}
/* Trust Policy */
.trust-policy {
background: rgba(255, 255, 255, 0.95);
border-radius: 0 0 16px 16px;
padding: 16px 20px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.policy-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.policy-icon {
font-size: 1.2rem;
}
.policy-title {
font-weight: 700;
color: #333;
font-size: 0.85rem;
}
.policy-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.policy-item {
background: rgba(102, 126, 234, 0.1);
border-radius: 6px;
padding: 8px;
font-size: 0.75rem;
display: flex;
flex-direction: column;
gap: 2px;
}
.principal {
font-weight: 600;
color: #667eea;
}
.action {
color: #4caf50;
}
.condition {
color: #ff9800;
}
/* Connection Lines SVG */
.connection-lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.connection-line {
stroke: rgba(255, 255, 255, 0.3);
stroke-width: 2;
fill: none;
pointer-events: stroke;
cursor: pointer;
transition: all 0.3s ease;
}
.connection-line.allow {
stroke: #4caf50;
}
.connection-line.deny {
stroke: #f44336;
stroke-dasharray: 5, 5;
}
.connection-line:hover,
.connection-line.active {
stroke-width: 4;
opacity: 1;
}
/* Attached Policies */
.attached-policies {
position: absolute;
right: 5%;
top: 50%;
transform: translateY(-50%);
width: 240px;
}
.policy-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
position: relative;
}
.policy-card:hover,
.policy-card.active {
transform: translateX(-8px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.policy-card.selected {
border: 2px solid #667eea;
}
.policy-card .policy-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.policy-card .policy-icon {
font-size: 1.4rem;
}
.policy-card .policy-name {
font-weight: 700;
color: #333;
font-size: 0.9rem;
}
.policy-permissions {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.permission-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px;
margin-bottom: 4px;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
font-size: 0.7rem;
}
.perm-effect {
padding: 2px 6px;
border-radius: 3px;
font-weight: 600;
font-size: 0.65rem;
text-transform: uppercase;
}
.perm-effect.Allow {
background: #4caf50;
color: white;
}
.perm-effect.Deny {
background: #f44336;
color: white;
}
.perm-action {
font-family: monospace;
color: #667eea;
}
.perm-resource {
color: #999;
font-size: 0.6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 1024px) {
.visualization-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.central-role,
.attached-policies {
position: static;
transform: none;
width: 100%;
}
.connection-lines {
display: none;
}
}
</style>
@@ -0,0 +1,356 @@
<template>
<div class="aws-vs-aliyun-demo">
<div class="demo-header">
<h4>AWS vs 阿里云 核心差异</h4>
<p class="demo-desc">点击切换查看不同维度的对比</p>
</div>
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab-btn"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<div class="comparison-content">
<transition name="fade" mode="out-in">
<div :key="activeTab" class="tab-content">
<div class="vs-cards">
<div class="vs-card aws-card">
<div class="card-header">
<div class="logo">AWS</div>
<div class="subtitle">Amazon Web Services</div>
</div>
<div class="card-body">
<div
v-for="(point, idx) in currentComparison.aws"
:key="idx"
class="point"
>
<span class="check"></span>
<span>{{ point }}</span>
</div>
</div>
</div>
<div class="vs-divider">
<div class="vs-text">VS</div>
</div>
<div class="vs-card aliyun-card">
<div class="card-header">
<div class="logo aliyun-logo">阿里云</div>
<div class="subtitle">Alibaba Cloud</div>
</div>
<div class="card-body">
<div
v-for="(point, idx) in currentComparison.aliyun"
:key="idx"
class="point"
>
<span class="check aliyun-check"></span>
<span>{{ point }}</span>
</div>
</div>
</div>
</div>
<div class="verdict-box">
<div class="verdict-title">💡 选型建议</div>
<div class="verdict-text">{{ currentComparison.verdict }}</div>
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('global')
const tabs = [
{ key: 'global', label: '全球布局' },
{ key: 'ecosystem', label: '生态体系' },
{ key: 'pricing', label: '价格策略' },
{ key: 'enterprise', label: '企业服务' },
{ key: 'developer', label: '开发者体验' }
]
const comparisons = {
global: {
aws: [
'全球 30+ 区域,覆盖率最广',
'发达国家基础设施成熟',
'跨境数据合规经验丰富'
],
aliyun: [
'亚太地区覆盖密度最高',
'中国大陆节点数量领先',
'一带一路区域布局积极'
],
verdict: '出海欧美选 AWS,深耕亚太选阿里云。跨国企业可考虑双云或多云架构。'
},
ecosystem: {
aws: [
'服务种类最丰富(200+ 服务)',
'第三方 SaaS 集成度极高',
'开源生态支持最全面'
],
aliyun: [
'阿里系产品无缝集成',
'电商/零售场景方案成熟',
'国产化替代支持完善'
],
verdict: '技术栈复杂、需丰富组件选 AWS;阿里系业务、电商零售场景选阿里云。'
},
pricing: {
aws: [
'预留实例折扣力度大',
'Spot 竞价实例价格极低',
'免费额度相对保守'
],
aliyun: [
'新用户优惠力度大',
'包年包月性价比高',
'学生/开发者福利多'
],
verdict: '长期稳定负载选 AWS 预留实例;初创公司、预算敏感选阿里云新客优惠。'
},
enterprise: {
aws: [
'企业级支持体系成熟',
'合规认证最全面',
'混合云方案(Outposts'
],
aliyun: [
'本地化服务响应快',
'政府/央企合作深度高',
'专有云/混合云方案完善'
],
verdict: '外企、强合规要求选 AWS;政企客户、需本地化支持选阿里云。'
},
developer: {
aws: [
'文档质量业界标杆',
'认证体系完善',
'社区活跃度最高'
],
aliyun: [
'中文文档详尽',
'学习路径清晰',
'技术社区活跃度高'
],
verdict: '英文好、追求国际认证选 AWS;中文开发者、喜欢中文资料选阿里云。'
}
}
const currentComparison = computed(() => comparisons[activeTab.value])
</script>
<style scoped>
.aws-vs-aliyun-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #ff9900);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.comparison-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
overflow-x: auto;
padding-bottom: 4px;
}
.tab-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #8892b0;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.875rem;
white-space: nowrap;
transition: all 0.3s ease;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #e6f1ff;
}
.tab-btn.active {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-color: transparent;
color: #fff;
}
.vs-cards {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 20px;
}
.vs-card {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.aws-card {
border-top: 3px solid #ff9900;
}
.aliyun-card {
border-top: 3px solid #ff6a00;
}
.card-header {
text-align: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: #ff9900;
margin-bottom: 4px;
}
.aliyun-logo {
color: #ff6a00;
}
.subtitle {
font-size: 0.75rem;
color: #8892b0;
}
.card-body {
display: flex;
flex-direction: column;
gap: 10px;
}
.point {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 0.875rem;
color: #e6f1ff;
line-height: 1.5;
}
.check {
color: #ff9900;
font-weight: 700;
flex-shrink: 0;
}
.aliyun-check {
color: #ff6a00;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-text {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
color: #fff;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
}
.verdict-box {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(123, 44, 191, 0.1));
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 12px;
padding: 16px;
}
.verdict-title {
font-weight: 600;
color: #00d4ff;
margin-bottom: 8px;
font-size: 0.9375rem;
}
.verdict-text {
color: #e6f1ff;
font-size: 0.875rem;
line-height: 1.6;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
@media (max-width: 768px) {
.vs-cards {
grid-template-columns: 1fr;
gap: 12px;
}
.vs-divider {
display: none;
}
.comparison-tabs {
gap: 6px;
}
.tab-btn {
padding: 6px 12px;
font-size: 0.8rem;
}
}
</style>
@@ -0,0 +1,590 @@
<template>
<div class="cloud-services-map-demo">
<div class="demo-header">
<h4>云计算服务版图全景图</h4>
<p class="demo-desc">点击各个板块查看 AWS 与阿里云的对应服务</p>
</div>
<div class="map-container">
<!-- 计算层 -->
<div
class="service-layer compute-layer"
:class="{ active: activeLayer === 'compute' }"
@click="setActiveLayer('compute')"
>
<div class="layer-icon"></div>
<div class="layer-title">计算服务</div>
<div class="layer-services">
<span class="service-tag">EC2/ECS</span>
<span class="service-tag">Lambda/函数计算</span>
</div>
</div>
<!-- 存储层 -->
<div
class="service-layer storage-layer"
:class="{ active: activeLayer === 'storage' }"
@click="setActiveLayer('storage')"
>
<div class="layer-icon">💾</div>
<div class="layer-title">存储服务</div>
<div class="layer-services">
<span class="service-tag">S3/OSS</span>
<span class="service-tag">EBS/云盘</span>
</div>
</div>
<!-- 网络层 -->
<div
class="service-layer network-layer"
:class="{ active: activeLayer === 'network' }"
@click="setActiveLayer('network')"
>
<div class="layer-icon">🌐</div>
<div class="layer-title">网络服务</div>
<div class="layer-services">
<span class="service-tag">VPC/专有网络</span>
<span class="service-tag">ELB/SLB</span>
</div>
</div>
<!-- 安全层 -->
<div
class="service-layer security-layer"
:class="{ active: activeLayer === 'security' }"
@click="setActiveLayer('security')"
>
<div class="layer-icon">🔒</div>
<div class="layer-title">安全服务</div>
<div class="layer-services">
<span class="service-tag">IAM/RAM</span>
<span class="service-tag">KMS/密钥管理</span>
</div>
</div>
<!-- 数据库层 -->
<div
class="service-layer database-layer"
:class="{ active: activeLayer === 'database' }"
@click="setActiveLayer('database')"
>
<div class="layer-icon">🗄</div>
<div class="layer-title">数据库服务</div>
<div class="layer-services">
<span class="service-tag">RDS/PolarDB</span>
<span class="service-tag">DynamoDB/Tablestore</span>
</div>
</div>
<!-- 中间件层 -->
<div
class="service-layer middleware-layer"
:class="{ active: activeLayer === 'middleware' }"
@click="setActiveLayer('middleware')"
>
<div class="layer-icon">🔧</div>
<div class="layer-title">中间件服务</div>
<div class="layer-services">
<span class="service-tag">MQ/RocketMQ</span>
<span class="service-tag">ElastiCache/Redis</span>
</div>
</div>
</div>
<!-- 详情面板 -->
<div v-if="activeLayer" class="detail-panel">
<div class="detail-header">
<h5>{{ layerDetails[activeLayer].title }}</h5>
<button class="close-btn" @click="activeLayer = null">×</button>
</div>
<div class="detail-content">
<div class="comparison-table">
<div class="table-header">
<div class="col aws">AWS</div>
<div class="col aliyun">阿里云</div>
<div class="col desc">功能描述</div>
</div>
<div
v-for="(item, index) in layerDetails[activeLayer].services"
:key="index"
class="table-row"
>
<div class="col aws">{{ item.aws }}</div>
<div class="col aliyun">{{ item.aliyun }}</div>
<div class="col desc">{{ item.desc }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeLayer = ref(null)
const setActiveLayer = (layer) => {
activeLayer.value = layer
}
const layerDetails = {
compute: {
title: '计算服务对比',
services: [
{
aws: 'Amazon EC2',
aliyun: 'ECS 云服务器',
desc: '虚拟服务器,可完全控制计算资源'
},
{
aws: 'AWS Lambda',
aliyun: '函数计算 FC',
desc: '无服务器计算,按需运行代码'
},
{
aws: 'Amazon ECS/EKS',
aliyun: 'ACK 容器服务',
desc: '容器编排和管理服务'
},
{
aws: 'AWS Fargate',
aliyun: 'Serverless Kubernetes',
desc: '无服务器容器计算引擎'
},
{
aws: 'AWS Batch',
aliyun: '批量计算',
desc: '批量作业调度服务'
},
{
aws: 'AWS Elastic Beanstalk',
aliyun: 'EDAS',
desc: '应用部署和托管平台'
}
]
},
storage: {
title: '存储服务对比',
services: [
{
aws: 'Amazon S3',
aliyun: 'OSS 对象存储',
desc: '海量、安全、低成本的对象存储'
},
{
aws: 'Amazon EBS',
aliyun: '云盘 ESSD',
desc: '块存储服务,为EC2/ECS提供持久存储'
},
{
aws: 'Amazon EFS',
aliyun: 'NAS 文件存储',
desc: '托管的弹性文件存储'
},
{
aws: 'Amazon Glacier',
aliyun: 'OSS 归档存储',
desc: '低成本长期归档存储'
},
{
aws: 'AWS Storage Gateway',
aliyun: '混合云存储阵列',
desc: '混合云存储服务'
},
{
aws: 'AWS Backup',
aliyun: '云备份服务',
desc: '集中式备份管理'
}
]
},
network: {
title: '网络服务对比',
services: [
{
aws: 'Amazon VPC',
aliyun: '专有网络 VPC',
desc: '虚拟私有云网络环境'
},
{
aws: 'Elastic Load Balancing',
aliyun: 'SLB 负载均衡',
desc: '流量分发服务'
},
{
aws: 'Amazon CloudFront',
aliyun: 'CDN 内容分发',
desc: '全球内容分发网络'
},
{
aws: 'AWS Transit Gateway',
aliyun: '云企业网 CEN',
desc: '网络传输网关'
},
{
aws: 'AWS Direct Connect',
aliyun: '高速通道',
desc: '专线连接服务'
},
{
aws: 'AWS App Mesh',
aliyun: '服务网格 ASM',
desc: '微服务网格管理'
},
{
aws: 'AWS Global Accelerator',
aliyun: '全球加速 GA',
desc: '网络加速服务'
}
]
},
security: {
title: '安全服务对比',
services: [
{
aws: 'AWS IAM',
aliyun: 'RAM 访问控制',
desc: '身份和访问管理服务'
},
{
aws: 'AWS KMS',
aliyun: 'KMS 密钥管理',
desc: '密钥管理服务'
},
{
aws: 'AWS WAF',
aliyun: 'WAF 防火墙',
desc: 'Web应用防火墙'
},
{
aws: 'AWS Shield',
aliyun: 'DDoS 防护',
desc: 'DDoS攻击防护'
},
{
aws: 'Amazon GuardDuty',
aliyun: '云安全中心',
desc: '智能威胁检测'
},
{
aws: 'AWS Certificate Manager',
aliyun: 'SSL 证书服务',
desc: 'SSL/TLS证书管理'
},
{
aws: 'AWS Secrets Manager',
aliyun: '凭据管家',
desc: '机密信息托管'
},
{
aws: 'Amazon Macie',
aliyun: '敏感数据保护',
desc: '敏感数据发现与保护'
}
]
},
database: {
title: '数据库服务对比',
services: [
{
aws: 'Amazon RDS',
aliyun: 'RDS 关系型数据库',
desc: '托管的关系型数据库服务'
},
{
aws: 'Amazon Aurora',
aliyun: 'PolarDB',
desc: '云原生关系型数据库'
},
{
aws: 'Amazon DynamoDB',
aliyun: 'Tablestore',
desc: 'NoSQL键值和文档数据库'
},
{
aws: 'Amazon ElastiCache',
aliyun: '云数据库 Redis',
desc: '托管的内存缓存服务'
},
{
aws: 'Amazon DocumentDB',
aliyun: 'MongoDB 副本集',
desc: '兼容MongoDB的文档数据库'
},
{
aws: 'Amazon Keyspaces',
aliyun: 'Cassandra 服务',
desc: '托管的Cassandra兼容服务'
},
{
aws: 'Amazon Neptune',
aliyun: '图数据库 GDB',
desc: '全托管图数据库'
},
{
aws: 'Amazon QLDB',
aliyun: '区块链 BaaS',
desc: '全托管分类账数据库'
},
{
aws: 'Amazon Timestream',
aliyun: '时序数据库 TSDB',
desc: '全托管时序数据库'
}
]
},
middleware: {
title: '中间件服务对比',
services: [
{
aws: 'Amazon MQ',
aliyun: '消息队列 MQ',
desc: '托管的消息代理服务'
},
{
aws: 'Amazon SQS',
aliyun: '消息服务 MNS',
desc: '全托管消息队列服务'
},
{
aws: 'Amazon SNS',
aliyun: '事件总线 EventBridge',
desc: '全托管发布/订阅服务'
},
{
aws: 'Amazon Kinesis',
aliyun: '实时计算 Flink',
desc: '实时数据流处理'
},
{
aws: 'AWS Step Functions',
aliyun: 'Serverless 工作流',
desc: '工作流编排服务'
},
{
aws: 'AWS AppSync',
aliyun: 'API 网关',
desc: '托管GraphQL服务'
},
{
aws: 'Amazon EventBridge',
aliyun: '事件总线',
desc: '无服务器事件总线'
}
]
}
}
</script>
<style scoped>
.cloud-services-map-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.map-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.service-layer {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.service-layer:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.service-layer.active {
background: rgba(0, 212, 255, 0.15);
border-color: #00d4ff;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.layer-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.layer-title {
font-weight: 600;
font-size: 0.9375rem;
margin-bottom: 8px;
color: #e6f1ff;
}
.layer-services {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
.service-tag {
background: rgba(123, 44, 191, 0.3);
color: #c084fc;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
}
.service-layer.active .service-tag {
background: rgba(0, 212, 255, 0.3);
color: #00d4ff;
}
.detail-panel {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
margin-top: 16px;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-header h5 {
margin: 0;
color: #00d4ff;
font-size: 1.1rem;
}
.close-btn {
background: none;
border: none;
color: #8892b0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.comparison-table {
width: 100%;
}
.table-header {
display: grid;
grid-template-columns: 1.2fr 1.2fr 2fr;
gap: 12px;
padding: 10px 12px;
background: rgba(0, 212, 255, 0.1);
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
color: #e6f1ff;
margin-bottom: 8px;
}
.table-row {
display: grid;
grid-template-columns: 1.2fr 1.2fr 2fr;
gap: 12px;
padding: 10px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.875rem;
transition: background 0.2s;
}
.table-row:hover {
background: rgba(255, 255, 255, 0.03);
border-radius: 6px;
}
.col.aws {
color: #ff9900;
font-weight: 500;
}
.col.aliyun {
color: #ff6a00;
font-weight: 500;
}
.col.desc {
color: #8892b0;
}
@media (max-width: 768px) {
.map-container {
grid-template-columns: repeat(2, 1fr);
}
.table-header,
.table-row {
grid-template-columns: 1fr 1fr;
}
.col.desc {
display: none;
}
}
@media (max-width: 480px) {
.map-container {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,463 @@
<template>
<div class="compute-services-demo">
<div class="demo-header">
<h4>计算服务选型指南</h4>
<p class="demo-desc">拖动滑块调整场景参数获取最佳计算方案</p>
</div>
<div class="scenario-sliders">
<div class="slider-group">
<label>负载稳定性</label>
<input
v-model.number="scenario.stability"
type="range"
min="0"
max="100"
/>
<div class="slider-labels">
<span>波动大</span>
<span>非常稳定</span>
</div>
</div>
<div class="slider-group">
<label>平均负载率</label>
<input
v-model.number="scenario.utilization"
type="range"
min="0"
max="100"
/>
<div class="slider-labels">
<span>很低</span>
<span>接近100%</span>
</div>
</div>
<div class="slider-group">
<label>任务持续时间</label>
<input
v-model.number="scenario.duration"
type="range"
min="0"
max="100"
/>
<div class="slider-labels">
<span>几分钟</span>
<span>持续运行</span>
</div>
</div>
<div class="slider-group">
<label>流量突发程度</label>
<input
v-model.number="scenario.burstiness"
type="range"
min="0"
max="100"
/>
<div class="slider-labels">
<span>平稳</span>
<span>大起大落</span>
</div>
</div>
</div>
<div class="recommendation-panel">
<div class="recommendation-title">
<span class="icon">🎯</span>
推荐方案
</div>
<div class="solution-cards">
<div
v-for="(solution, index) in recommendations"
:key="index"
class="solution-card"
:class="{ 'top-pick': index === 0 }"
>
<div class="solution-rank" :class="{ 'rank-1': index === 0 }">
{{ index === 0 ? '👑' : index + 1 }}
</div>
<div class="solution-content">
<div class="solution-name">{{ solution.name }}</div>
<div class="solution-services">
<span class="service-tag aws">{{ solution.aws }}</span>
<span class="vs-mini">vs</span>
<span class="service-tag aliyun">{{ solution.aliyun }}</span>
</div>
<div class="solution-reason">{{ solution.reason }}</div>
<div class="solution-savings" v-if="solution.savings">
💰 预计节省: {{ solution.savings }}
</div>
</div>
</div>
</div>
</div>
<div class="scenario-presets">
<span class="preset-label">快速场景:</span>
<button
v-for="preset in presets"
:key="preset.name"
class="preset-btn"
@click="applyPreset(preset)"
>
{{ preset.name }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const scenario = ref({
stability: 50,
utilization: 60,
duration: 70,
burstiness: 30
})
const presets = [
{
name: '电商大促',
values: { stability: 20, utilization: 40, duration: 90, burstiness: 90 }
},
{
name: '企业内部系统',
values: { stability: 90, utilization: 70, duration: 95, burstiness: 10 }
},
{
name: '初创公司官网',
values: { stability: 40, utilization: 20, duration: 80, burstiness: 30 }
},
{
name: '数据处理任务',
values: { stability: 30, utilization: 95, duration: 10, burstiness: 80 }
},
{
name: 'SaaS 平台',
values: { stability: 60, utilization: 50, duration: 95, burstiness: 60 }
}
]
const applyPreset = (preset) => {
scenario.value = { ...preset.values }
}
const recommendations = computed(() => {
const s = scenario.value
const solutions = []
// 计算各方案得分
let serverlessScore = 0
let ec2Score = 0
let spotScore = 0
let reservedScore = 0
// Serverless (Lambda/FC)
if (s.duration < 30) serverlessScore += 30
if (s.burstiness > 70) serverlessScore += 25
if (s.utilization < 30) serverlessScore += 20
if (s.stability < 30) serverlessScore += 15
// Spot 实例
if (s.burstiness > 60) spotScore += 25
if (s.stability < 40) spotScore += 30
if (s.duration < 40) spotScore += 20
if (s.utilization < 50) spotScore += 15
// 预留实例
if (s.stability > 70) reservedScore += 35
if (s.duration > 80) reservedScore += 25
if (s.utilization > 60) reservedScore += 20
if (s.burstiness < 30) reservedScore += 10
// 按需实例 (兜底)
ec2Score = 40
// 排序并生成推荐
const scores = [
{ type: 'serverless', score: serverlessScore, savings: '40-70%' },
{ type: 'spot', score: spotScore, savings: '60-90%' },
{ type: 'reserved', score: reservedScore, savings: '30-60%' },
{ type: 'ondemand', score: ec2Score, savings: null }
].sort((a, b) => b.score - a.score)
const solutionMap = {
serverless: {
name: '无服务器架构',
aws: 'AWS Lambda',
aliyun: '函数计算 FC',
reason: '流量波动大、任务短时,按调用计费最划算,自动扩缩容免运维'
},
spot: {
name: '竞价实例',
aws: 'EC2 Spot',
aliyun: '抢占式实例',
reason: '可容忍中断的计算任务,价格极低,适合批处理、渲染等场景'
},
reserved: {
name: '预留实例',
aws: 'Reserved Instances',
aliyun: '包年包月',
reason: '长期稳定负载,提前承诺使用时长换取大幅折扣,成本最优'
},
ondemand: {
name: '按需实例',
aws: 'EC2 On-Demand',
aliyun: '按量付费 ECS',
reason: '灵活性最高,按小时计费,适合测试环境或 unpredictable 负载'
}
}
return scores.slice(0, 3).map((s, idx) => ({
...solutionMap[s.type],
savings: s.savings
}))
})
</script>
<style scoped>
.compute-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.scenario-sliders {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.slider-group {
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
padding: 12px;
}
.slider-group label {
display: block;
font-size: 0.875rem;
color: #e6f1ff;
margin-bottom: 8px;
font-weight: 500;
}
.slider-group input[type='range'] {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
margin-bottom: 8px;
}
.slider-group input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #8892b0;
}
.recommendation-panel {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.recommendation-title {
font-size: 1rem;
font-weight: 600;
color: #e6f1ff;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.icon {
font-size: 1.25rem;
}
.solution-cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.solution-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 16px;
display: flex;
gap: 12px;
transition: all 0.3s ease;
}
.solution-card:hover {
background: rgba(255, 255, 255, 0.08);
}
.solution-card.top-pick {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.15), rgba(123, 44, 191, 0.15));
border-color: rgba(0, 212, 255, 0.3);
}
.solution-rank {
width: 36px;
height: 36px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
color: #8892b0;
flex-shrink: 0;
}
.solution-rank.rank-1 {
background: linear-gradient(135deg, #ffd700, #ffaa00);
color: #1a1a2e;
font-size: 1.25rem;
}
.solution-content {
flex: 1;
}
.solution-name {
font-weight: 600;
font-size: 1rem;
color: #e6f1ff;
margin-bottom: 6px;
}
.solution-services {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.service-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.service-tag.aws {
background: rgba(255, 153, 0, 0.2);
color: #ff9900;
}
.service-tag.aliyun {
background: rgba(255, 106, 0, 0.2);
color: #ff6a00;
}
.vs-mini {
color: #8892b0;
font-size: 0.75rem;
}
.solution-reason {
font-size: 0.875rem;
color: #8892b0;
line-height: 1.5;
}
.solution-savings {
margin-top: 8px;
font-size: 0.8125rem;
color: #00d4ff;
font-weight: 500;
}
.scenario-presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.preset-label {
font-size: 0.875rem;
color: #8892b0;
margin-right: 8px;
}
.preset-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e6f1ff;
padding: 6px 14px;
border-radius: 16px;
cursor: pointer;
font-size: 0.8125rem;
transition: all 0.2s ease;
}
.preset-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
@media (max-width: 768px) {
.scenario-sliders {
grid-template-columns: 1fr;
}
.solution-card {
flex-direction: column;
}
.scenario-presets {
justify-content: flex-start;
}
}
</style>
@@ -0,0 +1,289 @@
<template>
<div class="database-services-demo">
<div class="demo-header">
<h4>数据库选型助手</h4>
<p class="demo-desc">根据您的业务特点推荐最适合的数据库方案</p>
</div>
<div class="db-selection">
<div class="db-categories">
<button
v-for="cat in categories"
:key="cat.id"
class="cat-btn"
:class="{ active: selectedCategory === cat.id }"
@click="selectCategory(cat.id)"
>
<span class="cat-icon">{{ cat.icon }}</span>
<span class="cat-name">{{ cat.name }}</span>
</button>
</div>
<div v-if="selectedCategory" class="db-comparison">
<div class="comparison-header">
<span class="aws-badge">AWS</span>
<span class="vs-text">对比</span>
<span class="aliyun-badge">阿里云</span>
</div>
<div class="db-cards">
<div class="db-card">
<div class="db-header aws">
<div class="db-name">{{ currentCategory.aws }}</div>
</div>
<div class="db-body">
<div class="feature-list">
<div v-for="(feat, i) in currentCategory.awsFeatures" :key="i" class="feature">
{{ feat }}
</div>
</div>
<div class="price-tag">{{ currentCategory.awsPrice }}</div>
</div>
</div>
<div class="db-card">
<div class="db-header aliyun">
<div class="db-name">{{ currentCategory.aliyun }}</div>
</div>
<div class="db-body">
<div class="feature-list">
<div v-for="(feat, i) in currentCategory.aliyunFeatures" :key="i" class="feature">
{{ feat }}
</div>
</div>
<div class="price-tag aliyun">{{ currentCategory.aliyunPrice }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedCategory = ref('relational')
const categories = [
{ id: 'relational', name: '关系型数据库', icon: '📊' },
{ id: 'nosql', name: 'NoSQL 数据库', icon: '📦' },
{ id: 'cache', name: '缓存服务', icon: '⚡' },
{ id: 'analytics', name: '分析型数据库', icon: '📈' }
]
const categoryData = {
relational: {
aws: 'Amazon RDS / Aurora',
aliyun: 'RDS / PolarDB',
awsFeatures: ['MySQL/PostgreSQL/Oracle/SQL Server 支持', 'Aurora 5 倍性能提升', '自动故障转移和读副本', 'Serverless 自动扩缩容'],
aliyunFeatures: ['MySQL/SQL Server/PostgreSQL/Oracle 支持', 'PolarDB 计算存储分离', '秒级备份恢复', 'Oracle 语法兼容模式'],
awsPrice: '$0.017/小时起',
aliyunPrice: '¥0.12/小时起'
},
nosql: {
aws: 'Amazon DynamoDB',
aliyun: 'Tablestore',
awsFeatures: ['全托管 NoSQL 键值和文档数据库', '单表设计支持 PB 级规模', 'DAX 内存缓存加速', '全局表多区域复制'],
aliyunFeatures: ['分布式 NoSQL 数据库存储', '自动分片和负载均衡', '二级索引和全文检索', '毫秒级单点读写延迟'],
awsPrice: '按需 $1.25/百万次写',
aliyunPrice: '按量 0.4元/万次写'
},
cache: {
aws: 'Amazon ElastiCache',
aliyun: '云数据库 Redis',
awsFeatures: ['托管 Redis 和 Memcached', '集群模式自动分片', '只读副本和自动故障转移', '备份恢复和快照'],
aliyunFeatures: ['主从双节点架构', '自动故障切换', '读写分离能力', '数据持久化备份'],
awsPrice: '$0.012/小时起',
aliyunPrice: '¥0.08/小时起'
},
analytics: {
aws: 'Amazon Redshift',
aliyun: 'AnalyticDB',
awsFeatures: ['PB 级数据仓库', '列式存储和压缩', 'Spectrum 查询 S3 数据', '并发扩展和自动优化'],
aliyunFeatures: ['实时分析型数据库', 'MPP 大规模并行处理', '高并发低延迟查询', '自动索引和优化'],
awsPrice: '$0.25/小时起',
aliyunPrice: '¥2.0/小时起'
}
}
const selectCategory = (id) => {
selectedCategory.value = id
}
const currentCategory = computed(() => categoryData[selectedCategory.value])
</script>
<style scoped>
.database-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.db-selection {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 20px;
}
.db-categories {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.cat-btn {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e6f1ff;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.cat-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.cat-btn.active {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-color: transparent;
color: #fff;
}
.cat-icon {
font-size: 1rem;
}
.comparison-header {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.aws-badge, .aliyun-badge {
padding: 6px 14px;
border-radius: 16px;
font-size: 0.8125rem;
font-weight: 600;
}
.aws-badge {
background: rgba(255, 153, 0, 0.2);
color: #ff9900;
}
.aliyun-badge {
background: rgba(255, 106, 0, 0.2);
color: #ff6a00;
}
.vs-text {
color: #8892b0;
font-size: 0.75rem;
}
.db-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.db-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
.db-header {
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.db-header.aws {
background: rgba(255, 153, 0, 0.1);
}
.db-header.aliyun {
background: rgba(255, 106, 0, 0.1);
}
.db-name {
font-size: 1rem;
font-weight: 600;
color: #e6f1ff;
}
.db-body {
padding: 16px;
}
.feature-list {
margin-bottom: 12px;
}
.feature {
font-size: 0.8125rem;
color: #e6f1ff;
padding: 4px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.price-tag {
background: rgba(0, 212, 255, 0.1);
color: #00d4ff;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.8125rem;
font-weight: 500;
text-align: center;
}
.price-tag.aliyun {
color: #ff6a00;
background: rgba(255, 106, 0, 0.1);
}
@media (max-width: 768px) {
.db-categories {
justify-content: center;
}
.db-cards {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,223 @@
<template>
<div class="k8s-services-demo">
<div class="demo-header">
<h4>Kubernetes 服务生态全景</h4>
<p class="demo-desc">探索 AWS 和阿里云上的 K8s 服务及配套生态</p>
</div>
<div class="k8s-architecture">
<div class="arch-layer control-plane">
<div class="layer-title">控制平面</div>
<div class="layer-content">
<div class="service-box">
<div class="service-name">EKS / ACK</div>
<div class="service-desc">托管 Kubernetes 控制平面</div>
</div>
</div>
</div>
<div class="arch-layer worker-nodes">
<div class="layer-title">工作节点</div>
<div class="layer-content">
<div class="node-types">
<div class="node-box">
<div class="node-icon">💻</div>
<div class="node-name">EC2/ECS</div>
<div class="node-desc">标准计算节点</div>
</div>
<div class="node-box">
<div class="node-icon"></div>
<div class="node-name">Fargate/ECI</div>
<div class="node-desc">Serverless 节点</div>
</div>
<div class="node-box">
<div class="node-icon">🎯</div>
<div class="node-name">Spot/抢占式</div>
<div class="node-desc">低成本竞价节点</div>
</div>
</div>
</div>
</div>
<div class="arch-layer addons">
<div class="layer-title">插件生态</div>
<div class="layer-content">
<div class="addon-grid">
<div class="addon-card">
<div class="addon-name">Ingress/Nginx</div>
<div class="addon-aws">AWS Load Balancer</div>
<div class="addon-aliyun">ALB Ingress</div>
</div>
<div class="addon-card">
<div class="addon-name">Storage</div>
<div class="addon-aws">EBS/EFS CSI</div>
<div class="addon-aliyun">云盘/NAS CSI</div>
</div>
<div class="addon-card">
<div class="addon-name">Monitoring</div>
<div class="addon-aws">CloudWatch/AMP</div>
<div class="addon-aliyun">ARMS/Prometheus</div>
</div>
<div class="addon-card">
<div class="addon-name">Service Mesh</div>
<div class="addon-aws">App Mesh</div>
<div class="addon-aliyun">Service Mesh ASM</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// Component logic here if needed
</script>
<style scoped>
.k8s-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.k8s-architecture {
display: flex;
flex-direction: column;
gap: 16px;
}
.arch-layer {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.layer-title {
font-weight: 600;
font-size: 0.875rem;
color: #00d4ff;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.service-box {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.15), rgba(123, 44, 191, 0.15));
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 10px;
padding: 16px;
text-align: center;
}
.service-name {
font-size: 1.25rem;
font-weight: 700;
color: #e6f1ff;
margin-bottom: 4px;
}
.service-desc {
font-size: 0.8125rem;
color: #8892b0;
}
.node-types {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.node-box {
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
padding: 14px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.node-icon {
font-size: 1.5rem;
margin-bottom: 6px;
}
.node-name {
font-size: 0.8125rem;
font-weight: 600;
color: #e6f1ff;
margin-bottom: 2px;
}
.node-desc {
font-size: 0.6875rem;
color: #8892b0;
}
.addon-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.addon-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.addon-name {
font-size: 0.875rem;
font-weight: 600;
color: #e6f1ff;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.addon-aws, .addon-aliyun {
font-size: 0.75rem;
padding: 2px 0;
}
.addon-aws {
color: #ff9900;
}
.addon-aliyun {
color: #ff6a00;
}
@media (max-width: 768px) {
.node-types {
grid-template-columns: 1fr;
}
.addon-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,637 @@
<template>
<div class="network-services-demo">
<div class="demo-header">
<h4>网络架构可视化配置</h4>
<p class="demo-desc">拖拽组件构建您的云上网络架构</p>
</div>
<div class="network-builder">
<div class="components-panel">
<div class="panel-title">可用组件</div>
<div class="component-list">
<div
v-for="component in networkComponents"
:key="component.id"
class="component-item"
draggable="true"
@dragstart="onDragStart($event, component)"
>
<span class="component-icon">{{ component.icon }}</span>
<span class="component-name">{{ component.name }}</span>
</div>
</div>
</div>
<div class="canvas-area">
<div
class="network-canvas"
@drop="onDrop"
@dragover.prevent
>
<div v-if="canvasItems.length === 0" class="empty-state">
<div class="empty-icon">🏗</div>
<div class="empty-text">拖拽左侧组件到此处</div>
<div class="empty-subtext">开始构建您的网络架构</div>
</div>
<div
v-for="(item, index) in canvasItems"
:key="item.id"
class="canvas-item"
:class="item.type"
:style="itemStyle(index)"
@click="selectItem(item)"
>
<div class="item-icon">{{ item.icon }}</div>
<div class="item-name">{{ item.name }}</div>
<button class="remove-btn" @click.stop="removeItem(index)">×</button>
</div>
</div>
</div>
</div>
<div v-if="selectedItem" class="config-panel">
<div class="config-header">
<span class="config-icon">{{ selectedItem.icon }}</span>
<span class="config-title">{{ selectedItem.name }} 配置</span>
<button class="close-config" @click="selectedItem = null">×</button>
</div>
<div class="config-content">
<div class="config-section">
<div class="section-title">AWS 配置</div>
<div class="service-name">{{ selectedItem.awsService }}</div>
<div class="config-options">
<div
v-for="(option, idx) in selectedItem.awsOptions"
:key="idx"
class="option-item"
>
<span class="option-check"></span>
<span>{{ option }}</span>
</div>
</div>
</div>
<div class="config-divider"></div>
<div class="config-section">
<div class="section-title aliyun-title">阿里云配置</div>
<div class="service-name aliyun-service">{{ selectedItem.aliyunService }}</div>
<div class="config-options">
<div
v-for="(option, idx) in selectedItem.aliyunOptions"
:key="idx"
class="option-item"
>
<span class="option-check aliyun-check"></span>
<span>{{ option }}</span>
</div>
</div>
</div>
</div>
<div class="config-footer">
<div class="price-compare">
<div class="price-item">
<span class="price-label">AWS:</span>
<span class="price-value">{{ selectedItem.awsPrice }}</span>
</div>
<div class="price-item">
<span class="price-label">阿里云:</span>
<span class="price-value aliyun-price">{{ selectedItem.aliyunPrice }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const canvasItems = ref([])
const selectedItem = ref(null)
let draggedItem = null
const networkComponents = [
{
id: 'vpc',
name: '专有网络',
icon: '🏠',
type: 'network',
awsService: 'Amazon VPC',
aliyunService: '专有网络 VPC',
awsOptions: [
'自定义 IP 地址范围',
'多可用区子网划分',
'网络 ACL 和安全组',
'VPC 对等连接和 Transit Gateway'
],
aliyunOptions: [
'自定义私网网段',
'交换机跨可用区部署',
'安全组和网络 ACL',
'VPC 互通和云企业网'
],
awsPrice: '免费(子网内流量)',
aliyunPrice: '免费(同 VPC 内流量)'
},
{
id: 'cdn',
name: '内容分发',
icon: '🚀',
type: 'network',
awsService: 'Amazon CloudFront',
aliyunService: 'CDN 内容分发',
awsOptions: [
'全球 400+ 边缘节点',
'支持静态和动态内容加速',
'Lambda@Edge 边缘计算',
'与 AWS Shield 集成防护'
],
aliyunOptions: [
'国内 2800+ 节点覆盖',
'全站加速和下载分发',
'边缘脚本和缓存优化',
'与 WAF 联动安全防护'
],
awsPrice: 'HTTP: $0.085/GB 起',
aliyunPrice: 'HTTP: ¥0.15/GB 起'
},
{
id: 'lb',
name: '负载均衡',
icon: '⚖️',
type: 'network',
awsService: 'Elastic Load Balancing',
aliyunService: 'SLB 负载均衡',
awsOptions: [
'ALB/NLB/CLB 多种类型',
'自动健康检查和故障转移',
'SSL/TLS 终止和证书管理',
'与 Auto Scaling 集成'
],
aliyunOptions: [
'ALB/NLB/CLB 全类型支持',
'主备和集群高可用模式',
'HTTPS 证书一键部署',
'与 ESS 弹性伸缩联动'
],
awsPrice: 'ALB: $0.0225/小时 + LCU',
aliyunPrice: 'ALB: ¥0.15/小时 + LCU'
},
{
id: 'waf',
name: 'WAF 防火墙',
icon: '🛡️',
type: 'security',
awsService: 'AWS WAF',
aliyunService: 'Web 应用防火墙',
awsOptions: [
'托管规则和自定义规则',
'速率限制和 IP 黑名单',
'与 CloudFront/ALB 集成',
'Bot Control 机器人管理'
],
aliyunOptions: [
'内置防护策略和自定义规则',
'CC 攻击防护和 IP 封禁',
'与 CDN/SLB 无缝集成',
'数据风控和爬虫管理'
],
awsPrice: '$5/月 + $0.6/百万请求',
aliyunPrice: '¥980/月起 + 流量费'
},
{
id: 'nat',
name: 'NAT 网关',
icon: '🚪',
type: 'network',
awsService: 'NAT Gateway',
aliyunService: 'NAT 网关',
awsOptions: [
'自动高可用,无需管理',
'每个 AZ 独立部署',
'支持 SNAT 出网',
'流量监控和告警'
],
aliyunOptions: [
'多可用区容灾',
'按规格选择带宽',
'SNAT/DNAT 支持',
'流量和连接数监控'
],
awsPrice: '$0.045/小时 + $0.045/GB',
aliyunPrice: '¥0.35/小时 + 流量费'
}
]
const onDragStart = (event, component) => {
draggedItem = component
event.dataTransfer.effectAllowed = 'copy'
}
const onDrop = (event) => {
event.preventDefault()
if (draggedItem) {
canvasItems.value.push({
...draggedItem,
id: `${draggedItem.id}-${Date.now()}`
})
draggedItem = null
}
}
const itemStyle = (index) => {
const positions = [
{ top: '10%', left: '10%' },
{ top: '10%', right: '10%' },
{ bottom: '10%', left: '10%' },
{ bottom: '10%', right: '10%' },
{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }
]
return positions[index % positions.length]
}
const selectItem = (item) => {
selectedItem.value = item
}
const removeItem = (index) => {
canvasItems.value.splice(index, 1)
if (selectedItem.value && !canvasItems.value.find(i => i.id === selectedItem.value.id)) {
selectedItem.value = null
}
}
</script>
<style scoped>
.network-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.network-builder {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
margin-bottom: 20px;
}
.components-panel {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
}
.panel-title {
font-weight: 600;
font-size: 0.875rem;
color: #e6f1ff;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.component-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.component-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
cursor: grab;
transition: all 0.2s ease;
}
.component-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.component-item:active {
cursor: grabbing;
}
.component-icon {
font-size: 1.25rem;
}
.component-name {
font-size: 0.8125rem;
color: #e6f1ff;
}
.canvas-area {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
min-height: 400px;
}
.network-canvas {
position: relative;
width: 100%;
height: 400px;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 12px;
}
.empty-text {
font-size: 1rem;
color: #e6f1ff;
margin-bottom: 4px;
}
.empty-subtext {
font-size: 0.8125rem;
color: #8892b0;
}
.canvas-item {
position: absolute;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 12px 16px;
min-width: 120px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.canvas-item:hover {
background: rgba(255, 255, 255, 0.12);
transform: scale(1.05);
}
.canvas-item.network {
border-color: rgba(0, 212, 255, 0.4);
background: rgba(0, 212, 255, 0.1);
}
.canvas-item.security {
border-color: rgba(255, 99, 99, 0.4);
background: rgba(255, 99, 99, 0.1);
}
.item-icon {
font-size: 1.5rem;
margin-bottom: 4px;
}
.item-name {
font-size: 0.8125rem;
color: #e6f1ff;
font-weight: 500;
}
.remove-btn {
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #ff4444;
border: none;
border-radius: 50%;
color: #fff;
font-size: 0.875rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.canvas-item:hover .remove-btn {
opacity: 1;
}
.config-panel {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.config-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.config-icon {
font-size: 1.25rem;
}
.config-title {
font-weight: 600;
font-size: 1rem;
color: #e6f1ff;
flex: 1;
}
.close-config {
background: none;
border: none;
color: #8892b0;
font-size: 1.25rem;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.close-config:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.config-content {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 16px;
}
.config-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
padding: 16px;
}
.section-title {
font-size: 0.75rem;
color: #ff9900;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.aliyun-title {
color: #ff6a00;
}
.service-name {
font-size: 1rem;
font-weight: 600;
color: #e6f1ff;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.aliyun-service {
color: #e6f1ff;
}
.config-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-item {
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 0.8125rem;
color: #e6f1ff;
line-height: 1.4;
}
.option-check {
color: #ff9900;
font-weight: 700;
flex-shrink: 0;
}
.aliyun-check {
color: #ff6a00;
}
.config-divider {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.config-footer {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 12px 16px;
}
.price-compare {
display: flex;
justify-content: space-around;
gap: 16px;
}
.price-item {
display: flex;
align-items: center;
gap: 8px;
}
.price-label {
font-size: 0.8125rem;
color: #8892b0;
}
.price-value {
font-size: 0.875rem;
color: #e6f1ff;
font-weight: 500;
}
.aliyun-price {
color: #ff6a00;
}
@media (max-width: 768px) {
.network-builder {
grid-template-columns: 1fr;
}
.components-panel {
max-height: 200px;
overflow-y: auto;
}
.config-content {
grid-template-columns: 1fr;
}
.config-divider {
display: none;
}
.price-compare {
flex-direction: column;
gap: 8px;
}
}
</style>
@@ -0,0 +1,181 @@
<template>
<div class="pricing-model-demo">
<div class="demo-header">
<h4>云服务器计费模式计算器</h4>
<p class="demo-desc">输入您的使用场景对比不同计费模式的成本</p>
</div>
<div class="calculator-inputs">
<div class="input-section">
<h5>基础配置</h5>
<div class="input-grid">
<div class="input-group">
<label>实例规格</label>
<select v-model="config.instanceType">
<option v-for="type in instanceTypes" :key="type.value" :value="type.value">
{{ type.label }}
</option>
</select>
</div>
<div class="input-group">
<label>数量 ()</label>
<input v-model.number="config.quantity" type="number" min="1" max="100" />
</div>
</div>
</div>
<div class="input-section">
<h5>使用模式</h5>
<div class="input-grid">
<div class="input-group">
<label>每日运行时长 (小时)</label>
<input v-model.number="config.dailyHours" type="range" min="1" max="24" />
<span class="range-value">{{ config.dailyHours }} 小时</span>
</div>
<div class="input-group">
<label>每月运行天数</label>
<input v-model.number="config.monthlyDays" type="range" min="1" max="31" />
<span class="range-value">{{ config.monthlyDays }} </span>
</div>
</div>
</div>
<div class="input-section">
<h5>计费偏好</h5>
<div class="billing-options">
<label v-for="option in billingOptions" :key="option.value" class="option-card"
:class="{ active: config.billingType === option.value }">
<input type="radio" v-model="config.billingType" :value="option.value" />
<span class="option-icon">{{ option.icon }}</span>
<span class="option-name">{{ option.label }}</span>
<span class="option-desc">{{ option.desc }}</span>
</label>
</div>
</div>
</div>
<div class="cost-comparison">
<h5>成本对比分析</h5>
<div class="comparison-chart">
<div v-for="model in costComparison" :key="model.type" class="chart-bar"
:class="{ recommended: model.recommended }">
<div class="bar-label">{{ model.label }}</div>
<div class="bar-visual">
<div class="bar-fill" :style="{ height: model.percentage + '%' }"
:class="model.type"></div>
</div>
<div class="bar-value">
<span class="amount">{{ model.cost }}</span>
<span v-if="model.savings" class="savings"> {{ model.savings }}</span>
</div>
<div v-if="model.recommended" class="recommend-badge">推荐</div>
</div>
</div>
</div>
<div class="recommendation-panel">
<div class="rec-header">
<span class="rec-icon">💡</span>
<span class="rec-title">优化建议</span>
</div>
<div class="rec-content">
<div v-for="(tip, index) in optimizationTips" :key="index" class="tip-item">
<span class="tip-num">{{ index + 1 }}</span>
<span class="tip-text">{{ tip }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const config = ref({
instanceType: 'medium',
quantity: 2,
dailyHours: 12,
monthlyDays: 22,
billingType: 'ondemand'
})
const instanceTypes = [
{ value: 'small', label: '小型 (2核4G)' },
{ value: 'medium', label: '中型 (4核8G)' },
{ value: 'large', label: '大型 (8核16G)' },
{ value: 'xlarge', label: '超大型 (16核32G)' }
]
const billingOptions = [
{ value: 'ondemand', label: '按需付费', icon: '⚡', desc: '按实际使用时长计费,灵活性最高' },
{ value: 'reserved', label: '预留实例', icon: '📅', desc: '预付费用换取更低单价,适合长期稳定负载' },
{ value: 'spot', label: '抢占式', icon: '💰', desc: '利用闲置资源,价格极低但可能被回收' }
]
const hourlyRates = {
small: { ondemand: 0.05, reserved: 0.03, spot: 0.015 },
medium: { ondemand: 0.10, reserved: 0.06, spot: 0.03 },
large: { ondemand: 0.20, reserved: 0.12, spot: 0.06 },
xlarge: { ondemand: 0.40, reserved: 0.24, spot: 0.12 }
}
const costComparison = computed(() => {
const rate = hourlyRates[config.value.instanceType]
const monthlyHours = config.value.dailyHours * config.value.monthlyDays * config.value.quantity
const costs = [
{ type: 'ondemand', label: '按需付费', rate: rate.ondemand },
{ type: 'reserved', label: '预留实例', rate: rate.reserved },
{ type: 'spot', label: '抢占式', rate: rate.spot }
]
const maxCost = Math.max(...costs.map(c => c.rate * monthlyHours))
return costs.map(c => {
const cost = c.rate * monthlyHours
const percentage = (cost / maxCost) * 100
const isRecommended = c.type === config.value.billingType
const savings = c.type === 'ondemand' ? null :
Math.round(((rate.ondemand - c.rate) / rate.ondemand) * 100) + '%'
return {
type: c.type,
label: c.label,
cost: '$' + cost.toFixed(2) + '/月',
percentage,
recommended: isRecommended,
savings
}
})
})
const optimizationTips = computed(() => {
const tips = []
if (config.value.dailyHours < 8) {
tips.push('每日运行时间较短,考虑使用抢占式实例降低成本')
}
if (config.value.monthlyDays > 25) {
tips.push('月度运行天数接近全月,预留实例可节省 30-60% 成本')
}
if (config.value.quantity > 5) {
tips.push('实例数量较多,建议混合使用预留实例和按需实例')
}
if (config.value.billingType === 'ondemand' && config.value.monthlyDays > 20) {
tips.push('当前使用按需付费但负载稳定,切换预留实例可显著降低成本')
}
if (tips.length === 0) {
tips.push('当前配置较为合理,建议定期监控实际使用率进行优化')
}
return tips
})
</script>
<style scoped>
/* Add styles here */
</style>
@@ -0,0 +1,511 @@
<template>
<div class="security-services-demo">
<div class="demo-header">
<h4>安全服务架构配置器</h4>
<p class="demo-desc">选择您的业务场景一键生成安全防护方案</p>
</div>
<div class="scenario-selector">
<div class="selector-title">选择业务场景</div>
<div class="scenario-cards">
<button
v-for="scenario in scenarios"
:key="scenario.id"
class="scenario-btn"
:class="{ active: selectedScenario === scenario.id }"
@click="selectScenario(scenario.id)"
>
<span class="scenario-icon">{{ scenario.icon }}</span>
<span class="scenario-name">{{ scenario.name }}</span>
</button>
</div>
</div>
<div v-if="selectedScenarioData" class="security-architecture">
<div class="architecture-header">
<span class="header-icon">🏗</span>
<span class="header-title">推荐安全架构</span>
</div>
<div class="architecture-layers">
<div class="layer edge-layer">
<div class="layer-title">
<span class="layer-icon">🌐</span>
边缘防护层
</div>
<div class="layer-services">
<div class="service-card">
<div class="service-header aws">
<span class="service-name">{{ selectedScenarioData.edge.aws }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.edge.awsFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
<div class="vs-mini">VS</div>
<div class="service-card">
<div class="service-header aliyun">
<span class="service-name">{{ selectedScenarioData.edge.aliyun }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.edge.aliyunFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
</div>
</div>
<div class="layer application-layer">
<div class="layer-title">
<span class="layer-icon">🔐</span>
应用安全层
</div>
<div class="layer-services">
<div class="service-card">
<div class="service-header aws">
<span class="service-name">{{ selectedScenarioData.app.aws }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.app.awsFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
<div class="vs-mini">VS</div>
<div class="service-card">
<div class="service-header aliyun">
<span class="service-name">{{ selectedScenarioData.app.aliyun }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.app.aliyunFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
</div>
</div>
<div class="layer data-layer">
<div class="layer-title">
<span class="layer-icon">🗝</span>
数据安全层
</div>
<div class="layer-services">
<div class="service-card">
<div class="service-header aws">
<span class="service-name">{{ selectedScenarioData.data.aws }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.data.awsFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
<div class="vs-mini">VS</div>
<div class="service-card">
<div class="service-header aliyun">
<span class="service-name">{{ selectedScenarioData.data.aliyun }}</span>
</div>
<div class="service-features">
<div v-for="(feat, idx) in selectedScenarioData.data.aliyunFeatures" :key="idx" class="feature">
{{ feat }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="security-recommendations">
<div class="rec-title">💡 安全建议</div>
<div class="rec-list">
<div v-for="(rec, idx) in selectedScenarioData.recommendations" :key="idx" class="rec-item">
<span class="rec-num">{{ idx + 1 }}</span>
<span class="rec-text">{{ rec }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedScenario = ref('web')
const scenarios = [
{ id: 'web', name: 'Web 应用', icon: '🌐' },
{ id: 'api', name: 'API 服务', icon: '🔌' },
{ id: 'mobile', name: '移动应用', icon: '📱' },
{ id: 'enterprise', name: '企业系统', icon: '🏢' }
]
const scenarioData = {
web: {
edge: {
aws: 'CloudFront + WAF',
aliyun: 'CDN + WAF',
awsFeatures: ['全球 400+ 边缘节点', 'DDoS 防护和 Bot 管理', '自动 SSL/TLS 加密'],
aliyunFeatures: ['国内 2800+ 节点', 'CC 攻击防护和防爬虫', 'HTTPS 证书一键部署']
},
app: {
aws: 'AWS WAF + Shield',
aliyun: 'Web 应用防火墙',
awsFeatures: ['SQL 注入和 XSS 防护', '速率限制和 IP 黑名单', '托管规则和自定义规则'],
aliyunFeatures: ['OWASP Top 10 防护', '敏感数据防泄漏', '智能 CC 防护策略']
},
data: {
aws: 'KMS + Secrets Manager',
aliyun: 'KMS + 凭据管家',
awsFeatures: ['AES-256 加密算法', '自动密钥轮换', '与 AWS 服务原生集成'],
aliyunFeatures: ['国密算法支持', '密钥版本管理', 'RAM 细粒度权限控制']
},
recommendations: [
'启用 HTTPS 强制跳转,配置 HSTS 头部',
'设置 WAF 规则防御 SQL 注入、XSS 等常见攻击',
'启用 CDN 缓存静态资源,减少源站压力',
'配置敏感数据加密存储,使用 KMS 管理密钥'
]
},
api: {
edge: {
aws: 'API Gateway + WAF',
aliyun: 'API 网关 + WAF',
awsFeatures: ['API 版本管理和流量控制', '缓存和节流策略', '请求/响应转换'],
aliyunFeatures: ['API 发布和生命周期管理', '流量控制和访问频次限制', '参数校验和Mock 数据']
},
app: {
aws: 'Cognito + IAM',
aliyun: '应用身份服务 + RAM',
awsFeatures: ['OAuth 2.0 和 OpenID Connect', '用户池和身份池', 'MFA 多因素认证'],
aliyunFeatures: ['OIDC 和 SAML 协议支持', '企业 AD/LDAP 集成', '实人认证和设备指纹']
},
data: {
aws: 'KMS + Parameter Store',
aliyun: 'KMS + 应用配置管理',
awsFeatures: ['API 密钥加密存储', '配置参数版本管理', '与 CloudFormation 集成'],
aliyunFeatures: ['敏感配置加密', '配置灰度发布', '配置变更审计']
},
recommendations: [
'实施 API 认证鉴权,使用 OAuth 2.0 或 API Key',
'配置 API 网关的速率限制,防止暴力破解',
'对敏感 API 实施 IP 白名单限制',
'加密存储 API 密钥和敏感配置'
]
},
mobile: {
edge: {
aws: 'CloudFront + WAF',
aliyun: 'CDN + WAF',
awsFeatures: ['移动网络优化', 'HTTP/2 和 QUIC 支持', '智能压缩和图像优化'],
aliyunFeatures: ['移动加速方案', '弱网环境优化', '自适应码率调整']
},
app: {
aws: 'Cognito + Device Farm',
aliyun: '应用身份服务 + 移动测试',
awsFeatures: ['设备指纹识别', '设备风险评估', '越狱/Root 检测'],
aliyunFeatures: ['设备可信认证', '作弊设备识别', '安全键盘输入']
},
data: {
aws: 'KMS + S3',
aliyun: 'KMS + OSS',
awsFeatures: ['移动端数据加密', '本地缓存加密', '密钥安全存储'],
aliyunFeatures: ['国密 SM4 支持', '本地数据库加密', '密钥白盒保护']
},
recommendations: [
'实施设备绑定和设备指纹识别',
'检测越狱/Root 设备并限制访问',
'本地敏感数据加密存储',
'使用 HTTPS 证书绑定防止中间人攻击'
]
},
enterprise: {
edge: {
aws: 'CloudFront + WAF + Shield Advanced',
aliyun: 'CDN + WAF + DDoS 高防',
awsFeatures: ['DDoS 攻击自动缓解', '24/7 DRT 团队支持', '成本保护保障'],
aliyunFeatures: ['T 级 DDoS 防护能力', 'CC 攻击智能清洗', '专家应急响应']
},
app: {
aws: 'IAM + SSO + Directory Service',
aliyun: 'RAM + IDaaS + 云 SSO',
awsFeatures: ['与企业 AD 集成', '单点登录 SSO', '临时凭证和权限边界'],
aliyunFeatures: ['LDAP/AD 目录同步', 'SaaS 应用集成', '细粒度权限管控']
},
data: {
aws: 'KMS + CloudHSM + Macie',
aliyun: 'KMS + 加密服务 + 敏感数据保护',
awsFeatures: ['FIPS 140-2 Level 3 HSM', '自动敏感数据发现', '密钥分级管理'],
aliyunFeatures: ['国密局认证 HSM', '敏感数据自动识别', '合规审计报告']
},
recommendations: [
'部署 DDoS 高防和 WAF 多层防护',
'实施统一身份管理和 SSO 单点登录',
'启用数据加密和敏感数据保护',
'建立安全审计和合规监控体系'
]
}
}
const selectScenario = (id) => {
selectedScenario.value = id
}
const currentScenario = computed(() => scenarioData[selectedScenario.value])
</script>
<style scoped>
.network-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.scenario-selector {
margin-bottom: 20px;
}
.selector-title {
font-size: 0.9375rem;
font-weight: 500;
color: #e6f1ff;
margin-bottom: 12px;
}
.scenario-cards {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.scenario-btn {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e6f1ff;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.scenario-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.scenario-btn.active {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-color: transparent;
color: #fff;
}
.scenario-icon {
font-size: 1rem;
}
.security-architecture {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 20px;
}
.architecture-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-icon {
font-size: 1.25rem;
}
.header-title {
font-weight: 600;
font-size: 1rem;
color: #e6f1ff;
}
.architecture-layers {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
padding: 16px;
}
.layer-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.9375rem;
color: #e6f1ff;
margin-bottom: 12px;
}
.layer-icon {
font-size: 1.25rem;
}
.layer-services {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 12px;
align-items: start;
}
.service-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
overflow: hidden;
}
.service-header {
padding: 10px 12px;
font-weight: 600;
font-size: 0.875rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.service-header.aws {
background: rgba(255, 153, 0, 0.2);
color: #ff9900;
}
.service-header.aliyun {
background: rgba(255, 106, 0, 0.2);
color: #ff6a00;
}
.service-features {
padding: 12px;
}
.feature {
font-size: 0.8125rem;
color: #e6f1ff;
padding: 4px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.feature:last-child {
border-bottom: none;
}
.vs-mini {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
color: #fff;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 0.625rem;
font-weight: 700;
align-self: center;
}
.security-recommendations {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.rec-title {
font-weight: 600;
font-size: 1rem;
color: #00d4ff;
margin-bottom: 12px;
}
.rec-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.rec-item {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(255, 255, 255, 0.03);
padding: 10px 12px;
border-radius: 8px;
border-left: 3px solid #00d4ff;
}
.rec-num {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
color: #fff;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
flex-shrink: 0;
}
.rec-text {
font-size: 0.875rem;
color: #e6f1ff;
line-height: 1.5;
}
@media (max-width: 768px) {
.scenario-cards {
grid-template-columns: repeat(2, 1fr);
}
.layer-services {
grid-template-columns: 1fr;
}
.vs-mini {
display: none;
}
.config-content {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,214 @@
<template>
<div class="service-selection-demo">
<div class="demo-header">
<h4>云服务选型决策树</h4>
<p class="demo-desc">回答几个简单问题获取最适合您的云服务方案</p>
</div>
<div v-if="!result" class="decision-flow">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<div class="question-card">
<div class="question-number">问题 {{ currentStep + 1 }}/{{ questions.length }}</div>
<h5 class="question-text">{{ currentQuestion.text }}</h5>
<div class="options-list">
<button
v-for="option in currentQuestion.options"
:key="option.value"
class="option-btn"
@click="selectOption(option)"
>
<span class="option-icon">{{ option.icon }}</span>
<span class="option-text">{{ option.text }}</span>
<span class="option-desc">{{ option.desc }}</span>
</button>
</div>
</div>
</div>
<div v-else class="result-panel">
<div class="result-header">
<span class="result-icon">🎯</span>
<h5>推荐方案</h5>
</div>
<div class="recommendation-cards">
<div class="rec-card primary">
<div class="rec-badge">最佳匹配</div>
<div class="rec-icon">{{ result.primary.icon }}</div>
<div class="rec-title">{{ result.primary.name }}</div>
<div class="rec-services">
<span class="service aws">{{ result.primary.aws }}</span>
<span class="vs">vs</span>
<span class="service aliyun">{{ result.primary.aliyun }}</span>
</div>
<div class="rec-reason">{{ result.primary.reason }}</div>
</div>
<div class="rec-card secondary">
<div class="rec-badge alt">备选</div>
<div class="rec-icon">{{ result.secondary.icon }}</div>
<div class="rec-title">{{ result.secondary.name }}</div>
<div class="rec-services">
<span class="service aws">{{ result.secondary.aws }}</span>
<span class="vs">vs</span>
<span class="service aliyun">{{ result.secondary.aliyun }}</span>
</div>
<div class="rec-reason">{{ result.secondary.reason }}</div>
</div>
</div>
<div class="result-actions">
<button class="restart-btn" @click="restart">
<span></span> 重新测试
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const answers = ref([])
const questions = [
{
text: '您的应用主要面向哪个地区?',
options: [
{ value: 'global', icon: '🌍', text: '全球用户', desc: '需要覆盖多个国家和地区' },
{ value: 'china', icon: '🇨🇳', text: '中国大陆', desc: '主要服务国内用户' },
{ value: 'asia', icon: '🌏', text: '亚太区域', desc: '覆盖亚洲及太平洋地区' },
{ value: 'us', icon: '🇺🇸', text: '北美/欧洲', desc: '主要服务欧美用户' }
]
},
{
text: '您的应用对计算资源的需求如何?',
options: [
{ value: 'serverless', icon: '⚡', text: '事件驱动/无服务器', desc: '按需运行,流量波动大' },
{ value: 'webapp', icon: '🌐', text: 'Web 应用服务', desc: '需要 24/7 在线运行' },
{ value: 'batch', icon: '📊', text: '批处理/计算任务', desc: '定时或按需批量执行' },
{ value: 'hpc', icon: '🔬', text: '高性能计算', desc: '需要 GPU 或大规模集群' }
]
},
{
text: '您对成本优化的优先级是?',
options: [
{ value: 'lowest', icon: '💰', text: '极致成本优化', desc: '可以接受复杂配置换取最低价' },
{ value: 'balanced', icon: '⚖️', text: '平衡型', desc: '在成本和易用性间找平衡' },
{ value: 'stable', icon: '📈', text: '成本可预测', desc: '偏好固定成本,方便预算' },
{ value: 'premium', icon: '💎', text: '性能优先', desc: '成本次之,追求最佳性能' }
]
},
{
text: '您的数据存储需求主要是?',
options: [
{ value: 'object', icon: '📦', text: '对象存储(文件/图片/视频)', desc: '海量非结构化数据' },
{ value: 'database', icon: '🗄️', text: '数据库存储', desc: '结构化数据和事务处理' },
{ value: 'cache', icon: '⚡', text: '缓存/会话存储', desc: '高性能临时数据存储' },
{ value: 'mixed', icon: '🔀', text: '混合存储', desc: '多种存储类型组合' }
]
}
]
const progress = computed(() => {
return ((currentStep.value + 1) / questions.length) * 100
})
const currentQuestion = computed(() => {
return questions[currentStep.value]
})
const selectOption = (option) => {
answers.value.push(option.value)
if (currentStep.value < questions.length - 1) {
currentStep.value++
}
}
const result = computed(() => {
if (answers.value.length < 4) return null
const [region, compute, cost, storage] = answers.value
// 计算推荐
let primary, secondary
if (compute === 'serverless') {
primary = {
icon: '⚡',
name: '无服务器架构',
aws: 'AWS Lambda + API Gateway',
aliyun: '函数计算 + API 网关',
reason: '事件驱动场景下,按调用计费,无需预置服务器资源'
}
secondary = {
icon: '🔲',
name: '容器服务',
aws: 'AWS Fargate',
aliyun: 'Serverless Kubernetes',
reason: '需要长时间运行但需要灵活扩缩容的场景'
}
} else if (compute === 'hpc') {
primary = {
icon: '🔬',
name: '高性能计算集群',
aws: 'AWS ParallelCluster',
aliyun: 'E-HPC + 超级计算集群',
reason: 'GPU 实例和高速互联网络,满足科学计算和 AI 训练需求'
}
secondary = {
icon: '⚡',
name: '弹性裸金属',
aws: 'EC2 Bare Metal',
aliyun: '弹性裸金属服务器',
reason: '需要物理机性能但希望云化管理的场景'
}
} else if (cost === 'lowest') {
primary = {
icon: '💰',
name: '抢占式实例',
aws: 'EC2 Spot Instances',
aliyun: '抢占式实例',
reason: '价格最低至按需实例的 10%,适合容错性高的批处理任务'
}
secondary = {
icon: '📅',
name: '预留实例',
aws: 'Reserved Instances',
aliyun: '包年包月',
reason: '长期稳定负载选择预留实例,可节省 30-60% 成本'
}
} else {
primary = {
icon: '☁️',
name: '云服务器 ECS',
aws: 'Amazon EC2',
aliyun: 'ECS 云服务器',
reason: '最通用的计算服务,支持多种计费模式和实例规格,生态完善'
}
secondary = {
icon: '📦',
name: '容器实例',
aws: 'AWS Fargate',
aliyun: 'ECI 容器实例',
reason: '无需管理服务器,直接运行容器,适合微服务架构'
}
}
return { primary, secondary }
})
const restart = () => {
currentStep.value = 0
answers.value = []
}
</script>
<style scoped>
/* Add styles here */
</style>
@@ -0,0 +1,489 @@
<template>
<div class="storage-services-demo">
<div class="demo-header">
<h4>存储服务选型助手</h4>
<p class="demo-desc">根据您的使用场景推荐最适合的存储方案</p>
</div>
<div class="scenario-selector">
<div class="selector-title">选择您的主要使用场景</div>
<div class="scenario-grid">
<button
v-for="scenario in scenarios"
:key="scenario.id"
class="scenario-card"
:class="{ active: selectedScenario === scenario.id }"
@click="selectScenario(scenario.id)"
>
<div class="scenario-icon">{{ scenario.icon }}</div>
<div class="scenario-name">{{ scenario.name }}</div>
<div class="scenario-desc">{{ scenario.shortDesc }}</div>
</button>
</div>
</div>
<div v-if="selectedScenario" class="recommendation-result">
<div class="result-header">
<span class="result-icon">🎯</span>
<span class="result-title">推荐方案</span>
</div>
<div class="storage-comparison">
<div class="provider-card aws">
<div class="provider-header">
<div class="provider-logo">AWS</div>
<div class="provider-service">{{ currentScenario.awsService }}</div>
</div>
<div class="provider-features">
<div v-for="(feature, idx) in currentScenario.awsFeatures" :key="idx" class="feature-item">
<span class="check"></span>
<span>{{ feature }}</span>
</div>
</div>
<div class="provider-pricing">
<div class="price-label">定价模式</div>
<div class="price-value">{{ currentScenario.awsPricing }}</div>
</div>
</div>
<div class="vs-divider">
<div class="vs-line"></div>
<div class="vs-badge">VS</div>
<div class="vs-line"></div>
</div>
<div class="provider-card aliyun">
<div class="provider-header">
<div class="provider-logo aliyun-logo">阿里云</div>
<div class="provider-service">{{ currentScenario.aliyunService }}</div>
</div>
<div class="provider-features">
<div v-for="(feature, idx) in currentScenario.aliyunFeatures" :key="idx" class="feature-item">
<span class="check aliyun-check"></span>
<span>{{ feature }}</span>
</div>
</div>
<div class="provider-pricing">
<div class="price-label">定价模式</div>
<div class="price-value">{{ currentScenario.aliyunPricing }}</div>
</div>
</div>
</div>
<div class="decision-guide">
<div class="guide-title">🤔 如何选择</div>
<div class="guide-content">
<div class="guide-item">
<div class="guide-condition">选择 AWS 如果</div>
<div class="guide-reason">{{ currentScenario.chooseAwsWhen }}</div>
</div>
<div class="guide-item">
<div class="guide-condition">选择阿里云如果</div>
<div class="guide-reason">{{ currentScenario.chooseAliyunWhen }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedScenario = ref(null)
const scenarios = [
{
id: 'website',
name: '静态网站托管',
icon: '🌐',
shortDesc: '托管 HTML/CSS/JS 等静态资源',
awsService: 'Amazon S3 + CloudFront',
aliyunService: 'OSS + CDN',
awsFeatures: [
'全球 400+ 边缘节点加速',
'自动压缩和 HTTP/2 支持',
'与 Route 53 无缝集成',
'支持静态网站托管配置'
],
aliyunFeatures: [
'国内 2800+ 节点覆盖',
'智能压缩和 QUIC 协议支持',
'与万网域名一键绑定',
'实时日志分析和监控'
],
awsPricing: '存储 $0.023/GB/月 + 流量 $0.085-0.12/GB',
aliyunPricing: '存储 ¥0.12/GB/月 + 流量 ¥0.24-0.80/GB',
chooseAwsWhen: '用户主要在海外,需要全球加速,或已使用 AWS 其他服务',
chooseAliyunWhen: '用户主要在中国大陆,需要备案支持,追求国内访问速度'
},
{
id: 'database',
name: '数据库存储',
icon: '🗄️',
shortDesc: '关系型和非关系型数据库',
awsService: 'Amazon RDS/Aurora',
aliyunService: 'RDS/PolarDB',
awsFeatures: [
'Aurora 性能是 MySQL 的 5 倍',
'自动故障转移和读副本',
'支持 6 种数据库引擎',
'Serverless 自动扩缩容'
],
aliyunFeatures: [
'PolarDB 计算存储分离架构',
'一写多读,读写分离',
'秒级备份和恢复',
'Oracle 语法兼容模式'
],
awsPricing: '按需 $0.017-0.68/小时,预留可省 40-60%',
aliyunPricing: '按量 ¥0.12-4.8/小时,包年包月更优惠',
chooseAwsWhen: '需要 Aurora 的高性能,或有多种数据库引擎需求',
chooseAliyunWhen: '需要 Oracle 兼容,或追求性价比和本地化支持'
},
{
id: 'backup',
name: '备份与归档',
icon: '💾',
shortDesc: '冷数据和长期归档存储',
awsService: 'Amazon S3 Glacier',
aliyunService: 'OSS 归档存储',
awsFeatures: [
'Glacier Deep Archive cheapest',
'检索时间从分钟到小时可选',
'S3 生命周期策略自动迁移',
'WORM 合规保留策略'
],
aliyunFeatures: [
'归档存储单价行业最低',
'解冻时间可配置',
'跨地域冗余存储',
'符合国内合规要求'
],
awsPricing: 'Glacier $0.004/GB/月,Deep Archive $0.00099/GB/月',
aliyunPricing: '归档存储 ¥0.033/GB/月,冷归档更低',
chooseAwsWhen: '需要 Deep Archive 超低成本,或有复杂生命周期策略',
chooseAliyunWhen: '数据需在国内归档,或追求极致性价比'
},
{
id: 'media',
name: '媒体处理',
icon: '🎬',
shortDesc: '音视频存储和分发',
awsService: 'S3 + Elemental',
aliyunService: 'OSS + 媒体处理',
awsFeatures: [
'Elemental 专业级视频处理',
'MediaConvert 格式转码',
'MediaLive 直播流处理',
'CloudFront 低延迟分发'
],
aliyunFeatures: [
'视频截帧、转码、水印',
'智能封面和内容审核',
'直播录制和时移回看',
'CDN 全球加速分发'
],
awsPricing: '按使用量计费,转码 $0.007-0.1/分钟',
aliyunPricing: '按量计费,转码 ¥0.03-0.5/分钟',
chooseAwsWhen: '需要广播级专业处理,或全球直播分发',
chooseAliyunWhen: '需要智能内容审核,或国内视频处理'
}
]
const selectScenario = (id) => {
selectedScenario.value = id
}
const currentScenario = computed(() => {
return scenarios.find(s => s.id === selectedScenario.value)
})
</script>
<style scoped>
.storage-services-demo {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
font-size: 1.25rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-desc {
margin: 0;
color: #8892b0;
font-size: 0.875rem;
}
.scenario-selector {
margin-bottom: 24px;
}
.selector-title {
font-size: 0.9375rem;
font-weight: 500;
color: #e6f1ff;
margin-bottom: 12px;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.scenario-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
}
.scenario-card:hover {
background: rgba(255, 255, 255, 0.06);
transform: translateY(-2px);
}
.scenario-card.active {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.15), rgba(123, 44, 191, 0.15));
border-color: rgba(0, 212, 255, 0.3);
}
.scenario-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.scenario-name {
font-weight: 600;
font-size: 0.9375rem;
color: #e6f1ff;
margin-bottom: 4px;
}
.scenario-desc {
font-size: 0.75rem;
color: #8892b0;
}
.recommendation-result {
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.result-icon {
font-size: 1.25rem;
}
.result-title {
font-weight: 600;
font-size: 1rem;
color: #e6f1ff;
}
.storage-comparison {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 12px;
margin-bottom: 20px;
}
.provider-card {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.provider-card.aws {
border-top: 3px solid #ff9900;
}
.provider-card.aliyun {
border-top: 3px solid #ff6a00;
}
.provider-header {
text-align: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.provider-logo {
font-size: 1.25rem;
font-weight: 700;
color: #ff9900;
}
.provider-logo.aliyun-logo {
color: #ff6a00;
}
.provider-service {
font-size: 0.8125rem;
color: #8892b0;
margin-top: 4px;
}
.provider-features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 0.8125rem;
color: #e6f1ff;
line-height: 1.4;
}
.check {
color: #ff9900;
font-weight: 700;
flex-shrink: 0;
}
.aliyun-check {
color: #ff6a00;
}
.provider-pricing {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 10px;
}
.price-label {
font-size: 0.75rem;
color: #8892b0;
margin-bottom: 4px;
}
.price-value {
font-size: 0.8125rem;
color: #e6f1ff;
font-weight: 500;
}
.vs-divider {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.vs-line {
width: 1px;
flex: 1;
background: rgba(255, 255, 255, 0.1);
}
.vs-badge {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
color: #fff;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
}
.decision-guide {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 16px;
}
.guide-title {
font-weight: 600;
font-size: 1rem;
color: #e6f1ff;
margin-bottom: 12px;
}
.guide-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.guide-item {
padding: 12px;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border-left: 3px solid #00d4ff;
}
.guide-condition {
font-size: 0.8125rem;
color: #00d4ff;
font-weight: 500;
margin-bottom: 4px;
}
.guide-reason {
font-size: 0.875rem;
color: #e6f1ff;
line-height: 1.5;
}
@media (max-width: 768px) {
.scenario-grid {
grid-template-columns: 1fr;
}
.storage-comparison {
grid-template-columns: 1fr;
gap: 16px;
}
.vs-divider {
display: none;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
访问分析演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('访问分析演示')
const description = ref('展示CDN和对象存储的访问统计分析,包括流量、带宽、访问热点等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存策略演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('缓存策略演示')
const description = ref('展示CDN和对象存储的缓存策略配置,包括缓存时间、刷新机制等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,696 @@
<!--
CdnAccelerationDemo.vue
CDN 加速原理演示 - 展示边缘节点源站回源等概念
-->
<template>
<div class="cdn-acceleration-demo">
<div class="header">
<div class="title">CDN 加速原理</div>
<div class="subtitle">边缘节点源站与回源的协同工作</div>
</div>
<div class="cdn-architecture">
<!-- 用户层 -->
<div class="layer users-layer">
<div class="layer-title">
<span class="icon">👥</span>
<span>全球用户</span>
</div>
<div class="users-map">
<div
v-for="user in users"
:key="user.id"
class="user-marker"
:class="{ active: activeUser === user.id, requesting: requestingUser === user.id }"
:style="{ left: user.x + '%', top: user.y + '%' }"
@click="selectUser(user)"
>
<div class="user-icon">{{ user.icon }}</div>
<div class="user-label">{{ user.name }}</div>
</div>
<!-- 请求动画线 -->
<div v-if="requestAnimation" class="request-line" :style="requestLineStyle"></div>
</div>
</div>
<!-- 边缘节点层 -->
<div class="layer edge-layer">
<div class="layer-title">
<span class="icon">🌐</span>
<span>CDN 边缘节点 (Edge Nodes)</span>
<span class="layer-status" :class="{ hit: cacheHit, miss: !cacheHit && showCacheStatus }">
{{ cacheStatusText }}
</span>
</div>
<div class="edge-nodes">
<div
v-for="node in edgeNodes"
:key="node.id"
class="edge-node"
:class="{ active: activeNode === node.id, serving: servingNode === node.id }"
@click="selectNode(node)"
>
<div class="node-icon">{{ node.icon }}</div>
<div class="node-info">
<div class="node-name">{{ node.name }}</div>
<div class="node-location">{{ node.location }}</div>
</div>
<div class="node-stats">
<div class="stat">
<span class="stat-label">缓存</span>
<span class="stat-value">{{ node.cacheSize }}</span>
</div>
<div class="stat">
<span class="stat-label">命中</span>
<span class="stat-value" :style="{ color: node.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
{{ node.hitRate }}%
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 源站层 -->
<div class="layer origin-layer">
<div class="layer-title">
<span class="icon">🏢</span>
<span>源站 (Origin Server)</span>
<span class="layer-status" :class="{ active: showBackToSource }">
{{ backToSourceText }}
</span>
</div>
<div class="origin-servers">
<div class="origin-server">
<div class="server-icon">🗄</div>
<div class="server-info">
<div class="server-name">对象存储源站</div>
<div class="server-address">bucket.oss-cn-beijing.aliyuncs.com</div>
</div>
<div class="server-status">
<span class="status-dot active"></span>
<span class="status-text">健康</span>
</div>
</div>
<div v-if="showBackToSource" class="back-to-source-flow">
<div class="flow-arrow">
<span> 回源请求</span>
</div>
<div class="flow-detail">
<div class="flow-step">1. CDN 节点未命中缓存</div>
<div class="flow-step">2. 向源站发起回源请求</div>
<div class="flow-step">3. 源站返回文件内容</div>
<div class="flow-step">4. CDN 缓存并响应用户</div>
</div>
</div>
</div>
</div>
</div>
<!-- 交互控制区 -->
<div class="demo-controls">
<div class="controls-title">🎮 模拟演示</div>
<div class="controls-row">
<button class="control-btn" @click="simulateCacheHit">
<span></span>
<span>模拟缓存命中</span>
</button>
<button class="control-btn" @click="simulateCacheMiss">
<span></span>
<span>模拟缓存未命中回源</span>
</button>
<button class="control-btn reset" @click="resetDemo">
<span>🔄</span>
<span>重置</span>
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-panel">
<div class="stats-title">📊 访问统计</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" :style="{ color: '#22c55e' }">{{ stats.cacheHit }}</div>
<div class="stat-label">缓存命中</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: '#ef4444' }">{{ stats.cacheMiss }}</div>
<div class="stat-label">缓存未命中</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
{{ stats.hitRate }}%
</div>
<div class="stat-label">命中率</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: '#3b82f6' }">{{ stats.avgResponseTime }}ms</div>
<div class="stat-label">平均响应</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
// 用户数据
const users = [
{ id: 'user1', name: '北京用户', icon: '👤', x: 75, y: 35 },
{ id: 'user2', name: '上海用户', icon: '👤', x: 80, y: 55 },
{ id: 'user3', name: '广州用户', icon: '👤', x: 70, y: 75 },
{ id: 'user4', name: '成都用户', icon: '👤', x: 50, y: 60 },
{ id: 'user5', name: '海外用户', icon: '👤', x: 90, y: 25 }
]
// 边缘节点数据
const edgeNodes = [
{ id: 'node1', name: '北京节点', icon: '🌐', location: '华北', cacheSize: '2.5 TB', hitRate: 92 },
{ id: 'node2', name: '上海节点', icon: '🌐', location: '华东', cacheSize: '3.1 TB', hitRate: 89 },
{ id: 'node3', name: '广州节点', icon: '🌐', location: '华南', cacheSize: '1.8 TB', hitRate: 87 },
{ id: 'node4', name: '成都节点', icon: '🌐', location: '西南', cacheSize: '1.2 TB', hitRate: 85 }
]
// 状态
const activeUser = ref(null)
const requestingUser = ref(null)
const activeNode = ref(null)
const servingNode = ref(null)
const cacheHit = ref(false)
const showCacheStatus = ref(false)
const showBackToSource = ref(false)
const requestAnimation = ref(false)
// 统计
const stats = reactive({
cacheHit: 0,
cacheMiss: 0,
hitRate: 0,
avgResponseTime: 0
})
// 计算属性
const requestLineStyle = computed(() => {
if (!activeUser.value || !activeNode.value) return {}
// 这里简化处理,实际应该计算从用户到节点的线
return {}
})
const cacheStatusText = computed(() => {
if (!showCacheStatus.value) return ''
return cacheHit.value ? '✅ 缓存命中' : '❌ 未命中'
})
const backToSourceText = computed(() => {
if (!showBackToSource.value) return ''
return '📥 回源中...'
})
// 方法
const selectUser = (user) => {
activeUser.value = user.id
}
const selectNode = (node) => {
activeNode.value = node.id
}
const simulateCacheHit = () => {
resetDemo()
stats.cacheHit++
updateStats()
// 模拟缓存命中流程
activeUser.value = 'user1'
requestingUser.value = 'user1'
activeNode.value = 'node1'
servingNode.value = 'node1'
setTimeout(() => {
showCacheStatus.value = true
cacheHit.value = true
}, 500)
}
const simulateCacheMiss = () => {
resetDemo()
stats.cacheMiss++
updateStats()
// 模拟缓存未命中(回源)流程
activeUser.value = 'user3'
requestingUser.value = 'user3'
activeNode.value = 'node3'
servingNode.value = 'node3'
setTimeout(() => {
showCacheStatus.value = true
cacheHit.value = false
showBackToSource.value = true
}, 500)
}
const updateStats = () => {
const total = stats.cacheHit + stats.cacheMiss
stats.hitRate = total > 0 ? Math.round((stats.cacheHit / total) * 100) : 0
// 模拟平均响应时间:命中约 20ms,未命中约 200ms
stats.avgResponseTime = total > 0
? Math.round((stats.cacheHit * 20 + stats.cacheMiss * 200) / total)
: 0
}
const resetDemo = () => {
activeUser.value = null
requestingUser.value = null
activeNode.value = null
servingNode.value = null
cacheHit.value = false
showCacheStatus.value = false
showBackToSource.value = false
requestAnimation.value = false
}
</script>
<style scoped>
.cdn-acceleration-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.cdn-architecture {
display: flex;
flex-direction: column;
gap: 1rem;
}
.layer {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
}
.layer-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.layer-title .icon {
font-size: 1.2rem;
}
.layer-status {
margin-left: auto;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 999px;
font-weight: 600;
}
.layer-status.hit {
background: #dcfce7;
color: #166534;
}
.layer-status.miss {
background: #fee2e2;
color: #991b1b;
}
.layer-status.active {
background: #dbeafe;
color: #1e40af;
}
/* 用户层 */
.users-map {
position: relative;
height: 120px;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
border-radius: 8px;
border: 1px solid #bae6fd;
overflow: hidden;
}
.user-marker {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.3s;
transform: translate(-50%, -50%);
}
.user-marker:hover {
transform: translate(-50%, -50%) scale(1.1);
}
.user-marker.active {
z-index: 10;
}
.user-marker.requesting .user-icon {
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.user-icon {
font-size: 1.5rem;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.user-label {
font-size: 0.65rem;
font-weight: 600;
color: #0369a1;
margin-top: 0.25rem;
white-space: nowrap;
background: rgba(255, 255, 255, 0.9);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
/* 边缘节点层 */
.edge-nodes {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
@media (max-width: 640px) {
.edge-nodes {
grid-template-columns: 1fr;
}
}
.edge-node {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.edge-node:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
}
.edge-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.edge-node.serving {
animation: servingPulse 1s ease-in-out;
}
@keyframes servingPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
50% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0.3); }
}
.node-icon {
font-size: 1.5rem;
}
.node-info {
flex: 1;
min-width: 0;
}
.node-name {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.node-location {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.node-stats {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.7rem;
min-width: 60px;
}
.stat {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.stat-label {
color: var(--vp-c-text-2);
}
.stat-value {
font-weight: 600;
color: var(--vp-c-text-1);
}
/* 源站层 */
.origin-servers {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.origin-server {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
border-radius: 8px;
}
.server-icon {
font-size: 1.75rem;
}
.server-info {
flex: 1;
min-width: 0;
}
.server-name {
font-weight: 600;
font-size: 0.9rem;
color: #92400e;
}
.server-address {
font-size: 0.75rem;
color: #b45309;
font-family: var(--vp-font-family-mono);
}
.server-status {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.75rem;
font-weight: 600;
color: #15803d;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
animation: statusPulse 2s infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.back-to-source-flow {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 1rem;
margin-top: 0.5rem;
}
.flow-arrow {
text-align: center;
font-size: 0.9rem;
font-weight: 600;
color: #dc2626;
margin-bottom: 0.5rem;
}
.flow-detail {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
@media (max-width: 640px) {
.flow-detail {
grid-template-columns: 1fr;
}
}
.flow-step {
font-size: 0.75rem;
color: #991b1b;
background: white;
padding: 0.4rem 0.6rem;
border-radius: 4px;
border-left: 3px solid #dc2626;
}
/* 控制区 */
.demo-controls {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
margin-top: 1.5rem;
}
.controls-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.controls-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.control-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
color: var(--vp-c-text-1);
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
background: var(--vp-c-bg-mute);
border-color: var(--vp-c-brand);
}
.control-btn.reset {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
}
.control-btn.reset:hover {
background: #fee2e2;
}
/* 统计面板 */
.stats-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
margin-top: 1rem;
}
.stats-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.75rem;
text-align: center;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
边缘节点分布演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('边缘节点分布演示')
const description = ref('展示CDN边缘节点在全球的分布情况和调度策略')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
HTTPS优化演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('HTTPS优化演示')
const description = ref('展示CDN的HTTPS优化技术,包括TLS握手优化、证书管理、HSTS等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,590 @@
<!--
ObjectStorageDemo.vue
对象存储架构演示 - 展示桶对象元数据的核心概念
-->
<template>
<div class="object-storage-demo">
<div class="header">
<div class="title">对象存储架构</div>
<div class="subtitle">理解 BucketObject Metadata 的关系</div>
</div>
<div class="storage-architecture">
<!-- 账户层 -->
<div class="account-layer">
<div class="account-icon">👤</div>
<div class="account-name">云账户 (Account)</div>
<div class="account-desc">管理权限计费全局配置</div>
</div>
<div class="connector"></div>
<!-- 桶层 -->
<div class="buckets-container">
<div class="section-title">
<span>📦</span>
<span>存储桶 (Buckets)</span>
<span class="section-desc">命名空间隔离权限控制</span>
</div>
<div class="buckets-row">
<div
v-for="bucket in buckets"
:key="bucket.name"
class="bucket-card"
:class="{ active: selectedBucket === bucket.name }"
@click="selectBucket(bucket.name)"
>
<div class="bucket-icon">{{ bucket.icon }}</div>
<div class="bucket-name">{{ bucket.name }}</div>
<div class="bucket-meta">{{ bucket.objects }} 对象</div>
<div class="bucket-size">{{ bucket.size }}</div>
</div>
</div>
</div>
<div class="connector"></div>
<!-- 对象层 -->
<div class="objects-container">
<div class="section-title">
<span>📄</span>
<span>对象 (Objects)</span>
<span class="section-desc">文件数据 + 元数据</span>
</div>
<div v-if="selectedBucket" class="objects-list">
<div
v-for="obj in currentObjects"
:key="obj.key"
class="object-item"
:class="{ selected: selectedObject === obj.key }"
@click="selectObject(obj)"
>
<div class="object-icon">{{ getFileIcon(obj.type) }}</div>
<div class="object-info">
<div class="object-key">{{ obj.key }}</div>
<div class="object-meta">{{ obj.size }} · {{ obj.lastModified }}</div>
</div>
<div class="object-arrow"></div>
</div>
</div>
<div v-else class="objects-placeholder">
点击上方存储桶查看对象列表
</div>
</div>
<div class="connector"></div>
<!-- 元数据层 -->
<div class="metadata-container">
<div class="section-title">
<span>🏷</span>
<span>元数据 (Metadata)</span>
<span class="section-desc">系统元数据 + 自定义元数据</span>
</div>
<div v-if="selectedObject && currentMetadata" class="metadata-content">
<div class="metadata-section">
<div class="metadata-section-title">系统元数据 (System)</div>
<div class="metadata-list">
<div v-for="(value, key) in currentMetadata.system" :key="key" class="metadata-item">
<span class="metadata-key">{{ key }}:</span>
<span class="metadata-value">{{ value }}</span>
</div>
</div>
</div>
<div class="metadata-section">
<div class="metadata-section-title">自定义元数据 (Custom)</div>
<div class="metadata-list">
<div v-for="(value, key) in currentMetadata.custom" :key="key" class="metadata-item">
<span class="metadata-key">{{ key }}:</span>
<span class="metadata-value">{{ value }}</span>
</div>
</div>
</div>
</div>
<div v-else class="metadata-placeholder">
点击左侧对象查看详细元数据
</div>
</div>
</div>
<div class="architecture-summary">
<div class="summary-title">架构要点总结</div>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-icon">📦</div>
<div class="summary-text">
<strong>Bucket</strong>
<span>全局命名空间用于组织和隔离数据</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">📄</div>
<div class="summary-text">
<strong>Object对象</strong>
<span>键值对存储包含数据元数据和唯一 Key</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">🏷</div>
<div class="summary-text">
<strong>Metadata元数据</strong>
<span>系统元数据 + 自定义标签支持检索和管理</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">🔐</div>
<div class="summary-text">
<strong>Access Control访问控制</strong>
<span>Bucket PolicyACLSTS 临时凭证多层权限</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 存储桶数据
const buckets = [
{
name: 'myapp-images-prod',
icon: '🖼️',
objects: 12543,
size: '256 GB'
},
{
name: 'myapp-videos-prod',
icon: '🎬',
objects: 892,
size: '1.2 TB'
},
{
name: 'myapp-backups',
icon: '💾',
objects: 3456,
size: '500 GB'
}
]
// 对象数据
const objectsData = {
'myapp-images-prod': [
{ key: 'avatars/user123.jpg', type: 'image/jpeg', size: '156 KB', lastModified: '2024-01-15' },
{ key: 'products/shoes-01.png', type: 'image/png', size: '2.3 MB', lastModified: '2024-01-14' },
{ key: 'banners/sale-2024.webp', type: 'image/webp', size: '456 KB', lastModified: '2024-01-13' }
],
'myapp-videos-prod': [
{ key: 'tutorials/intro.mp4', type: 'video/mp4', size: '156 MB', lastModified: '2024-01-15' },
{ key: 'ads/promo-2024.mp4', type: 'video/mp4', size: '234 MB', lastModified: '2024-01-14' }
],
'myapp-backups': [
{ key: 'db/daily-20240115.sql.gz', type: 'application/gzip', size: '456 MB', lastModified: '2024-01-15' },
{ key: 'logs/access-20240114.log.gz', type: 'application/gzip', size: '123 MB', lastModified: '2024-01-14' }
]
}
// 元数据
const metadataData = {
'avatars/user123.jpg': {
system: {
'Content-Type': 'image/jpeg',
'Content-Length': '159745',
'Last-Modified': '2024-01-15T08:30:00Z',
'ETag': '"abc123def456"',
'x-oss-storage-class': 'Standard'
},
custom: {
'x-oss-meta-owner': 'user123',
'x-oss-meta-usage': 'avatar',
'x-oss-meta-uploaded-by': 'web-upload'
}
},
'products/shoes-01.png': {
system: {
'Content-Type': 'image/png',
'Content-Length': '2412555',
'Last-Modified': '2024-01-14T16:20:00Z',
'ETag': '"xyz789ghi012"',
'x-oss-storage-class': 'Standard'
},
custom: {
'x-oss-meta-product-id': 'shoes-01',
'x-oss-meta-category': 'footwear',
'x-oss-meta-price': '199.99'
}
}
}
// 状态
const selectedBucket = ref(null)
const selectedObject = ref(null)
// 计算属性
const currentObjects = computed(() => {
if (!selectedBucket.value) return []
return objectsData[selectedBucket.value] || []
})
const currentMetadata = computed(() => {
if (!selectedObject.value) return null
return metadataData[selectedObject.value] || null
})
// 方法
const selectBucket = (name) => {
selectedBucket.value = name
selectedObject.value = null
}
const selectObject = (obj) => {
selectedObject.value = obj.key
}
const getFileIcon = (type) => {
if (type.startsWith('image/')) return '🖼️'
if (type.startsWith('video/')) return '🎬'
if (type.startsWith('audio/')) return '🎵'
if (type.includes('pdf')) return '📄'
if (type.includes('zip') || type.includes('gzip')) return '📦'
return '📄'
}
</script>
<style scoped>
.object-storage-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.storage-architecture {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.account-layer {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
padding: 1rem;
border-radius: 10px;
text-align: center;
border: 2px solid #6366f1;
}
.account-icon {
font-size: 2rem;
margin-bottom: 0.25rem;
}
.account-name {
font-weight: 600;
font-size: 0.95rem;
color: #4338ca;
}
.account-desc {
font-size: 0.75rem;
color: #6366f1;
margin-top: 0.25rem;
}
.connector {
text-align: center;
color: var(--vp-c-text-3);
font-size: 1.25rem;
}
.buckets-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.section-desc {
font-size: 0.75rem;
font-weight: normal;
color: var(--vp-c-text-2);
margin-left: auto;
}
.buckets-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.bucket-card {
flex: 1;
min-width: 140px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.bucket-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.bucket-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 0 0 3px var(--vp-c-brand-dimm);
}
.bucket-icon {
font-size: 1.75rem;
margin-bottom: 0.25rem;
}
.bucket-name {
font-weight: 600;
font-size: 0.8rem;
color: var(--vp-c-text-1);
word-break: break-all;
}
.bucket-meta {
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.bucket-size {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 600;
margin-top: 0.25rem;
}
.objects-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
}
.objects-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.object-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.object-item:hover {
background: var(--vp-c-bg-mute);
}
.object-item.selected {
background: var(--vp-c-brand-soft);
border: 1px solid var(--vp-c-brand);
}
.object-icon {
font-size: 1.25rem;
}
.object-info {
flex: 1;
min-width: 0;
}
.object-key {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.object-meta {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.object-arrow {
color: var(--vp-c-text-3);
}
.objects-placeholder {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.metadata-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
}
.metadata-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.metadata-content {
grid-template-columns: 1fr;
}
}
.metadata-section {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.75rem;
}
.metadata-section-title {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.metadata-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.1rem;
font-size: 0.75rem;
}
.metadata-key {
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.metadata-value {
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
word-break: break-all;
}
.metadata-placeholder {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.architecture-summary {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-top: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.summary-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
@media (max-width: 640px) {
.summary-grid {
grid-template-columns: 1fr;
}
}
.summary-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.summary-icon {
font-size: 1.5rem;
}
.summary-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.summary-text strong {
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.summary-text span {
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
流量调度演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('流量调度演示')
const description = ref('展示CDN的智能流量调度机制,包括负载均衡、就近访问、故障切换等')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,612 @@
<!--
UploadProcessDemo.vue
上传流程演示 - 展示直传分片断点续传等上传方式
-->
<template>
<div class="upload-process-demo">
<div class="header">
<div class="title">文件上传流程</div>
<div class="subtitle">直传 vs 分片上传 vs 断点续传</div>
</div>
<!-- 上传方式选择 -->
<div class="upload-methods">
<div
v-for="method in uploadMethods"
:key="method.id"
class="method-card"
:class="{ active: selectedMethod === method.id }"
@click="selectMethod(method.id)"
>
<div class="method-icon">{{ method.icon }}</div>
<div class="method-name">{{ method.name }}</div>
<div class="method-desc">{{ method.description }}</div>
<div class="method-size">适合: {{ method.suitable }}</div>
</div>
</div>
<!-- 上传流程可视化 -->
<div class="upload-flow">
<div class="flow-title">
<span v-if="selectedMethod === 'direct'">🚀 直传流程</span>
<span v-else-if="selectedMethod === 'multipart'">🔪 分片上传流程</span>
<span v-else>💾 断点续传流程</span>
</div>
<!-- 直传流程 -->
<div v-if="selectedMethod === 'direct'" class="flow-steps">
<div class="flow-step" :class="{ active: currentStep >= 1 }">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">用户选择文件</div>
<div class="step-detail">浏览器选择 5MB 图片文件</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 2 }">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">申请上传凭证</div>
<div class="step-detail">前端 后端 STS 临时凭证</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 3 }">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">直传到对象存储</div>
<div class="step-detail">浏览器 OSS/COS5MB 一次性上传</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 4 }">
<div class="step-num">4</div>
<div class="step-content">
<div class="step-title">上传完成</div>
<div class="step-detail">返回 URL前端通知后端保存记录</div>
</div>
</div>
</div>
<!-- 分片上传流程 -->
<div v-else-if="selectedMethod === 'multipart'" class="flow-steps multipart-flow">
<div class="flow-step" :class="{ active: currentStep >= 1 }">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">文件分片</div>
<div class="step-detail">500MB 视频 50 10MB 分片</div>
<div class="chunks-preview">
<div v-for="i in 10" :key="i" class="chunk" :class="{ uploaded: i <= 3 }">{{ i }}</div>
<span class="chunks-more">...</span>
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 2 }">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">初始化分片上传</div>
<div class="step-detail">获取 uploadId上传会话 ID</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 3 }">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">并行上传分片</div>
<div class="step-detail">3 个并发每片 10MB</div>
<div class="parallel-upload">
<div class="upload-slot" :class="{ active: parallelActive >= 1 }">分片 1</div>
<div class="upload-slot" :class="{ active: parallelActive >= 2 }">分片 2</div>
<div class="upload-slot" :class="{ active: parallelActive >= 3 }">分片 3</div>
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 4 }">
<div class="step-num">4</div>
<div class="step-content">
<div class="step-title">合并分片</div>
<div class="step-detail">服务端合并所有分片为完整文件</div>
</div>
</div>
</div>
<!-- 断点续传流程 -->
<div v-else class="flow-steps resume-flow">
<div class="flow-step" :class="{ active: currentStep >= 1 }">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">开始上传 1GB 视频</div>
<div class="step-detail">已上传 6 个分片60MB正在上传第 7 </div>
<div class="progress-bar">
<div class="progress-fill" style="width: 6%;"></div>
<div class="progress-text">6% (60MB / 1GB)</div>
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step error-step" :class="{ active: currentStep >= 2 }">
<div class="step-num"></div>
<div class="step-content">
<div class="step-title">网络中断</div>
<div class="step-detail">WiFi 切换到 4G上传中断 7 个分片上传失败</div>
<div class="error-info">
<span> Error: ETIMEDOUT</span>
<span>已上传分片: 6/100</span>
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 3 }">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">查询已上传分片</div>
<div class="step-detail">恢复网络后查询服务端已保存的分片列表</div>
<div class="resume-info">
<div class="resume-item success">
<span> 分片 1-6</span>
<span>已上传</span>
</div>
<div class="resume-item pending">
<span> 分片 7-100</span>
<span>待上传</span>
</div>
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: currentStep >= 4 }">
<div class="step-num">4</div>
<div class="step-content">
<div class="step-title">断点续传成功</div>
<div class="step-detail">从第 7 个分片继续上传无需重传前 6 个分片</div>
<div class="success-info">
<div class="success-item">
<span>💾 节省流量</span>
<span>60MB</span>
</div>
<div class="success-item">
<span> 节省时间</span>
<span>~6s</span>
</div>
<div class="success-item">
<span>🎯 续传进度</span>
<span>6% 100%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 上传方式数据
const uploadMethods = [
{
id: 'direct',
name: '直传',
icon: '🚀',
description: '小文件一次性上传到对象存储',
suitable: '< 100MB'
},
{
id: 'multipart',
name: '分片上传',
icon: '🔪',
description: '大文件切分多片并行上传',
suitable: '> 100MB'
},
{
id: 'resume',
name: '断点续传',
icon: '💾',
description: '网络中断后从断点继续上传',
suitable: '任何大小'
}
]
// 状态
const selectedMethod = ref('direct')
const currentStep = ref(0)
const parallelActive = ref(0)
const stats = ref({
uploadedChunks: 3,
totalChunks: 50,
uploadedSize: '60MB',
totalSize: '1GB',
progress: 6
})
// 方法
const selectMethod = (id) => {
selectedMethod.value = id
resetDemo()
}
const simulateCacheHit = () => {
resetDemo()
currentStep.value = 4
}
const simulateCacheMiss = () => {
resetDemo()
currentStep.value = 4
}
const resetDemo = () => {
currentStep.value = 0
parallelActive.value = 0
}
// 计算属性
const uploadProgress = computed(() => {
return Math.round((stats.value.uploadedChunks / stats.value.totalChunks) * 100)
})
// 方法
const selectMethod = (id) => {
selectedMethod.value = id
resetDemo()
}
const simulateCacheHit = () => {
resetDemo()
currentStep.value = 4
}
const simulateCacheMiss = () => {
resetDemo()
currentStep.value = 4
}
const resetDemo = () => {
currentStep.value = 0
parallelActive.value = 0
}
</script>
<style scoped>
.upload-process-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.upload-methods {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.upload-methods {
grid-template-columns: 1fr;
}
}
.method-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.method-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.method-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 0 0 3px var(--vp-c-brand-dimm);
}
.method-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.method-name {
font-weight: 600;
font-size: 1rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.method-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
line-height: 1.4;
}
.method-size {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 600;
background: var(--vp-c-brand-soft);
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-block;
}
.upload-flow {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.25rem;
}
.flow-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-step {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-divider);
transition: all 0.3s;
}
.flow-step.active {
border-left-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.flow-step.error-step {
background: #fef2f2;
border-left-color: #dc2626;
}
.step-num {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.flow-step.error-step .step-num {
background: #dc2626;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.step-detail {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.flow-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 1rem;
}
/* 分片预览 */
.chunks-preview {
display: flex;
align-items: center;
gap: 0.25rem;
margin-top: 0.5rem;
}
.chunk {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-divider);
border-radius: 4px;
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.chunk.uploaded {
background: var(--vp-c-brand);
color: white;
}
.chunks-more {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
/* 并行上传 */
.parallel-upload {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.upload-slot {
flex: 1;
padding: 0.5rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.upload-slot.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
font-weight: 600;
}
/* 进度条 */
.progress-bar {
position: relative;
height: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 12px;
transition: width 0.3s;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-1);
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
}
/* 错误信息 */
.error-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: white;
border-radius: 6px;
border-left: 3px solid #dc2626;
}
.error-info span {
font-size: 0.75rem;
color: #dc2626;
font-family: var(--vp-font-family-mono);
}
/* 恢复信息 */
.resume-info {
margin-top: 0.5rem;
}
.resume-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.6rem;
margin-bottom: 0.25rem;
background: white;
border-radius: 4px;
font-size: 0.75rem;
}
.resume-item.success {
border-left: 3px solid #22c55e;
}
.resume-item.success span:first-child {
color: #166534;
}
.resume-item.pending {
border-left: 3px solid #f59e0b;
}
.resume-item.pending span:first-child {
color: #92400e;
}
.resume-item span:last-child {
font-weight: 600;
color: var(--vp-c-text-2);
}
/* 成功信息 */
.success-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-top: 0.5rem;
}
.success-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
background: white;
border-radius: 6px;
border: 1px solid #bbf7d0;
}
.success-item span:first-child {
font-size: 0.7rem;
color: #166534;
margin-bottom: 0.25rem;
}
.success-item span:last-child {
font-size: 0.85rem;
font-weight: 700;
color: #16a34a;
}
</style>
@@ -0,0 +1,580 @@
<template>
<div class="availability-zone-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="normal">正常运行</el-radio-button>
<el-radio-button label="az-failure"> AZ 故障</el-radio-button>
<el-radio-button label="maintenance">维护模式</el-radio-button>
<el-radio-button label="scaling">弹性扩容</el-radio-button>
</el-radio-group>
<el-switch
v-model="showTraffic"
active-text="显示流量"
style="margin-left: 20px"
/>
</div>
<!-- 架构图 -->
<div class="architecture-container">
<!-- 流量入口层 -->
<div class="layer entry-layer">
<div class="layer-title">🚪 流量入口层</div>
<div class="entry-components">
<div class="component dns">
<div class="component-icon">📖</div>
<div class="component-name">DNS 解析</div>
</div>
<div class="arrow"></div>
<div class="component cdn">
<div class="component-icon">🌐</div>
<div class="component-name">CDN 加速</div>
</div>
<div class="arrow"></div>
<div class="component waf">
<div class="component-icon">🛡</div>
<div class="component-name">WAF 防护</div>
</div>
</div>
</div>
<!-- 流量分发层 -->
<div class="layer distribution-layer">
<div class="layer-title"> 流量分发层 (SLB)</div>
<div class="slb-cluster"
:class="{ 'failover-active': viewMode === 'az-failure' }">
<div class="slb-instance primary"
:class="{ failed: viewMode === 'az-failure' }">
<div class="instance-header">
<span class="status-indicator"
:class="viewMode === 'az-failure' ? 'offline' : 'online'"></span>
<span class="instance-name">SLB-A ()</span>
</div>
<div class="instance-meta">可用区 A</div>
<!-- 流量动画 -->
<div class="traffic-flow" v-if="showTraffic && viewMode !== 'az-failure'">
<div class="flow-dot"></div>
</div>
</div>
<div class="failover-arrow" v-if="viewMode === 'az-failure'">
<span class="failover-text">故障转移</span>
<div class="arrow-line"></div>
</div>
<div class="slb-instance secondary"
:class="{ 'taking-over': viewMode === 'az-failure' }">
<div class="instance-header">
<span class="status-indicator"
:class="viewMode === 'az-failure' ? 'online' : 'standby'"></span>
<span class="instance-name">SLB-B ()</span>
</div>
<div class="instance-meta">可用区 B</div>
<div class="traffic-flow" v-if="showTraffic && viewMode === 'az-failure'">
<div class="flow-dot"></div>
</div>
</div>
</div>
</div>
<!-- 可用区层 -->
<div class="layer azs-layer">
<div class="layer-title">🏢 可用区层 (Multi-AZ)</div>
<div class="azs-grid">
<div
v-for="az in availabilityZones"
:key="az.id"
class="az-card"
:class="{
'az-a': az.id === 'az-a',
'az-b': az.id === 'az-b',
'az-c': az.id === 'az-c',
'degraded': viewMode === 'az-failure' && az.id === 'az-a',
'scaling': viewMode === 'scaling'
}"
>
<div class="az-header">
<div class="az-title">
<span class="az-name">{{ az.name }}</span>
<span class="az-id">{{ az.id }}</span>
</div>
<div class="az-status">
<span class="status-badge"
:class="getAzStatusClass(az)">
{{ getAzStatusText(az) }}
</span>
</div>
</div>
<div class="az-resources">
<div
v-for="resource in az.resources"
:key="resource.type"
class="resource-item"
>
<span class="resource-icon">{{ resource.icon }}</span>
<span class="resource-name">{{ resource.name }}</span>
<span class="resource-count">{{ resource.count }}</span>
</div>
</div>
<!-- 维护模式遮罩 -->
<div class="maintenance-overlay" v-if="viewMode === 'maintenance' && az.id === 'az-a'">
<div class="overlay-content">
<div class="overlay-icon">🔧</div>
<div class="overlay-text">维护中</div>
</div>
</div>
<!-- 弹性扩容动画 -->
<div class="scaling-indicator" v-if="viewMode === 'scaling'">
<div class="scaling-dot"></div>
<div class="scaling-text">扩容中</div>
</div>
</div>
</div>
</div>
</div>
<!-- 状态说明 -->
<div class="status-legend">
<div class="legend-title">状态说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-dot healthy"></span>
<span>健康运行</span>
</div>
<div class="legend-item">
<span class="legend-dot standby"></span>
<span>待机中</span>
</div>
<div class="legend-item">
<span class="legend-dot degraded"></span>
<span>降级/故障</span>
</div>
<div class="legend-item">
<span class="legend-dot maintenance"></span>
<span>维护中</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('normal')
const showTraffic = ref(false)
const availabilityZones = [
{
id: 'az-a',
name: '可用区 A',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 8 },
{ type: 'rds', name: 'RDS 主库', icon: '🗄️', count: 1 },
{ type: 'redis', name: 'Redis 主库', icon: '📦', count: 1 },
{ type: 'slb', name: 'SLB 主', icon: '⚖️', count: 1 }
]
},
{
id: 'az-b',
name: '可用区 B',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 6 },
{ type: 'rds', name: 'RDS 备库', icon: '🗄️', count: 1 },
{ type: 'redis', name: 'Redis 备库', icon: '📦', count: 1 },
{ type: 'slb', name: 'SLB 备', icon: '⚖️', count: 1 }
]
},
{
id: 'az-c',
name: '可用区 C',
resources: [
{ type: 'ecs', name: 'ECS 实例', icon: '🖥️', count: 4 },
{ type: 'slb', name: 'SLB 备', icon: '⚖️', count: 1 }
]
}
]
const getAzStatusClass = (az) => {
switch (viewMode.value) {
case 'az-failure':
return az.id === 'az-a' ? 'degraded' : 'healthy'
case 'maintenance':
return az.id === 'az-a' ? 'maintenance' : 'standby'
default:
return 'healthy'
}
}
const getAzStatusText = (az) => {
switch (viewMode.value) {
case 'az-failure':
return az.id === 'az-a' ? '故障中' : '接管中'
case 'maintenance':
return az.id === 'az-a' ? '维护中' : '待机中'
default:
return '正常运行'
}
}
</script>
<style scoped>
.availability-zone-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
}
.architecture-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Entry Layer */
.entry-components {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.component {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
min-width: 80px;
}
.component-icon {
font-size: 24px;
}
.component-name {
font-size: 12px;
color: #606266;
font-weight: 500;
}
.arrow {
font-size: 20px;
color: #c0c4cc;
font-weight: bold;
}
/* AZs Layer */
.azs-layer {
background: transparent;
box-shadow: none;
padding: 0;
}
.azs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.az-card {
background: white;
border-radius: 12px;
padding: 16px;
border: 2px solid #e4e7ed;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.az-card.az-a {
border-left: 4px solid #409eff;
}
.az-card.az-b {
border-left: 4px solid #67c23a;
}
.az-card.az-c {
border-left: 4px solid #e6a23c;
}
.az-card.degraded {
border-color: #f56c6c;
background: #fef0f0;
}
.az-card.maintenance {
border-color: #909399;
background: #f4f4f5;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.az-title {
display: flex;
align-items: center;
gap: 8px;
}
.az-name {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.az-id {
font-size: 11px;
padding: 2px 6px;
background: #f0f2f5;
border-radius: 4px;
color: #909399;
}
.status-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 10px;
font-weight: 500;
}
.status-badge.healthy {
background: #e1f3d8;
color: #67c23a;
}
.status-badge.standby {
background: #f4f4f5;
color: #909399;
}
.status-badge.degraded {
background: #fde2e2;
color: #f56c6c;
}
.status-badge.maintenance {
background: #e9e9eb;
color: #909399;
}
.az-resources {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: #f5f7fa;
border-radius: 6px;
transition: all 0.2s;
}
.resource-item:hover {
background: #ecf5ff;
}
.resource-icon {
font-size: 16px;
}
.resource-name {
flex: 1;
font-size: 13px;
color: #606266;
}
.resource-count {
font-size: 12px;
padding: 2px 8px;
background: #409eff;
color: white;
border-radius: 10px;
font-weight: 500;
}
/* Maintenance Overlay */
.maintenance-overlay {
position: absolute;
inset: 0;
background: rgba(144, 147, 153, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.overlay-content {
text-align: center;
color: white;
}
.overlay-icon {
font-size: 48px;
margin-bottom: 8px;
}
.overlay-text {
font-size: 18px;
font-weight: 600;
}
/* Scaling Indicator */
.scaling-indicator {
position: absolute;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #e6a23c;
border-radius: 12px;
}
.scaling-dot {
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
animation: pulse 1s infinite;
}
.scaling-text {
font-size: 11px;
color: white;
font-weight: 500;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.1); }
}
/* Status Legend */
.status-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #606266;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-dot.healthy {
background: #67c23a;
}
.legend-dot.standby {
background: #909399;
}
.legend-dot.degraded {
background: #f56c6c;
}
.legend-dot.maintenance {
background: #c0c4cc;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.entry-components {
flex-direction: column;
}
.arrow {
transform: rotate(90deg);
}
.slb-cluster {
flex-direction: column;
}
.failover-arrow {
transform: rotate(90deg);
margin: 12px 0;
}
}
</style>
@@ -0,0 +1,803 @@
<template>
<div class="compute-topology-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">概览</el-radio-button>
<el-radio-button label="vm">虚拟机</el-radio-button>
<el-radio-button label="container">容器</el-radio-button>
<el-radio-button label="serverless">无服务器</el-radio-button>
</el-radio-group>
<el-switch v-model="showMetrics" active-text="显示指标" style="margin-left: 20px" />
</div>
<!-- 计算架构图 -->
<div class="compute-architecture">
<!-- 物理基础设施层 -->
<div class="layer physical-layer" v-if="viewMode === 'overview' || viewMode === 'vm'">
<div class="layer-header">
<span class="layer-icon">🏭</span>
<span class="layer-title">物理基础设施</span>
</div>
<div class="layer-content">
<div class="server-rack" v-for="rack in serverRacks" :key="rack.id">
<div class="rack-header">{{ rack.name }}</div>
<div class="rack-servers">
<div v-for="server in rack.servers" :key="server.id" class="server-node">
<div class="server-indicator" :class="server.status"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 虚拟化层 -->
<div class="layer virtualization-layer" v-if="viewMode === 'overview' || viewMode === 'vm'">
<div class="layer-header">
<span class="layer-icon">🔧</span>
<span class="layer-title">虚拟化层</span>
</div>
<div class="layer-content">
<div class="hypervisor-cluster">
<div class="hypervisor" v-for="hv in hypervisors" :key="hv.id">
<div class="hv-header">
<span class="hv-icon">🔨</span>
<span class="hv-name">{{ hv.name }}</span>
</div>
<div class="vms-list">
<div v-for="vm in hv.vms" :key="vm.id" class="vm-item">
<div class="vm-info">
<span class="vm-icon">💻</span>
<span class="vm-name">{{ vm.name }}</span>
</div>
<div class="vm-metrics" v-if="showMetrics">
<div class="metric">
<div class="metric-bar">
<div class="metric-fill" :style="{ width: vm.cpu + '%' }"></div>
</div>
<span class="metric-label">CPU</span>
</div>
<div class="metric">
<div class="metric-bar">
<div class="metric-fill memory" :style="{ width: vm.memory + '%' }"></div>
</div>
<span class="metric-label">内存</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 容器层 -->
<div class="layer container-layer" v-if="viewMode === 'overview' || viewMode === 'container'">
<div class="layer-header">
<span class="layer-icon">📦</span>
<span class="layer-title">容器编排层 (Kubernetes)</span>
</div>
<div class="layer-content">
<div class="k8s-cluster">
<!-- 控制平面 -->
<div class="control-plane">
<div class="cp-title">控制平面</div>
<div class="cp-components">
<div class="cp-comp" v-for="comp in controlPlaneComponents" :key="comp.name">
<div class="comp-icon">{{ comp.icon }}</div>
<div class="comp-name">{{ comp.name }}</div>
</div>
</div>
</div>
<!-- 工作节点 -->
<div class="worker-nodes">
<div class="nodes-title">工作节点</div>
<div class="nodes-grid">
<div class="node" v-for="node in workerNodes" :key="node.name">
<div class="node-header">
<span class="node-icon">🔧</span>
<span class="node-name">{{ node.name }}</span>
<span class="node-status" :class="node.status"></span>
</div>
<div class="pods-list">
<div class="pod" v-for="pod in node.pods" :key="pod.name">
<div class="pod-color" :style="{ background: pod.color }"></div>
<span class="pod-name">{{ pod.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无服务器层 -->
<div class="layer serverless-layer" v-if="viewMode === 'overview' || viewMode === 'serverless'">
<div class="layer-header">
<span class="layer-icon"></span>
<span class="layer-title">无服务器计算 (Function Compute)</span>
</div>
<div class="layer-content">
<div class="serverless-arch">
<!-- 触发器 -->
<div class="triggers-section">
<div class="section-title">触发器</div>
<div class="triggers-list">
<div class="trigger" v-for="trigger in triggers" :key="trigger.name">
<div class="trigger-icon">{{ trigger.icon }}</div>
<div class="trigger-name">{{ trigger.name }}</div>
</div>
</div>
</div>
<!-- 函数计算 -->
<div class="functions-section">
<div class="section-title">函数计算实例</div>
<div class="functions-list">
<div class="function-card" v-for="func in functions" :key="func.name">
<div class="func-header">
<span class="func-icon"></span>
<span class="func-name">{{ func.name }}</span>
</div>
<div class="func-metrics" v-if="showMetrics">
<div class="metric-row">
<span class="metric-label">并发</span>
<div class="concurrency-bar">
<div class="concurrency-fill" :style="{ width: (func.concurrency / 100 * 100) + '%' }"></div>
</div>
<span class="metric-value">{{ func.concurrency }}</span>
</div>
<div class="metric-row">
<span class="metric-label">冷启动</span>
<span class="metric-value">{{ func.coldStart }}ms</span>
</div>
</div>
</div>
</div>
</div>
<!-- 后端服务 -->
<div class="backend-section">
<div class="section-title">后端服务</div>
<div class="backend-services">
<div class="service" v-for="svc in backendServices" :key="svc.name">
<div class="service-icon">{{ svc.icon }}</div>
<div class="service-name">{{ svc.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 说明 -->
<div class="architecture-legend">
<div class="legend-title">计算资源类型说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-icon">🔧</span>
<span class="legend-text">虚拟机 (ECS)完整 OS 控制适合传统应用</span>
</div>
<div class="legend-item">
<span class="legend-icon">📦</span>
<span class="legend-text">容器 (K8s)轻量级隔离适合微服务</span>
</div>
<div class="legend-item">
<span class="legend-icon"></span>
<span class="legend-text">无服务器 (FC)事件驱动按需付费</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('overview')
const showMetrics = ref(false)
// 物理服务器
const serverRacks = [
{
id: 'rack-1',
name: '机柜 A',
servers: Array(8).fill(null).map((_, i) => ({
id: `srv-a-${i}`,
status: i < 6 ? 'online' : 'offline'
}))
},
{
id: 'rack-2',
name: '机柜 B',
servers: Array(8).fill(null).map((_, i) => ({
id: `srv-b-${i}`,
status: i < 7 ? 'online' : 'standby'
}))
}
]
// 虚拟化层
const hypervisors = [
{
id: 'hv-1',
name: 'Hypervisor-01',
vms: [
{ id: 'vm-1', name: 'Web-01', cpu: 45, memory: 60 },
{ id: 'vm-2', name: 'Web-02', cpu: 32, memory: 45 },
{ id: 'vm-3', name: 'App-01', cpu: 67, memory: 78 }
]
},
{
id: 'hv-2',
name: 'Hypervisor-02',
vms: [
{ id: 'vm-4', name: 'Web-03', cpu: 28, memory: 35 },
{ id: 'vm-5', name: 'DB-01', cpu: 82, memory: 88 },
{ id: 'vm-6', name: 'Cache-01', cpu: 45, memory: 55 }
]
}
]
// K8s 控制平面
const controlPlaneComponents = [
{ name: 'API Server', icon: '🔌' },
{ name: 'etcd', icon: '📚' },
{ name: 'Scheduler', icon: '📅' },
{ name: 'Controller', icon: '🎮' }
]
// 工作节点
const workerNodes = [
{
name: 'Node-1',
status: 'ready',
pods: [
{ name: 'frontend-1', color: '#409eff' },
{ name: 'frontend-2', color: '#409eff' },
{ name: 'api-1', color: '#67c23a' }
]
},
{
name: 'Node-2',
status: 'ready',
pods: [
{ name: 'api-2', color: '#67c23a' },
{ name: 'worker-1', color: '#e6a23c' },
{ name: 'cache-1', color: '#f56c6c' }
]
},
{
name: 'Node-3',
status: 'ready',
pods: [
{ name: 'api-3', color: '#67c23a' },
{ name: 'worker-2', color: '#e6a23c' }
]
}
]
// Serverless 触发器
const triggers = [
{ name: 'HTTP 请求', icon: '🌐' },
{ name: '定时任务', icon: '⏰' },
{ name: 'OSS 事件', icon: '📦' },
{ name: '消息队列', icon: '📨' }
]
// 函数列表
const functions = [
{ name: 'user-service', runtime: 'Node.js', concurrency: 45, coldStart: 120 },
{ name: 'order-processor', runtime: 'Python', concurrency: 32, coldStart: 85 },
{ name: 'image-resizer', runtime: 'Go', concurrency: 18, coldStart: 45 }
]
// 后端服务
const backendServices = [
{ name: 'API 网关', icon: '🚪' },
{ name: '对象存储', icon: '🪣' },
{ name: '数据库', icon: '🗄️' },
{ name: '缓存', icon: '⚡' }
]
</script>
<style scoped>
.compute-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.compute-architecture {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Physical Layer */
.layer-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.server-rack {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.rack-header {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
.rack-servers {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
}
.server-node {
aspect-ratio: 1;
background: #dcdfe6;
border-radius: 4px;
position: relative;
}
.server-indicator {
position: absolute;
inset: 2px;
border-radius: 2px;
}
.server-indicator.online {
background: #67c23a;
}
.server-indicator.offline {
background: #f56c6c;
}
.server-indicator.standby {
background: #e6a23c;
}
/* Hypervisor Layer */
.hypervisor-cluster {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.hypervisor {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.hv-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.hv-icon {
font-size: 18px;
}
.hv-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.vms-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.vm-item {
background: white;
border-radius: 6px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.vm-info {
display: flex;
align-items: center;
gap: 6px;
}
.vm-icon {
font-size: 14px;
}
.vm-name {
font-size: 13px;
color: #606266;
font-weight: 500;
}
.vm-metrics {
display: flex;
flex-direction: column;
gap: 6px;
}
.metric {
display: flex;
align-items: center;
gap: 6px;
}
.metric-bar {
flex: 1;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.metric-fill {
height: 100%;
background: #409eff;
border-radius: 2px;
transition: width 0.3s;
}
.metric-fill.memory {
background: #67c23a;
}
.metric-label {
font-size: 11px;
color: #909399;
width: 40px;
}
/* Container Layer */
.k8s-cluster {
display: flex;
flex-direction: column;
gap: 16px;
}
.control-plane {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 12px;
}
.cp-title {
font-size: 13px;
font-weight: 600;
color: #0369a1;
margin-bottom: 10px;
}
.cp-components {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cp-comp {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: white;
border-radius: 6px;
border: 1px solid #e0f2fe;
}
.comp-icon {
font-size: 14px;
}
.comp-name {
font-size: 12px;
color: #0c4a6e;
}
.worker-nodes {
display: flex;
flex-direction: column;
gap: 12px;
}
.nodes-title {
font-size: 13px;
font-weight: 600;
color: #606266;
}
.nodes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.node {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
.node-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
}
.node-icon {
font-size: 14px;
}
.node-name {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #334155;
}
.node-status {
width: 8px;
height: 8px;
border-radius: 50%;
}
.node-status.ready {
background: #22c55e;
}
.pods-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.pod {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: white;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.pod-color {
width: 8px;
height: 8px;
border-radius: 50%;
}
.pod-name {
font-size: 11px;
color: #64748b;
}
/* Serverless Layer */
.serverless-arch {
display: flex;
flex-direction: column;
gap: 16px;
}
.triggers-section,
.functions-section,
.backend-section {
background: #fafafa;
border-radius: 8px;
padding: 12px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 10px;
}
.triggers-list,
.backend-services {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.trigger,
.service {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.trigger-icon,
.service-icon {
font-size: 16px;
}
.trigger-name,
.service-name {
font-size: 12px;
color: #606266;
}
.functions-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.function-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
}
.func-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
.func-icon {
font-size: 14px;
}
.func-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.func-metrics {
display: flex;
flex-direction: column;
gap: 8px;
}
.metric-row {
display: flex;
align-items: center;
gap: 8px;
}
.metric-row .metric-label {
width: 60px;
font-size: 11px;
}
.concurrency-bar {
flex: 1;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.concurrency-fill {
height: 100%;
background: #67c23a;
border-radius: 2px;
transition: width 0.3s;
}
.metric-value {
font-size: 11px;
color: #909399;
width: 40px;
text-align: right;
}
/* Status Legend */
.architecture-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 6px;
}
.legend-icon {
font-size: 20px;
}
.legend-text {
font-size: 13px;
color: #606266;
line-height: 1.4;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.hypervisor-cluster,
.nodes-grid,
.functions-list {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,764 @@
<template>
<div class="disaster-recovery-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="drMode" size="small">
<el-radio-button label="same-city">同城双活</el-radio-button>
<el-radio-button label="remote">异地灾备</el-radio-button>
<el-radio-button label="three-center">两地三中心</el-radio-button>
<el-radio-button label="switchover">故障切换</el-radio-button>
</el-radio-group>
<el-switch v-model="showRPO" active-text="显示 RPO/RTO" style="margin-left: 20px" />
</div>
<!-- 灾备架构图 -->
<div class="dr-architecture">
<!-- 生产中心 -->
<div class="dr-center production" :class="{ degraded: drMode === 'switchover' && switchoverStep >= 2 }">
<div class="center-header">
<div class="center-badge production">生产</div>
<div class="center-title">生产中心 (Region A)</div>
<div class="center-location">📍 北京</div>
</div>
<div class="center-content">
<!-- 可用区 A -->
<div class="az-block" :class="{ failed: drMode === 'switchover' && switchoverStep >= 1 }">
<div class="az-header">
<span class="az-name">可用区 A</span>
<span class="az-status" :class="getAzStatus('A')">{{ getAzStatusText('A') }}</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 8</span>
<span class="tag primary">SLB </span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span class="tag primary">RDS </span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
<!-- 可用区 B (同城灾备) -->
<div class="az-block standby" v-if="drMode !== 'remote'">
<div class="az-header">
<span class="az-name">可用区 B</span>
<span class="az-status standby">热备</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 6</span>
<span class="tag standby">SLB </span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span class="tag standby">RDS </span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
</div>
<!-- RPO/RTO 指示器 -->
<div class="rpo-indicator" v-if="showRPO">
<div class="rpo-item">
<span class="rpo-label">RPO</span>
<span class="rpo-value">{{ getRPO() }}</span>
</div>
<div class="rpo-item">
<span class="rpo-label">RTO</span>
<span class="rpo-value">{{ getRTO() }}</span>
</div>
</div>
</div>
<!-- 复制链路 -->
<div class="replication-links">
<div class="link-group same-city" v-if="drMode === 'same-city' || drMode === 'three-center'">
<div class="link-line"></div>
<div class="link-label">同步复制</div>
<div class="link-bandwidth">延迟 &lt; 5ms</div>
</div>
<div class="link-group remote" v-if="drMode === 'remote' || drMode === 'three-center'">
<div class="link-line async"></div>
<div class="link-label">异步复制</div>
<div class="link-bandwidth">RPO 5s</div>
</div>
</div>
<!-- 灾备中心 -->
<div class="dr-center disaster-recovery" :class="{ active: drMode === 'switchover' && switchoverStep >= 2 }">
<div class="center-header">
<div class="center-badge dr">灾备</div>
<div class="center-title">灾备中心 (Region B)</div>
<div class="center-location">📍 {{ drMode === 'same-city' ? '北京 (可用区 C)' : '上海' }}</div>
</div>
<div class="center-content">
<div class="az-block dr-standby" :class="{ promoted: drMode === 'switchover' && switchoverStep >= 3 }">
<div class="az-header">
<span class="az-name">{{ drMode === 'same-city' ? '可用区 C' : '可用区 A' }}</span>
<span class="az-status" :class="getDrAzStatus()">{{ getDrAzStatusText() }}</span>
</div>
<div class="az-resources">
<div class="resource-group">
<div class="group-title">计算</div>
<div class="resource-tags">
<span class="tag">ECS × 4</span>
<span :class="['tag', drMode === 'switchover' && switchoverStep >= 3 ? 'primary' : 'standby']">
SLB {{ drMode === 'switchover' && switchoverStep >= 3 ? '主' : '备' }}
</span>
</div>
</div>
<div class="resource-group">
<div class="group-title">数据库</div>
<div class="resource-tags">
<span :class="['tag', drMode === 'switchover' && switchoverStep >= 3 ? 'primary' : 'standby']">
RDS {{ drMode === 'switchover' && switchoverStep >= 3 ? '主' : '备' }}
</span>
<span class="tag">Redis </span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 切换进度 (仅在故障切换模式显示) -->
<div class="switchover-progress" v-if="drMode === 'switchover'">
<div class="progress-title">故障切换进度</div>
<el-steps :active="switchoverStep" finish-status="success">
<el-step title="检测故障" description="监控系统发现可用区 A 故障" />
<el-step title="停止写入" description="切离主库,暂停业务写入" />
<el-step title="提升备库" description="灾备中心数据库提升为主库" />
<el-step title="流量切换" description="DNS 切换到灾备中心 SLB" />
<el-step title="恢复服务" description="业务在灾备中心正常运行" />
</el-steps>
<div class="progress-actions">
<el-button @click="prevStep" :disabled="switchoverStep === 0">上一步</el-button>
<el-button type="primary" @click="nextStep" :disabled="switchoverStep === 5">下一步</el-button>
<el-button @click="resetSwitchover">重置</el-button>
</div>
</div>
<!-- 架构对比表 -->
<div class="comparison-section">
<div class="comparison-title">📊 灾备架构方案对比</div>
<div class="comparison-table">
<div class="table-header">
<div class="header-cell">对比维度</div>
<div class="header-cell">同城双活</div>
<div class="header-cell">异地灾备</div>
<div class="header-cell">两地三中心</div>
</div>
<div class="table-row" v-for="row in drComparisonData" :key="row.dimension">
<div class="cell dimension">{{ row.dimension }}</div>
<div class="cell">{{ row.sameCity }}</div>
<div class="cell">{{ row.remote }}</div>
<div class="cell highlight">{{ row.threeCenter }}</div>
</div>
</div>
</div>
<!-- RPO/RTO 说明 -->
<div class="rpo-rto-explanation">
<div class="explanation-title">💡 RPO RTO 说明</div>
<div class="explanation-grid">
<div class="explanation-card">
<div class="card-icon"></div>
<div class="card-title">RPO (恢复点目标)</div>
<div class="card-desc">可接受的数据丢失量即最后一次备份到故障发生的时间间隔</div>
<div class="card-example">示例RPO = 5意味着最多丢失5秒的数据</div>
</div>
<div class="explanation-card">
<div class="card-icon">🔄</div>
<div class="card-title">RTO (恢复时间目标)</div>
<div class="card-desc">从故障发生到业务恢复所需的最长时间</div>
<div class="card-example">示例RTO = 30分钟意味着30分钟内必须恢复服务</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const drMode = ref('same-city')
const showRPO = ref(false)
const switchoverStep = ref(0)
// 获取可用区状态
const getAzStatus = (az) => {
if (drMode.value === 'switchover' && switchoverStep.value >= 1 && az === 'A') {
return 'failed'
}
return 'running'
}
const getAzStatusText = (az) => {
if (drMode.value === 'switchover' && switchoverStep.value >= 1 && az === 'A') {
return '故障'
}
return '运行中'
}
const getDrAzStatus = () => {
if (drMode.value === 'switchover' && switchoverStep.value >= 3) {
return 'promoted'
}
return 'standby'
}
const getDrAzStatusText = () => {
if (drMode.value === 'switchover' && switchoverStep.value >= 3) {
return '主库'
}
return '冷备'
}
const getRPO = () => {
switch (drMode.value) {
case 'same-city': return '0 (同步复制)'
case 'remote': return '~5s (异步复制)'
case 'three-center': return '0 (同城) / ~5s (异地)'
default: return '-'
}
}
const getRTO = () => {
switch (drMode.value) {
case 'same-city': return '~5min'
case 'remote': return '~30min'
case 'three-center': return '~5min (同城) / ~30min (异地)'
default: return '-'
}
}
const nextStep = () => {
if (switchoverStep.value < 5) {
switchoverStep.value++
}
}
const prevStep = () => {
if (switchoverStep.value > 0) {
switchoverStep.value--
}
}
const resetSwitchover = () => {
switchoverStep.value = 0
}
// 灾备对比数据
const drComparisonData = [
{ dimension: '部署成本', sameCity: '中等', remote: '较低', threeCenter: '高' },
{ dimension: '运维复杂度', sameCity: '中等', remote: '低', threeCenter: '高' },
{ dimension: '数据保护', sameCity: 'RPO=0', remote: 'RPO~5s', threeCenter: '最全面' },
{ dimension: '恢复速度', sameCity: '~5分钟', remote: '~30分钟', threeCenter: '分层恢复' },
{ dimension: '适用场景', sameCity: '金融核心', remote: '中小企业', threeCenter: '大型核心' }
]
</script>
<style scoped>
.disaster-recovery-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.dr-architecture {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.dr-center {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border: 2px solid transparent;
transition: all 0.3s;
}
.dr-center.production {
border-color: #409eff;
}
.dr-center.production.degraded {
border-color: #f56c6c;
background: #fef0f0;
}
.dr-center.disaster-recovery {
border-color: #67c23a;
}
.dr-center.disaster-recovery.active {
border-color: #409eff;
background: #ecf5ff;
}
.center-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.center-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
color: white;
}
.center-badge.production {
background: #409eff;
}
.center-badge.dr {
background: #67c23a;
}
.center-title {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
}
.center-location {
font-size: 13px;
color: #909399;
}
.center-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* AZ Block */
.az-block {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid #409eff;
transition: all 0.3s;
}
.az-block.failed {
border-left-color: #f56c6c;
background: #fef0f0;
}
.az-block.standby {
border-left-color: #67c23a;
background: #f0f9eb;
}
.az-block.dr-standby {
border-left-color: #e6a23c;
background: #fdf6ec;
}
.az-block.dr-standby.promoted {
border-left-color: #409eff;
background: #ecf5ff;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.az-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.az-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.az-status.running {
background: #e1f3d8;
color: #67c23a;
}
.az-status.failed {
background: #fde2e2;
color: #f56c6c;
}
.az-status.standby {
background: #e1f3d8;
color: #67c23a;
}
.az-status.promoted {
background: #ecf5ff;
color: #409eff;
}
.az-resources {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-group {
display: flex;
align-items: center;
gap: 8px;
}
.group-title {
font-size: 12px;
color: #909399;
width: 50px;
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
}
.tag {
font-size: 11px;
padding: 2px 8px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
}
.tag.primary {
background: #409eff;
color: white;
}
.tag.standby {
background: #67c23a;
color: white;
}
/* RPO Indicator */
.rpo-indicator {
display: flex;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #dcdfe6;
}
.rpo-item {
display: flex;
align-items: center;
gap: 6px;
}
.rpo-label {
font-size: 11px;
color: #909399;
text-transform: uppercase;
}
.rpo-value {
font-size: 13px;
font-weight: 600;
color: #409eff;
}
/* Replication Links */
.replication-links {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
}
.link-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
width: 100%;
}
.link-line {
width: 80%;
height: 3px;
background: linear-gradient(90deg, #409eff, #67c23a);
border-radius: 2px;
position: relative;
}
.link-line::before,
.link-line::after {
content: '';
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background: #409eff;
border-radius: 50%;
transform: translateY(-50%);
}
.link-line::before {
left: -4px;
}
.link-line::after {
right: -4px;
background: #67c23a;
}
.link-line.async {
background: linear-gradient(90deg, #409eff, #e6a23c);
background-image: repeating-linear-gradient(
90deg,
transparent,
transparent 10px,
rgba(255, 255, 255, 0.3) 10px,
rgba(255, 255, 255, 0.3) 20px
);
}
.link-label {
font-size: 12px;
font-weight: 600;
color: #606266;
}
.link-bandwidth {
font-size: 11px;
color: #909399;
}
/* Switchover Progress */
.switchover-progress {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.progress-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.progress-actions {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
/* Comparison Section */
.comparison-section {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.comparison-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.comparison-table {
overflow-x: auto;
}
.table-header {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.header-cell {
padding: 12px;
background: #f5f7fa;
font-size: 13px;
font-weight: 600;
color: #606266;
text-align: center;
}
.table-row {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-bottom: 1px solid #e4e7ed;
}
.table-row:last-child {
border-radius: 0 0 8px 8px;
overflow: hidden;
border-bottom: none;
}
.cell {
padding: 10px 12px;
background: white;
font-size: 12px;
color: #606266;
text-align: center;
}
.cell.dimension {
text-align: left;
font-weight: 500;
color: #303133;
background: #fafafa;
}
.cell.highlight {
font-weight: 600;
color: #67c23a;
}
/* RPO/RTO Explanation */
.rpo-rto-explanation {
margin-top: 20px;
padding: 20px;
background: #f0f9ff;
border-radius: 12px;
border-left: 4px solid #409eff;
}
.explanation-title {
font-size: 16px;
font-weight: 600;
color: #0969da;
margin-bottom: 16px;
}
.explanation-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.explanation-card {
background: white;
border-radius: 8px;
padding: 16px;
}
.card-icon {
font-size: 32px;
margin-bottom: 8px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 13px;
color: #606266;
line-height: 1.5;
margin-bottom: 8px;
}
.card-example {
font-size: 12px;
color: #909399;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.center-content {
flex-direction: column;
}
.comparison-table {
font-size: 11px;
}
.table-header,
.table-row {
grid-template-columns: 80px repeat(3, 1fr);
}
}
</style>
@@ -0,0 +1,651 @@
<template>
<div class="network-flow-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="flowMode" size="small">
<el-radio-button label="inbound">入向流量</el-radio-button>
<el-radio-button label="outbound">出向流量</el-radio-button>
<el-radio-button label="east-west">东西向流量</el-radio-button>
<el-radio-button label="full">完整拓扑</el-radio-button>
</el-radio-group>
<el-switch v-model="showMetrics" active-text="显示流量数据" style="margin-left: 20px" />
</div>
<!-- 网络拓扑图 -->
<div class="network-topology">
<!-- 互联网区域 -->
<div class="zone internet-zone" v-if="showInternet">
<div class="zone-header">
<span class="zone-icon">🌐</span>
<span class="zone-title">互联网 (Internet)</span>
</div>
<div class="zone-content">
<div class="internet-entities">
<div class="entity" v-for="entity in internetEntities" :key="entity.name">
<div class="entity-icon">{{ entity.icon }}</div>
<div class="entity-name">{{ entity.name }}</div>
</div>
</div>
</div>
</div>
<!-- 流量箭头 -->
<div class="flow-arrows" v-if="showFlowArrows">
<div class="arrow-container">
<div class="flow-line" :class="flowMode"></div>
<div class="flow-particles" v-if="showMetrics">
<div class="particle" v-for="n in 5" :key="n"
:style="{ animationDelay: (n * 0.5) + 's' }"></div>
</div>
</div>
<div class="flow-stats" v-if="showMetrics">
<div class="stat-item">
<div class="stat-label">带宽</div>
<div class="stat-value">2.5 Gbps</div>
</div>
<div class="stat-item">
<div class="stat-label">流量</div>
<div class="stat-value">1.2 TB/</div>
</div>
<div class="stat-item">
<div class="stat-label">延迟</div>
<div class="stat-value">15 ms</div>
</div>
</div>
</div>
<!-- VPC 区域 -->
<div class="zone vpc-zone">
<div class="zone-header">
<span class="zone-icon">🏠</span>
<span class="zone-title">VPC 网络 (172.16.0.0/12)</span>
</div>
<div class="zone-content">
<!-- 网络设备层 -->
<div class="network-devices">
<div class="device" v-for="device in networkDevices" :key="device.name"
:class="device.type">
<div class="device-icon">{{ device.icon }}</div>
<div class="device-name">{{ device.name }}</div>
<div class="device-stats" v-if="showMetrics">
<div class="stat">
<span class="stat-label">吞吐</span>
<span class="stat-value">{{ device.throughput }}</span>
</div>
<div class="stat">
<span class="stat-label">并发</span>
<span class="stat-value">{{ device.connections }}</span>
</div>
</div>
</div>
</div>
<!-- 子网层 -->
<div class="subnets-container">
<div class="subnet" v-for="subnet in subnets" :key="subnet.name"
:class="subnet.type">
<div class="subnet-header">
<span class="subnet-type-icon">{{ subnet.type === 'public' ? '🌐' : '🔒' }}</span>
<span class="subnet-name">{{ subnet.name }}</span>
<span class="subnet-cidr">{{ subnet.cidr }}</span>
</div>
<div class="subnet-resources">
<div class="resource" v-for="resource in subnet.resources" :key="resource.name">
<div class="resource-icon">{{ resource.icon }}</div>
<div class="resource-info">
<div class="resource-name">{{ resource.name }}</div>
<div class="resource-ip">{{ resource.ip }}</div>
</div>
<div class="resource-traffic" v-if="showMetrics">
<div class="traffic-in">
<span class="traffic-label"></span>
<span class="traffic-value">{{ resource.inTraffic }}</span>
</div>
<div class="traffic-out">
<span class="traffic-label"></span>
<span class="traffic-value">{{ resource.outTraffic }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图例说明 -->
<div class="network-legend">
<div class="legend-title">流量类型说明</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-color inbound"></span>
<span>入向流量用户 服务器</span>
</div>
<div class="legend-item">
<span class="legend-color outbound"></span>
<span>出向流量服务器 外部</span>
</div>
<div class="legend-item">
<span class="legend-color east-west"></span>
<span>东西向流量服务间通信</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const flowMode = ref('inbound')
const showMetrics = ref(false)
// 显示互联网
const showInternet = computed(() => {
return ['inbound', 'outbound', 'full'].includes(flowMode.value)
})
// 显示流量箭头
const showFlowArrows = computed(() => {
return ['inbound', 'outbound', 'east-west', 'full'].includes(flowMode.value)
})
// 互联网实体
const internetEntities = [
{ name: '移动用户', icon: '📱' },
{ name: 'PC 用户', icon: '💻' },
{ name: '企业网络', icon: '🏢' },
{ name: '第三方 API', icon: '🔗' }
]
// 网络设备
const networkDevices = [
{ name: 'Internet Gateway', icon: '🌐', type: 'igw', throughput: '10 Gbps', connections: '10M' },
{ name: 'NAT Gateway', icon: '🔄', type: 'nat', throughput: '5 Gbps', connections: '1M' },
{ name: 'Load Balancer', icon: '⚖️', type: 'slb', throughput: '8 Gbps', connections: '500K' },
{ name: 'VPN Gateway', icon: '🔒', type: 'vpn', throughput: '1 Gbps', connections: '1K' }
]
// 子网
const subnets = [
{
name: 'Public-Subnet-A',
type: 'public',
cidr: '172.16.1.0/24',
resources: [
{ name: 'Nginx-LB-01', icon: '⚖️', ip: '172.16.1.10', inTraffic: '850 MB/s', outTraffic: '2.1 GB/s' },
{ name: 'Nginx-LB-02', icon: '⚖️', ip: '172.16.1.11', inTraffic: '780 MB/s', outTraffic: '1.9 GB/s' },
{ name: 'Bastion-Host', icon: '🔧', ip: '172.16.1.20', inTraffic: '5 MB/s', outTraffic: '12 MB/s' }
]
},
{
name: 'Private-Subnet-A',
type: 'private',
cidr: '172.16.2.0/24',
resources: [
{ name: 'App-Server-01', icon: '💻', ip: '172.16.2.10', inTraffic: '450 MB/s', outTraffic: '320 MB/s' },
{ name: 'App-Server-02', icon: '💻', ip: '172.16.2.11', inTraffic: '420 MB/s', outTraffic: '290 MB/s' },
{ name: 'App-Server-03', icon: '💻', ip: '172.16.2.12', inTraffic: '380 MB/s', outTraffic: '260 MB/s' }
]
},
{
name: 'Data-Subnet-A',
type: 'private',
cidr: '172.16.3.0/24',
resources: [
{ name: 'MySQL-Primary', icon: '🗄️', ip: '172.16.3.10', inTraffic: '120 MB/s', outTraffic: '180 MB/s' },
{ name: 'MySQL-Replica', icon: '🗄️', ip: '172.16.3.11', inTraffic: '80 MB/s', outTraffic: '95 MB/s' },
{ name: 'Redis-Cluster', icon: '⚡', ip: '172.16.3.20', inTraffic: '45 MB/s', outTraffic: '68 MB/s' }
]
}
]
</script>
<style scoped>
.network-flow-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.network-topology {
display: flex;
flex-direction: column;
gap: 16px;
}
.zone {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.zone-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.zone-icon {
font-size: 20px;
}
.zone-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
/* Internet Zone */
.internet-entities {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
}
.entity {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
min-width: 80px;
}
.entity-icon {
font-size: 24px;
}
.entity-name {
font-size: 12px;
color: #606266;
}
/* Flow Arrows */
.flow-arrows {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px;
}
.arrow-container {
position: relative;
width: 100%;
height: 40px;
}
.flow-line {
position: absolute;
top: 50%;
left: 10%;
right: 10%;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
transform: translateY(-50%);
}
.flow-line.inbound {
background: linear-gradient(to right, #409eff, #67c23a);
}
.flow-line.outbound {
background: linear-gradient(to left, #409eff, #e6a23c);
}
.flow-line.east-west {
background: linear-gradient(to right, #67c23a, #409eff, #67c23a);
}
.flow-line.full {
background: linear-gradient(90deg, #409eff, #67c23a, #e6a23c, #f56c6c);
}
.flow-particles {
position: absolute;
inset: 0;
}
.particle {
position: absolute;
top: 50%;
left: 10%;
width: 8px;
height: 8px;
background: #409eff;
border-radius: 50%;
transform: translateY(-50%);
animation: flow 2s linear infinite;
}
@keyframes flow {
0% {
left: 10%;
opacity: 1;
}
100% {
left: 90%;
opacity: 0;
}
}
.flow-stats {
display: flex;
gap: 24px;
justify-content: center;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 11px;
color: #909399;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #409eff;
}
/* VPC Zone */
.vpc-zone {
border: 2px solid #409eff;
}
/* Network Devices */
.network-devices {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed #e4e7ed;
}
.device {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
min-width: 100px;
border: 2px solid transparent;
transition: all 0.3s;
}
.device:hover {
border-color: #409eff;
transform: translateY(-2px);
}
.device.igw {
border-color: #409eff;
background: #ecf5ff;
}
.device.nat {
border-color: #67c23a;
background: #f0f9eb;
}
.device.slb {
border-color: #e6a23c;
background: #fdf6ec;
}
.device.vpn {
border-color: #909399;
background: #f4f4f5;
}
.device-icon {
font-size: 24px;
}
.device-name {
font-size: 12px;
font-weight: 500;
color: #303133;
}
.device-stats {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
padding-top: 6px;
border-top: 1px solid #e4e7ed;
}
.stat {
display: flex;
justify-content: space-between;
font-size: 10px;
}
.stat-label {
color: #909399;
}
.stat-value {
color: #409eff;
font-weight: 500;
}
/* Subnets */
.subnets-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.subnet {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid;
}
.subnet.public {
border-left-color: #409eff;
}
.subnet.private {
border-left-color: #67c23a;
}
.subnet-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.subnet-type-icon {
font-size: 14px;
}
.subnet-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.subnet-cidr {
font-size: 11px;
padding: 2px 8px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
font-family: monospace;
}
.subnet-resources {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.resource {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e4e7ed;
flex: 1;
min-width: 200px;
}
.resource-icon {
font-size: 18px;
}
.resource-info {
flex: 1;
}
.resource-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.resource-ip {
font-size: 11px;
color: #909399;
font-family: monospace;
}
.resource-traffic {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 8px;
border-left: 1px solid #e4e7ed;
}
.traffic-in,
.traffic-out {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.traffic-label {
color: #909399;
font-weight: 600;
}
.traffic-value {
color: #409eff;
font-weight: 500;
}
/* Network Legend */
.network-legend {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #606266;
}
.legend-color {
width: 20px;
height: 4px;
border-radius: 2px;
}
.legend-color.inbound {
background: linear-gradient(to right, #409eff, #67c23a);
}
.legend-color.outbound {
background: linear-gradient(to right, #409eff, #e6a23c);
}
.legend-color.east-west {
background: linear-gradient(to right, #67c23a, #409eff, #67c23a);
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.network-devices {
justify-content: center;
}
.resource {
min-width: 100%;
}
.flow-stats {
flex-direction: column;
gap: 12px;
}
}
</style>
@@ -0,0 +1,400 @@
<template>
<div class="resource-topology-demo">
<div class="controls">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">全景视图</el-radio-button>
<el-radio-button label="compute">计算视角</el-radio-button>
<el-radio-button label="network">网络视角</el-radio-button>
<el-radio-button label="storage">存储视角</el-radio-button>
</el-radio-group>
</div>
<div class="topology-container" ref="topologyRef">
<!-- 云服务商层 -->
<div class="layer cloud-provider">
<div class="layer-title"> 云服务商</div>
<div class="provider-grid">
<div v-for="provider in providers" :key="provider.name"
class="provider-card"
:class="{ active: selectedProvider === provider.name }"
@click="selectProvider(provider.name)">
<div class="provider-icon">{{ provider.icon }}</div>
<div class="provider-name">{{ provider.name }}</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-arrow"></div>
<!-- 地域/可用区层 -->
<div class="layer region-layer">
<div class="layer-title">🌍 地域 & 可用区</div>
<div class="region-grid">
<div v-for="region in regions" :key="region.id"
class="region-card"
:class="{ active: selectedRegion === region.id }"
@click="selectRegion(region.id)">
<div class="region-name">{{ region.name }}</div>
<div class="region-azs">
<span v-for="az in region.azs" :key="az" class="az-badge">{{ az }}</span>
</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-arrow"></div>
<!-- 资源拓扑层 -->
<div class="layer resource-layer">
<div class="layer-title">🎯 资源拓扑</div>
<div class="resource-grid">
<!-- 计算资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'compute' || viewMode === 'overview' }">
<div class="category-title">💻 计算</div>
<div class="resource-list">
<div v-for="item in computeResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
<!-- 网络资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'network' || viewMode === 'overview' }">
<div class="category-title">🌐 网络</div>
<div class="resource-list">
<div v-for="item in networkResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
<!-- 存储资源 -->
<div class="resource-category" :class="{ highlight: viewMode === 'storage' || viewMode === 'overview' }">
<div class="category-title">💾 存储</div>
<div class="resource-list">
<div v-for="item in storageResources" :key="item.name" class="resource-item">
<span class="resource-icon">{{ item.icon }}</span>
<span class="resource-name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 信息面板 -->
<div class="info-panel" v-if="showInfo">
<div class="info-header">
<h4>💡 拓扑说明</h4>
<el-button type="text" @click="showInfo = false">关闭</el-button>
</div>
<div class="info-content">
<p><strong>当前视图</strong>{{ viewModeText }}</p>
<p><strong>选中云商</strong>{{ selectedProvider || '未选择' }}</p>
<p><strong>选中地域</strong>{{ selectedRegion || '未选择' }}</p>
<p class="tip">💡 提示点击云服务商和地域可以查看不同组合的资源拓扑</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const viewMode = ref('overview')
const selectedProvider = ref('阿里云')
const selectedRegion = ref('cn-beijing')
const showInfo = ref(true)
const providers = [
{ name: '阿里云', icon: '☁️' },
{ name: '腾讯云', icon: '🌟' },
{ name: '华为云', icon: '🔥' },
{ name: 'AWS', icon: '📦' }
]
const regions = [
{ id: 'cn-beijing', name: '华北2 (北京)', azs: ['A', 'B', 'C', 'D', 'E'] },
{ id: 'cn-shanghai', name: '华东2 (上海)', azs: ['A', 'B', 'C', 'D', 'E', 'F'] },
{ id: 'cn-shenzhen', name: '华南1 (深圳)', azs: ['A', 'B', 'C', 'D'] },
{ id: 'cn-hangzhou', name: '华东1 (杭州)', azs: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] }
]
const computeResources = [
{ name: '云服务器 ECS', icon: '🖥️' },
{ name: '容器服务 K8s', icon: '📦' },
{ name: '函数计算 FC', icon: '⚡' },
{ name: '裸金属服务器', icon: '🔧' }
]
const networkResources = [
{ name: '专有网络 VPC', icon: '🕸️' },
{ name: '负载均衡 SLB', icon: '⚖️' },
{ name: '弹性公网 IP', icon: '🌍' },
{ name: 'VPN 网关', icon: '🔒' }
]
const storageResources = [
{ name: '对象存储 OSS', icon: '🪣' },
{ name: '块存储 EBS', icon: '💽' },
{ name: '文件存储 NAS', icon: '📁' },
{ name: '日志服务 SLS', icon: '📋' }
]
const viewModeText = computed(() => {
const map = {
overview: '全景视图 - 查看完整资源拓扑',
compute: '计算视角 - 聚焦计算资源',
network: '网络视角 - 聚焦网络资源',
storage: '存储视角 - 聚焦存储资源'
}
return map[viewMode.value]
})
const selectProvider = (name) => {
selectedProvider.value = name
}
const selectRegion = (id) => {
selectedRegion.value = id
}
</script>
<style scoped>
.resource-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.controls {
margin-bottom: 20px;
text-align: center;
}
.topology-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.layer {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #e4e7ed;
}
.connection-arrow {
text-align: center;
font-size: 24px;
color: #909399;
padding: 8px 0;
}
/* Provider Grid */
.provider-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.provider-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.provider-card:hover {
border-color: #409eff;
transform: translateY(-2px);
}
.provider-card.active {
border-color: #409eff;
background: #ecf5ff;
}
.provider-icon {
font-size: 32px;
margin-bottom: 8px;
}
.provider-name {
font-size: 14px;
font-weight: 500;
color: #606266;
}
/* Region Grid */
.region-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.region-card {
padding: 12px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.region-card:hover {
border-color: #67c23a;
}
.region-card.active {
border-color: #67c23a;
background: #f0f9eb;
}
.region-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.region-azs {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.az-badge {
padding: 2px 6px;
background: #e4e7ed;
border-radius: 4px;
font-size: 11px;
color: #606266;
}
.region-card.active .az-badge {
background: #67c23a;
color: white;
}
/* Resource Grid */
.resource-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.resource-category {
padding: 16px;
border: 2px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s;
}
.resource-category.highlight {
border-color: #409eff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
}
.category-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.resource-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f5f7fa;
border-radius: 6px;
transition: all 0.2s;
}
.resource-item:hover {
background: #ecf5ff;
transform: translateX(4px);
}
.resource-icon {
font-size: 18px;
}
.resource-name {
font-size: 13px;
color: #606266;
}
/* Info Panel */
.info-panel {
margin-top: 20px;
padding: 16px;
background: #f0f9eb;
border-radius: 8px;
border-left: 4px solid #67c23a;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.info-header h4 {
margin: 0;
color: #67c23a;
}
.info-content p {
margin: 8px 0;
color: #606266;
font-size: 14px;
}
.info-content .tip {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #dcdfe6;
color: #909399;
font-size: 13px;
}
@media (max-width: 768px) {
.provider-grid,
.region-grid {
grid-template-columns: repeat(2, 1fr);
}
.resource-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,875 @@
<template>
<div class="storage-topology-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">存储概览</el-radio-button>
<el-radio-button label="object">对象存储</el-radio-button>
<el-radio-button label="block">块存储</el-radio-button>
<el-radio-button label="file">文件存储</el-radio-button>
</el-radio-group>
<el-switch v-model="showDetails" active-text="显示详情" style="margin-left: 20px" />
</div>
<!-- 存储架构图 -->
<div class="storage-architecture">
<!-- 应用接入层 -->
<div class="layer access-layer">
<div class="layer-title">🔌 应用接入层</div>
<div class="access-methods">
<div class="method-card" v-for="method in accessMethods" :key="method.name"
@mouseenter="hoverMethod = method.name" @mouseleave="hoverMethod = null">
<div class="method-icon">{{ method.icon }}</div>
<div class="method-name">{{ method.name }}</div>
<div class="method-desc">{{ method.description }}</div>
<div class="method-tooltip" v-if="hoverMethod === method.name && showDetails">
<div v-for="detail in method.details" :key="detail">{{ detail }}</div>
</div>
</div>
</div>
</div>
<!-- 存储网关层 -->
<div class="layer gateway-layer">
<div class="layer-title">🚪 存储网关层</div>
<div class="gateways-grid">
<div class="gateway-card" v-for="gateway in storageGateways" :key="gateway.name"
:class="gateway.type">
<div class="gateway-header">
<span class="gateway-icon">{{ gateway.icon }}</span>
<span class="gateway-name">{{ gateway.name }}</span>
</div>
<div class="gateway-metrics" v-if="showDetails">
<div class="metric">
<span class="metric-label">TPS</span>
<span class="metric-value">{{ gateway.tps }}</span>
</div>
<div class="metric">
<span class="metric-label">延迟</span>
<span class="metric-value">{{ gateway.latency }}ms</span>
</div>
</div>
</div>
</div>
</div>
<!-- 存储服务层 -->
<div class="layer storage-services-layer">
<div class="layer-title">💾 存储服务层</div>
<div class="storage-types-grid">
<!-- 对象存储 -->
<div class="storage-type-card object-storage"
:class="{ active: viewMode === 'object' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">🪣</div>
<div class="storage-title">对象存储 OSS</div>
</div>
<div class="storage-desc">适合存储图片视频日志等非结构化数据</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>海量存储</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>低成本</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>CDN 加速</span>
</div>
</div>
<div class="storage-buckets" v-if="showDetails">
<div class="bucket" v-for="bucket in buckets" :key="bucket.name">
<div class="bucket-info">
<span class="bucket-name">{{ bucket.name }}</span>
<span class="bucket-objects">{{ bucket.objects }} 个对象</span>
</div>
<div class="bucket-size">{{ bucket.size }}</div>
</div>
</div>
</div>
<!-- 块存储 -->
<div class="storage-type-card block-storage"
:class="{ active: viewMode === 'block' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">💽</div>
<div class="storage-title">块存储 EBS</div>
</div>
<div class="storage-desc">为云服务器提供高性能低延迟的数据块存储</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>高性能 SSD</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>快照备份</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>弹性扩容</span>
</div>
</div>
<div class="volumes-list" v-if="showDetails">
<div class="volume" v-for="vol in volumes" :key="vol.id">
<div class="volume-info">
<div class="volume-header">
<span class="volume-name">{{ vol.name }}</span>
<span class="volume-type" :class="vol.type">{{ vol.type }}</span>
</div>
<div class="volume-meta">
<span>{{ vol.size }}</span>
<span></span>
<span>挂载到: {{ vol.attachedTo }}</span>
</div>
</div>
<div class="volume-iops" v-if="vol.iops">
<div class="iops-label">IOPS</div>
<div class="iops-value">{{ vol.iops }}</div>
</div>
</div>
</div>
</div>
<!-- 文件存储 -->
<div class="storage-type-card file-storage"
:class="{ active: viewMode === 'file' || viewMode === 'overview' }">
<div class="storage-header">
<div class="storage-icon">📁</div>
<div class="storage-title">文件存储 NAS</div>
</div>
<div class="storage-desc">为多个计算节点提供共享文件系统访问</div>
<div class="storage-features">
<div class="feature">
<span class="feature-icon"></span>
<span>共享访问</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>POSIX 兼容</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>自动扩容</span>
</div>
</div>
<div class="filesystems-list" v-if="showDetails">
<div class="filesystem" v-for="fs in filesystems" :key="fs.name">
<div class="fs-header">
<div class="fs-info">
<span class="fs-name">{{ fs.name }}</span>
<span class="fs-protocol" :class="fs.protocol">{{ fs.protocol }}</span>
</div>
<div class="fs-capacity">
<span class="capacity-used">{{ fs.used }}</span>
<span class="capacity-total">/ {{ fs.total }}</span>
</div>
</div>
<div class="fs-capacity-bar">
<div class="capacity-progress" :style="{ width: fs.percentage + '%' }"
:class="{ warning: fs.percentage > 80, danger: fs.percentage > 90 }"></div>
</div>
<div class="fs-mounts">
<div class="mounts-label">挂载点</div>
<div class="mounts-list">
<span class="mount-point" v-for="mount in fs.mounts" :key="mount">{{ mount }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 存储选型对比 -->
<div class="comparison-section">
<div class="comparison-title">📊 存储类型选型对比</div>
<div class="comparison-table">
<div class="table-header">
<div class="header-cell">特性</div>
<div class="header-cell object">对象存储</div>
<div class="header-cell block">块存储</div>
<div class="header-cell file">文件存储</div>
</div>
<div class="table-row" v-for="row in comparisonData" :key="row.feature">
<div class="cell feature">{{ row.feature }}</div>
<div class="cell" :class="{ highlight: row.object === '优秀' || row.object === '强' }">{{ row.object }}</div>
<div class="cell" :class="{ highlight: row.block === '优秀' || row.block === '强' }">{{ row.block }}</div>
<div class="cell" :class="{ highlight: row.file === '优秀' || row.file === '强' }">{{ row.file }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('overview')
const showDetails = ref(false)
const hoverMethod = ref(null)
// 接入方式
const accessMethods = [
{
name: 'API/SDK',
icon: '🔧',
description: '通过编程接口访问存储',
details: ['支持 RESTful API', '提供多语言 SDK', '支持批量操作', '可编程访问控制']
},
{
name: '挂载访问',
icon: '🔗',
description: '像本地磁盘一样使用',
details: ['支持 NFS 协议', '支持 SMB 协议', 'POSIX 兼容', '透明访问']
},
{
name: '控制台',
icon: '🖥️',
description: '通过 Web 界面管理',
details: ['可视化操作', '权限管理', '监控报表', '日志审计']
}
]
// 存储网关
const storageGateways = [
{ name: '对象网关', icon: '🪣', type: 'object', tps: '10000', latency: '5' },
{ name: '块网关', icon: '💽', type: 'block', tps: '50000', latency: '1' },
{ name: '文件网关', icon: '📁', type: 'file', tps: '8000', latency: '3' }
]
// 存储桶
const buckets = [
{ name: 'images-bucket', protocol: 'S3', used: '2.5 TB', total: '10 TB', percentage: 25, mounts: ['CDN 加速', '图片处理'] },
{ name: 'logs-bucket', protocol: 'S3', used: '850 GB', total: '5 TB', percentage: 17, mounts: ['日志归档', '数据分析'] },
{ name: 'backup-bucket', protocol: 'S3', used: '4.2 TB', total: '5 TB', percentage: 84, mounts: ['自动备份', '跨区域复制'] }
]
// 云盘
const volumes = [
{ name: 'data-disk-01', type: 'ESSD', size: '500 GB', used: '320 GB', percentage: 64, attachedTo: 'DB-Server-01', iops: 50000 },
{ name: 'data-disk-02', type: 'SSD', size: '200 GB', used: '145 GB', percentage: 72, attachedTo: 'App-Server-02', iops: 25000 },
{ name: 'log-disk-01', type: 'SATA', size: '1 TB', used: '680 GB', percentage: 68, attachedTo: 'Log-Server-01', iops: 5000 }
]
// 文件系统
const filesystems = [
{ name: 'shared-data', protocol: 'NFS', used: '1.2 TB', total: '5 TB', percentage: 24, mounts: ['/mnt/shared'] },
{ name: 'dev-env', protocol: 'NFS', used: '450 GB', total: '2 TB', percentage: 22, mounts: ['/mnt/dev'] },
{ name: 'team-share', protocol: 'SMB', used: '890 GB', total: '3 TB', percentage: 30, mounts: ['\\\\nas\\team'] }
]
// 对比数据
const comparisonData = [
{ feature: '访问协议', object: 'HTTP/HTTPS', block: 'iSCSI/NVMe', file: 'NFS/SMB' },
{ feature: '性能', object: '高吞吐', block: '低延迟', file: '中等' },
{ feature: '数据共享', object: '弱', block: '不支持', file: '强' },
{ feature: '容量扩展', object: '强', block: '中等', file: '中等' },
{ feature: '成本', object: '低', block: '高', file: '中等' },
{ feature: '典型场景', object: '图片/视频/备份', block: '数据库/应用', file: '共享文件/开发' }
]
</script>
<style scoped>
.storage-topology-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.storage-architecture {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.layer {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.layer-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Access Layer */
.access-methods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.method-card {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
text-align: center;
position: relative;
cursor: pointer;
transition: all 0.3s;
}
.method-card:hover {
background: #ecf5ff;
transform: translateY(-2px);
}
.method-icon {
font-size: 32px;
margin-bottom: 8px;
}
.method-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.method-desc {
font-size: 12px;
color: #909399;
}
.method-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
z-index: 10;
margin-bottom: 8px;
white-space: nowrap;
}
/* Gateway Layer */
.gateways-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.gateway-card {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border-left: 4px solid;
}
.gateway-card.object {
border-left-color: #409eff;
}
.gateway-card.block {
border-left-color: #67c23a;
}
.gateway-card.file {
border-left-color: #e6a23c;
}
.gateway-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.gateway-icon {
font-size: 20px;
}
.gateway-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.gateway-metrics {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.metric-label {
color: #909399;
}
.metric-value {
color: #409eff;
font-weight: 500;
}
/* Storage Types Grid */
.storage-types-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
.storage-type-card {
background: #f5f7fa;
border-radius: 12px;
padding: 16px;
border: 2px solid transparent;
transition: all 0.3s;
}
.storage-type-card:hover,
.storage-type-card.active {
border-color: #409eff;
background: #ecf5ff;
}
.storage-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.storage-icon {
font-size: 28px;
}
.storage-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.storage-desc {
font-size: 12px;
color: #606266;
margin-bottom: 12px;
line-height: 1.5;
}
.storage-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.feature {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #606266;
}
.feature-icon {
color: #67c23a;
}
/* Buckets List */
.storage-buckets {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.bucket {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: white;
border-radius: 6px;
}
.bucket-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.bucket-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.bucket-objects {
font-size: 11px;
color: #909399;
}
.bucket-size {
font-size: 12px;
color: #409eff;
font-weight: 500;
}
/* Volumes List */
.volumes-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.volume {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: white;
border-radius: 6px;
}
.volume-info {
flex: 1;
}
.volume-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.volume-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.volume-type {
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
text-transform: uppercase;
}
.volume-type.essd {
background: #409eff;
color: white;
}
.volume-type.ssd {
background: #67c23a;
color: white;
}
.volume-type.sata {
background: #909399;
color: white;
}
.volume-meta {
font-size: 11px;
color: #909399;
}
.volume-meta span {
margin: 0 4px;
}
.volume-iops {
text-align: center;
padding-left: 12px;
border-left: 1px solid #e4e7ed;
}
.iops-label {
font-size: 10px;
color: #909399;
text-transform: uppercase;
}
.iops-value {
font-size: 14px;
font-weight: 600;
color: #409eff;
}
/* Filesystems List */
.filesystems-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
}
.filesystem {
background: white;
border-radius: 6px;
padding: 12px;
}
.fs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.fs-info {
display: flex;
align-items: center;
gap: 8px;
}
.fs-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.fs-protocol {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.fs-protocol.nfs {
background: #409eff;
color: white;
}
.fs-protocol.smb {
background: #67c23a;
color: white;
}
.fs-capacity {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}
.capacity-used {
color: #303133;
font-weight: 500;
}
.capacity-total {
color: #909399;
}
.fs-capacity-bar {
height: 4px;
background: #e4e7ed;
border-radius: 2px;
margin-bottom: 8px;
overflow: hidden;
}
.capacity-progress {
height: 100%;
background: #67c23a;
border-radius: 2px;
transition: width 0.3s;
}
.capacity-progress.warning {
background: #e6a23c;
}
.capacity-progress.danger {
background: #f56c6c;
}
.fs-mounts {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mounts-label {
font-size: 12px;
color: #909399;
}
.mounts-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.mount-point {
font-size: 11px;
padding: 2px 8px;
background: #ecf5ff;
color: #409eff;
border-radius: 10px;
}
/* Comparison Section */
.comparison-section {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.comparison-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.comparison-table {
overflow-x: auto;
}
.table-header {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.header-cell {
padding: 12px;
background: #f5f7fa;
font-size: 13px;
font-weight: 600;
color: #606266;
text-align: center;
}
.header-cell.object {
background: #ecf5ff;
color: #409eff;
}
.header-cell.block {
background: #f0f9eb;
color: #67c23a;
}
.header-cell.file {
background: #fdf6ec;
color: #e6a23c;
}
.table-row {
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
gap: 1px;
background: #e4e7ed;
border-bottom: 1px solid #e4e7ed;
}
.table-row:last-child {
border-radius: 0 0 8px 8px;
overflow: hidden;
border-bottom: none;
}
.cell {
padding: 10px 12px;
background: white;
font-size: 12px;
color: #606266;
text-align: center;
}
.cell.feature {
text-align: left;
font-weight: 500;
color: #303133;
background: #fafafa;
}
.cell.highlight {
font-weight: 600;
color: #67c23a;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.storage-types-grid {
grid-template-columns: 1fr;
}
.comparison-table {
font-size: 11px;
}
.table-header,
.table-row {
grid-template-columns: 80px repeat(3, 1fr);
}
.header-cell,
.cell {
padding: 6px 8px;
}
}
</style>
@@ -0,0 +1,508 @@
<template>
<div class="subnet-design-demo">
<!-- 控制面板 -->
<div class="control-panel">
<div class="panel-section">
<span class="panel-label">VPC 网段</span>
<el-radio-group v-model="vpcCidr" size="small">
<el-radio-button label="172.16.0.0/12">172.16.0.0/12</el-radio-button>
<el-radio-button label="10.0.0.0/8">10.0.0.0/8</el-radio-button>
<el-radio-button label="192.168.0.0/16">192.168.0.0/16</el-radio-button>
</el-radio-group>
</div>
<div class="panel-section">
<span class="panel-label">子网划分</span>
<el-slider v-model="subnetBits" :min="2" :max="4" show-stops :marks="{2: '/24', 3: '/25', 4: '/26'}" style="width: 200px;" />
</div>
<el-switch v-model="showCalculation" active-text="显示计算过程" style="margin-left: 20px;" />
</div>
<!-- 网段可视化 -->
<div class="network-visualization">
<div class="vpc-block">
<div class="vpc-header">
<span class="vpc-name">VPC 网段</span>
<span class="vpc-cidr">{{ vpcCidr }}</span>
<span class="vpc-stats">可用 IP: {{ totalIps.toLocaleString() }} </span>
</div>
<div class="subnet-grid">
<div
v-for="(subnet, index) in subnets"
:key="index"
class="subnet-cell"
:class="[subnet.type, { active: selectedSubnet === index }]"
@click="selectSubnet(index)"
@mouseenter="hoverSubnet = index"
@mouseleave="hoverSubnet = null"
>
<div class="cell-header">
<span class="cell-type">{{ subnet.type === 'public' ? '🌐' : '🔒' }}</span>
<span class="cell-name">{{ subnet.name }}</span>
</div>
<div class="cell-cidr">{{ subnet.cidr }}</div>
<div class="cell-stats">
<span class="ip-count">{{ subnet.ipCount }} IP</span>
<span class="az-badge">{{ subnet.az }}</span>
</div>
<!-- 悬停提示 -->
<div class="cell-tooltip" v-if="hoverSubnet === index && showCalculation">
<div class="tooltip-row">
<span>网段范围</span>
<code>{{ subnet.range }}</code>
</div>
<div class="tooltip-row">
<span>可用 IP</span>
<span>{{ subnet.usableIps }} </span>
</div>
<div class="tooltip-row">
<span>预留 IP</span>
<span>网络地址 + 广播地址 + 网关</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 网段计算说明 -->
<div class="calculation-panel" v-if="showCalculation">
<h4>📐 子网划分计算说明</h4>
<div class="calc-section">
<h5>1. 基础概念</h5>
<div class="concept-grid">
<div class="concept-item">
<span class="concept-label">CIDR 表示法</span>
<code>/24</code> 表示网络位占 24 主机位 8
</div>
<div class="concept-item">
<span class="concept-label"> IP </span>
<code>2^(32-24) = 256</code>
</div>
<div class="concept-item">
<span class="concept-label">可用 IP </span>
<code>256 - 3 = 253</code> 减去网络广播网关地址
</div>
</div>
</div>
<div class="calc-section">
<h5>2. 当前配置计算</h5>
<div class="calc-result">
<div class="result-item">
<span class="result-label">VPC 网段</span>
<code class="result-value">{{ vpcCidr }}</code>
</div>
<div class="result-item">
<span class="result-label">子网掩码</span>
<code class="result-value">/{{ subnetMask }}</code>
</div>
<div class="result-item">
<span class="result-label">子网数量</span>
<code class="result-value">{{ subnets.length }} </code>
</div>
<div class="result-item">
<span class="result-label">每个子网 IP </span>
<code class="result-value">{{ ipsPerSubnet }} </code>
</div>
</div>
</div>
</div>
<!-- 最佳实践提示 -->
<div class="tips-panel">
<h4>💡 子网设计最佳实践</h4>
<div class="tips-grid">
<div class="tip-item">
<div class="tip-icon">🎯</div>
<div class="tip-content">
<h5>预留足够 IP</h5>
<p>每个子网至少预留 20% IP 作为扩容缓冲</p>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">🔒</div>
<div class="tip-content">
<h5>公网私网分离</h5>
<p>核心数据放在私网子网通过 NAT 访问外网</p>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">🌐</div>
<div class="tip-content">
<h5> AZ 部署</h5>
<p>同一 VPC 的不同子网放在不同可用区</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const vpcCidr = ref('172.16.0.0/12')
const subnetBits = ref(2)
const showCalculation = ref(false)
const selectedSubnet = ref(null)
const hoverSubnet = ref(null)
const subnetMask = computed(() => {
const baseMask = parseInt(vpcCidr.value.split('/')[1])
return baseMask + subnetBits.value
})
const ipsPerSubnet = computed(() => {
return Math.pow(2, 32 - subnetMask.value)
})
const totalIps = computed(() => {
const mask = parseInt(vpcCidr.value.split('/')[1])
return Math.pow(2, 32 - mask)
})
const subnets = computed(() => {
const baseCidr = vpcCidr.value.split('/')[0]
const octets = baseCidr.split('.').map(Number)
const count = Math.pow(2, subnetBits.value)
const result = []
for (let i = 0; i < count; i++) {
const thirdOctet = octets[2] + Math.floor(i / 256)
const fourthOctet = i % 256
const cidr = `${octets[0]}.${octets[1]}.${thirdOctet}.${fourthOctet}/${subnetMask.value}`
const startIp = `${octets[0]}.${octets[1]}.${thirdOctet}.${fourthOctet}`
const endIp = `${octets[0]}.${octets[1]}.${thirdOctet + Math.floor((ipsPerSubnet.value - 1) / 256)}.${(fourthOctet + ipsPerSubnet.value - 1) % 256}`
result.push({
name: `子网-${String.fromCharCode(65 + i)}`,
cidr,
type: i % 2 === 0 ? 'public' : 'private',
ipCount: ipsPerSubnet.value,
az: `可用区 ${String.fromCharCode(65 + (i % 3))}`,
range: `${startIp} - ${endIp}`,
usableIps: ipsPerSubnet.value - 3
})
}
return result
})
const selectSubnet = (index) => {
selectedSubnet.value = selectedSubnet.value === index ? null : index
}
</script>
<style scoped>
.subnet-design-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
}
.panel-section {
display: flex;
align-items: center;
gap: 8px;
}
.panel-label {
font-size: 13px;
color: #606266;
font-weight: 500;
}
/* Network Visualization */
.network-visualization {
margin-bottom: 20px;
}
.vpc-block {
background: white;
border-radius: 12px;
padding: 20px;
border: 2px solid #409eff;
}
.vpc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.vpc-name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.vpc-cidr {
font-size: 13px;
padding: 4px 8px;
background: #ecf5ff;
color: #409eff;
border-radius: 4px;
font-family: monospace;
}
.vpc-stats {
font-size: 12px;
color: #909399;
}
/* Subnet Grid */
.subnet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
}
.subnet-cell {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.subnet-cell:hover {
border-color: #c0c4cc;
transform: translateY(-2px);
}
.subnet-cell.active {
border-color: #409eff;
background: #ecf5ff;
}
.subnet-cell.public {
border-left: 4px solid #409eff;
}
.subnet-cell.private {
border-left: 4px solid #67c23a;
}
.cell-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.cell-type {
font-size: 14px;
}
.cell-name {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.cell-cidr {
font-size: 11px;
color: #606266;
font-family: monospace;
margin-bottom: 8px;
}
.cell-stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.ip-count {
font-size: 11px;
color: #909399;
}
.az-badge {
font-size: 10px;
padding: 2px 6px;
background: #e4e7ed;
border-radius: 10px;
color: #606266;
}
/* Cell Tooltip */
.cell-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
z-index: 10;
margin-bottom: 8px;
white-space: nowrap;
}
.tooltip-row {
display: flex;
justify-content: space-between;
gap: 12px;
margin: 3px 0;
}
/* Calculation Panel */
.calculation-panel {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.calculation-panel h4 {
margin: 0 0 16px 0;
color: #303133;
font-size: 16px;
}
.calc-section {
margin-bottom: 20px;
}
.calc-section:last-child {
margin-bottom: 0;
}
.calc-section h5 {
margin: 0 0 12px 0;
color: #606266;
font-size: 14px;
}
.concept-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
}
.concept-item {
background: #f5f7fa;
padding: 12px;
border-radius: 6px;
font-size: 13px;
color: #606266;
}
.concept-label {
font-weight: 600;
color: #303133;
}
.calc-result {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: #f5f7fa;
border-radius: 6px;
}
.result-label {
font-size: 13px;
color: #606266;
}
.result-value {
font-size: 13px;
color: #409eff;
font-weight: 600;
font-family: monospace;
}
/* Tips Panel */
.tips-panel {
background: #f0f9eb;
border-radius: 8px;
padding: 20px;
border-left: 4px solid #67c23a;
}
.tips-panel h4 {
margin: 0 0 16px 0;
color: #67c23a;
font-size: 16px;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.tip-item {
display: flex;
gap: 12px;
background: white;
padding: 12px;
border-radius: 6px;
}
.tip-icon {
font-size: 24px;
flex-shrink: 0;
}
.tip-content h5 {
margin: 0 0 4px 0;
font-size: 14px;
color: #303133;
}
.tip-content p {
margin: 0;
font-size: 12px;
color: #606266;
line-height: 1.5;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.panel-section {
flex-direction: column;
align-items: flex-start;
}
.subnet-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,529 @@
<template>
<div class="vpc-architecture-demo">
<!-- 控制面板 -->
<div class="control-panel">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="full">完整架构</el-radio-button>
<el-radio-button label="public">公网访问</el-radio-button>
<el-radio-button label="private">私网隔离</el-radio-button>
<el-radio-button label="hybrid">混合云</el-radio-button>
</el-radio-group>
<el-switch
v-model="showDetails"
active-text="显示详情"
style="margin-left: 20px"
/>
</div>
<!-- VPC 架构图 -->
<div class="vpc-container">
<!-- 外部互联网 -->
<div class="internet-zone" v-if="showInternet">
<div class="zone-header">
<span class="zone-icon">🌐</span>
<span class="zone-title">互联网 (Internet)</span>
</div>
<div class="zone-content">
<div class="internet-user">
<div class="user-avatar">👤</div>
<div class="user-label">用户</div>
</div>
<div class="internet-user">
<div class="user-avatar">🏢</div>
<div class="user-label">企业</div>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-flow" v-if="showInternet">
<div class="flow-line"></div>
<div class="flow-devices">
<div class="device" v-for="device in borderDevices" :key="device.name"
:class="device.type"
@mouseenter="hoverDevice = device.name"
@mouseleave="hoverDevice = null"
003e
<div class="device-icon">{{ device.icon }}</div>
<div class="device-name">{{ device.name }}</div>
<div class="device-tooltip" v-if="hoverDevice === device.name && showDetails">
{{ device.description }}
</div>
</div>
</div>
</div>
<!-- VPC 主体 -->
<div class="vpc-zone">
<div class="vpc-header">
<div class="vpc-title">
<span class="vpc-icon">🏠</span>
<span>专有网络 VPC</span>
<span class="vpc-id">vpc-2ze7p8w7c9d6x5y4</span>
</div>
<div class="vpc-meta">
<span class="meta-item">📍 华北2 (北京)</span>
<span class="meta-item">🌐 172.16.0.0/12</span>
</div>
</div>
<div class="vpc-content">
<!-- 可用区 1 -->
<div class="az-container">
<div class="az-header">
<span class="az-name">可用区 A</span>
<span class="az-status online">在线</span>
</div>
<div class="subnets">
<div class="subnet public"
@mouseenter="hoverSubnet = 'public-a'"
@mouseleave="hoverSubnet = null">
<div class="subnet-header">
<span class="subnet-type">🌐 公网子网</span>
<span class="subnet-cidr">172.16.1.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 2</div>
<div class="resource-tag"> SLB</div>
<div class="resource-tag">🌐 NAT</div>
</div>
<div class="subnet-tooltip" v-if="hoverSubnet === 'public-a' && showDetails">
公网子网可直接访问互联网部署对外服务
</div>
</div>
<div class="subnet private"
@mouseenter="hoverSubnet = 'private-a'"
@mouseleave="hoverSubnet = null">
<div class="subnet-header">
<span class="subnet-type">🔒 私网子网</span>
<span class="subnet-cidr">172.16.2.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 4</div>
<div class="resource-tag">🗄 RDS</div>
<div class="resource-tag">📦 Redis</div>
</div>
<div class="subnet-tooltip" v-if="hoverSubnet === 'private-a' && showDetails">
私网子网无法直接访问互联网部署核心服务
</div>
</div>
</div>
</div>
<!-- 可用区 2 -->
<div class="az-container">
<div class="az-header">
<span class="az-name">可用区 B</span>
<span class="az-status online">在线</span>
</div>
<div class="subnets">
<div class="subnet public">
<div class="subnet-header">
<span class="subnet-type">🌐 公网子网</span>
<span class="subnet-cidr">172.16.3.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 2</div>
<div class="resource-tag"> SLB</div>
</div>
</div>
<div class="subnet private">
<div class="subnet-header">
<span class="subnet-type">🔒 私网子网</span>
<span class="subnet-cidr">172.16.4.0/24</span>
</div>
<div class="subnet-resources">
<div class="resource-tag">🖥 ECS × 4</div>
<div class="resource-tag">🗄 RDS Slave</div>
<div class="resource-tag">📦 Redis Slave</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('full')
const showDetails = ref(false)
const hoverDevice = ref(null)
const hoverSubnet = ref(null)
const showInternet = computed(() => {
return ['full', 'public', 'hybrid'].includes(viewMode.value)
})
const borderDevices = [
{
name: '边界路由器',
icon: '📡',
type: 'router',
description: '连接VPC与互联网的核心路由设备'
},
{
name: 'NAT网关',
icon: '🔄',
type: 'nat',
description: '实现私网资源访问互联网的地址转换'
},
{
name: '负载均衡',
icon: '⚖️',
type: 'slb',
description: '分发公网流量到多台后端服务器'
}
]
</script>
<style scoped>
.vpc-architecture-demo {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.vpc-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Internet Zone */
.internet-zone {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 12px;
padding: 16px;
border: 2px solid #90caf9;
}
.zone-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.zone-icon {
font-size: 20px;
}
.zone-title {
font-size: 16px;
font-weight: 600;
color: #1565c0;
}
.zone-content {
display: flex;
gap: 16px;
justify-content: center;
}
.internet-user {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.user-avatar {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-label {
font-size: 12px;
color: #546e7a;
}
/* Connection Flow */
.connection-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.flow-line {
width: 4px;
height: 24px;
background: linear-gradient(to bottom, #90caf9, #4caf50);
border-radius: 2px;
}
.flow-devices {
display: flex;
gap: 24px;
justify-content: center;
}
.device {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: white;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.device:hover {
border-color: #409eff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.device.router {
border-color: #ff9800;
}
.device.nat {
border-color: #9c27b0;
}
.device.slb {
border-color: #2196f3;
}
.device-icon {
font-size: 24px;
}
.device-name {
font-size: 12px;
font-weight: 500;
color: #424242;
}
.device-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
background: #333;
color: white;
font-size: 12px;
border-radius: 6px;
white-space: nowrap;
z-index: 10;
margin-bottom: 8px;
}
.device-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #333;
}
/* VPC Zone */
.vpc-zone {
background: white;
border-radius: 12px;
padding: 20px;
border: 2px solid #409eff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.1);
}
.vpc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.vpc-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.vpc-icon {
font-size: 20px;
}
.vpc-id {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 8px;
}
.vpc-meta {
display: flex;
gap: 16px;
}
.meta-item {
font-size: 13px;
color: #606266;
}
.vpc-content {
display: flex;
flex-direction: column;
gap: 16px;
}
/* AZ Container */
.az-container {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.az-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.az-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.az-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.az-status.online {
background: #e1f3d8;
color: #67c23a;
}
.subnets {
display: flex;
flex-direction: column;
gap: 8px;
}
.subnet {
background: white;
border-radius: 6px;
padding: 10px;
border: 2px solid #e4e7ed;
transition: all 0.3s;
position: relative;
}
.subnet:hover {
transform: translateX(4px);
}
.subnet.public {
border-left: 4px solid #409eff;
}
.subnet.private {
border-left: 4px solid #67c23a;
}
.subnet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.subnet-type {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.subnet-cidr {
font-size: 11px;
padding: 2px 6px;
background: #f0f2f5;
border-radius: 4px;
color: #606266;
font-family: monospace;
}
.subnet-resources {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.resource-tag {
font-size: 11px;
padding: 3px 8px;
background: #ecf5ff;
border-radius: 4px;
color: #409eff;
}
.subnet-tooltip {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 8px;
padding: 8px 12px;
background: #333;
color: white;
font-size: 12px;
border-radius: 6px;
z-index: 10;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;
align-items: stretch;
}
.vpc-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.vpc-meta {
flex-wrap: wrap;
}
}
</style>
@@ -0,0 +1,347 @@
<template>
<div class="component-hierarchy-demo">
<div class="demo-header">
<h4>组件层级可视化</h4>
<p class="hint">点击组件查看详情观察组件树如何组织</p>
</div>
<div class="tree-container">
<div class="tree-node root-node" :class="{ active: selectedNode === 'app' }" @click="selectNode('app')">
<div class="node-icon">🌳</div>
<div class="node-label">App (根组件)</div>
<div class="node-desc">管理全局状态</div>
</div>
<div class="tree-children">
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node" :class="{ active: selectedNode === 'header' }" @click="selectNode('header')">
<div class="node-icon">📌</div>
<div class="node-label">Header</div>
<div class="node-desc">导航 + 用户信息</div>
</div>
</div>
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node" :class="{ active: selectedNode === 'main' }" @click="selectNode('main')">
<div class="node-icon">📄</div>
<div class="node-label">Main Content</div>
<div class="node-desc">页面主要内容</div>
</div>
<div class="tree-children">
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node" :class="{ active: selectedNode === 'sidebar' }" @click="selectNode('sidebar')">
<div class="node-icon">📑</div>
<div class="node-label">Sidebar</div>
<div class="node-desc">侧边栏菜单</div>
</div>
</div>
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node" :class="{ active: selectedNode === 'productlist' }" @click="selectNode('productlist')">
<div class="node-icon">🛍</div>
<div class="node-label">ProductList</div>
<div class="node-desc">商品列表展示</div>
</div>
<div class="tree-children">
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node leaf" :class="{ active: selectedNode === 'productcard' }" @click="selectNode('productcard')">
<div class="node-icon">🏷</div>
<div class="node-label">ProductCard</div>
<div class="node-desc">单个商品卡片</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tree-branch">
<div class="connector"></div>
<div class="tree-node" :class="{ active: selectedNode === 'footer' }" @click="selectNode('footer')">
<div class="node-icon">🔻</div>
<div class="node-label">Footer</div>
<div class="node-desc">页脚信息</div>
</div>
</div>
</div>
</div>
<div v-if="selectedNodeInfo" class="node-details">
<h5>{{ selectedNodeInfo.title }}</h5>
<p>{{ selectedNodeInfo.description }}</p>
<div class="props-list" v-if="selectedNodeInfo.props">
<strong>接收的 Props:</strong>
<ul>
<li v-for="prop in selectedNodeInfo.props" :key="prop">{{ prop }}</li>
</ul>
</div>
<div class="events-list" v-if="selectedNodeInfo.events">
<strong>触发的事件:</strong>
<ul>
<li v-for="event in selectedNodeInfo.events" :key="event">{{ event }}</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedNode = ref(null)
const nodeInfoMap = {
app: {
title: 'App (根组件)',
description: '应用的入口组件,负责初始化全局状态、路由配置和全局样式。通常包含 RouterView 来渲染页面级组件。',
props: [],
events: []
},
header: {
title: 'Header (导航栏)',
description: '顶部导航组件,显示 Logo、主导航菜单、用户信息、购物车入口等。通常是全局组件,在大多数页面都显示。',
props: ['user', 'cartCount'],
events: ['logout', 'search']
},
main: {
title: 'Main Content (主内容区)',
description: '页面的主要内容区域,包含侧边栏和具体内容。使用 flex 或 grid 布局来组织内容。',
props: [],
events: []
},
sidebar: {
title: 'Sidebar (侧边栏)',
description: '左侧导航菜单,通常用于后台管理系统或分类浏览页面。包含可折叠的菜单组。',
props: ['menuItems', 'collapsed'],
events: ['select', 'toggle']
},
productlist: {
title: 'ProductList (商品列表)',
description: '展示商品列表的容器组件,负责数据获取、分页、排序和筛选逻辑。包含多个 ProductCard 组件。',
props: ['products', 'loading', 'total'],
events: ['loadMore', 'sort', 'filter']
},
productcard: {
title: 'ProductCard (商品卡片)',
description: '单个商品的展示卡片,显示商品图片、名称、价格、评分等信息。是最基础的 UI 组件之一。',
props: ['product', 'showAddToCart'],
events: ['addToCart', 'click']
},
footer: {
title: 'Footer (页脚)',
description: '页面底部的信息区域,包含版权信息、友情链接、联系方式、社交媒体链接等。',
props: [],
events: []
}
}
const selectedNodeInfo = computed(() => {
return selectedNode.value ? nodeInfoMap[selectedNode.value] : null
})
const selectNode = (nodeId) => {
selectedNode.value = nodeId
}
</script>
<style scoped>
.component-hierarchy-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.tree-container {
overflow-x: auto;
padding: 10px 0;
}
.tree-children {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
margin-left: 28px;
}
.tree-branch {
position: relative;
display: flex;
align-items: flex-start;
gap: 12px;
}
.connector {
width: 20px;
height: 2px;
background: var(--vp-c-divider);
margin-top: 24px;
position: relative;
}
.connector::before {
content: '';
position: absolute;
left: 0;
top: -10px;
width: 2px;
height: 12px;
background: var(--vp-c-divider);
}
.tree-node {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 180px;
}
.tree-node:hover {
border-color: var(--vp-c-brand);
transform: translateX(4px);
}
.tree-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
}
.root-node {
background: linear-gradient(135deg, var(--vp-c-brand-soft), var(--vp-c-bg));
border-width: 3px;
}
.leaf .node-icon {
opacity: 0.8;
transform: scale(0.9);
}
.node-icon {
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.node-label {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.node-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-left: auto;
padding-left: 12px;
border-left: 1px solid var(--vp-c-divider);
}
.node-details {
margin-top: 20px;
padding: 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.node-details h5 {
margin: 0 0 8px 0;
color: var(--vp-c-brand);
font-size: 16px;
}
.node-details p {
margin: 0 0 12px 0;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
}
.props-list, .events-list {
margin-top: 12px;
}
.props-list strong, .events-list strong {
display: block;
margin-bottom: 4px;
color: var(--vp-c-text-1);
font-size: 13px;
}
.props-list ul, .events-list ul {
margin: 0;
padding-left: 20px;
}
.props-list li, .events-list li {
color: var(--vp-c-text-2);
font-size: 13px;
font-family: monospace;
margin: 2px 0;
}
@media (max-width: 768px) {
.tree-node {
min-width: auto;
flex-wrap: wrap;
}
.node-desc {
width: 100%;
margin-top: 4px;
padding-left: 42px;
border-left: none;
}
.tree-children {
margin-left: 16px;
}
}
</style>
@@ -0,0 +1,599 @@
<template>
<div class="event-bus-demo">
<div class="demo-header">
<h4>事件总线通信演示</h4>
<p class="hint">观察多个独立组件如何通过事件总线进行通信注意内存管理的重要性</p>
</div>
<div class="architecture-view">
<div class="central-hub" :class="{ active: isTransmitting }">
<div class="hub-core">
<div class="hub-icon">🔌</div>
<div class="hub-label">Event Bus</div>
<div class="hub-status">{{ isTransmitting ? '传输中...' : '待机' }}</div>
</div>
<div class="hub-rings">
<div class="ring ring-1"></div>
<div class="ring ring-2"></div>
<div class="ring ring-3"></div>
</div>
</div>
<div class="connected-components">
<div
v-for="comp in components"
:key="comp.id"
class="component-node"
:class="{ active: comp.isActive, emitting: comp.isEmitting, listening: comp.isListening }"
:style="{ top: comp.y + '%', left: comp.x + '%' }"
>
<div class="node-header">
<span class="node-icon">{{ comp.icon }}</span>
<span class="node-name">{{ comp.name }}</span>
</div>
<div class="node-status">
<span class="status-dot" :class="{ active: comp.isListening }"></span>
{{ comp.isListening ? '监听中' : '未监听' }}
</div>
<button class="emit-btn" @click="emitEvent(comp)">
发送事件
</button>
</div>
</div>
<svg class="connection-lines" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<marker id="arrowhead" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
<polygon points="0 0, 6 3, 0 6" :fill="isTransmitting ? 'var(--vp-c-brand)' : 'var(--vp-c-divider)'" />
</marker>
</defs>
<line
v-for="(comp, index) in components"
:key="index"
class="connection-line"
:class="{ active: comp.isEmitting || isTransmitting }"
:x1="comp.x"
:y1="comp.y"
x2="50"
y2="50"
marker-end="url(#arrowhead)"
/>
</svg>
</div>
<div class="event-log">
<div class="log-header">
<h5>📨 事件日志</h5>
<button class="clear-btn" @click="clearLogs">清空</button>
</div>
<div class="log-content">
<div v-if="logs.length === 0" class="empty-log">
暂无事件记录点击组件上的"发送事件"按钮开始测试
</div>
<div
v-for="(log, index) in logs"
:key="index"
class="log-item"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-type">{{ log.type === 'emit' ? '发送' : '接收' }}</span>
<span class="log-from">{{ log.from }}</span>
<span class="log-arrow"></span>
<span class="log-to">{{ log.to }}</span>
<span class="log-data">{{ log.data }}</span>
</div>
</div>
</div>
<div class="memory-warning">
<div class="warning-icon"></div>
<div class="warning-content">
<h6>内存泄漏风险提醒</h6>
<p>使用 Event Bus 如果组件销毁前没有取消订阅$off会导致内存泄漏推荐在 beforeUnmount 钩子中清理订阅</p>
<pre><code>// 正确做法
export default {
created() {
this.$bus.$on('event', this.handler)
},
beforeUnmount() {
this.$bus.$off('event', this.handler) // 必须取消订阅
}
}</code></pre>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onBeforeUnmount } from 'vue'
// 组件定义
const components = reactive([
{ id: 1, name: 'Header', icon: '📌', x: 15, y: 15, isActive: false, isEmitting: false, isListening: true },
{ id: 2, name: 'Sidebar', icon: '📑', x: 85, y: 15, isActive: false, isEmitting: false, isListening: true },
{ id: 3, name: 'ProductList', icon: '🛍️', x: 15, y: 85, isActive: false, isEmitting: false, isListening: true },
{ id: 4, name: 'Cart', icon: '🛒', x: 85, y: 85, isActive: false, isEmitting: false, isListening: true }
])
const isTransmitting = ref(false)
const logs = ref([])
// 格式化时间
const formatTime = () => {
const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`
}
// 添加日志
const addLog = (type, from, to, data) => {
logs.value.unshift({
type,
time: formatTime(),
from,
to,
data: JSON.stringify(data)
})
if (logs.value.length > 50) {
logs.value = logs.value.slice(0, 50)
}
}
// 发送事件
const emitEvent = (comp) => {
// 触发发送动画
comp.isEmitting = true
isTransmitting.value = true
// 记录发送日志
addLog('emit', comp.name, 'Event Bus', { event: 'user:action', payload: { from: comp.name } })
// 模拟其他组件接收
components.forEach(target => {
if (target.id !== comp.id && target.isListening) {
setTimeout(() => {
target.isActive = true
addLog('receive', 'Event Bus', target.name, { event: 'user:action', payload: { from: comp.name } })
setTimeout(() => {
target.isActive = false
}, 500)
}, 300 + Math.random() * 200)
}
})
// 清理动画状态
setTimeout(() => {
comp.isEmitting = false
isTransmitting.value = false
}, 1000)
}
// 清空日志
const clearLogs = () => {
logs.value = []
}
onBeforeUnmount(() => {
// 模拟清理订阅
components.forEach(comp => {
comp.isListening = false
})
})
</script>
<style scoped>
.event-bus-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.architecture-view {
position: relative;
min-height: 400px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
overflow: hidden;
}
.central-hub {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.hub-core {
position: relative;
z-index: 2;
width: 100px;
height: 100px;
background: linear-gradient(135deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.central-hub.active .hub-core {
transform: scale(1.1);
box-shadow: 0 0 30px var(--vp-c-brand);
}
.hub-icon {
font-size: 28px;
margin-bottom: 2px;
}
.hub-label {
font-size: 11px;
font-weight: 600;
color: white;
text-align: center;
}
.hub-status {
font-size: 9px;
color: rgba(255, 255, 255, 0.8);
margin-top: 2px;
}
.hub-rings {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--vp-c-divider);
border-radius: 50%;
opacity: 0.3;
transition: all 0.3s ease;
}
.central-hub.active .ring {
border-color: var(--vp-c-brand);
opacity: 0.6;
}
.ring-1 { width: 140px; height: 140px; }
.ring-2 { width: 180px; height: 180px; }
.ring-3 { width: 220px; height: 220px; }
.central-hub.active .ring-1 { animation: pulse1 1s ease infinite; }
.central-hub.active .ring-2 { animation: pulse2 1s ease infinite 0.2s; }
.central-hub.active .ring-3 { animation: pulse3 1s ease infinite 0.4s; }
@keyframes pulse1 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.6; }
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.3; }
}
@keyframes pulse2 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.4; }
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.2; }
}
@keyframes pulse3 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.2; }
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.1; }
}
.connected-components {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.component-node {
position: absolute;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px;
min-width: 120px;
pointer-events: auto;
transition: all 0.3s ease;
cursor: pointer;
}
.component-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.component-node.active {
border-color: #22c55e;
background: #f0fdf4;
box-shadow: 0 0 0 3px #bbf7d0;
}
.component-node.emitting {
border-color: var(--vp-c-brand);
animation: emitPulse 0.5s ease;
}
@keyframes emitPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.node-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.node-icon {
font-size: 16px;
}
.node-name {
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-1);
}
.node-status {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--vp-c-divider);
transition: all 0.3s ease;
}
.status-dot.active {
background: #22c55e;
box-shadow: 0 0 4px #22c55e;
animation: blink 1.5s ease infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.emit-btn {
width: 100%;
padding: 6px 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
}
.emit-btn:hover {
background: var(--vp-c-brand-dark);
}
.connection-lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.connection-line {
stroke: var(--vp-c-divider);
stroke-width: 2;
fill: none;
transition: all 0.3s ease;
}
.connection-line.active {
stroke: var(--vp-c-brand);
stroke-width: 3;
filter: drop-shadow(0 0 4px var(--vp-c-brand));
}
.event-log {
margin-top: 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.log-header h5 {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.clear-btn {
padding: 4px 12px;
background: transparent;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 12px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.clear-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.log-content {
max-height: 200px;
overflow-y: auto;
padding: 8px;
}
.empty-log {
text-align: center;
padding: 40px;
color: var(--vp-c-text-3);
font-size: 13px;
}
.log-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.log-item.emit {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
.log-item.receive {
background: #f0fdf4;
border-left: 3px solid #22c55e;
}
.log-time {
color: var(--vp-c-text-3);
font-size: 11px;
}
.log-type {
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.log-item.emit .log-type {
background: #dbeafe;
color: #1d4ed8;
}
.log-item.receive .log-type {
background: #bbf7d0;
color: #15803d;
}
.log-from,
.log-to {
color: var(--vp-c-text-2);
}
.log-arrow {
color: var(--vp-c-divider);
}
.log-data {
color: var(--vp-c-brand);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.memory-warning {
margin-top: 20px;
padding: 16px;
background: #fffbeb;
border: 1px solid #fcd34d;
border-radius: 8px;
display: flex;
gap: 12px;
}
.warning-icon {
font-size: 24px;
flex-shrink: 0;
}
.warning-content h6 {
margin: 0 0 4px 0;
color: #92400e;
font-size: 14px;
}
.warning-content p {
margin: 0 0 8px 0;
color: #a16207;
font-size: 13px;
line-height: 1.5;
}
.warning-content pre {
margin: 0;
padding: 12px;
background: #fef3c7;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
}
.warning-content code {
color: #92400e;
font-family: monospace;
}
</style>
@@ -0,0 +1,639 @@
<template>
<div class="mobx-reactivity-demo">
<div class="demo-header">
<h4>MobX 响应式原理演示</h4>
<p class="hint">体验 MobX 的自动依赖追踪机制理解 ObservableAction Reaction 的关系</p>
</div>
<!-- 可视化图表 -->
<div class="visualization-area">
<div class="flow-diagram">
<!-- Observable -->
<div class="box-container observable-side">
<div class="box-title">Observable (可观察状态)</div>
<div class="state-boxes">
<div
v-for="item in todos"
:key="item.id"
class="state-item"
:class="{ completed: item.completed, changed: recentlyChanged === item.id }"
>
<span class="item-text">{{ item.text }}</span>
<span class="item-status">{{ item.completed ? '✓' : '○' }}</span>
</div>
</div>
</div>
<!-- 连接箭头 -->
<div class="connection-area">
<div class="arrow-bidirectional">
<div class="arrow-label top">追踪依赖</div>
<div class="arrow-line">
<div class="particles">
<span v-for="i in 3" :key="i" class="particle"></span>
</div>
</div>
<div class="arrow-label bottom">触发更新</div>
</div>
</div>
<!-- Reaction -->
<div class="box-container reaction-side">
<div class="box-title">Reaction (响应/副作用)</div>
<div class="reactions-list">
<div class="reaction-item computed">
<div class="reaction-header">
<span class="reaction-icon">🧮</span>
<span class="reaction-name">Computed: 待办统计</span>
</div>
<div class="reaction-value">
{{ todos.length }} 已完成 {{ completedCount }}
</div>
</div>
<div class="reaction-item autorun">
<div class="reaction-header">
<span class="reaction-icon">🔄</span>
<span class="reaction-name">Autorun: 自动保存</span>
</div>
<div class="reaction-status" :class="{ active: autoSaveActive }">
{{ autoSaveActive ? '💾 已自动保存到 localStorage' : '⏸️ 等待变更...' }}
</div>
</div>
<div class="reaction-item reaction">
<div class="reaction-header">
<span class="reaction-icon">👀</span>
<span class="reaction-name">Reaction: 变更日志</span>
</div>
<div class="reaction-log">
<div v-for="(log, index) in changeLogs" :key="index" class="log-entry">
<span class="log-time">{{ log.time }}</span>
<span class="log-action">{{ log.action }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action 区域 -->
<div class="action-area">
<div class="action-title">🎮 交互控制台 (Action)</div>
<div class="action-controls">
<div class="input-group">
<input v-model="newTodoText" placeholder="输入待办事项..." @keyup.enter="addTodo" />
<button @click="addTodo">添加</button>
</div>
<div class="quick-actions">
<button @click="completeAll">全部完成</button>
<button @click="clearCompleted">清除已完成</button>
<button @click="reset">重置</button>
</div>
</div>
</div>
<!-- 说明区域 -->
<div class="explanation-area">
<div class="explanation-card">
<h5>📦 Observable (可观察状态)</h5>
<p>使用 <code>observable</code> 或类属性装饰器 <code>@observable</code> 定义的状态当状态变化时所有依赖它的 Reaction 会自动重新执行</p>
</div>
<div class="explanation-card">
<h5> Action (动作)</h5>
<p>使用 <code>action</code> <code>@action</code> 装饰器标记的方法用于修改 Observable 状态Action 会批量处理变更通知避免中间状态的重复渲染</p>
</div>
<div class="explanation-card"
>
<h5>🔄 Reaction (响应)</h5>
<p> Observable 状态变化时自动执行的副作用包括</p>
<ul>
<li><code>autorun</code>: 自动追踪依赖并执行</li>
<li><code>reaction</code>: 对特定数据变化作出反应</li>
<li><code>when</code>: 条件满足时执行一次</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// 状态
const todos = ref([
{ id: 1, text: '学习 MobX', completed: false },
{ id: 2, text: '理解响应式原理', completed: true }
])
const newTodoText = ref('')
const recentlyChanged = ref(null)
const autoSaveActive = ref(false)
const changeLogs = ref([])
// 计算属性(模拟 MobX 的 computed
const completedCount = computed(() => {
return todos.value.filter(t => t.completed).length
})
// 方法(模拟 MobX 的 action
const addTodo = () => {
if (!newTodoText.value.trim()) return
const newTodo = {
id: Date.now(),
text: newTodoText.value,
completed: false
}
todos.value.push(newTodo)
recentlyChanged.value = newTodo.id
newTodoText.value = ''
setTimeout(() => {
recentlyChanged.value = null
}, 500)
addLog('添加待办', newTodo.text)
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
recentlyChanged.value = id
setTimeout(() => {
recentlyChanged.value = null
}, 500)
addLog(todo.completed ? '完成待办' : '取消完成', todo.text)
}
}
const completeAll = () => {
todos.value.forEach(t => t.completed = true)
addLog('全部完成', `${todos.value.length}`)
}
const clearCompleted = () => {
const count = todos.value.filter(t => t.completed).length
todos.value = todos.value.filter(t => !t.completed)
addLog('清除已完成', `${count}`)
}
const reset = () => {
todos.value = [
{ id: 1, text: '学习 MobX', completed: false },
{ id: 2, text: '理解响应式原理', completed: true }
]
changeLogs.value = []
addLog('重置', '恢复初始状态')
}
const addLog = (action, detail) => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
changeLogs.value.unshift({ time, action, detail })
if (changeLogs.value.length > 10) {
changeLogs.value = changeLogs.value.slice(0, 10)
}
}
// 模拟 autorun - 自动保存
watch(todos, () => {
autoSaveActive.value = true
setTimeout(() => {
autoSaveActive.value = false
}, 1000)
}, { deep: true })
</script>
<style scoped>
.mobx-reactivity-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.visualization-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.flow-diagram {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 20px;
align-items: start;
}
@media (max-width: 968px) {
.flow-diagram {
grid-template-columns: 1fr;
}
.connection-area {
transform: rotate(90deg);
padding: 40px 0 !important;
}
}
.box-container {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.box-container.observable-side {
border-color: #ff6b6b;
}
.box-container.reaction-side {
border-color: #4ecdc4;
}
.box-title {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.state-boxes {
display: flex;
flex-direction: column;
gap: 8px;
}
.state-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.state-item:hover {
border-color: var(--vp-c-brand);
transform: translateX(4px);
}
.state-item.completed {
background: #f0fdf4;
border-color: #86efac;
}
.state-item.changed {
animation: highlight 0.5s ease;
}
@keyframes highlight {
0%, 100% { background: var(--vp-c-bg); }
50% { background: #fef3c7; }
}
.item-text {
font-size: 13px;
color: var(--vp-c-text-1);
}
.state-item.completed .item-text {
text-decoration: line-through;
color: var(--vp-c-text-3);
}
.item-status {
font-size: 14px;
color: #22c55e;
}
.connection-area {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.arrow-bidirectional {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.arrow-label {
font-size: 11px;
color: var(--vp-c-text-3);
padding: 4px 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
white-space: nowrap;
}
.arrow-label.top {
margin-bottom: 4px;
}
.arrow-label.bottom {
margin-top: 4px;
}
.arrow-line {
position: relative;
width: 3px;
height: 80px;
background: linear-gradient(to bottom, #ff6b6b, #4ecdc4);
border-radius: 2px;
}
.particles {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 10px 0;
}
.particle {
font-size: 8px;
color: var(--vp-c-brand);
animation: flow 1.5s linear infinite;
opacity: 0;
}
.particle:nth-child(1) { animation-delay: 0s; }
.particle:nth-child(2) { animation-delay: 0.5s; }
.particle:nth-child(3) { animation-delay: 1s; }
@keyframes flow {
0% {
opacity: 0;
transform: translateY(0);
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(60px);
}
}
.reactions-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.reaction-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
transition: all 0.3s ease;
}
.reaction-item.computed {
border-left: 4px solid #8b5cf6;
}
.reaction-item.autorun {
border-left: 4px solid #f59e0b;
}
.reaction-item.reaction {
border-left: 4px solid #ec4899;
}
.reaction-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.reaction-icon {
font-size: 14px;
}
.reaction-name {
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-1);
}
.reaction-value {
font-size: 13px;
color: var(--vp-c-text-2);
font-family: monospace;
padding: 4px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.reaction-status {
font-size: 12px;
color: var(--vp-c-text-3);
padding: 4px 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.reaction-status.active {
color: #22c55e;
background: #dcfce7;
}
.reaction-log {
max-height: 100px;
overflow-y: auto;
}
.log-entry {
display: flex;
gap: 8px;
font-size: 11px;
padding: 3px 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--vp-c-text-3);
font-family: monospace;
}
.log-action {
color: var(--vp-c-text-2);
}
.action-area {
margin-top: 20px;
padding: 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.action-title {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-size: 14px;
}
.action-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group {
display: flex;
gap: 8px;
}
.input-group input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 14px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.input-group input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.input-group button {
padding: 8px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.input-group button:hover {
background: var(--vp-c-brand-dark);
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-actions button {
padding: 6px 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 12px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.quick-actions button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.explanation-area {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.explanation-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.explanation-card h5 {
margin: 0 0 10px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.explanation-card p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.explanation-card code {
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
color: var(--vp-c-brand);
}
.explanation-card ul {
margin: 8px 0 0 0;
padding-left: 18px;
}
.explanation-card li {
font-size: 12px;
color: var(--vp-c-text-2);
margin: 4px 0;
}
@media (max-width: 640px) {
.quick-actions {
justify-content: center;
}
}
</style>
@@ -0,0 +1,587 @@
<template>
<div class="props-flow-demo">
<div class="demo-header">
<h4>Props 数据流演示</h4>
<p class="hint">观察父组件如何通过 props 向子组件传递数据以及子组件如何通过事件向父组件通信</p>
</div>
<div class="flow-container">
<!-- 父组件可视化 -->
<div class="parent-component">
<div class="component-header">
<span class="tag">Parent.vue</span>
<span class="badge blue">数据持有者</span>
</div>
<div class="data-box">
<div class="data-title">data() {</div>
<div class="data-item" :class="{ changed: hasChanged }">
<span class="key">user:</span>
<span class="value">{ name: '{{ userName }}', age: {{ userAge }} }</span>
</div>
<div class="data-item">
<span class="key">theme:</span>
<span class="value">'{{ theme }}'</span>
</div>
<div class="data-title">}</div>
</div>
<div class="props-config">
<div class="config-title">传递给子组件的 Props:</div>
<div class="prop-tag" v-for="prop in activeProps" :key="prop">
:{{ prop }}="{{ prop }}"
</div>
</div>
</div>
<!-- 流动动画 -->
<div class="flow-animation">
<div class="flow-line" :class="{ active: isFlowing }">
<div class="flow-particles" v-if="isFlowing">
<span v-for="i in 5" :key="i" class="particle"></span>
</div>
</div>
<div class="flow-label" :class="{ active: isFlowing }">
{{ isFlowing ? '传递 Props' : '等待交互' }}
</div>
</div>
<!-- 子组件可视化 -->
<div class="child-component">
<div class="component-header">
<span class="tag">Child.vue</span>
<span class="badge green">数据展示</span>
</div>
<div class="props-box">
<div class="props-title">props: {</div>
<div class="prop-item" v-for="prop in activeProps" :key="prop" :class="{ receiving: isFlowing }">
<span class="prop-name">{{ prop }}</span>
<span class="prop-type">{ type: {{ getPropType(prop) }} }</span>
</div>
<div class="props-title">}</div>
</div>
<div class="render-preview">
<div class="preview-title">渲染预览:</div>
<div class="preview-content">
<div class="user-card">
<div class="avatar">👤</div>
<div class="user-info">
<div class="user-name">{{ userName || '未知用户' }}</div>
<div class="user-meta">
<span class="age">{{ userAge }}</span>
<span class="theme-badge" :class="theme">{{ theme }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="emit-section">
<div class="emit-title">向父组件通信:</div>
<button class="emit-btn" @click="emitUpdate">$emit('update', { name: '王五' })</button>
</div>
</div>
</div>
<div class="interaction-panel">
<div class="panel-title">🎮 交互控制台</div>
<div class="control-group">
<label>修改父组件数据:</label>
<input v-model="userName" placeholder="用户名" @input="triggerFlow" />
<input v-model.number="userAge" type="number" placeholder="年龄" @input="triggerFlow" />
<select v-model="theme" @change="triggerFlow">
<option value="light">Light 主题</option>
<option value="dark">Dark 主题</option>
</select>
</div>
<div class="control-group">
<label>选择传递的 Props:</label>
<label class="checkbox"><input type="checkbox" v-model="propSelection.user" /> user</label>
<label class="checkbox"><input type="checkbox" v-model="propSelection.theme" /> theme</label>
</div>
<div class="flow-status" :class="{ active: isFlowing }">
{{ isFlowing ? '⬇️ 数据正在流动...' : '⏸️ 等待数据变化' }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// 父组件数据
const userName = ref('张三')
const userAge = ref(25)
const theme = ref('light')
const hasChanged = ref(false)
// Props 选择
const propSelection = ref({
user: true,
theme: true
})
const activeProps = computed(() => {
return Object.entries(propSelection.value)
.filter(([, v]) => v)
.map(([k]) => k)
})
// 流动动画控制
const isFlowing = ref(false)
let flowTimeout = null
const triggerFlow = () => {
hasChanged.value = true
isFlowing.value = true
clearTimeout(flowTimeout)
flowTimeout = setTimeout(() => {
isFlowing.value = false
hasChanged.value = false
}, 1500)
}
// 监听数据变化
watch([userName, userAge, theme], () => {
triggerFlow()
}, { deep: true })
const getPropType = (prop) => {
const types = {
user: 'Object',
theme: 'String'
}
return types[prop] || 'Any'
}
const emitUpdate = () => {
userName.value = '王五'
triggerFlow()
}
</script>
<style scoped>
.props-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.flow-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 968px) {
.flow-container {
grid-template-columns: 1fr;
}
.flow-animation {
flex-direction: row !important;
padding: 12px !important;
}
.flow-line {
width: 100% !important;
height: 2px !important;
}
}
.parent-component,
.child-component {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.component-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.tag {
font-family: monospace;
font-size: 13px;
padding: 4px 8px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-radius: 4px;
}
.badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
}
.badge.blue {
background: #dbeafe;
color: #1e40af;
}
.badge.green {
background: #dcfce7;
color: #166534;
}
.data-box,
.props-box {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
font-family: monospace;
font-size: 13px;
}
.data-title,
.props-title {
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.data-item,
.prop-item {
padding: 4px 8px;
margin: 2px 0;
border-radius: 3px;
transition: all 0.3s ease;
}
.data-item.changed {
background: #fef3c7;
animation: pulse 0.5s ease;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.data-item .key {
color: var(--vp-c-brand);
}
.data-item .value {
color: var(--vp-c-text-2);
}
.prop-item.receiving {
background: #dcfce7;
animation: receive 0.5s ease;
}
@keyframes receive {
0% { transform: translateX(-10px); opacity: 0.5; }
100% { transform: translateX(0); opacity: 1; }
}
.prop-name {
color: var(--vp-c-brand);
}
.prop-type {
color: var(--vp-c-text-3);
font-size: 11px;
margin-left: 8px;
}
.props-config {
margin-bottom: 12px;
}
.config-title {
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.prop-tag {
display: inline-block;
font-family: monospace;
font-size: 12px;
padding: 4px 8px;
margin: 2px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-radius: 4px;
}
.flow-animation {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.flow-line {
width: 2px;
height: 80px;
background: var(--vp-c-divider);
position: relative;
transition: all 0.3s ease;
}
.flow-line.active {
background: var(--vp-c-brand);
box-shadow: 0 0 10px var(--vp-c-brand);
}
.flow-particles {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.particle {
color: var(--vp-c-brand);
font-size: 8px;
animation: flowDown 1s linear infinite;
opacity: 0;
}
.particle:nth-child(1) { animation-delay: 0s; }
.particle:nth-child(2) { animation-delay: 0.2s; }
.particle:nth-child(3) { animation-delay: 0.4s; }
.particle:nth-child(4) { animation-delay: 0.6s; }
.particle:nth-child(5) { animation-delay: 0.8s; }
@keyframes flowDown {
0% {
opacity: 0;
transform: translateY(0);
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(60px);
}
}
.flow-label {
margin-top: 12px;
font-size: 13px;
color: var(--vp-c-text-3);
text-align: center;
transition: all 0.3s ease;
}
.flow-label.active {
color: var(--vp-c-brand);
font-weight: 600;
}
.render-preview {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.preview-title {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.preview-content {
background: var(--vp-c-bg);
border-radius: 4px;
padding: 12px;
}
.user-card {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand-soft);
border-radius: 50%;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 600;
color: var(--vp-c-text-1);
font-size: 14px;
}
.user-meta {
display: flex;
gap: 8px;
margin-top: 4px;
font-size: 12px;
}
.age {
color: var(--vp-c-text-2);
}
.theme-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
text-transform: uppercase;
}
.theme-badge.light {
background: #fef3c7;
color: #92400e;
}
.theme-badge.dark {
background: #374151;
color: #f3f4f6;
}
.emit-section {
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.emit-title {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.emit-btn {
width: 100%;
padding: 8px 12px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
font-size: 13px;
font-family: monospace;
cursor: pointer;
transition: all 0.2s ease;
}
.emit-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-1px);
}
.interaction-panel {
margin-top: 20px;
padding: 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.panel-title {
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-size: 14px;
}
.control-group {
margin-bottom: 16px;
}
.control-group label {
display: block;
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.control-group input,
.control-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 14px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.control-group input:focus,
.control-group select:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.checkbox {
display: inline-flex !important;
align-items: center;
gap: 6px;
margin-right: 16px;
cursor: pointer;
}
.checkbox input {
width: auto !important;
margin: 0 !important;
}
.flow-status {
padding: 10px 16px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
text-align: center;
font-size: 14px;
color: var(--vp-c-text-2);
transition: all 0.3s ease;
}
.flow-status.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
font-weight: 600;
}
</style>
@@ -0,0 +1,540 @@
<template>
<div class="redux-flow-demo">
<div class="demo-header">
<h4>Redux 数据流演示</h4>
<p class="hint">理解 Redux 的单向数据流Action Reducer Store View</p>
</div>
<div class="flow-diagram">
<!-- 视图层 -->
<div class="flow-layer view-layer">
<div class="layer-header">
<span class="layer-icon">👁</span>
<span class="layer-title">View (视图层)</span>
</div>
<div class="view-content">
<div class="counter-display">
<span class="counter-label">当前计数:</span>
<span class="counter-value" :class="{ changed: countChanged }">{{ count }}</span>
</div>
<div class="action-buttons">
<button class="action-btn" @click="dispatchAction('INCREMENT')">
<span class="btn-icon"></span>
增加
</button>
<button class="action-btn" @click="dispatchAction('DECREMENT')">
<span class="btn-icon"></span>
减少
</button>
<button class="action-btn" @click="dispatchAction('RESET')">
<span class="btn-icon">🔄</span>
重置
</button>
</div>
</div>
</div>
<!-- 箭头View -> Action -->
<div class="flow-arrow-container">
<div class="flow-arrow" :class="{ active: flowStage === 'action' }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-label" :class="{ active: flowStage === 'action' }">
dispatch(action)
</div>
</div>
<!-- Action -->
<div class="flow-layer action-layer" :class="{ active: flowStage === 'action' }">
<div class="layer-header">
<span class="layer-icon">📨</span>
<span class="layer-title">Action (动作)</span>
</div>
<div class="action-content">
<div class="action-object">
<div class="code-block">
<pre><code>{
type: "{{ currentAction.type }}",
payload: {{ currentAction.payload || 'undefined' }}
}</code></pre>
</div>
</div>
</div>
</div>
<!-- 箭头Action -> Reducer -->
<div class="flow-arrow-container">
<div class="flow-arrow" :class="{ active: flowStage === 'reducer' }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-label" :class="{ active: flowStage === 'reducer' }">
reduce(state, action)
</div>
</div>
<!-- Reducer -->
<div class="flow-layer reducer-layer" :class="{ active: flowStage === 'reducer' }">
<div class="layer-header">
<span class="layer-icon"></span>
<span class="layer-title">Reducer (纯函数)</span>
</div>
<div class="reducer-content">
<div class="reducer-function">
<div class="code-block">
<pre><code>function reducer(state, action) {
switch (action.type) {
case "{{ currentAction.type }}":
return {
...state,
count: (state?.count ?? 0) {{ currentAction.operator }} {{ currentAction.step || 1 }}
};
default:
return state;
}
}</code></pre>
</div>
</div>
</div>
</div>
<!-- 箭头Reducer -> Store -->
<div class="flow-arrow-container">
<div class="flow-arrow" :class="{ active: flowStage === 'store' }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-label" :class="{ active: flowStage === 'store' }">
update store
</div>
</div>
<!-- Store -->
<div class="flow-layer store-layer" :class="{ active: flowStage === 'store' }">
<div class="layer-header">
<span class="layer-icon">🏪</span>
<span class="layer-title">Store (单一数据源)</span>
</div>
<div class="store-content">
<div class="store-state">
<div class="state-label">Current State:</div>
<div class="code-block">
<pre><code>{
count: {{ count }}
}</code></pre>
</div>
</div>
</div>
</div>
<!-- 箭头Store -> View -->
<div class="flow-arrow-container">
<div class="flow-arrow" :class="{ active: flowStage === 'view' }">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-label" :class="{ active: flowStage === 'view' }">
notify subscribers
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="redux-principles">
<h5>📋 Redux 三大原则</h5>
<div class="principles-grid">
<div class="principle-card">
<div class="principle-number">1</div>
<h6>单一数据源</h6>
<p>整个应用的 state 储存在唯一的 store </p>
</div>
<div class="principle-card">
<div class="principle-number">2</div>
<h6>State 只读</h6>
<p>唯一改变 state 的方法是触发 action</p>
</div>
<div class="principle-card">
<div class="principle-number">3</div>
<h6>纯函数修改</h6>
<p>Reducer 必须是纯函数接收旧 state 返回新 state</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// 状态
const count = ref(0)
const countChanged = ref(false)
const flowStage = ref('')
// 当前 action
const currentAction = reactive({
type: '',
payload: null,
operator: '+',
step: 1
})
// 调度 action
const dispatchAction = async (actionType) => {
flowStage.value = 'action'
// 设置当前 action
currentAction.type = actionType
switch (actionType) {
case 'INCREMENT':
currentAction.payload = undefined
currentAction.operator = '+'
currentAction.step = 1
break
case 'DECREMENT':
currentAction.payload = undefined
currentAction.operator = '-'
currentAction.step = 1
break
case 'RESET':
currentAction.payload = undefined
currentAction.operator = '='
currentAction.step = 0
break
}
// 模拟流程
await wait(600)
flowStage.value = 'reducer'
await wait(800)
flowStage.value = 'store'
await wait(600)
flowStage.value = 'view'
// 更新状态
switch (actionType) {
case 'INCREMENT':
count.value++
break
case 'DECREMENT':
count.value--
break
case 'RESET':
count.value = 0
break
}
countChanged.value = true
setTimeout(() => {
countChanged.value = false
}, 300)
await wait(400)
flowStage.value = ''
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.redux-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.flow-diagram {
display: flex;
flex-direction: column;
gap: 16px;
}
.flow-layer {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
transition: all 0.3s ease;
}
.flow-layer.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
transform: scale(1.02);
}
.layer-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.layer-icon {
font-size: 20px;
}
.layer-title {
font-weight: 600;
color: var(--vp-c-text-1);
}
.view-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.counter-display {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.counter-label {
font-size: 14px;
color: var(--vp-c-text-2);
}
.counter-value {
font-size: 36px;
font-weight: 700;
color: var(--vp-c-brand);
transition: all 0.3s ease;
}
.counter-value.changed {
transform: scale(1.2);
color: #22c55e;
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-1px);
}
.btn-icon {
font-size: 14px;
}
.flow-arrow-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
}
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0.5;
transition: all 0.3s ease;
}
.flow-arrow.active {
opacity: 1;
}
.arrow-line {
width: 2px;
height: 20px;
background: var(--vp-c-divider);
transition: all 0.3s ease;
}
.flow-arrow.active .arrow-line {
background: var(--vp-c-brand);
box-shadow: 0 0 8px var(--vp-c-brand);
}
.arrow-head {
color: var(--vp-c-divider);
font-size: 12px;
line-height: 1;
transition: all 0.3s ease;
}
.flow-arrow.active .arrow-head {
color: var(--vp-c-brand);
animation: bounce 0.5s ease infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(2px); }
}
.arrow-label {
margin-top: 4px;
font-size: 11px;
color: var(--vp-c-text-3);
font-family: monospace;
transition: all 0.3s ease;
}
.arrow-label.active {
color: var(--vp-c-brand);
font-weight: 600;
}
.action-content,
.reducer-content,
.store-content {
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.action-object {
display: flex;
flex-direction: column;
gap: 8px;
}
.code-block {
background: #1e1e1e;
border-radius: 6px;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: 12px;
overflow-x: auto;
}
.code-block code {
font-family: 'Fira Code', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
color: #d4d4d4;
}
.reducer-function {
display: flex;
flex-direction: column;
gap: 8px;
}
.store-state {
display: flex;
flex-direction: column;
gap: 8px;
}
.state-label {
font-size: 12px;
color: var(--vp-c-text-2);
}
.redux-principles {
margin-top: 24px;
padding: 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.redux-principles h5 {
margin: 0 0 16px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.principle-card {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.principle-number {
width: 28px;
height: 28px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
}
.principle-card h6 {
margin: 0 0 6px 0;
color: var(--vp-c-text-1);
font-size: 14px;
}
.principle-card p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.principles-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,544 @@
<template>
<div class="state-management-comparison">
<div class="demo-header">
<h4>状态管理库全景对比</h4>
<p class="hint">全面对比主流状态管理方案的特性适用场景和学习曲线</p>
</div>
<!-- 简化版对比表格 -->
<div class="comparison-table-wrapper">
<table class="comparison-table">
<thead>
<tr>
<th class="feature-col">特性</th>
<th v-for="lib in libraries" :key="lib.id" class="lib-col">
<div class="lib-header">
<span class="lib-icon">{{ lib.icon }}</span>
<span class="lib-name">{{ lib.name }}</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="feature-name">学习曲线</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<div class="curve-bar">
<div class="curve-fill" :style="{ width: lib.learningCurve + '%', background: getCurveColor(lib.learningCurve) }"></div>
</div>
<span class="curve-label">{{ getCurveLabel(lib.learningCurve) }}</span>
</td>
</tr>
<tr>
<td class="feature-name">包大小</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<span class="size-badge" :class="getSizeClass(lib.bundleSize)">{{ lib.bundleSize }}</span>
</td>
</tr>
<tr>
<td class="feature-name">TypeScript</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<span class="boolean-badge" :class="{ yes: lib.typescript, no: !lib.typescript }">
{{ lib.typescript ? '✓' : '✗' }}
</span>
</td>
</tr>
<tr>
<td class="feature-name">开发工具</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<span class="boolean-badge" :class="{ yes: lib.devtools, no: !lib.devtools }">
{{ lib.devtools ? '✓' : '✗' }}
</span>
</td>
</tr>
<tr>
<td class="feature-name">SSR 支持</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<span class="boolean-badge" :class="{ yes: lib.ssr, no: !lib.ssr }">
{{ lib.ssr ? '✓' : '✗' }}
</span>
</td>
</tr>
<tr>
<td class="feature-name">适用框架</td>
<td v-for="lib in libraries" :key="lib.id" class="feature-value">
<span class="text-value">{{ lib.framework }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 选中库的详细信息 -->
<div v-if="selectedLibrary" class="library-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLibrary.icon }}</span>
<div class="detail-title">
<h5>{{ selectedLibrary.name }}</h5>
<span class="detail-tagline">{{ selectedLibrary.tagline }}</span>
</div>
<a :href="selectedLibrary.docsUrl" target="_blank" class="docs-link">
官方文档
</a>
</div>
<div class="detail-grid">
<div class="detail-section">
<h6>🎯 适用场景</h6>
<ul>
<li v-for="(scenario, index) in selectedLibrary.scenarios" :key="index">{{ scenario }}</li>
</ul>
</div>
<div class="detail-section">
<h6> 优势</h6>
<ul class="advantages">
<li v-for="(pro, index) in selectedLibrary.pros" :key="index">{{ pro }}</li>
</ul>
</div>
<div class="detail-section">
<h6> 劣势</h6>
<ul class="disadvantages">
<li v-for="(con, index) in selectedLibrary.cons" :key="index">{{ con }}</li>
</ul>
</div>
</div>
</div>
<!-- 决策流程图 -->
<div class="decision-flow">
<h5>🤔 如何选择</h5>
<div class="flow-chart">
<div class="flow-node start">开始</div>
<div class="flow-arrow"></div>
<div class="flow-node question">需要跨框架支持</div>
<div class="flow-arrow"> </div>
<div class="flow-node result">考虑 Pinia / Vuex</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const libraries = [
{
id: 'redux',
name: 'Redux',
icon: '🔄',
tagline: 'JavaScript 应用的可预测状态容器',
docsUrl: 'https://redux.js.org/',
scenarios: ['大型企业级应用', '需要严格数据流控制', '复杂的状态逻辑'],
pros: ['严格的数据流,易于调试', '强大的中间件生态', '时间旅行调试', '可预测的状态更新'],
cons: ['学习曲线陡峭', '样板代码较多', '小型项目可能过于复杂'],
codeExample: '// Redux 示例代码',
learningCurve: 80,
bundleSize: '7KB',
typescript: true,
devtools: true,
ssr: true,
framework: 'React/Vue/Angular'
},
{
id: 'vuex',
name: 'Vuex',
icon: '🌿',
tagline: 'Vue.js 的官方状态管理库',
docsUrl: 'https://vuex.vuejs.org/',
scenarios: ['Vue 2/3 中大型项目', '需要模块化管理状态', '团队成员熟悉 Vue 生态'],
pros: ['与 Vue 深度集成', '响应式系统', '模块化管理', '优秀的开发工具'],
cons: ['仅适用于 Vue', 'Vue 3 中被 Pinia 取代', '相对冗余的 API'],
codeExample: '// Vuex 示例代码',
learningCurve: 60,
bundleSize: '4KB',
typescript: true,
devtools: true,
ssr: true,
framework: 'Vue Only'
},
{
id: 'pinia',
name: 'Pinia',
icon: '🍍',
tagline: '直观、类型安全、灵活的 Vue Store',
docsUrl: 'https://pinia.vuejs.org/',
scenarios: ['Vue 3 新项目首选', '重视 TypeScript 支持', '希望简化状态管理'],
pros: ['轻量级设计', '原生 TypeScript 支持', '组合式 API 风格', '代码更简洁'],
cons: ['Vue 3 专属', '生态系统相对年轻', '大型项目需自定义规范'],
codeExample: '// Pinia 示例代码',
learningCurve: 30,
bundleSize: '2KB',
typescript: true,
devtools: true,
ssr: true,
framework: 'Vue 3 Only'
}
]
const features = [
{ key: 'learningCurve', label: '学习曲线', icon: '📈' },
{ key: 'bundleSize', label: '包大小', icon: '📦' },
{ key: 'typescript', label: 'TypeScript', icon: '🔷' },
{ key: 'devtools', label: '开发工具', icon: '🛠️' },
{ key: 'ssr', label: 'SSR 支持', icon: '🚀' },
{ key: 'framework', label: '适用框架', icon: '🔧' }
]
const selectedLib = ref(null)
const selectedLibrary = computed(() => {
if (!selectedLib.value) return null
return libraries.find(lib => lib.id === selectedLib.value)
})
function selectLib(id) {
selectedLib.value = id
}
function getValue(lib, key) {
return lib[key]
}
function getCurveColor(value) {
if (value <= 30) return '#22c55e'
if (value <= 60) return '#f59e0b'
return '#ef4444'
}
function getCurveLabel(value) {
if (value <= 30) return '简单'
if (value <= 60) return '中等'
return '陡峭'
}
function getSizeClass(size) {
const num = parseInt(size)
if (num <= 2) return 'small'
if (num <= 5) return 'medium'
return 'large'
}
</script>
<style scoped>
.state-management-comparison {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.demo-header {
margin-bottom: 1.5rem;
text-align: center;
}
.demo-header h4 {
margin: 0 0 0.5rem;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.comparison-table-wrapper {
overflow-x: auto;
margin-bottom: 1.5rem;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
text-align: left;
}
.comparison-table th {
background: var(--vp-c-bg);
font-weight: 600;
}
.feature-col {
width: 120px;
background: var(--vp-c-bg-soft);
}
.lib-col {
min-width: 120px;
cursor: pointer;
transition: background 0.2s;
}
.lib-col:hover,
.lib-col.selected {
background: rgba(102, 126, 234, 0.1);
}
.lib-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.lib-icon {
font-size: 1.2rem;
}
.lib-name {
font-weight: 500;
}
.feature-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
}
.feature-value {
text-align: center;
}
.curve-bar {
width: 100%;
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.25rem;
}
.curve-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s;
}
.curve-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.size-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.size-badge.small {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.size-badge.medium {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.size-badge.large {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.boolean-badge {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 50%;
font-size: 0.8rem;
font-weight: 600;
}
.boolean-badge.yes {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.boolean-badge.no {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.text-value {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.library-detail {
margin-top: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 2rem;
}
.detail-title {
flex: 1;
}
.detail-title h5 {
margin: 0 0 0.25rem;
font-size: 1.2rem;
}
.detail-tagline {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.docs-link {
padding: 0.5rem 1rem;
background: var(--vp-c-brand);
color: white;
border-radius: 6px;
text-decoration: none;
font-size: 0.85rem;
transition: opacity 0.2s;
}
.docs-link:hover {
opacity: 0.9;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.detail-section {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.detail-section h6 {
margin: 0 0 0.75rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.detail-section ul {
margin: 0;
padding-left: 1.2rem;
}
.detail-section li {
margin: 0.5rem 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.code-block {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
}
.code-block pre {
margin: 0;
font-size: 0.85rem;
line-height: 1.6;
}
.code-block code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: var(--vp-c-text-1);
}
.decision-flow {
margin-top: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.decision-flow h5 {
margin: 0 0 1rem;
text-align: center;
font-size: 1.1rem;
}
.flow-chart {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.flow-node {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
text-align: center;
}
.flow-node.start {
background: var(--vp-c-brand);
color: white;
}
.flow-node.question {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
}
.flow-node.result {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 2px solid #22c55e;
}
.flow-arrow {
font-size: 1.2rem;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.comparison-table {
font-size: 0.8rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.5rem;
}
.lib-icon {
font-size: 1rem;
}
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,546 @@
<template>
<div class="vuex-pinia-demo">
<div class="demo-header">
<h4>Vuex vs Pinia 深度对比</h4>
<p class="hint">体验 Vue 生态两种主流状态管理方案在语法类型支持和开发体验上的差异</p>
</div>
<div class="comparison-container">
<!-- Vuex 面板 -->
<div class="panel vuex-panel">
<div class="panel-header">
<div class="panel-title">
<span class="panel-icon">🌿</span>
<span>Vuex</span>
</div>
<span class="panel-badge legacy">经典</span>
</div>
<div class="panel-content">
<!-- Store 定义 -->
<div class="code-section">
<div class="code-header">
<span class="file-icon">📄</span>
<span class="file-name">store/index.js</span>
</div>
<div class="code-block" v-pre>
<pre><code>import { createStore } from 'vuex'
export default createStore({
// State
state: {
count: 0,
user: null
},
// Getters
getters: {
doubleCount: state => {
return (state?.count ?? 0) * 2
},
isLoggedIn: state => !!(state?.user)
},
// Mutations (同步)
mutations: {
INCREMENT(state) {
state.count = (state?.count ?? 0) + 1
},
SET_USER(state, user) {
state.user = user
}
},
// Actions (可异步)
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('INCREMENT')
}, 1000)
},
async fetchUser({ commit }, userId) {
const response =
await fetch(\`/api/users/\${userId}\`)
const user = await response.json()
commit('SET_USER', user)
}
}
})</code></pre>
</div>
</div>
<!-- 组件中使用 -->
<div class="code-section">
<div class="code-header">
<span class="file-icon">📄</span>
<span class="file-name">Counter.vue</span>
</div>
<div class="code-block" v-pre>
<pre><code>&lt;template&gt;
&lt;div&gt;
&lt;p&gt;Count: {{ count }}&lt;/p&gt;
&lt;p&gt;Double: {{ doubleCount }}&lt;/p&gt;
&lt;button @click="increment"&gt;+&lt;/button&gt;
&lt;button @click="incrementAsync"&gt;+ (async)&lt;/button&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['INCREMENT']),
...mapActions(['incrementAsync']),
increment() {
this.INCREMENT()
}
}
}
&lt;/script&gt;</code></pre>
</div>
</div>
</div>
</div>
<!-- 中间对比区 -->
<div class="comparison-divider">
<div class="vs-badge">VS</div>
<div class="comparison-points">
<div class="point">
<span class="point-icon">📝</span>
<span class="point-text">Vuex 样板代码较多</span>
</div>
<div class="point">
<span class="point-icon">🔷</span>
<span class="point-text">TS 类型需额外定义</span>
</div>
<div class="point">
<span class="point-icon">⚙️</span>
<span class="point-text">选项式 API 风格</span>
</div>
</div>
</div>
<!-- Pinia 面板 -->
<div class="panel pinia-panel">
<div class="panel-header">
<div class="panel-title">
<span class="panel-icon">🍍</span>
<span>Pinia</span>
</div>
<span class="panel-badge modern">推荐</span>
</div>
<div class="panel-content">
<!-- Store 定义 -->
<div class="code-section">
<div class="code-header">
<span class="file-icon">📄</span>
<span class="file-name">stores/counter.js</span>
</div>
<div class="code-block" v-pre>
<pre><code>import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 方式1: 组合式 API (推荐)
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const user = ref(null)
// Getters
const doubleCount = computed(() => (count.value ?? 0) * 2)
const isLoggedIn = computed(() => !!user.value)
// Actions
function increment() {
count.value = (count.value ?? 0) + 1
}
async function incrementAsync() {
await new Promise(r => setTimeout(r, 1000))
increment()
}
async function fetchUser(userId) {
const response = await fetch(\`/api/users/\${userId}\`)
user.value = await response.json()
}
return {
count, user,
doubleCount, isLoggedIn,
increment, incrementAsync, fetchUser
}
})
// 方式2: 选项式 API
export const useCounterStoreOld = defineStore('counter', {
state: () => ({
count: 0,
user: null
}),
getters: {
doubleCount: (state) => (state?.count ?? 0) * 2
},
actions: {
increment() {
this.count = (this?.count ?? 0) + 1
}
}
})</code></pre>
</div>
</div>
<!-- 组件中使用 -->
<div class="code-section">
<div class="code-header">
<span class="file-icon">📄</span>
<span class="file-name">Counter.vue</span>
</div>
<div class="code-block" v-pre>
<pre><code>&lt;template&gt;
&lt;div&gt;
&lt;p&gt;Count: {{ counter.count }}&lt;/p&gt;
&lt;p&gt;Double: {{ counter.doubleCount }}&lt;/p&gt;
&lt;button @click="counter.increment()"&gt;+&lt;/button&gt;
&lt;button @click="counter.incrementAsync()"&gt;+ (async)&lt;/button&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script setup&gt;
import { useCounterStore } from '@/stores/counter'
// 直接获取 store 实例
const counter = useCounterStore()
// 或者直接解构(但会失去响应式!)
// const { count, increment } = useCounterStore() // ❌ 错误
// 正确解构方式:使用 storeToRefs
// import { storeToRefs } from 'pinia'
// const { count, doubleCount } = storeToRefs(counter)
// const { increment } = counter
&lt;/script&gt;</code></pre>
</div>
</div>
</div>
</div>
</div>
<!-- 特性对比表格 -->
<div class="features-comparison">
<h5>🔄 核心特性对比</h5>
<div class="features-table">
<div class="feature-row header">
<div class="feature-name">特性</div>
<div class="feature-vuex">Vuex</div>
<div class="feature-pinia">Pinia</div>
</div>
<div v-for="feature in comparisonFeatures" :key="feature.name" class="feature-row">
<div class="feature-name">{{ feature.name }}</div>
<div class="feature-vuex" :class="{ check: feature.vuex === '✓', cross: feature.vuex === '✗' }">{{ feature.vuex }}</div>
<div class="feature-pinia" :class="{ check: feature.pinia === '✓', cross: feature.pinia === '✗' }">{{ feature.pinia }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const comparisonFeatures = [
{ name: '组合式 API 支持', vuex: '✗', pinia: '✓' },
{ name: 'TypeScript 支持', vuex: '△', pinia: '✓' },
{ name: '无需 mutations', vuex: '✗', pinia: '✓' },
{ name: '自动模块化', vuex: '✗', pinia: '✓' },
{ name: '更轻量的体积', vuex: '✗', pinia: '✓' },
{ name: 'Vue 2 支持', vuex: '✓', pinia: '△' },
{ name: '开发工具支持', vuex: '✓', pinia: '✓' },
{ name: 'SSR 支持', vuex: '✓', pinia: '✓' }
]
</script>
<style scoped>
.vuex-pinia-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 24px;
}
@media (max-width: 968px) {
.comparison-container {
grid-template-columns: 1fr;
}
.comparison-divider {
flex-direction: row !important;
padding: 12px !important;
}
.comparison-points {
flex-direction: row !important;
flex-wrap: wrap;
}
}
.panel {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.vuex-panel {
border-color: #42b883;
}
.pinia-panel {
border-color: #ffd859;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.panel-icon {
font-size: 24px;
}
.panel-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.panel-badge.legacy {
background: #e0f2fe;
color: #0369a1;
}
.panel-badge.modern {
background: #fef3c7;
color: #92400e;
}
.panel-content {
padding: 16px;
}
.code-section {
margin-bottom: 16px;
}
.code-section:last-child {
margin-bottom: 0;
}
.code-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
padding: 6px 10px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.file-icon {
font-size: 14px;
}
.file-name {
font-size: 12px;
color: var(--vp-c-text-2);
font-family: monospace;
}
.code-block {
background: #1e1e1e;
border-radius: 6px;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: 12px;
overflow-x: auto;
}
.code-block code {
font-family: 'Fira Code', 'Monaco', monospace;
font-size: 11px;
line-height: 1.5;
color: #d4d4d4;
}
.comparison-divider {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px 0;
}
.vs-badge {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #42b883, #ffd859);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
color: white;
margin-bottom: 16px;
}
.comparison-points {
display: flex;
flex-direction: column;
gap: 8px;
}
.point {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 12px;
}
.point-icon {
font-size: 14px;
}
.point-text {
color: var(--vp-c-text-2);
}
.features-comparison {
margin-top: 24px;
padding: 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.features-comparison h5 {
margin: 0 0 16px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.features-table {
display: flex;
flex-direction: column;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.feature-row {
display: grid;
grid-template-columns: 1fr 100px 100px;
border-bottom: 1px solid var(--vp-c-divider);
}
.feature-row:last-child {
border-bottom: none;
}
.feature-row.header {
background: var(--vp-c-bg-soft);
font-weight: 600;
}
.feature-name,
.feature-vuex,
.feature-pinia {
padding: 10px 12px;
font-size: 13px;
}
.feature-vuex,
.feature-pinia {
text-align: center;
border-left: 1px solid var(--vp-c-divider);
}
.feature-vuex.check,
.feature-pinia.check {
color: #22c55e;
font-weight: 600;
}
.feature-vuex.cross,
.feature-pinia.cross {
color: #ef4444;
}
.feature-vuex:not(.check):not(.cross),
.feature-pinia:not(.check):not(.cross) {
color: #f59e0b;
}
@media (max-width: 640px) {
.feature-row {
grid-template-columns: 1fr 60px 60px;
}
.feature-name,
.feature-vuex,
.feature-pinia {
padding: 8px;
font-size: 11px;
}
}
</style>
@@ -0,0 +1,823 @@
<template>
<div class="zustand-jotai-demo">
<div class="demo-header">
<h4>Zustand & Jotai轻量级状态管理</h4>
<p class="hint">探索现代 React 生态中最简洁的状态管理方案体验"钩子即状态"的开发模式</p>
</div>
<div class="demo-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-button"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-name">{{ tab.name }}</span>
</button>
</div>
<!-- Zustand 演示 -->
<div v-if="activeTab === 'zustand'" class="tab-content">
<div class="split-view">
<!-- 左侧代码 -->
<div class="code-panel">
<div class="code-tabs">
<button
v-for="file in zustandFiles"
:key="file.name"
class="code-tab"
:class="{ active: activeZustandFile === file.name }"
@click="activeZustandFile = file.name"
>
{{ file.name }}
</button>
</div>
<div class="code-content">
<pre><code>{{ getZustandFileContent() }}</code></pre>
</div>
</div>
<!-- 右侧演示 -->
<div class="demo-panel">
<div class="panel-header">
<span class="header-icon">🐻</span>
<span class="header-title">Zustand Store Demo</span>
</div>
<div class="bear-counter">
<div class="bear-display">
<span v-for="n in Math.min(bears, 10)" :key="n" class="bear-icon">🐻</span>
<span v-if="bears > 10" class="more-bears">+{{ bears - 10 }}</span>
</div>
<div class="count-display">
<span class="count-number">{{ bears }}</span>
<span class="count-label">bears around here</span>
</div>
</div>
<div class="fish-tank">
<div class="tank-header">
<span>🐟 Fish Tank</span>
<span class="fish-count">{{ fishes }} fishes</span>
</div>
<div class="tank-content">
<div v-for="fish in Math.min(fishes, 8)" :key="fish" class="fish">
🐠
</div>
</div>
</div>
<div class="action-buttons">
<button class="action-btn primary" @click="addBear">
<span class="btn-icon"></span>
Add Bear
</button>
<button class="action-btn secondary" @click="addFish">
<span class="btn-icon">🐟</span>
Add Fish
</button>
<button class="action-btn danger" @click="eatFish">
<span class="btn-icon">🍽</span>
Eat Fish ({{ fishes }})
</button>
<button class="action-btn warning" @click="removeAllBears">
<span class="btn-icon">🗑</span>
Remove All
</button>
</div>
<div class="async-demo">
<div class="async-header">Async Action Demo</div>
<div class="async-controls">
<button class="async-btn" :disabled="loading" @click="fetchBears">
{{ loading ? ' Fetching...' : '🌐 Fetch Bears from API' }}
</button>
</div>
<div v-if="apiResponse" class="api-response">
<pre>{{ apiResponse }}</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Jotai 演示 -->
<div v-if="activeTab === 'jotai'" class="tab-content">
<!-- Jotai 内容类似结构 -->
<div class="split-view">
<div class="code-panel">
<!-- Jotai 代码示例 -->
<div class="code-tabs">
<button class="code-tab active">atoms/counter.js</button>
</div>
<div class="code-content">
<pre><code>import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// 基础原子
const countAtom = atom(0)
// 派生原子 - 自动追踪依赖
const doubleAtom = atom((get) => get(countAtom) * 2)
const isEvenAtom = atom((get) => get(countAtom) % 2 === 0)
// 可写派生原子
const countAndDoubleAtom = atom(
(get) => ({
count: get(countAtom),
double: get(doubleAtom)
}),
(get, set, newCount) => {
set(countAtom, newCount)
}
)
// 原子家族 - 动态创建原子
const todoAtomFamily = atomFamily((id) =>
atom({ id, text: '', completed: false })
)
// 异步原子
const userAtom = atom(null)
const fetchUserAtom = atom(
(get) => get(userAtom),
async (get, set, userId) => {
const response = await fetch(\`/api/users/\${userId}\`)
const user = await response.json()
set(userAtom, user)
}
)
export {
countAtom,
doubleAtom,
isEvenAtom,
countAndDoubleAtom,
todoAtomFamily,
fetchUserAtom
}</code></pre>
</div>
</div>
<div class="demo-panel">
<div class="panel-header">
<span class="header-icon">⚛️</span>
<span class="header-title">Jotai Atom Demo</span>
</div>
<div class="atom-demo">
<div class="atom-visualization">
<div class="atom-core" :class="{ active: count > 0 }">
<div class="atom-nucleus">
<span class="nucleus-label">countAtom</span>
<span class="nucleus-value">{{ count }}</span>
</div>
<div class="electron-orbits">
<div class="orbit orbit-1"></div>
<div class="orbit orbit-2"></div>
<div class="orbit orbit-3"></div>
</div>
</div>
<div class="derived-atoms">
<div class="derived-atom" :class="{ active: double > 0 }">
<div class="atom-label">doubleAtom</div>
<div class="atom-value">{{ double }}</div>
<div class="atom-formula">count × 2</div>
</div>
<div class="derived-atom" :class="{ active: true }">
<div class="atom-label">isEvenAtom</div>
<div class="atom-value" :class="{ even: isEven }">{{ isEven ? 'YES' : 'NO' }}</div>
<div class="atom-formula">count % 2 === 0</div>
</div>
</div>
</div>
<div class="atom-controls">
<div class="control-group">
<button class="control-btn" @click="increment">
<span class="btn-icon"></span>
increment()
</button>
<button class="control-btn" @click="decrement">
<span class="btn-icon"></span>
decrement()
</button>
<button class="control-btn" @click="reset">
<span class="btn-icon">🔄</span>
reset()
</button>
</div>
<div class="control-group">
<button class="control-btn async" :disabled="loading" @click="fetchRandom">
<span class="btn-icon">{{ loading ? '⏳' : '🎲' }}</span>
{{ loading ? 'Fetching...' : 'Fetch Random (Async)' }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="info-section">
<div class="info-card">
<h5>🎯 核心概念</h5>
<ul>
<li><strong>Atom</strong>: 状态的基本单元,可读写</li>
<li><strong>Derived Atom</strong>: 派生状态,自动追踪依赖</li>
<li><strong>Atom Family</strong>: 动态创建原子集合</li>
</ul>
</div>
<div class="info-card">
<h5>⚡ 与 Redux/MobX 对比</h5>
<ul>
<li>更细粒度的状态管理</li>
<li>天然支持 TypeScript</li>
<li>不需要 Provider 包裹</li>
<li>与 React Suspense 配合良好</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// Tab 切换
const activeTab = ref('zustand')
const tabs = [
{ id: 'zustand', name: 'Zustand', icon: '🐻' },
{ id: 'jotai', name: 'Jotai', icon: '⚛️' }
]
// Zustand 状态
const bears = ref(0)
const fishes = ref(5)
const loading = ref(false)
const apiResponse = ref('')
// 计算属性(模拟派生状态)
const double = computed(() => bears.value * 2)
const isEven = computed(() => bears.value % 2 === 0)
// 方法
const increment = () => {
bears.value++
}
const decrement = () => {
if (bears.value > 0) bears.value--
}
const reset = () => {
bears.value = 0
fishes.value = 5
}
const addBear = () => {
bears.value++
}
const addFish = () => {
fishes.value++
}
const eatFish = () => {
if (fishes.value > 0) {
fishes.value--
bears.value += 0.1
}
}
const removeAllBears = () => {
bears.value = 0
}
const fetchBears = async () => {
loading.value = true
apiResponse.value = ''
await new Promise(resolve => setTimeout(resolve, 1500))
bears.value = Math.floor(Math.random() * 10) + 1
apiResponse.value = `{\n "status": "success",\n "data": {\n "bears": ${bears.value},\n "message": "Bears fetched successfully"\n }\n}`
loading.value = false
}
const fetchRandom = async () => {
loading.value = true
await new Promise(resolve => setTimeout(resolve, 1000))
bears.value = Math.floor(Math.random() * 100)
loading.value = false
}
// 文件切换
const zustandFiles = [
{ name: 'store.js', content: `import { create } from 'zustand'
const useStore = create((set, get) => ({
// State
bears: 0,
fishes: 5,
// Computed (in component using selectors)
// const doubleBears = useStore(state => state.bears * 2)
// Actions
increasePopulation: () =>
set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
eatFish: () => set((state) => ({
fishes: state.fishes - 1,
bears: state.bears + 0.1
})),
// Async action
fetchBears: async () => {
try {
const response = await fetch('/api/bears')
const data = await response.json()
if (data && typeof data.count === 'number') {
set({ bears: data.count })
}
} catch (error) {
console.error('Failed to fetch bears:', error)
}
},
// Get current state
getState: () => get()
}))
export default useStore` },
{ name: 'Component.jsx', content: `import useStore from './store'
// Component using Zustand
function BearCounter() {
// Select only what you need - fine-grained updates
const bears = useStore((state) => state.bears)
const increase = useStore((state) => state.increasePopulation)
return (
<div>
<h1>{bears} bears around here...</h1>
<button onClick={increase}>Add bear</button>
</div>
)
}
// Async action usage
function BearFetcher() {
const fetchBears = useStore((state) => state.fetchBears)
const bears = useStore((state) => state.bears)
return (
<div>
<p>Bears: {bears}</p>
<button onClick={fetchBears}>
Fetch Bears
</button>
</div>
)
}
// Multiple state selections
function FishTank() {
const { fishes, bears, eatFish } = useStore(
(state) => ({
fishes: state.fishes,
bears: state.bears,
eatFish: state.eatFish
})
)
return (
<div>
<p>Fishes: {fishes}</p>
<p>Bears: {bears}</p>
<button onClick={eatFish}>Eat Fish</button>
</div>
)
}` }
]
const activeZustandFile = ref('store.js')
const getZustandFileContent = () => {
return zustandFiles.find(f => f.name === activeZustandFile.value)?.content || ''
}
</script>
<style scoped>
.zustand-jotai-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 12px;
}
.tab-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.tab-button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.tab-button.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.tab-icon {
font-size: 20px;
}
.tab-name {
font-weight: 500;
}
.split-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 968px) {
.split-view {
grid-template-columns: 1fr;
}
}
.code-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.code-tabs {
display: flex;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-tab {
padding: 10px 16px;
background: transparent;
border: none;
font-size: 13px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.code-tab:hover {
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
}
.code-tab.active {
color: var(--vp-c-brand);
background: var(--vp-c-bg);
border-bottom: 2px solid var(--vp-c-brand);
}
.code-content {
max-height: 600px;
overflow: auto;
}
.code-content pre {
margin: 0;
padding: 16px;
font-family: 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
background: #1e1e1e;
}
.demo-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.panel-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-icon {
font-size: 24px;
}
.header-title {
font-weight: 600;
font-size: 16px;
color: var(--vp-c-text-1);
}
.bear-counter {
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.bear-display {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
margin-bottom: 12px;
min-height: 40px;
}
.bear-icon {
font-size: 24px;
}
.more-bears {
font-size: 14px;
color: var(--vp-c-text-2);
align-self: center;
}
.count-display {
text-align: center;
}
.count-number {
font-size: 36px;
font-weight: 700;
color: var(--vp-c-brand);
}
.count-label {
display: block;
font-size: 14px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.fish-tank {
background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.tank-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-weight: 600;
color: #0369a1;
}
.tank-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
min-height: 50px;
}
.fish {
font-size: 28px;
animation: swim 2s ease-in-out infinite;
}
.fish:nth-child(2) { animation-delay: 0.3s; }
.fish:nth-child(3) { animation-delay: 0.6s; }
.fish:nth-child(4) { animation-delay: 0.9s; }
@keyframes swim {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-3px) rotate(-2deg); }
75% { transform: translateY(3px) rotate(2deg); }
}
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
}
.action-btn.primary:hover {
background: var(--vp-c-brand-dark);
}
.action-btn.secondary {
background: #0ea5e9;
color: white;
}
.action-btn.secondary:hover {
background: #0284c7;
}
.action-btn.danger {
background: #ef4444;
color: white;
}
.action-btn.danger:hover {
background: #dc2626;
}
.action-btn.warning {
background: #f59e0b;
color: white;
}
.action-btn.warning:hover {
background: #d97706;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-icon {
font-size: 14px;
}
.async-demo {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 12px;
}
.async-header {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.async-controls {
margin-bottom: 8px;
}
.async-btn {
width: 100%;
padding: 10px;
background: #8b5cf6;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.async-btn:hover:not(:disabled) {
background: #7c3aed;
}
.async-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.api-response {
background: #1e1e1e;
border-radius: 6px;
padding: 10px;
overflow: auto;
}
.api-response pre {
margin: 0;
font-family: 'Fira Code', monospace;
font-size: 11px;
line-height: 1.5;
color: #d4d4d4;
}
.info-section {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.info-card h5 {
margin: 0 0 10px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.info-card ul {
margin: 0;
padding-left: 18px;
}
.info-card li {
font-size: 12px;
color: var(--vp-c-text-2);
margin: 5px 0;
line-height: 1.5;
}
.info-card li strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.action-buttons {
grid-template-columns: 1fr;
}
.atom-core {
transform: scale(0.9);
}
}
</style>
@@ -0,0 +1,401 @@
<template>
<div class="demo-container">
<h4>async/await 机制演示</h4>
<div class="controls">
<el-button type="primary" size="small" @click="runExample" :disabled="isRunning">
{{ isRunning ? '运行中...' : '运行示例' }}
</el-button>
<el-button size="small" @click="reset">重置</el-button>
<el-checkbox v-model="showDetails" size="small">显示详细信息</el-checkbox>
</div>
<div class="code-section">
<div class="code-block">
<div class="code-header">
<span class="code-title">Python asyncio 示例</span>
</div>
<pre class="code-content"><code><span class="keyword">import</span> asyncio
<span class="keyword">async def</span> <span class="function">fetch_data</span>(url):
<span class="comment"># await 挂起让出 CPU</span>
response = <span class="keyword">await</span> aiohttp.get(url)
<span class="comment"># I/O 完成后继续执行</span>
<span class="keyword">return</span> response.json()
<span class="keyword">async def</span> <span class="function">main</span>():
<span class="comment"># 并发执行</span>
tasks = [fetch_data(url) <span class="keyword">for</span> url <span class="keyword">in</span> urls]
results = <span class="keyword">await</span> asyncio.gather(*tasks)</code></pre>
</div>
</div>
<div class="visualization">
<div class="timeline-container">
<h5>执行时间线</h5>
<div class="timeline">
<div class="time-axis">
<div class="axis-label">0ms</div>
<div class="axis-label">50ms</div>
<div class="axis-label">100ms</div>
<div class="axis-label">150ms</div>
<div class="axis-label">200ms</div>
</div>
<div class="thread-rows">
<div class="thread-row">
<div class="row-label">事件循环</div>
<div class="row-track">
<div class="execution-segment event-loop" style="width: 100%;">
调度中
</div>
</div>
</div>
<div v-for="(task, idx) in tasks" :key="task.id" class="thread-row">
<div class="row-label">任务 {{ task.id }}</div>
<div class="row-track">
<template v-for="(segment, sidx) in task.segments" :key="sidx">
<div class="execution-segment"
:class="{ 'active': segment.type === 'active', 'io': segment.type === 'io' }"
:style="{ left: segment.start + '%', width: segment.width + '%', backgroundColor: segment.color }">
<span v-if="segment.width > 8" class="segment-label">
{{ segment.type === 'active' ? '执行' : 'I/O' }}
</span>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-title">并发任务数</div>
<div class="stat-value">{{ tasks.length }}</div>
</div>
<div class="stat-card">
<div class="stat-title">总执行时间</div>
<div class="stat-value">{{ totalTime }}ms</div>
</div>
<div class="stat-card">
<div class="stat-title">I/O 等待时间</div>
<div class="stat-value">{{ ioWaitTime }}ms</div>
</div>
<div class="stat-card">
<div class="stat-title">CPU 利用率</div>
<div class="stat-value">{{ cpuUtilization }}%</div>
</div>
</div>
</div>
<div class="explanation">
<el-alert title="async/await 的优势" type="success"
description="当一个任务遇到 I/O 操作(如网络请求)时,await 会让出 CPU,事件循环调度其他任务执行。I/O 完成后,任务从断点恢复。这种方式让单个线程可以并发处理数千个任务。"
show-icon :closable="false" />
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const comparisonMode = ref('memory')
const coroutineCount = ref(1000)
const isRunning = ref(false)
const showDetails = ref(false)
const tasks = ref([])
// 颜色
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
// 计算统计数据
const totalTime = computed(() => {
if (tasks.value.length === 0) return 0
// 模拟总时间
return Math.round(50 + tasks.value.length * 10)
})
const ioWaitTime = computed(() => {
return Math.round(totalTime.value * 0.6)
})
const cpuUtilization = computed(() => {
return Math.round(100 - (ioWaitTime.value / totalTime.value) * 100)
})
// 生成任务数据
function generateTasks() {
const count = Math.min(Math.floor(coroutineCount.value / 200), 5)
const newTasks = []
for (let i = 0; i < count; i++) {
const segments = []
let currentPos = 5
// 生成交替的执行和I/O段
for (let j = 0; j < 3; j++) {
// 执行段
const execWidth = 10 + Math.random() * 10
segments.push({
type: 'active',
start: currentPos,
width: execWidth,
color: colors[i % colors.length]
})
currentPos += execWidth
// I/O段
const ioWidth = 15 + Math.random() * 10
segments.push({
type: 'io',
start: currentPos,
width: ioWidth,
color: '#dcdfe6'
})
currentPos += ioWidth
}
newTasks.push({
id: i + 1,
segments,
state: 'ready'
})
}
tasks.value = newTasks
}
// 运行示例
function runExample() {
isRunning.value = true
generateTasks()
// 模拟运行
setTimeout(() => {
isRunning.value = false
}, 2000)
}
// 重置
function reset() {
tasks.value = []
isRunning.value = false
coroutineCount.value = 1000
}
// 监听协程数量变化
watch(coroutineCount, () => {
if (tasks.value.length > 0) {
generateTasks()
}
})
// 初始化
generateTasks()
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.slider-label {
font-size: 14px;
color: #606266;
min-width: 100px;
}
.code-section {
margin-bottom: 20px;
}
.code-block {
background: #282c34;
border-radius: 8px;
overflow: hidden;
}
.code-header {
background: #21252b;
padding: 8px 16px;
border-bottom: 1px solid #181a1f;
}
.code-title {
color: #abb2bf;
font-size: 13px;
font-weight: 500;
}
.code-content {
padding: 16px;
margin: 0;
overflow-x: auto;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
}
.keyword {
color: #c678dd;
}
.function {
color: #61afef;
}
.comment {
color: #5c6370;
font-style: italic;
}
.visualization {
margin-bottom: 20px;
}
.timeline-container {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.timeline-container h5 {
margin: 0 0 12px 0;
color: #303133;
}
.timeline {
position: relative;
}
.time-axis {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 8px;
}
.axis-label {
font-size: 11px;
color: #909399;
}
.thread-rows {
display: flex;
flex-direction: column;
gap: 8px;
}
.thread-row {
display: grid;
grid-template-columns: 100px 1fr;
gap: 8px;
align-items: center;
}
.row-label {
font-size: 12px;
color: #606266;
text-align: right;
}
.row-track {
position: relative;
height: 24px;
background: #f5f7fa;
border-radius: 4px;
overflow: hidden;
}
.execution-segment {
position: absolute;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: white;
font-weight: 500;
}
.execution-segment.io {
background: #dcdfe6 !important;
color: #606266;
}
.execution-segment.event-loop {
background: #409eff;
}
.current-indicator {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #f56c6c;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.stat-title {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.stat-value {
font-size: 20px;
font-weight: bold;
color: #303133;
}
.explanation {
margin-top: 16px;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.controls {
flex-direction: column;
align-items: stretch;
}
.thread-row {
grid-template-columns: 60px 1fr;
}
}
</style>
@@ -0,0 +1,481 @@
<template>
<div class="demo-container">
<h4>并发 (Concurrency) vs 并行 (Parallelism) 演示</h4>
<div class="controls">
<el-radio-group v-model="demoMode" size="small">
<el-radio-button label="concurrent">单核并发</el-radio-button>
<el-radio-button label="parallel">多核并行</el-radio-button>
<el-radio-button label="hybrid">混合模式</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" @click="startDemo" :disabled="isRunning">
{{ isRunning ? '运行中...' : '开始演示' }}
</el-button>
<el-button size="small" @click="reset">重置</el-button>
<el-slider v-model="workerCount" :min="1" :max="8" :step="1" show-stops style="width: 150px;"
v-if="demoMode === 'parallel' || demoMode === 'hybrid'" />
</div>
<div class="demo-grid">
<!-- CPU 核心显示 -->
<div class="section">
<div class="section-title">
{{ demoMode === 'concurrent' ? 'CPU 核心 (单核)' : 'CPU 核心 (' + cpuCores.length + '核)' }}
</div>
<div class="cpu-grid" :class="{ 'single-core': demoMode === 'concurrent' }">
<div v-for="(core, idx) in cpuCores" :key="idx" class="cpu-core" :class="{
'active': core.active,
'concurrent-mode': demoMode === 'concurrent',
'parallel-mode': demoMode === 'parallel'
}"
:style="{ backgroundColor: core.active ? core.color : '#f5f7fa' }">
<div class="core-number">CPU {{ idx + 1 }}</div>
<div class="core-task" v-if="core.task">{{ core.task }}</div>
<div class="core-status">{{ core.active ? '运行中' : '空闲' }}</div>
</div>
</div>
</div>
<!-- 任务视图 -->
<div class="section">
<div class="section-title">任务执行</div>
<div class="task-timeline">
<div v-for="(task, idx) in demoTasks" :key="task.id" class="task-row">
<div class="task-info">
<div class="task-name">任务 {{ task.id }}</div>
<div class="task-duration">{{ task.duration }}ms</div>
</div>
<div class="task-track">
<div v-for="(segment, sidx) in task.segments" :key="sidx" class="task-segment"
:class="{ 'execution': segment.type === 'execution', 'waiting': segment.type === 'waiting' }"
:style="{ left: segment.start + '%', width: segment.width + '%' }">
<span v-if="segment.width > 5" class="segment-text">
{{ segment.type === 'execution' ? '执行' : '等待' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-header">并发 vs 并行 对比</div>
<div class="comparison-grid">
<div class="comparison-item">
<div class="item-icon">🔄</div>
<div class="item-title">并发 (Concurrency)</div>
<div class="item-desc">多个任务交替执行宏观上同时推进</div>
<div class="item-examples"><strong>例子:</strong> 单核CPU多线程协程调度异步I/O</div>
</div>
<div class="comparison-item">
<div class="item-icon"></div>
<div class="item-title">并行 (Parallelism)</div>
<div class="item-desc">多个任务真正同时执行</div>
<div class="item-examples"><strong>例子:</strong> 多核CPU计算GPU并行计算分布式处理</div>
</div>
</div>
<div class="need-table">
<div class="need-title">需要什么条件?</div>
<div class="need-items">
<div class="need-item">
<span class="need-check"></span>
<span class="need-text"><strong>并发:</strong> 单核 CPU 即可实现</span>
</div>
<div class="need-item">
<span class="need-check need-multi"></span>
<span class="need-text"><strong>并行:</strong> 需要多核 CPU 或多台机器</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const demoMode = ref('concurrent')
const isRunning = ref(false)
const workerCount = ref(4)
// CPU 核心
const cpuCores = ref([
{ active: false, color: '#409eff', task: null },
{ active: false, color: '#67c23a', task: null },
{ active: false, color: '#e6a23c', task: null },
{ active: false, color: '#f56c6c', task: null },
])
// 演示任务
const demoTasks = ref([
{ id: 1, duration: 40, segments: [] },
{ id: 2, duration: 30, segments: [] },
{ id: 3, duration: 50, segments: [] },
{ id: 4, duration: 35, segments: [] },
])
function startDemo() {
if (isRunning.value) return
isRunning.value = true
// 生成任务时间线
generateTaskTimeline()
// 模拟执行
setTimeout(() => {
isRunning.value = false
}, 3000)
}
function generateTaskTimeline() {
demoTasks.value.forEach((task, idx) => {
const segments = []
const mode = demoMode.value
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
if (mode === 'concurrent') {
// 单核并发:任务交替执行
const baseStart = 5 + idx * 3
segments.push({
type: 'execution',
start: baseStart,
width: task.duration / 3,
color: colors[idx % colors.length]
})
segments.push({
type: 'waiting',
start: baseStart + task.duration / 3,
width: 20,
color: '#dcdfe6'
})
segments.push({
type: 'execution',
start: baseStart + task.duration / 3 + 20,
width: task.duration / 3,
color: colors[idx % colors.length]
})
} else if (mode === 'parallel') {
// 多核并行:任务同时执行
segments.push({
type: 'execution',
start: 5,
width: task.duration,
color: colors[idx % colors.length]
})
} else {
// 混合模式
if (idx < workerCount.value) {
segments.push({
type: 'execution',
start: 5,
width: task.duration,
color: colors[idx % colors.length]
})
} else {
const baseStart = 5 + (idx - workerCount.value) * 5
segments.push({
type: 'execution',
start: baseStart,
width: task.duration / 2,
color: colors[idx % colors.length]
})
}
}
task.segments = segments
})
}
function reset() {
isRunning.value = false
demoTasks.value.forEach(task => {
task.segments = []
})
}
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.section {
background: white;
border-radius: 8px;
padding: 16px;
}
.section-title {
font-weight: bold;
color: #303133;
margin-bottom: 12px;
font-size: 14px;
}
.cpu-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.cpu-grid.single-core {
grid-template-columns: 1fr;
}
.cpu-core {
background: #f5f7fa;
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
text-align: center;
transition: all 0.3s;
}
.cpu-core.active {
border-color: currentColor;
box-shadow: 0 0 10px currentColor;
}
.cpu-core.concurrent-mode.active {
animation: blink 0.5s infinite;
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.core-number {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
}
.core-task {
font-size: 14px;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.core-status {
font-size: 11px;
color: #909399;
}
.task-timeline {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-row {
display: grid;
grid-template-columns: 80px 1fr;
gap: 8px;
align-items: center;
}
.task-info {
font-size: 11px;
}
.task-name {
font-weight: bold;
color: #303133;
}
.task-duration {
color: #909399;
}
.task-track {
position: relative;
height: 20px;
background: #f5f7fa;
border-radius: 4px;
overflow: hidden;
}
.task-segment {
position: absolute;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: white;
font-weight: 500;
}
.task-segment.waiting {
background: #dcdfe6 !important;
color: #606266;
}
.comparison-table {
background: white;
border-radius: 8px;
padding: 20px;
}
.table-header {
font-size: 18px;
font-weight: bold;
color: #303133;
text-align: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #e4e7ed;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.comparison-item {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
}
.item-icon {
font-size: 32px;
text-align: center;
margin-bottom: 8px;
}
.item-title {
font-size: 16px;
font-weight: bold;
color: #303133;
text-align: center;
margin-bottom: 8px;
}
.item-desc {
font-size: 13px;
color: #606266;
text-align: center;
margin-bottom: 12px;
}
.item-examples {
font-size: 12px;
color: #909399;
background: white;
padding: 8px;
border-radius: 4px;
}
.need-table {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
}
.need-title {
font-size: 14px;
font-weight: bold;
color: #303133;
margin-bottom: 12px;
text-align: center;
}
.need-items {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.need-item {
display: flex;
align-items: center;
gap: 8px;
background: white;
padding: 12px;
border-radius: 4px;
}
.need-check {
width: 20px;
height: 20px;
border-radius: 50%;
background: #67c23a;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.need-check.need-multi {
background: #409eff;
}
.need-text {
font-size: 13px;
color: #606266;
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
.comparison-grid {
grid-template-columns: 1fr;
}
.need-items {
grid-template-columns: 1fr;
}
.cpu-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,417 @@
<template>
<div class="demo-container">
<h4>协程轻量级对比演示</h4>
<div class="controls">
<el-radio-group v-model="comparisonMode" size="small">
<el-radio-button label="memory">内存占用对比</el-radio-button>
<el-radio-button label="switch">切换开销对比</el-radio-button>
<el-radio-button label="creation">创建速度对比</el-radio-button>
</el-radio-group>
<el-slider v-model="coroutineCount" :min="100" :max="10000" :step="100" show-input style="width: 300px;" />
<span class="slider-label">{{ coroutineCount }} 个协程</span>
</div>
<div class="comparison-view">
<div class="comparison-column">
<h5>线程模型</h5>
<div class="resource-visualization">
<div class="resource-bar">
<div class="bar-label">内存占用</div>
<div class="bar-container">
<div class="bar-fill thread-bar" :style="{ width: threadMemoryPercent + '%', backgroundColor: '#e6a23c' }">
{{ threadMemory }} MB
</div>
</div>
</div>
<div class="resource-bar">
<div class="bar-label">创建时间</div>
<div class="bar-container">
<div class="bar-fill thread-bar" :style="{ width: threadCreationPercent + '%', backgroundColor: '#e6a23c' }">
{{ threadCreationTime }} ms
</div>
</div>
</div>
<div class="resource-bar">
<div class="bar-label">上下文切换</div>
<div class="bar-container">
<div class="bar-fill thread-bar" :style="{ width: 100 + '%', backgroundColor: '#e6a23c' }">
~1-10 μs
</div>
</div>
</div>
</div>
<div class="thread-visualization">
<div class="memory-blocks">
<div v-for="n in Math.min(coroutineCount / 100, 50)" :key="n" class="thread-block"
:style="{ backgroundColor: '#e6a23c', opacity: 0.6 + Math.random() * 0.4 }">
</div>
<div v-if="coroutineCount / 100 > 50" class="more-indicator">
+{{ Math.floor(coroutineCount / 100 - 50) }} 更多...
</div>
</div>
</div>
</div>
<div class="vs-divider">
<div class="vs-circle">VS</div>
</div>
<div class="comparison-column">
<h5>协程模型</h5>
<div class="resource-visualization">
<div class="resource-bar">
<div class="bar-label">内存占用</div>
<div class="bar-container">
<div class="bar-fill coroutine-bar"
:style="{ width: Math.max(coroutineMemoryPercent, 5) + '%', backgroundColor: '#67c23a' }">
{{ coroutineMemory }} MB
</div>
</div>
</div>
<div class="resource-bar">
<div class="bar-label">创建时间</div>
<div class="bar-container">
<div class="bar-fill coroutine-bar"
:style="{ width: Math.max(coroutineCreationPercent, 5) + '%', backgroundColor: '#67c23a' }">
{{ coroutineCreationTime }} ms
</div>
</div>
</div>
<div class="resource-bar">
<div class="bar-label">上下文切换</div>
<div class="bar-container">
<div class="bar-fill coroutine-bar" :style="{ width: 15 + '%', backgroundColor: '#67c23a' }">
~100 ns
</div>
</div>
</div>
</div>
<div class="coroutine-visualization">
<div class="coroutine-grid">
<div v-for="n in Math.min(coroutineCount / 10, 100)" :key="n" class="coroutine-cell"
:style="{ backgroundColor: '#67c23a', opacity: 0.5 + Math.random() * 0.5 }">
</div>
<div v-if="coroutineCount / 10 > 100" class="more-indicator">
+{{ Math.floor(coroutineCount / 10 - 100) }} 更多...
</div>
</div>
</div>
<div class="efficiency-badge" v-if="coroutineCount >= 1000">
<el-tag type="success" effect="dark" size="large">
🚀 节省 {{ savingsPercent }}% 内存
</el-tag>
</div>
</div>
</div>
<div class="insight-panel">
<el-alert :title="insightTitle" :type="insightType" :description="insightDescription" show-icon
:closable="false" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const comparisonMode = ref('memory')
const coroutineCount = ref(1000)
// 基础参数
const THREAD_STACK_SIZE = 1024 * 1024 // 1MB 线程栈
const COROUTINE_STACK_SIZE = 2 * 1024 // 2KB 协程栈
const THREAD_CREATION_TIME = 100 // 10us * 10000 = 100ms
const COROUTINE_CREATION_TIME = 10 // 10ns * 10000 = 100us
// 计算值
const threadMemory = computed(() => {
return Math.round((coroutineCount.value * THREAD_STACK_SIZE) / (1024 * 1024))
})
const coroutineMemory = computed(() => {
return Math.round((coroutineCount.value * COROUTINE_STACK_SIZE) / (1024))
})
const threadCreationTime = computed(() => {
return Math.round((coroutineCount.value * THREAD_CREATION_TIME) / 1000)
})
const coroutineCreationTime = computed(() => {
return Math.round((coroutineCount.value * COROUTINE_CREATION_TIME) / 1000)
})
// 百分比计算
const threadMemoryPercent = computed(() => {
const max = threadMemory.value
return max > 0 ? 100 : 0
})
const coroutineMemoryPercent = computed(() => {
if (threadMemory.value === 0) return 0
return (coroutineMemory.value / threadMemory.value) * 100
})
const threadCreationPercent = computed(() => {
const max = threadCreationTime.value
return max > 0 ? 100 : 0
})
const coroutineCreationPercent = computed(() => {
if (threadCreationTime.value === 0) return 0
return (coroutineCreationTime.value / threadCreationTime.value) * 100
})
const savingsPercent = computed(() => {
if (threadMemory.value === 0) return 0
return Math.round((1 - coroutineMemory.value / threadMemory.value) * 100)
})
// 洞察信息
const insightTitle = computed(() => {
if (coroutineCount.value < 100) {
return '小规模场景'
} else if (coroutineCount.value < 5000) {
return '中等规模场景'
} else {
return '大规模高并发场景'
}
})
const insightType = computed(() => {
if (coroutineCount.value >= 5000) return 'success'
if (coroutineCount.value >= 1000) return 'warning'
return 'info'
})
const insightDescription = computed(() => {
const savings = savingsPercent.value
const memSaved = threadMemory.value - coroutineMemory.value
if (coroutineCount.value < 100) {
return `当前 ${coroutineCount.value} 个并发单元,线程和协程的差别还不明显。建议增加到 1000+ 来观察显著差异。`
} else if (coroutineCount.value < 5000) {
return `使用协程可以节省 ${savings}% 的内存(约 ${memSaved}MB),创建速度快 ${Math.round(threadCreationTime.value / coroutineCreationTime.value)} 倍。`
} else {
return `🚀 在高并发场景下,协程优势巨大!节省 ${savings}% 内存(${memSaved}MB),${threadMemory.value}MB vs ${coroutineMemory.value}MB。这是 C10K/C10M 问题的关键解决方案。`
}
})
// 方法
function addThread() {
// 模拟添加线程
}
function killThread() {
// 模拟结束线程
}
function simulateCrash() {
// 模拟崩溃
}
function reset() {
coroutineCount.value = 1000
}
function startSimulation() {
// 开始模拟
}
function toggleSimulation() {
// 切换模拟状态
}
function pauseSimulation() {
// 暂停模拟
}
function runSimulation() {
// 运行模拟
}
function stateText(state) {
const texts = {
ready: '就绪',
running: '运行中',
blocked: '阻塞',
completed: '已完成'
}
return texts[state] || state
}
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.slider-label {
font-size: 14px;
color: #606266;
min-width: 80px;
}
.comparison-view {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 20px;
margin-bottom: 20px;
}
.comparison-column {
background: white;
border-radius: 8px;
padding: 16px;
}
.comparison-column h5 {
margin: 0 0 16px 0;
color: #303133;
text-align: center;
padding-bottom: 8px;
border-bottom: 2px solid #e4e7ed;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-circle {
width: 48px;
height: 48px;
border-radius: 50%;
background: #409eff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.resource-visualization {
margin-bottom: 16px;
}
.resource-bar {
margin-bottom: 12px;
}
.bar-label {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
}
.bar-container {
height: 24px;
background: #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
color: white;
font-size: 11px;
font-weight: bold;
transition: width 0.3s ease;
}
.thread-visualization,
.coroutine-visualization {
margin-bottom: 16px;
}
.memory-blocks {
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
min-height: 80px;
align-content: flex-start;
}
.thread-block {
width: 16px;
height: 16px;
border-radius: 2px;
}
.coroutine-grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 2px;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
}
.coroutine-cell {
aspect-ratio: 1;
border-radius: 2px;
}
.more-indicator {
grid-column: 1 / -1;
text-align: center;
color: #909399;
font-size: 12px;
padding: 4px;
}
.efficiency-badge {
text-align: center;
margin-top: 12px;
}
.insight-panel {
margin-top: 16px;
}
@media (max-width: 768px) {
.comparison-view {
grid-template-columns: 1fr;
}
.vs-divider {
order: -1;
}
.vs-circle {
transform: rotate(90deg);
}
}
</style>
@@ -0,0 +1,505 @@
<template>
<div class="demo-container">
<h4>事件循环 (Event Loop) 演示</h4>
<div class="controls">
<el-button type="primary" size="small" @click="startSimulation" :disabled="isRunning">
{{ isRunning ? '运行中...' : '开始模拟' }}
</el-button>
<el-button size="small" @click="addTask" :disabled="tasks.length >= 10">
添加任务
</el-button>
<el-button size="small" @click="addMicrotask" :disabled="microtasks.length >= 5">
添加微任务
</el-button>
<el-button size="small" @click="reset">重置</el-button>
<el-select v-model="simulationSpeed" size="small" style="width: 120px;">
<el-option :value="1" label="慢速" />
<el-option :value="2" label="正常" />
<el-option :value="3" label="快速" />
<el-option :value="4" label="极快" />
<el-option :value="5" label="即时" />
</el-select>
</div>
<div class="event-loop-container">
<!-- 调用栈 -->
<div class="section">
<div class="section-title">调用栈 (Call Stack)</div>
<div class="stack-container">
<div v-for="(frame, idx) in callStack" :key="idx" class="stack-frame" :class="{ active: idx === 0 }"
:style="{ animationDelay: idx * 0.1 + 's' }">
<div class="frame-name">{{ frame.name }}</div>
<div class="frame-line" v-if="frame.line"> {{ frame.line }} </div>
</div>
<div v-if="callStack.length === 0" class="empty-stack">
栈为空
</div>
</div>
</div>
<!-- 事件循环 -->
<div class="section event-loop">
<div class="section-title">事件循环 (Event Loop)</div>
<div class="loop-container">
<div class="loop-arrow" :class="{ rotating: isRunning }">
<svg viewBox="0 0 100 100" class="loop-svg">
<circle cx="50" cy="50" r="40" fill="none" stroke="#409eff" stroke-width="4" stroke-dasharray="200" stroke-linecap="round"
class="loop-circle" />
<polygon points="85,50 75,45 75,55" fill="#409eff" class="loop-arrowhead" />
</svg>
</div>
<div class="loop-label">检查</div>
</div>
<div class="loop-description">
<div class="step" :class="{ active: currentStep === 1 }">
<span class="step-num">1</span>
<span class="step-text">执行调用栈中的同步代码</span>
</div>
<div class="step" :class="{ active: currentStep === 2 }">
<span class="step-num">2</span>
<span class="step-text">执行所有微任务 (microtasks)</span>
</div>
<div class="step" :class="{ active: currentStep === 3 }">
<span class="step-num">3</span>
<span class="step-text">渲染 UI (如果需要)</span>
</div>
<div class="step" :class="{ active: currentStep === 4 }">
<span class="step-num">4</span>
<span class="step-text">执行宏任务 (macrotask)</span>
</div>
</div>
</div>
<!-- 任务队列 -->
<div class="section">
<div class="section-title">任务队列</div>
<div class="queue microtask-queue">
<div class="queue-title">微任务队列 (Microtasks)</div>
<div class="queue-items">
<div v-for="(task, idx) in microtasks" :key="task.id" class="queue-item microtask"
:style="{ animationDelay: idx * 0.1 + 's' }">
<span class="task-name">{{ task.name }}</span>
<span class="task-priority">高优先级</span>
</div>
<div v-if="microtasks.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
<div class="queue macrotask-queue">
<div class="queue-title">宏任务队列 (Macrotasks)</div>
<div class="queue-items">
<div v-for="(task, idx) in tasks" :key="task.id" class="queue-item macrotask"
:style="{ animationDelay: idx * 0.15 + 's' }">
<span class="task-name">{{ task.name }}</span>
<span class="task-type">{{ task.type }}</span>
</div>
<div v-if="tasks.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isRunning = ref(false)
const simulationSpeed = ref(2)
const currentStep = ref(1)
const callStack = ref([])
const microtasks = ref([])
const tasks = ref([])
let taskIdCounter = 1
let microtaskIdCounter = 1
function addTask() {
if (tasks.value.length >= 10) return
const types = ['setTimeout', 'setInterval', 'I/O', 'DOM事件']
tasks.value.push({
id: taskIdCounter++,
name: `任务 ${taskIdCounter - 1}`,
type: types[Math.floor(Math.random() * types.length)]
})
}
function addMicrotask() {
if (microtasks.value.length >= 5) return
microtasks.value.push({
id: microtaskIdCounter++,
name: `微任务 ${microtaskIdCounter - 1}`
})
}
function startSimulation() {
if (isRunning.value) return
// 确保有任务
if (tasks.value.length === 0) {
addTask()
addTask()
}
if (microtasks.value.length === 0) {
addMicrotask()
}
isRunning.value = true
currentStep.value = 1
// 模拟执行过程
runSimulationStep()
}
function runSimulationStep() {
if (!isRunning.value) return
// 模拟步骤执行
const speed = 6 - simulationSpeed.value // 转换为延迟
const delay = speed * 200
setTimeout(() => {
if (!isRunning.value) return
currentStep.value = (currentStep.value % 4) + 1
// 更新调用栈
updateCallStack()
// 消耗任务
if (currentStep.value === 2 && microtasks.value.length > 0) {
microtasks.value.shift()
} else if (currentStep.value === 4 && tasks.value.length > 0) {
tasks.value.shift()
}
// 检查是否完成
if (tasks.value.length === 0 && microtasks.value.length === 0) {
isRunning.value = false
callStack.value = []
} else {
runSimulationStep()
}
}, delay)
}
function updateCallStack() {
const stack = []
switch (currentStep.value) {
case 1:
stack.push({ name: 'main()', line: 1 })
stack.push({ name: 'executeSync()', line: 15 })
break
case 2:
stack.push({ name: 'main()', line: 1 })
stack.push({ name: 'processMicrotask()', line: 25 })
break
case 3:
stack.push({ name: 'main()', line: 1 })
stack.push({ name: 'render()', line: 35 })
break
case 4:
stack.push({ name: 'main()', line: 1 })
stack.push({ name: 'processMacrotask()', line: 45 })
break
}
callStack.value = stack
}
function reset() {
isRunning.value = false
currentStep.value = 1
callStack.value = []
microtasks.value = []
tasks.value = []
taskIdCounter = 1
microtaskIdCounter = 1
}
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
.event-loop-container {
display: grid;
grid-template-columns: 1fr 1.2fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.section {
background: white;
border-radius: 8px;
padding: 16px;
}
.section-title {
font-weight: bold;
color: #303133;
margin-bottom: 12px;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
padding-bottom: 8px;
}
.stack-container {
min-height: 200px;
}
.stack-frame {
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 8px 12px;
margin-bottom: 4px;
font-size: 12px;
}
.stack-frame.active {
background: #ecf5ff;
border-color: #409eff;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.frame-name {
font-weight: bold;
color: #303133;
}
.frame-line {
font-size: 11px;
color: #909399;
}
.empty-stack {
text-align: center;
color: #909399;
padding: 40px;
font-size: 12px;
}
.event-loop {
display: flex;
flex-direction: column;
}
.loop-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.loop-arrow {
width: 120px;
height: 120px;
margin-bottom: 12px;
}
.loop-arrow.rotating .loop-svg {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loop-circle {
animation: dash 2s linear infinite;
}
@keyframes dash {
0% {
stroke-dashoffset: 200;
}
100% {
stroke-dashoffset: 0;
}
}
.loop-label {
font-size: 16px;
font-weight: bold;
color: #409eff;
}
.loop-description {
margin-top: 16px;
}
.step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
margin-bottom: 4px;
border-radius: 4px;
transition: all 0.3s;
}
.step.active {
background: #ecf5ff;
border-left: 3px solid #409eff;
}
.step-num {
width: 20px;
height: 20px;
border-radius: 50%;
background: #dcdfe6;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: bold;
}
.step.active .step-num {
background: #409eff;
color: white;
}
.step-text {
font-size: 12px;
color: #606266;
}
.step.active .step-text {
color: #303133;
font-weight: 500;
}
.queue {
margin-bottom: 16px;
}
.queue-title {
font-size: 12px;
font-weight: bold;
color: #606266;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 4px;
}
.queue-title::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
}
.microtask-queue .queue-title::before {
background: #f56c6c;
}
.macrotask-queue .queue-title::before {
background: #e6a23c;
}
.queue-items {
min-height: 60px;
background: #f5f7fa;
border-radius: 4px;
padding: 8px;
}
.queue-item {
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 8px 12px;
margin-bottom: 4px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: space-between;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.queue-item.microtask {
border-left: 3px solid #f56c6c;
}
.queue-item.macrotask {
border-left: 3px solid #e6a23c;
}
.task-name {
font-weight: 500;
color: #303133;
}
.task-type {
font-size: 11px;
color: #909399;
}
.empty-queue {
text-align: center;
color: #909399;
padding: 20px;
font-size: 12px;
}
@media (max-width: 1024px) {
.event-loop-container {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,439 @@
<template>
<div class="demo-container">
<h4>Go 协程 (Goroutine) GMP 调度演示</h4>
<div class="controls">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="overview">整体视图</el-radio-button>
<el-radio-button label="gmp">GMP 调度</el-radio-button>
<el-radio-button label="channel">Channel 通信</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" @click="startDemo" :disabled="isRunning">
{{ isRunning ? '运行中...' : '开始演示' }}
</el-button>
<el-button size="small" @click="addGoroutine" :disabled="goroutines.length >= 20">
+Goroutine
</el-button>
<el-button size="small" @click="reset">重置</el-button>
</div>
<!-- GMP 调度视图 -->
<div v-if="viewMode === 'gmp' || viewMode === 'overview'" class="gmp-view">
<div class="gmp-container">
<!-- Global Queue -->
<div class="queue-section global-queue">
<div class="queue-header">
<span class="queue-name">Global Queue (G)</span>
<el-tag size="small" type="info">{{ globalQueue.length }}</el-tag>
</div>
<div class="queue-content">
<div v-for="g in globalQueue" :key="g.id" class="g-item"
:style="{ backgroundColor: g.color }">
G{{ g.id }}
</div>
<div v-if="globalQueue.length === 0" class="empty-queue"></div>
</div>
</div>
<!-- P (Processors) -->
<div class="processors-section">
<div class="section-header">
<span class="section-name">P (Processors) - {{ processors.length }} </span>
</div>
<div class="processors-grid">
<div v-for="(p, idx) in processors" :key="idx" class="processor"
:class="{ 'active': p.active }"
:style="{ borderColor: p.active ? p.color : '#e4e7ed' }">
<div class="processor-header">
<span class="processor-name">P{{ idx }}</span>
<span class="processor-status" :class="{ 'running': p.active }">{{ p.active ? '运行中' : '空闲' }}</span>
</div>
<div class="local-queue">
<div class="queue-label">本地队列</div>
<div class="local-g-list">
<div v-for="g in p.localQueue" :key="g.id" class="local-g-item"
:style="{ backgroundColor: g.color }">
G{{ g.id }}
</div>
<div v-if="p.localQueue.length === 0" class="empty-local">-</div>
</div>
</div>
<div class="m-binding" v-if="p.m">
<span class="m-label">绑定 M{{ p.m.id }}</span>
</div>
</div>
</div>
</div>
<!-- M (Machine Threads) -->
<div class="machines-section">
<div class="section-header">
<span class="section-name">M (Machine Threads) - {{ machines.length }} </span>
</div>
<div class="machines-list">
<div v-for="m in machines" :key="m.id" class="machine-item"
:class="{ 'active': m.active }"
:style="{ borderColor: m.active ? '#67c23a' : '#e4e7ed' }">
<span class="machine-id">M{{ m.id }}</span>
<span class="machine-status">{{ m.active ? '运行中' : '休眠' }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="explanation">
<el-alert title="GMP 调度模型" type="success"
:description="gmpDescription"
show-icon :closable="false" />
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const gmpDescription = 'G (Goroutine): 待执行的任务。M (Machine): 操作系统线程,执行 G 的载体。P (Processor): 逻辑处理器,提供执行上下文。G 先放入 P 的本地队列,P 与 M 绑定后,M 从 P 获取 G 执行。当本地队列空时,会从全局队列或其他 P 偷任务。'
const viewMode = ref('gmp')
const isRunning = ref(false)
const goroutines = ref([])
// GMP 数据结构
const globalQueue = ref([])
const processors = ref([])
const machines = ref([])
// 初始化数据
function initGMP() {
// 创建一些 Goroutines
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#b3d8ff', '#c2e7b0', '#f5dab1']
const goroutinesData = []
for (let i = 0; i < 12; i++) {
goroutinesData.push({
id: i + 1,
color: colors[i % colors.length],
status: 'waiting'
})
}
goroutines.value = goroutinesData
// 分配全局队列
globalQueue.value = goroutinesData.slice(0, 3)
// 初始化 Processors (P)
processors.value = [
{ id: 0, active: true, color: '#409eff', localQueue: goroutinesData.slice(3, 6), m: { id: 0 } },
{ id: 1, active: false, color: '#67c23a', localQueue: goroutinesData.slice(6, 9), m: null },
{ id: 2, active: false, color: '#e6a23c', localQueue: goroutinesData.slice(9, 12), m: null },
{ id: 3, active: false, color: '#f56c6c', localQueue: [], m: null }
]
// 初始化 Machines (M)
machines.value = [
{ id: 0, active: true },
{ id: 1, active: false },
{ id: 2, active: false },
{ id: 3, active: false }
]
}
// 添加 Goroutine
function addGoroutine() {
if (goroutines.value.length >= 20) return
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#b3d8ff']
const id = goroutines.value.length + 1
const newG = {
id,
color: colors[id % colors.length],
status: 'waiting'
}
goroutines.value.push(newG)
// 添加到第一个有空位的 P
for (const p of processors.value) {
if (p.localQueue.length < 5) {
p.localQueue.push(newG)
break
}
}
}
// 开始演示
function startDemo() {
isRunning.value = true
// 模拟调度过程
let step = 0
const interval = setInterval(() => {
step++
// 轮流激活 P
processors.value.forEach((p, idx) => {
p.active = (idx === step % processors.value.length)
})
// 对应的 M 也激活
machines.value.forEach((m, idx) => {
m.active = processors.value[idx]?.active || false
})
if (step >= 20) {
clearInterval(interval)
isRunning.value = false
}
}, 500)
}
// 重置
function reset() {
isRunning.value = false
initGMP()
}
// 监听视图模式变化
watch(viewMode, () => {
if (viewMode.value === 'gmp') {
initGMP()
}
})
// 初始化
initGMP()
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
.gmp-view {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.gmp-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.queue-section {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.queue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.queue-name {
font-weight: bold;
color: #303133;
}
.queue-content {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 40px;
align-items: center;
}
.g-item {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 11px;
font-weight: bold;
}
.empty-queue {
color: #909399;
font-size: 12px;
text-align: center;
width: 100%;
}
.processors-section,
.machines-section {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
}
.section-header {
margin-bottom: 12px;
}
.section-name {
font-weight: bold;
color: #303133;
font-size: 13px;
}
.processors-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.processor {
background: white;
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
transition: all 0.3s;
}
.processor.active {
box-shadow: 0 0 10px currentColor;
}
.processor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.processor-name {
font-weight: bold;
font-size: 12px;
color: #303133;
}
.processor-status {
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
background: #dcdfe6;
color: #606266;
}
.processor-status.running {
background: #67c23a;
color: white;
}
.local-queue {
margin-bottom: 8px;
}
.queue-label {
font-size: 10px;
color: #909399;
margin-bottom: 4px;
}
.local-g-list {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.local-g-item {
padding: 2px 5px;
border-radius: 3px;
color: white;
font-size: 9px;
font-weight: bold;
}
.empty-local {
font-size: 10px;
color: #c0c4cc;
}
.m-binding {
font-size: 10px;
color: #409eff;
background: #ecf5ff;
padding: 2px 6px;
border-radius: 3px;
}
.m-label {
font-weight: 500;
}
.machines-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.machine-item {
background: white;
border: 2px solid #e4e7ed;
border-radius: 6px;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.machine-item.active {
border-color: #67c23a;
background: #f0f9eb;
}
.machine-id {
font-weight: bold;
font-size: 12px;
color: #303133;
}
.machine-status {
font-size: 11px;
color: #909399;
}
.machine-item.active .machine-status {
color: #67c23a;
}
.explanation {
margin-top: 16px;
}
@media (max-width: 768px) {
.processors-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,324 @@
<template>
<div class="demo-container">
<h4>进程内存隔离演示</h4>
<div class="controls">
<el-button type="primary" size="small" @click="addProcess" :disabled="processes.length >= 4">
创建进程
</el-button>
<el-button type="danger" size="small" @click="killProcess" :disabled="processes.length === 0">
结束进程
</el-button>
<el-button size="small" @click="simulateCrash">
模拟进程崩溃
</el-button>
<el-button size="small" @click="reset">
重置
</el-button>
</div>
<div class="memory-view">
<div class="memory-label">系统内存</div>
<div class="memory-blocks">
<div
v-for="process in processes"
:key="process.id"
class="process-block"
:class="{ crashed: process.crashed, active: process.active }"
:style="{ width: process.size + '%', backgroundColor: process.color }"
>
<div class="process-header">
<span class="process-name">进程 {{ process.id }}</span>
<span class="process-pid">PID: {{ process.pid }}</span>
</div>
<div class="process-memory">
<div class="memory-section code">
<span class="section-label">代码段</span>
<span class="section-size">{{ process.codeSize }}MB</span>
</div>
<div class="memory-section data">
<span class="section-label">数据段</span>
<span class="section-size">{{ process.dataSize }}MB</span>
</div>
<div class="memory-section heap">
<span class="section-label"></span>
<span class="section-size">{{ process.heapSize }}MB</span>
</div>
<div class="memory-section stack">
<span class="section-label"></span>
<span class="section-size">{{ process.stackSize }}MB</span>
</div>
</div>
<div v-if="process.crashed" class="crash-overlay">
<span class="crash-text">💥 已崩溃</span>
<span class="crash-info">不影响其他进程</span>
</div>
</div>
</div>
<div class="shared-memory" v-if="showSharedMemory">
<div class="shared-label">共享内存区域 (IPC)</div>
<div class="shared-content">
<div v-for="process in processes" :key="process.id" class="shared-access">
<span class="access-indicator" :style="{ backgroundColor: process.color }"></span>
<span>进程 {{ process.id }} 可以访问</span>
</div>
</div>
</div>
</div>
<div class="info-panel">
<el-alert
:title="currentInfo.title"
:type="currentInfo.type"
:description="currentInfo.description"
show-icon
:closable="false"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const processes = ref([])
const showSharedMemory = ref(false)
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
let pidCounter = 1000
const currentInfo = computed(() => {
if (processes.value.length === 0) {
return {
title: '进程隔离',
type: 'info',
description: '每个进程拥有独立的虚拟地址空间,一个进程崩溃不会影响其他进程。点击"创建进程"开始演示。'
}
}
const crashed = processes.value.filter(p => p.crashed).length
if (crashed > 0) {
return {
title: '隔离性验证',
type: 'success',
description: `进程已崩溃但其他进程正常运行,证明进程间内存隔离有效。崩溃的进程会被操作系统回收资源。`
}
}
return {
title: '内存布局',
type: 'info',
description: `当前有 ${processes.value.length} 个进程在运行。每个进程的内存分为代码段、数据段、堆和栈,相互隔离不可访问。`
}
})
function createProcess() {
if (processes.value.length >= 4) return
const id = processes.value.length + 1
const size = 20 + Math.random() * 10
processes.value.push({
id,
pid: pidCounter++,
size,
color: colors[id - 1],
codeSize: Math.floor(size * 0.15),
dataSize: Math.floor(size * 0.1),
heapSize: Math.floor(size * 0.6),
stackSize: Math.floor(size * 0.15),
crashed: false,
active: true
})
}
function killProcess() {
if (processes.value.length === 0) return
processes.value.pop()
}
function simulateCrash() {
if (processes.value.length === 0) return
// 随机让一个未崩溃的进程崩溃
const candidates = processes.value.filter(p => !p.crashed)
if (candidates.length > 0) {
const victim = candidates[Math.floor(Math.random() * candidates.length)]
victim.crashed = true
victim.active = false
}
}
function reset() {
processes.value = []
showSharedMemory.value = false
pidCounter = 1000
}
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.memory-view {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.memory-label {
font-weight: bold;
color: #303133;
margin-bottom: 12px;
}
.memory-blocks {
display: flex;
flex-direction: column;
gap: 8px;
}
.process-block {
border-radius: 6px;
padding: 12px;
color: white;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.process-block.crashed {
opacity: 0.5;
}
.process-block.active {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.process-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.process-name {
font-weight: bold;
}
.process-pid {
opacity: 0.8;
font-size: 12px;
}
.process-memory {
display: flex;
gap: 8px;
font-size: 11px;
}
.memory-section {
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
}
.section-label {
opacity: 0.7;
font-size: 10px;
}
.section-size {
font-weight: bold;
}
.crash-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.crash-text {
font-size: 24px;
margin-bottom: 8px;
}
.crash-info {
font-size: 12px;
opacity: 0.8;
}
.shared-memory {
margin-top: 16px;
padding: 12px;
background: #f4f4f5;
border-radius: 6px;
border: 2px dashed #c0c4cc;
}
.shared-label {
font-weight: bold;
color: #606266;
margin-bottom: 8px;
}
.shared-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.shared-access {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #606266;
}
.access-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.info-panel {
margin-top: 16px;
}
@media (max-width: 768px) {
.process-memory {
flex-wrap: wrap;
}
.cpu-cores {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,308 @@
<template>
<div class="demo-container">
<h4>进程 / 线程 / 协程 对比演示</h4>
<div class="controls">
<el-radio-group v-model="model" size="small">
<el-radio-button label="process">多进程</el-radio-button>
<el-radio-button label="thread">多线程</el-radio-button>
<el-radio-button label="coroutine">协程</el-radio-button>
</el-radio-group>
<el-button
type="primary"
size="small"
@click="startSimulation"
:disabled="isRunning"
>
{{ isRunning ? '运行中...' : '开始模拟' }}
</el-button>
</div>
<div class="stats-bar">
<el-statistic title="内存占用" :value="memoryUsage" suffix="MB" />
<el-statistic title="上下文切换" :value="contextSwitches" />
<el-statistic title="完成任务" :value="completedTasks" />
<el-statistic title="耗时" :value="elapsedTime" suffix="ms" />
</div>
<div class="visualization">
<div class="cpu-cores">
<div
v-for="(core, idx) in cpuCores"
:key="idx"
class="core"
:class="{ active: core.active, type: core.type }"
>
<span class="core-label">CPU {{ idx + 1 }}</span>
<div class="task-indicator" v-if="core.task">
{{ core.task }}
</div>
</div>
</div>
<div class="task-queue">
<h5>任务队列</h5>
<div class="queue-items">
<div
v-for="(task, idx) in pendingTasks"
:key="task.id"
class="queue-item"
:style="{ animationDelay: `${idx * 0.1}s` }"
>
Task {{ task.id }}
</div>
</div>
</div>
</div>
<div class="explanation">
<el-alert
:title="explanationTitle"
:type="explanationType"
:description="explanationText"
show-icon
:closable="false"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const model = ref('process')
const isRunning = ref(false)
const memoryUsage = ref(0)
const contextSwitches = ref(0)
const completedTasks = ref(0)
const elapsedTime = ref(0)
const cpuCores = ref([
{ active: false, type: 'process', task: null },
{ active: false, type: 'process', task: null },
{ active: false, type: 'process', task: null },
{ active: false, type: 'process', task: null },
])
const pendingTasks = ref([])
const explanationTitle = computed(() => {
const titles = {
process: '多进程模型',
thread: '多线程模型',
coroutine: '协程模型'
}
return titles[model.value]
})
const explanationType = computed(() => {
const types = {
process: 'success',
thread: 'warning',
coroutine: 'info'
}
return types[model.value]
})
const explanationText = computed(() => {
const texts = {
process: '每个进程拥有独立的内存空间,隔离性强但开销大。进程间通信需要 IPC 机制。适合需要强隔离的场景,如浏览器标签页、沙箱程序。',
thread: '线程共享进程内存,切换开销较小,但需要同步机制保护共享数据。适合 CPU 密集型任务和需要共享数据的场景。',
coroutine: '用户态轻量级线程,由运行时调度,切换极快。适合 I/O 密集型高并发场景,如 Web 服务器、网关、长连接服务。'
}
return texts[model.value]
})
watch(model, () => {
// 重置状态
resetSimulation()
})
function resetSimulation() {
isRunning.value = false
memoryUsage.value = model.value === 'process' ? 400 : model.value === 'thread' ? 100 : 20
contextSwitches.value = 0
completedTasks.value = 0
elapsedTime.value = 0
cpuCores.value.forEach(core => {
core.active = false
core.type = model.value
core.task = null
})
pendingTasks.value = Array.from({ length: 16 }, (_, i) => ({ id: i + 1 }))
}
async function startSimulation() {
if (isRunning.value) return
isRunning.value = true
const startTime = Date.now()
const taskCount = pendingTasks.value.length
const baseSwitchCost = model.value === 'process' ? 10 : model.value === 'thread' ? 2 : 1
// 模拟任务处理
while (pendingTasks.value.length > 0 && isRunning.value) {
// 分配任务到 CPU 核心
for (let i = 0; i < cpuCores.value.length; i++) {
if (!cpuCores.value[i].active && pendingTasks.value.length > 0) {
const task = pendingTasks.value.shift()
cpuCores.value[i].active = true
cpuCores.value[i].task = task.id
// 模拟任务执行时间
setTimeout(() => {
if (isRunning.value) {
cpuCores.value[i].active = false
cpuCores.value[i].task = null
completedTasks.value++
contextSwitches.value += baseSwitchCost
}
}, 300 + Math.random() * 200)
}
}
elapsedTime.value = Date.now() - startTime
await new Promise(resolve => setTimeout(resolve, 50))
}
isRunning.value = false
}
// 初始化
resetSimulation()
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
margin: 20px 0;
}
.controls {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.visualization {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.cpu-cores {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.core {
background: white;
border: 2px solid #dcdfe6;
border-radius: 8px;
padding: 16px;
text-align: center;
transition: all 0.3s;
}
.core.active {
border-color: #409eff;
background: #ecf5ff;
}
.core.active.process {
border-color: #67c23a;
background: #f0f9eb;
}
.core.active.thread {
border-color: #e6a23c;
background: #fdf6ec;
}
.core.active.coroutine {
border-color: #909399;
background: #f4f4f5;
}
.core-label {
font-size: 12px;
color: #606266;
display: block;
margin-bottom: 8px;
}
.task-indicator {
font-size: 14px;
font-weight: bold;
color: #409eff;
}
.task-queue {
background: white;
border-radius: 8px;
padding: 16px;
}
.task-queue h5 {
margin: 0 0 12px 0;
color: #303133;
}
.queue-items {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.queue-item {
background: #409eff;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.explanation {
margin-top: 20px;
}
@media (max-width: 768px) {
.stats-bar {
grid-template-columns: repeat(2, 1fr);
}
.visualization {
grid-template-columns: 1fr;
}
.cpu-cores {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,518 @@
<template>
<div class="demo-container">
<h4>线程调度演示</h4>
<div class="controls">
<el-radio-group v-model="schedulingPolicy" size="small">
<el-radio-button label="fifo">FIFO (先来先服务)</el-radio-button>
<el-radio-button label="roundrobin">时间片轮转</el-radio-button>
<el-radio-button label="priority">优先级调度</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" @click="addThread" :disabled="threads.length >= 6">
添加线程
</el-button>
<el-button
type="success"
size="small"
@click="toggleSimulation"
>
{{ isRunning ? '暂停' : '开始调度' }}
</el-button>
<el-button size="small" @click="reset">
重置
</el-button>
</div>
<div class="timeline-container">
<div class="timeline-header">
<span class="timeline-label">时间轴</span>
<div class="time-marker">0ms</div>
<div class="time-marker">100ms</div>
<div class="time-marker">200ms</div>
<div class="time-marker">300ms</div>
<div class="time-marker">400ms</div>
<div class="time-marker">500ms</div>
</div>
<div class="thread-rows">
<div
v-for="thread in threads"
:key="thread.id"
class="thread-row"
>
<div class="thread-info">
<div class="thread-name" :style="{ color: thread.color }">
{{ thread.name }}
</div>
<div class="thread-details">
<el-tag size="small" :type="thread.state === 'running' ? 'success' : thread.state === 'ready' ? 'warning' : 'info'">
{{ stateText(thread.state) }}
</el-tag>
<span class="priority">优先级: {{ thread.priority }}</span>
</div>
</div>
<div class="execution-track">
<div
v-for="(slot, idx) in thread.executionSlots"
:key="idx"
class="execution-slot"
:class="{ running: slot.state === 'running', blocked: slot.state === 'blocked' }"
:style="{ left: slot.start + '%', width: slot.width + '%', backgroundColor: slot.state === 'running' ? thread.color : '#dcdfe6' }"
>
<span v-if="slot.state === 'running'" class="slot-label">运行</span>
<span v-else class="slot-label">等待</span>
</div>
<div
v-if="thread.state === 'running'"
class="current-indicator"
:style="{ left: currentTime + '%', backgroundColor: thread.color }"
>
<div class="indicator-arrow"></div>
</div>
</div>
</div>
</div>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-value">{{ completedThreads }}</div>
<div class="stat-label">已完成线程</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ contextSwitches }}</div>
<div class="stat-label">上下文切换</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ avgWaitTime }}ms</div>
<div class="stat-label">平均等待时间</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ throughput }}</div>
<div class="stat-label">吞吐量 (线程/)</div>
</div>
</div>
<div class="algorithm-info">
<h5>当前调度算法: {{ algorithmName }}</h5>
<p>{{ algorithmDescription }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const schedulingPolicy = ref('roundrobin')
const threads = ref([])
const isRunning = ref(false)
const currentTime = ref(0)
const completedThreads = ref(0)
const contextSwitches = ref(0)
const totalWaitTime = ref(0)
const startTime = ref(null)
let animationId = null
let currentThreadIndex = 0
let timeQuantum = 50 // 时间片长度
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#b3d8ff']
const algorithmName = computed(() => {
const names = {
fifo: 'FIFO (First In First Out)',
roundrobin: 'Round Robin (时间片轮转)',
priority: 'Priority Scheduling (优先级调度)'
}
return names[schedulingPolicy.value]
})
const algorithmDescription = computed(() => {
const descriptions = {
fifo: '按照线程到达的先后顺序执行,直到当前线程完成才执行下一个。简单公平但可能导致短任务等待长任务。',
roundrobin: '每个线程轮流执行一个时间片,时间片用完就切换到下一个线程。响应性好,适合交互式系统。',
priority: '根据线程优先级决定执行顺序,高优先级线程优先执行。需要处理优先级反转和饥饿问题。'
}
return descriptions[schedulingPolicy.value]
})
const avgWaitTime = computed(() => {
if (completedThreads.value === 0) return 0
return Math.round(totalWaitTime.value / completedThreads.value)
})
const throughput = computed(() => {
if (!startTime.value) return 0
const elapsed = (Date.now() - startTime.value) / 1000
if (elapsed === 0) return 0
return (completedThreads.value / elapsed).toFixed(2)
})
const stateText = (state) => {
const map = {
running: '运行中',
ready: '就绪',
blocked: '阻塞',
completed: '完成'
}
return map[state] || state
}
function addThread() {
if (threads.value.length >= 6) return
const id = threads.value.length + 1
const priority = Math.floor(Math.random() * 10) + 1
const workAmount = 30 + Math.floor(Math.random() * 50) // 30-80% 的工作量
threads.value.push({
id,
name: `Thread-${id}`,
color: colors[id - 1],
priority,
state: 'ready',
progress: 0,
workAmount,
executionSlots: [],
startTime: null,
endTime: null
})
}
function reset() {
threads.value = []
isRunning.value = false
currentTime.value = 0
completedThreads.value = 0
contextSwitches.value = 0
totalWaitTime.value = 0
startTime.value = null
currentThreadIndex = 0
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
}
function toggleSimulation() {
if (isRunning.value) {
pauseSimulation()
} else {
startSimulation()
}
}
function startSimulation() {
if (threads.value.length === 0) {
// 自动创建一些线程
for (let i = 0; i < 3; i++) {
addThread()
}
}
isRunning.value = true
if (!startTime.value) {
startTime.value = Date.now()
}
// 初始化所有线程的开始时间
threads.value.forEach(thread => {
if (!thread.startTime) {
thread.startTime = Date.now()
}
})
runSimulation()
}
function pauseSimulation() {
isRunning.value = false
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
}
function runSimulation() {
if (!isRunning.value) return
// 根据调度策略选择下一个线程
let nextThread = null
let nextIndex = -1
switch (schedulingPolicy.value) {
case 'fifo':
// FIFO: 找到第一个未完成的线程
for (let i = 0; i < threads.value.length; i++) {
if (threads.value[i].progress < threads.value[i].workAmount) {
nextThread = threads.value[i]
nextIndex = i
break
}
}
break
case 'roundrobin':
// Round Robin: 轮流执行
let attempts = 0
while (attempts < threads.value.length) {
const idx = currentThreadIndex % threads.value.length
if (threads.value[idx].progress < threads.value[idx].workAmount) {
nextThread = threads.value[idx]
nextIndex = idx
currentThreadIndex = (idx + 1) % threads.value.length
break
}
currentThreadIndex++
attempts++
}
break
case 'priority':
// Priority: 选择优先级最高的就绪线程
let highestPriority = -1
for (let i = 0; i < threads.value.length; i++) {
const thread = threads.value[i]
if (thread.progress < thread.workAmount && thread.priority > highestPriority) {
highestPriority = thread.priority
nextThread = thread
nextIndex = i
}
}
break
}
// 执行选中的线程
if (nextThread) {
// 记录状态变化
if (nextThread.state !== 'running') {
contextSwitches.value++
nextThread.state = 'running'
}
// 其他线程设为就绪状态
threads.value.forEach((thread, idx) => {
if (idx !== nextIndex && thread.state === 'running') {
thread.state = 'ready'
}
})
// 记录执行槽
const lastSlot = nextThread.executionSlots[nextThread.executionSlots.length - 1]
if (!lastSlot || lastSlot.state !== 'running') {
nextThread.executionSlots.push({
start: nextThread.progress,
width: 0,
state: 'running'
})
} else {
lastSlot.width = 2
}
// 增加进度
const increment = schedulingPolicy.value === 'roundrobin' ? 5 : 3
nextThread.progress = Math.min(nextThread.progress + increment, nextThread.workAmount)
// 检查是否完成
if (nextThread.progress >= nextThread.workAmount) {
nextThread.state = 'completed'
nextThread.endTime = Date.now()
completedThreads.value++
totalWaitTime.value += (nextThread.endTime - nextThread.startTime)
}
// 更新时间显示
currentTime.value = nextThread.progress
}
// 检查是否所有线程都完成
const allCompleted = threads.value.every(t => t.progress >= t.workAmount)
if (allCompleted) {
isRunning.value = false
} else {
animationId = requestAnimationFrame(runSimulation)
}
}
// 生命周期
onMounted(() => {
// 自动创建初始线程
for (let i = 0; i < 3; i++) {
addThread()
}
})
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
})
</script>
<style scoped>
.demo-container {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h4 {
margin: 0 0 16px 0;
color: #303133;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.memory-view {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.memory-label {
font-weight: bold;
color: #303133;
margin-bottom: 12px;
}
.memory-blocks {
display: flex;
flex-direction: column;
gap: 8px;
}
.process-block {
border-radius: 6px;
padding: 12px;
color: white;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.process-block.crashed {
opacity: 0.5;
}
.process-block.active {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.process-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.process-name {
font-weight: bold;
}
.process-pid {
opacity: 0.8;
font-size: 12px;
}
.process-memory {
display: flex;
gap: 8px;
font-size: 11px;
}
.memory-section {
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
}
.section-label {
opacity: 0.7;
font-size: 10px;
}
.section-size {
font-weight: bold;
}
.crash-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.crash-text {
font-size: 24px;
margin-bottom: 8px;
}
.crash-info {
font-size: 12px;
opacity: 0.8;
}
.shared-memory {
margin-top: 16px;
padding: 12px;
background: #f4f4f5;
border-radius: 6px;
border: 2px dashed #c0c4cc;
}
.shared-label {
font-weight: bold;
color: #606266;
margin-bottom: 8px;
}
.shared-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.shared-access {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #606266;
}
.access-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.info-panel {
margin-top: 16px;
}
@media (max-width: 768px) {
.process-memory {
flex-wrap: wrap;
}
}
</style>
@@ -2,196 +2,375 @@
import { ref, computed } from 'vue'
const round = ref(1)
const minRound = 1
const maxRound = 5
const maxRound = 20
const windowLimit = 4000
const contextTokens = computed(() => 120 + (round.value - 1) * 80)
// 模拟数据配置
const systemPromptTokens = 1000
const tokensPerRound = 300
const costPer1kTokens = 0.002
const cacheHitRate = computed(() =>
round.value === 1 ? 0 : Math.min(80, (round.value - 1) * 20)
)
// 计算属性
const historyTokens = computed(() => (round.value - 1) * tokensPerRound)
const currentInputTokens = 200
const totalTokens = computed(() => systemPromptTokens + historyTokens.value + currentInputTokens)
const baseCostPerRound = 0.025
// 是否溢出
const isOverflow = computed(() => totalTokens.value > windowLimit)
const overflowAmount = computed(() => Math.max(0, totalTokens.value - windowLimit))
const forgottenRounds = computed(() => Math.floor(overflowAmount.value / tokensPerRound))
const currentCost = computed(() => {
const rate = cacheHitRate.value / 100
const cost = baseCostPerRound * (1 - rate * 0.9)
return cost.toFixed(4)
})
// 成本计算
const currentCost = computed(() => (totalTokens.value / 1000 * costPer1kTokens).toFixed(4))
const savedPercent = computed(() => {
const cost = Number(currentCost.value)
const saved = ((baseCostPerRound - cost) / baseCostPerRound) * 100
return saved.toFixed(1)
})
// 高度计算 (相对于 windowLimit)
const systemHeight = computed(() => (systemPromptTokens / windowLimit) * 100)
const inputHeight = computed(() => (currentInputTokens / windowLimit) * 100)
// History 高度展示逻辑:
// 我们希望展示“总高度”,即使超过 100%。
// 父容器会限制显示区域,溢出部分通过视觉暗示。
const historyHeight = computed(() => (historyTokens.value / windowLimit) * 100)
const increaseRound = () => {
if (round.value < maxRound) round.value += 1
}
const decreaseRound = () => {
if (round.value > minRound) round.value -= 1
}
const totalHeight = computed(() => systemHeight.value + historyHeight.value + inputHeight.value)
</script>
<template>
<div class="agent-context-intro">
<div class="header">
<h3>三个关键数字轮次上下文长度缓存命中率</h3>
<p>拖动轮次看看这三个数字是怎么一起变化的</p>
<div class="agent-context-flow">
<!-- 1. 顶部统计栏 -->
<div class="control-panel">
<div class="stat-group">
<div class="stat-item">
<span class="value">{{ round }}</span>
<span class="label">当前轮次</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="value" :class="{ error: isOverflow }">{{ totalTokens }}</span>
<span class="label">Token 占用</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="value">${{ currentCost }}</span>
<span class="label">本轮成本</span>
</div>
</div>
</div>
<div class="round-control">
<button class="round-btn" @click="decreaseRound" :disabled="round === minRound">
-
</button>
<div class="round-text">
当前假设我们已经聊到了
<strong> {{ round }} </strong>拖动右侧滑块看看聊多几轮之后黑板会写满到什么程度背课文本比例会涨到多高
<!-- 2. 可视化区域 -->
<div class="visualization-area">
<!-- 上方预留空间给溢出提示 -->
<div class="overflow-zone">
<transition name="fade">
<div v-if="isOverflow" class="overflow-badge">
<span class="icon">🗑</span>
<span>溢出截断 {{ forgottenRounds }} 轮对话已被遗忘</span>
</div>
<div v-else class="safe-badge">
<span class="icon"></span>
<span>记忆完整</span>
</div>
</transition>
</div>
<!-- 窗口容器 -->
<div class="window-frame">
<div class="limit-line">
<span>Context Window Limit ({{ windowLimit }})</span>
</div>
<!-- 堆叠内容容器 -->
<!-- 使用 flex-direction: column-reverse 让底部对齐 -->
<div class="stack-container">
<!-- System (基座) -->
<div class="block system" :style="{ height: `${systemHeight}%` }">
<span class="block-text">System Prompt ({{ systemPromptTokens }})</span>
</div>
<!-- History (中间) -->
<div class="block history" :style="{ height: `${historyHeight}%` }">
<span class="block-text" v-if="historyHeight > 10">
History ({{ round - 1 }} rounds)
</span>
<!-- 溢出遮罩当溢出时History 的底部实际上是被挤出去 -->
<!-- 但为了可视化简单我们让顶部溢出或者我们让整个 stack 向上移动 -->
<!-- 修正逻辑Context Window 只有那么大内容是先进先出 -->
<!-- 所以 System 永远在History 的旧内容被挤出New 在最上 -->
<!-- 这里的可视化如果不溢出自底向上堆叠 -->
<!-- 如果溢出System 在底New 在顶History 中间部分被挤压/溢出 -->
<!-- 真实的 LLM 是滑动窗口System 通常是 Pinned -->
<!-- 让我们展示总量超过窗口 -->
</div>
<!-- Input (最新) -->
<div class="block input" :style="{ height: `${inputHeight}%` }">
<span class="block-text">New Input</span>
</div>
</div>
<!-- 溢出遮罩层如果 totalHeight > 100%显示一个红色的遮罩在顶部表示这部分虽然生成了但塞不进去/或者旧的被挤走了 -->
<!-- 为了简化我们让 stack-container 的高度允许超过 100%然后 window-frame overflow: hidden -->
<!-- 但这样用户看不到溢出了多少 -->
<!-- 更好的方式window-frame 是视口stack-container 绝对定位 -->
</div>
</div>
<!-- 3. 底部控制 -->
<div class="input-section">
<div class="slider-wrapper">
<span class="slider-hint">拖动滑块增加对话轮次</span>
<input
class="round-slider"
type="range"
:min="minRound"
min="1"
:max="maxRound"
v-model.number="round"
class="custom-slider"
/>
<button class="round-btn" @click="increaseRound" :disabled="round === maxRound">
+
</button>
</div>
<div class="metrics-row">
<div class="metric-card">
<div class="metric-label">聊了几轮</div>
<div class="metric-value"> {{ round }} </div>
<div class="metric-desc">对话轮次</div>
</div>
<div class="metric-card">
<div class="metric-label">记了多少字</div>
<div class="metric-value">{{ contextTokens }}</div>
<div class="metric-desc">大致对应 token </div>
</div>
<div class="metric-card">
<div class="metric-label">背课文本比例</div>
<div class="metric-value">{{ cacheHitRate }}%</div>
<div class="metric-desc">前缀复用比例</div>
</div>
<div class="metric-card">
<div class="metric-label">这轮大概多少钱</div>
<div class="metric-value">${{ currentCost }}</div>
<div class="metric-desc">比不做优化便宜了 {{ savedPercent }}%</div>
<div class="slider-labels">
<span> 1 </span>
<span> {{ maxRound }} </span>
</div>
</div>
<div class="summary-line">
参考基准一轮完全不做优化大约 {{ baseCostPerRound.toFixed(4) }} 美元
在当前轮次下通过复用前缀这轮的成本约为 {{ currentCost }} 美元
<div class="info-box">
<p v-if="!isOverflow">
💡 <strong>一切正常</strong>当前 Token ({{ totalTokens }}) 未超过窗口限制模型能完美回忆起所有对话细节
</p>
<p v-else class="warning-text">
<strong>发生遗忘</strong>Token 总量 ({{ totalTokens }}) 已超过窗口限制 ({{ windowLimit }})
为了放入新对话系统被迫丢弃了最早的 <strong>{{ forgottenRounds }}</strong> 轮历史记录
</p>
</div>
</div>
</div>
</template>
<style scoped>
.agent-context-intro {
.agent-context-flow {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
overflow: hidden;
margin: 1rem 0;
}
/* 1. 顶部统计栏 */
.control-panel {
padding: 1.25rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.stat-group {
display: flex;
justify-content: space-around;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.stat-item .value {
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.header {
margin-bottom: 0.75rem;
.stat-item .value.error {
color: var(--vp-c-red);
}
.header h3 {
margin: 0 0 0.25rem;
font-size: 1rem;
}
.header p {
margin: 0;
font-size: 0.85rem;
.stat-item .label {
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.round-control {
.stat-divider {
width: 1px;
height: 2rem;
background-color: var(--vp-c-divider);
}
/* 2. 可视化区域 */
.visualization-area {
padding: 1rem 2rem;
background-color: var(--vp-c-bg-alt); /* 稍微深一点的背景 */
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.overflow-zone {
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.overflow-badge {
color: var(--vp-c-red);
font-weight: 600;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.round-btn {
padding: 0.2rem 0.6rem;
background: var(--vp-c-red-dimm);
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.safe-badge {
color: var(--vp-c-green);
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.window-frame {
width: 100%;
max-width: 300px; /* 限制宽度,像手机屏幕 */
height: 300px;
border: 2px solid var(--vp-c-divider);
border-top: 2px dashed var(--vp-c-red); /* 顶部虚线表示 Limit */
border-radius: 0 0 8px 8px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.85rem;
position: relative;
display: flex;
flex-direction: column-reverse; /* 底部对齐 */
overflow: visible; /* 允许溢出显示 */
}
.round-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
.limit-line {
position: absolute;
top: -12px;
left: 0;
right: 0;
display: flex;
justify-content: center;
}
.round-text {
flex: 1;
color: var(--vp-c-text-2);
}
.round-slider {
width: 120px;
}
.metrics-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.metric-card {
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
text-align: center;
}
.metric-label {
.limit-line span {
background: var(--vp-c-red);
color: white;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
padding: 0 8px;
border-radius: 10px;
}
.metric-value {
font-size: 1.4rem;
.stack-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column-reverse; /* 让 System 在最底 */
/* 这里不设 overflow: hidden,让它自然溢出,但是我们通过高度控制 */
}
.block {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
border-top: 1px solid rgba(255,255,255,0.1);
}
.block-text {
z-index: 1;
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.block.system {
background-color: #10b981; /* Green */
flex-shrink: 0; /* System 不会被压缩 */
}
.block.history {
background-color: #3b82f6; /* Blue */
/* 溢出逻辑:当高度增加时,history 会向上顶 */
}
.block.input {
background-color: #f59e0b; /* Amber */
flex-shrink: 0;
}
/* 溢出样式处理 */
/* 当总高度超过 100% 时,stack-container 会溢出 window-frame */
/* 我们希望溢出的部分变红或者虚化 */
/* 3. 底部控制 */
.input-section {
padding: 1.25rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 1rem;
}
.slider-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.slider-hint {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.metric-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
.custom-slider {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
}
.summary-line {
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--vp-c-text-2);
padding: 0.6rem 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
@media (max-width: 768px) {
.metrics-row {
grid-template-columns: repeat(2, 1fr);
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.5;
color: var(--vp-c-text-2);
}
.info-box p {
margin: 0;
}
.warning-text {
color: var(--vp-c-red-text);
}
/* 移动端适配 */
@media (max-width: 640px) {
.stat-group {
gap: 0.5rem;
}
.stat-item .value {
font-size: 1.2rem;
}
.window-frame {
height: 250px;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
B+树索引演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('B+树索引演示')
const description = ref('展示数据库中B+树索引的结构和工作原理,理解索引如何加速数据查询')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
数据库演进历程演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('数据库演进历程演示')
const description = ref('展示数据库从层次数据库、网状数据库到关系数据库、NoSQL数据库的演进过程')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
数据库关系演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('数据库关系演示')
const description = ref('展示关系数据库中表与表之间的关系,包括一对一、一对多、多对多关系')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
查询优化演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('查询优化演示')
const description = ref('展示数据库查询优化的原理和方法,包括执行计划分析和索引优化')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
事务ACID特性演示组件占位符 - 待实现具体交互
</el-alert>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('事务ACID特性演示')
const description = ref('通过可视化方式演示事务的原子性、一致性、隔离性和持久性')
</script>
<style scoped>
.demo-container {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More