feat: enhance demo components with consistent styling and info boxes

- Add standardized header and info box components to all demo files
- Improve visual consistency with theme colors and spacing
- Add max-height and overflow-y for better content containment
- Update package.json build script with --force flag
- Add .gitignore entries for REFACTORING files
- Fix table formatting in audio-intro.md
This commit is contained in:
sanbuphy
2026-02-14 12:14:07 +08:00
parent cd2ce9e661
commit ebe2bf6109
70 changed files with 12307 additions and 10445 deletions
@@ -1,50 +1,496 @@
<!--
DocumentationDemo.vue - API 文档演示组件
展示 API 文档的编写规范和最佳实践
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">📚</span>
<span class="title">API 文档最好的 API 文档就是代码本身</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
文档演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="tools-tabs">
<button
v-for="tool in tools"
:key="tool.id"
class="tool-btn"
:class="{ active: selectedTool === tool.id }"
@click="selectedTool = tool.id"
>
<span class="tool-icon">{{ tool.icon }}</span>
<span class="tool-name">{{ tool.name }}</span>
</button>
</div>
<div class="tool-detail" v-if="currentTool">
<div class="tool-header">
<div class="tool-title">{{ currentTool.name }}</div>
<div class="tool-tags">
<span class="tag" :class="tag.class" v-for="tag in currentTool.tags" :key="tag.text">
{{ tag.text }}
</span>
</div>
</div>
<div class="tool-description">
<p>{{ currentTool.description }}</p>
</div>
<div class="feature-section">
<h4>核心特性</h4>
<div class="feature-list">
<div v-for="(feature, idx) in currentTool.features" :key="idx" class="feature-item">
<span class="feature-icon"></span>
<span class="feature-text">{{ feature }}</span>
</div>
</div>
</div>
<div class="example-section">
<h4>文档示例OpenAPI 3.0</h4>
<div class="code-block">
<pre><code>{{ currentTool.example }}</code></pre>
</div>
</div>
<div class="tools-section">
<h4>🔧 推荐工具</h4>
<div class="tools-grid">
<div v-for="(rec, idx) in currentTool.recommendations" :key="idx" class="tool-card">
<div class="rec-name">{{ rec.name }}</div>
<div class="rec-desc">{{ rec.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('API文档演示')
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
const tools = [
{
id: 'openapi',
name: 'OpenAPI 规范',
icon: '📋',
tags: [
{ text: '行业标准', class: 'primary' },
{ text: '语言无关', class: 'secondary' }
],
description: 'OpenAPI Specification(原 Swagger)是描述 REST API 的标准格式,可以被工具解析生成交互式文档、客户端 SDK、服务器存根等。',
features: [
'标准化的 YAML/JSON 格式描述 API',
'支持路径、参数、响应模型、认证等完整定义',
'生态系统丰富,支持 100+ 工具',
'可以生成交互式文档(Swagger UI',
'可以从代码注释自动生成',
'支持 API 版本控制和演进'
],
example: `openapi: 3.0.0
info:
title: 用户服务 API
version: 1.0.0
description: 提供用户管理相关接口
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email`,
recommendations: [
{ name: 'Swagger UI', description: '最流行的交互式文档界面' },
{ name: 'Redoc', description: '美观的现代文档生成器' },
{ name: 'Stoplight', description: '可视化的 API 设计平台' }
]
},
{
id: 'swagger',
name: 'Swagger 工具链',
icon: '🛠️',
tags: [
{ text: '工具集', class: 'success' },
{ text: '自动化', class: 'info' }
],
description: 'Swagger 是一套围绕 OpenAPI 规范构建的工具,包括编辑器、UI、代码生成器等,帮助开发者快速构建和使用 API。',
features: [
'Swagger Editor:在线编写和验证 OpenAPI 文档',
'Swagger UI:自动生成交互式文档',
'Swagger Codegen:根据文档生成客户端 SDK',
'支持主流编程语言和框架',
'集成到 CI/CD 流程',
'自动保持文档与代码同步'
],
example: `# Swagger Editor 示例配置
swagger: '2.0'
info:
title: 示例 API
version: '1.0.0'
host: api.example.com
basePath: /v1
schemes:
- https
paths:
/users:
get:
tags:
- Users
summary: 获取所有用户
produces:
- application/json
responses:
200:
description: 成功
schema:
type: object
properties:
code:
type: integer
data:
type: array`,
recommendations: [
{ name: 'Swagger Editor', description: '在线编辑器,实时预览' },
{ name: 'Swagger Codegen', description: '生成 40+ 种语言的客户端' },
{ name: 'Postman', description: '导入 OpenAPI 进行测试' }
]
},
{
id: 'best-practices',
name: '文档最佳实践',
icon: '⭐',
tags: [
{ text: '经验', class: 'warning' },
{ text: '规范', class: 'secondary' }
],
description: '好的 API 文档应该像用户手册一样清晰,让开发者不问问题就能完成集成。',
features: [
'每个接口都有完整的请求示例',
'提供多种语言的代码示例(curl、JavaScript、Python',
'错误码文档化,附带解决方案',
'提供沙箱环境或测试工具',
'包含认证流程和获取 Token 的方法',
'实时更新,与代码保持一致',
'版本变更日志和迁移指南'
],
example: `# 完整的接口文档示例
## 获取用户信息
**请求示例:**
\`\`\`bash
curl -X GET \\
https://api.example.com/v1/users/123 \\
-H "Authorization: Bearer YOUR_TOKEN"
\`\`\`
**成功响应:**
\`\`\`json
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
}
}
\`\`\`
**错误响应:**
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| 10010 | 用户不存在 | 检查 user_id 是否正确 |
| 10018 | Token 已过期 | 重新调用登录接口 |
**在线测试:**
[🚀 在 API Explorer 中测试](https://api.example.com/docs)`,
recommendations: [
{ name: 'API Blueprint', description: ' Markdown 风格的 API 文档' },
{ name: 'Docusaurus', description: ' Facebook 开源的文档平台' },
{ name: 'GitBook', description: '美观的文档托管平台' }
]
}
]
const selectedTool = ref('openapi')
const currentTool = computed(() =>
tools.find(t => t.id === selectedTool.value)
)
</script>
<style scoped>
.demo-container {
.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, #f59e0b 0%, #d97706 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.tools-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tool-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
}
.tool-btn:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.tool-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tool-icon {
font-size: 18px;
}
.tool-name {
font-weight: 600;
font-size: 14px;
}
.tool-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
overflow: hidden;
}
.tool-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.tool-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
.tool-tags {
display: flex;
gap: 8px;
}
.demo-content {
.tag {
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.tag.primary {
background: #dbeafe;
color: #1d4ed8;
}
.tag.secondary {
background: #e0e7ff;
color: #4338ca;
}
.tag.success {
background: #dcfce7;
color: #16a34a;
}
.tag.info {
background: #ccfbf1;
color: #0f766e;
}
.tag.warning {
background: #fef3c7;
color: #d97706;
}
.tool-description {
padding: 16px;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
border-bottom: 1px solid var(--vp-c-divider);
}
.feature-section, .example-section, .tools-section {
padding: 16px;
}
.feature-section h4, .example-section h4, .tools-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.feature-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.feature-item {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
gap: 8px;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.feature-icon {
color: #22c55e;
font-weight: 700;
font-size: 16px;
flex-shrink: 0;
}
.feature-text {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: 16px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-block code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.tools-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.tool-card {
padding: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
}
.rec-name {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.rec-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
@media (max-width: 768px) {
.tools-tabs {
flex-direction: column;
}
.feature-list {
grid-template-columns: 1fr;
}
.tools-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,97 +1,379 @@
<!--
ErrorHandlingDemo.vue - 错误处理演示组件
展示错误处理的正确和错误示例对比
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>错误处理演示</h4>
<p class="hint">展示RESTful API中的错误处理机制</p>
<div class="demo">
<div class="header">
<span class="icon">🚨</span>
<span class="title">错误处理优雅地"拒绝"</span>
</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 class="content">
<div class="comparison-tabs">
<button
class="tab-btn bad"
:class="{ active: selectedTab === 'bad' }"
@click="selectedTab = 'bad'"
>
错误示范
</button>
<button
class="tab-btn good"
:class="{ active: selectedTab === 'good' }"
@click="selectedTab = 'good'"
>
正确示范
</button>
</div>
<!-- 错误示范 -->
<div v-if="selectedTab === 'bad'" class="comparison bad">
<div class="response-preview">
<div class="status-line bad">
<span>HTTP/1.1</span>
<span class="code">200 OK</span>
</div>
<div class="response-body">
<pre><code>{
"error": "出错了"
}</code></pre>
</div>
</div>
<div class="error-item">
<span class="code">401</span>
<span class="name">Unauthorized</span>
<span class="desc">未授权访问</span>
<div class="problems">
<h4>问题分析</h4>
<ul>
<li>
<span class="icon"></span>
HTTP 状态码说"成功"但业务说"出错" - 前后端状态不一致
</li>
<li>
<span class="icon"></span>
错误信息太笼统无法定位问题
</li>
<li>
<span class="icon"></span>
没有错误代码难以程序化判断
</li>
<li>
<span class="icon"></span>
浏览器和 CDN 会缓存这个"成功的"响应
</li>
</ul>
</div>
<div class="error-item">
<span class="code">404</span>
<span class="name">Not Found</span>
<span class="desc">资源不存在</span>
</div>
<!-- 正确示范 -->
<div v-if="selectedTab === 'good'" class="comparison good">
<div class="response-preview">
<div class="status-line">
<span>HTTP/1.1</span>
<span class="code">422 Unprocessable Entity</span>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(goodResponse, null, 2) }}</code></pre>
</div>
</div>
<div class="error-item">
<span class="code">500</span>
<span class="name">Server Error</span>
<span class="desc">服务器内部错误</span>
<div class="highlights">
<h4>正确做法</h4>
<ul>
<li>
<span class="icon"></span>
<strong>正确的 HTTP 状态码</strong>: 422 表示语义错误
</li>
<li>
<span class="icon"></span>
<strong>业务错误码</strong>: `code: 20003` 可用于程序判断
</li>
<li>
<span class="icon"></span>
<strong>详细错误信息</strong>: `errors` 数组包含具体字段和原因
</li>
<li>
<span class="icon"></span>
<strong>可追踪性</strong>: `request_id` 用于日志查询
</li>
<li>
<span class="icon"></span>
<strong>帮助链接</strong>: `help_url` 引导用户查看文档
</li>
</ul>
</div>
<div class="error-codes">
<h4>错误码体系</h4>
<div class="code-list">
<div v-for="item in errorCodeItems" :key="item.code" class="code-item">
<span class="code-badge">{{ item.code }}</span>
<span class="code-desc">{{ item.desc }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedTab = ref('bad')
const goodResponse = {
code: 20003,
message: '密码强度不足',
errors: [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
}
],
request_id: 'req-550e8400-e29b-41d4-a716-44665544000',
timestamp: '2024-01-15T09:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
}
const errorCodeItems = [
{ code: '1XXYY', desc: '通用错误(第1位固定为1' },
{ code: '10001', desc: '参数错误' },
{ code: '10010', desc: '用户不存在' },
{ code: '10018', desc: 'Token 已过期' },
{ code: '10021', desc: '权限不足' },
{ code: '20003', desc: '密码强度不足' },
{ code: '20014', desc: '余额不足' }
]
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.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 {
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
display: flex;
flex-direction: column;
gap: 16px;
}
.error-types {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-item {
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.comparison-tabs {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
gap: 12px;
margin-bottom: 20px;
}
.tab-btn {
flex: 1;
padding: 12px;
border: 2px solid;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn.bad {
border-color: #ef4444;
background: var(--vp-c-bg);
color: #ef4444;
}
.tab-btn.bad:hover {
background: #fef2f2;
}
.tab-btn.bad.active {
background: #ef4444;
color: white;
}
.tab-btn.good {
border-color: #22c55e;
background: var(--vp-c-bg);
color: #22c55e;
}
.tab-btn.good:hover {
background: #f0fdf4;
}
.tab-btn.good.active {
background: #22c55e;
color: white;
}
.comparison {
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid #f56c6c;
overflow: hidden;
}
.error-item .code {
.response-preview {
margin-bottom: 20px;
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
font-family: monospace;
font-weight: 600;
color: #f56c6c;
font-size: 16px;
min-width: 50px;
font-size: 13px;
}
.error-item .name {
font-weight: 500;
.status-line.bad .code {
color: #ef4444;
font-weight: 700;
}
.status-line:not(.bad) .code {
color: #d97706;
font-weight: 700;
}
.response-body {
padding: 16px;
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
min-width: 120px;
}
.error-item .desc {
color: var(--vp-c-text-2);
.problems, .highlights {
padding: 16px;
}
.problems h4, .highlights h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.problems ul, .highlights ul {
margin: 0;
padding-left: 0;
list-style: none;
}
.problems li, .highlights li {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
border-radius: 6px;
line-height: 1.6;
font-size: 13px;
}
.problems li {
background: #fef2f2;
color: #991b1b;
}
.highlights li {
background: #f0fdf4;
color: #166534;
}
.problems li .icon, .highlights li .icon {
font-size: 16px;
flex-shrink: 0;
}
.problems li strong, .highlights li strong {
font-weight: 600;
}
.error-codes {
padding: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.error-codes h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.code-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.code-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.code-badge {
font-family: monospace;
font-size: 12px;
font-weight: 700;
padding: 4px 8px;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
min-width: 70px;
text-align: center;
}
.code-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.comparison-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
}
</style>
@@ -1,50 +1,397 @@
<!--
ResponseStructureDemo.vue - HTTP 响应结构演示组件
展示标准化 API 响应结构和分页响应
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">📦</span>
<span class="title">HTTP 响应结构标准化的数据契约</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
响应结构演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="response-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: selectedTab === tab.id }"
@click="selectedTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="response-detail">
<div class="response-header">
<div class="status-line">
<span class="http-version">HTTP/1.1</span>
<span class="status-code" :class="getStatusClass(currentResponse.status)">{{ currentResponse.status }}</span>
<span class="status-text">{{ currentResponse.statusText }}</span>
</div>
</div>
<div class="response-headers">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">X-Request-ID:</span>
<span class="header-value">req-550e8400-e29b-41d4</span>
</div>
<div class="header-item">
<span class="header-key">X-Response-Time:</span>
<span class="header-value">45ms</span>
</div>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(currentResponse.body, null, 2) }}</code></pre>
</div>
<div class="field-descriptions">
<h4>字段说明</h4>
<div class="field-list">
<div v-for="field in currentResponse.fields" :key="field.name" class="field-item">
<div class="field-name">
<code>{{ field.name }}</code>
<span class="field-type">{{ field.type }}</span>
</div>
<div class="field-desc">{{ field.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('HTTP响应结构演示')
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应')
const tabs = [
{ id: 'success', name: '成功响应' },
{ id: 'pagination', name: '分页响应' },
{ id: 'error', name: '错误响应' }
]
const responses = {
success: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
created_at: '2024-01-15T10:30:00.000Z'
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'code', type: 'integer', description: '业务状态码,0 表示成功' },
{ name: 'message', type: 'string', description: '状态描述,成功时为 "success"' },
{ name: 'data', type: 'object', description: '业务数据,成功时返回具体数据' },
{ name: 'request_id', type: 'string', description: '请求唯一标识,用于问题追踪' },
{ name: 'timestamp', type: 'string', description: '响应时间戳,ISO 8601 格式' }
]
},
pagination: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
list: [
{ id: 1, name: '商品A', price: 100 },
{ id: 2, name: '商品B', price: 200 }
],
pagination: {
page: 1,
page_size: 20,
total: 156,
total_pages: 8,
has_next: true,
has_prev: false
}
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'list', type: 'array', description: '数据列表' },
{ name: 'pagination', type: 'object', description: '分页信息对象' },
{ name: 'page', type: 'integer', description: '当前页码' },
{ name: 'page_size', type: 'integer', description: '每页数量' },
{ name: 'total', type: 'integer', description: '总记录数' },
{ name: 'total_pages', type: 'integer', description: '总页数' },
{ name: 'has_next', type: 'boolean', description: '是否有下一页' },
{ name: 'has_prev', type: 'boolean', description: '是否有上一页' }
]
},
error: {
status: 422,
statusText: 'Unprocessable Entity',
body: {
code: 20003,
message: '密码强度不足',
errors: [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
}
],
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
},
fields: [
{ name: 'code', type: 'integer', description: '错误码,非 0 表示失败' },
{ name: 'message', type: 'string', description: '错误描述,供用户阅读' },
{ name: 'errors', type: 'array', description: '详细错误信息数组(可选)' },
{ name: 'help_url', type: 'string', description: '错误文档链接(可选)' }
]
}
}
const selectedTab = ref('success')
const currentResponse = computed(() => responses[selectedTab.value])
function getStatusClass(status) {
const prefix = Math.floor(status / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
}
}
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.response-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.tab-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.hint {
margin: 0;
font-size: 14px;
.tab-btn:hover {
transform: translateY(-1px);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.response-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.response-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
}
.http-version {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-2);
}
.demo-content {
.status-code {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 13px;
font-family: monospace;
}
.status-code.success {
background: #dcfce7;
color: #16a34a;
}
.status-code.client-error {
background: #fef3c7;
color: #d97706;
}
.status-text {
font-size: 13px;
color: var(--vp-c-text-2);
}
.response-headers {
padding: 12px 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 12px;
}
.header-key {
font-family: monospace;
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 120px;
}
.header-value {
font-family: monospace;
color: var(--vp-c-text-1);
}
.response-body {
padding: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.field-descriptions {
padding: 16px;
}
.field-descriptions h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.field-list {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.field-item {
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.field-name {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.field-name code {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-brand);
font-weight: 600;
}
.field-type {
font-size: 11px;
padding: 2px 6px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
border-radius: 4px;
font-family: monospace;
}
.field-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 640px) {
.response-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
.header-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -1,50 +1,413 @@
<!--
VersioningStrategyDemo.vue - API 版本控制策略演示
展示 4 种版本控制策略的对比
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">🔢</span>
<span class="title">API 版本控制向后兼容的艺术</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
版本策略演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="strategies">
<div
v-for="strategy in strategies"
:key="strategy.id"
class="strategy-card"
:class="{ active: selectedStrategy === strategy.id }"
@click="selectedStrategy = strategy.id"
>
<div class="strategy-header">
<div class="strategy-name">{{ strategy.name }}</div>
<div class="strategy-stars">
<span v-for="n in strategy.stars" :key="n" class="star"></span>
</div>
</div>
<div class="strategy-example">{{ strategy.example }}</div>
</div>
</div>
<div class="strategy-detail" v-if="currentStrategy">
<div class="detail-header">
<div class="detail-title">{{ currentStrategy.name }}</div>
<div class="detail-recommendation" :class="currentStrategy.level">
{{ currentStrategy.level === 'high' ? '强烈推荐' : currentStrategy.level === 'medium' ? '可以使用' : '不推荐' }}
</div>
</div>
<div class="detail-sections">
<div class="detail-section">
<h4> 优点</h4>
<ul>
<li v-for="(pro, idx) in currentStrategy.pros" :key="idx">{{ pro }}</li>
</ul>
</div>
<div class="detail-section">
<h4> 缺点</h4>
<ul>
<li v-for="(con, idx) in currentStrategy.cons" :key="idx">{{ con }}</li>
</ul>
</div>
</div>
<div class="detail-section example">
<h4>💻 实现示例</h4>
<div class="code-box">
<div class="code-header">Request</div>
<pre><code>{{ currentStrategy.codeExample.request }}</code></pre>
<div class="code-header">Response Headers</div>
<pre><code>{{ currentStrategy.codeExample.response }}</code></pre>
</div>
</div>
<div class="detail-section tips">
<h4>💡 最佳实践</h4>
<ul>
<li v-for="(tip, idx) in currentStrategy.tips" :key="idx">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('版本策略演示')
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
const strategies = [
{
id: 'url-path',
name: 'URL Path 版本',
example: '/v1/users',
stars: 4,
level: 'high',
pros: [
'最直观,一目了然看到版本号',
'易于缓存和控制权限',
'文档清晰,社区主流做法',
'支持不同版本的并行部署'
],
cons: [
'URL 会变化,不符合 REST 资源唯一性',
'需要配置路由规则'
],
codeExample: {
request: `GET /v1/users HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'版本号放在路径开头:`/v1/users`',
'使用语义化版本号(Semantic Versioning',
'废弃版本返回 Sunset 头部',
'客户端升级提示可通过响应头提示'
]
},
{
id: 'header',
name: 'Header 版本',
example: 'API-Version: v1',
stars: 2,
level: 'medium',
pros: [
'URL 保持简洁不变',
'版本控制不影响路由'
],
cons: [
'不直观,需要在工具里配置 Header',
'缓存策略复杂',
'文档不够清晰',
'调试不便'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
API-Version: v1`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'使用自定义 Header`API-Version` 或 `Accept`',
'需在 API Gateway 中统一处理',
'适合内部系统或对 API 规范要求高的场景'
]
},
{
id: 'content-negotiation',
name: '内容协商',
example: 'Accept: application/vnd.api.v1+json',
stars: 2,
level: 'medium',
pros: [
'符合 HTTP 标准',
'URL 完全不变'
],
cons: [
'复杂,理解成本高',
'开发者容易用错',
'缓存和代理支持不佳'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.api.v1+json`,
response: `HTTP/1.1 200 OK
Content-Type: application/vnd.api.v1+json`
},
tips: [
'使用 Vendor MIME 类型:`application/vnd.{company}.{resource}.v{version}+json`',
'需要 API Gateway 或框架支持内容协商',
'GitHub API 使用此策略'
]
},
{
id: 'query-param',
name: 'Query 参数',
example: '/users?version=v1',
stars: 1,
level: 'low',
pros: [
'实现简单'
],
cons: [
'不专业,容易忽视',
'缓存麻烦(不同参数视为不同资源)',
'URL 混乱'
],
codeExample: {
request: `GET /users?version=v1 HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json`
},
tips: [
'仅用于快速原型或内部工具',
'生产环境不推荐使用'
]
}
]
const selectedStrategy = ref('url-path')
const currentStrategy = computed(() =>
strategies.find(s => s.id === selectedStrategy.value)
)
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
margin-bottom: 20px;
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.strategies {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.strategy-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.strategy-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.strategy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.strategy-name {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
.strategy-stars {
display: flex;
gap: 2px;
}
.demo-content {
.star {
font-size: 12px;
}
.strategy-example {
font-family: monospace;
font-size: 12px;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
padding: 6px 10px;
border-radius: 4px;
margin-top: 8px;
}
.strategy-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.detail-header {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.detail-recommendation {
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.detail-recommendation.high {
background: #dcfce7;
color: #16a34a;
}
.detail-recommendation.medium {
background: #fef3c7;
color: #d97706;
}
.detail-recommendation.low {
background: #fee2e2;
color: #dc2626;
}
.detail-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
}
.detail-section {
padding: 16px;
}
.detail-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.detail-section ul {
margin: 0;
padding-left: 20px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 6px 0;
}
.detail-section.example {
grid-column: 1 / -1;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.code-box {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-header {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-box pre {
margin: 0;
padding: 12px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-box code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.detail-section.tips {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
@media (max-width: 768px) {
.strategies {
grid-template-columns: 1fr;
}
.detail-sections {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="architecture-comparison-demo">
<div class="demo-header">
<h4>🏗 架构演进对比</h4>
<p>四个时代的核心架构特征对比</p>
<span class="icon">🏗</span>
<span class="title">架构演进对比</span>
<span class="subtitle">四个时代的核心架构特征</span>
</div>
<div class="comparison-grid">
@@ -50,6 +51,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>架构演进是为了解决上一个时代的痛点,但也带来了新的复杂度
</div>
</div>
</template>
@@ -108,24 +114,35 @@ const currentEra = computed(() => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.comparison-grid {
@@ -152,8 +169,7 @@ const currentEra = computed(() => {
.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);
background: var(--vp-c-brand-soft);
}
.era-icon {
@@ -271,6 +287,25 @@ const currentEra = computed(() => {
color: var(--vp-c-text-2);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: repeat(2, 1fr);
@@ -1,8 +1,9 @@
<template>
<div class="container-docker-demo">
<div class="demo-header">
<h4>🐳 Docker 容器化演示</h4>
<p>理解容器如何让应用"一次打包,到处运行"</p>
<span class="icon">🐳</span>
<span class="title">Docker 容器化演示</span>
<span class="subtitle">理解容器如何让应用"一次打包,到处运行"</span>
</div>
<div class="docker-visualization">
@@ -46,6 +47,11 @@
<div class="benefit-desc">{{ benefit.desc }}</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>容器化让应用"一次构建,到处运行"解决了环境一致性和快速部署的问题
</div>
</div>
</template>
@@ -69,24 +75,35 @@ const benefits = [
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.docker-visualization {
@@ -268,4 +285,23 @@ const benefits = [
grid-template-columns: 1fr;
}
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="evolution-intro-demo">
<div class="intro-header">
<h3>后端架构进化之旅</h3>
<p>用一个餐厅的成长历程理解后端架构的 30 年变迁</p>
<div class="demo-header">
<span class="icon">🏗</span>
<span class="title">后端架构进化之旅</span>
<span class="subtitle">用餐厅比喻理解 30 年架构演进</span>
</div>
<div class="timeline-cards">
@@ -20,7 +21,7 @@
</div>
</div>
<div class="stage-detail">
<div class="stage-detail" v-if="currentStage !== null">
<Transition name="fade" mode="out-in">
<div :key="currentStage" class="detail-panel">
<div class="detail-header">
@@ -46,6 +47,11 @@
</div>
</Transition>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>架构演进是为了解决上一个时代的痛点但也带来了新的复杂度没有最好的架构只有最适合的架构
</div>
</div>
</template>
@@ -116,84 +122,95 @@ const stages = [
<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;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.intro-header {
text-align: center;
margin-bottom: 32px;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.intro-header h3 {
font-size: 24px;
font-weight: 700;
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 1.25rem;
}
.intro-header p {
font-size: 14px;
opacity: 0.9;
margin: 0;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.timeline-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.stage-card {
background: rgba(255, 255, 255, 0.1);
background: var(--vp-c-bg);
border: 2px solid transparent;
border-radius: 12px;
padding: 16px 12px;
border-radius: 8px;
padding: 0.75rem 0.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.3s;
}
.stage-card:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--vp-c-brand);
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);
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stage-era {
font-size: 11px;
opacity: 0.7;
margin-bottom: 4px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.25rem;
}
.stage-icon {
font-size: 32px;
margin-bottom: 8px;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.stage-name {
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 2px;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.stage-arch {
font-size: 11px;
opacity: 0.7;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.stage-detail {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.detail-panel {
@@ -208,56 +225,76 @@ const stages = [
.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);
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 32px;
font-size: 1.5rem;
}
.detail-header h4 {
font-size: 20px;
font-size: 1rem;
font-weight: 600;
margin: 0;
color: var(--vp-c-text-1);
}
.detail-content {
display: grid;
gap: 16px;
gap: 0.75rem;
}
.detail-section h5 {
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
margin: 0 0 8px 0;
color: #ffd700;
margin: 0 0 0.5rem 0;
color: var(--vp-c-brand);
}
.detail-section p {
font-size: 13px;
font-size: 0.8rem;
line-height: 1.6;
margin: 0;
opacity: 0.9;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-2);
}
.detail-section ul {
margin: 0;
padding-left: 18px;
padding-left: 1rem;
}
.detail-section li {
font-size: 13px;
font-size: 0.8rem;
line-height: 1.6;
margin-bottom: 4px;
opacity: 0.9;
margin-bottom: 0.25rem;
color: var(--vp-c-text-2);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s ease;
transition: all 0.3s ease;
}
.fade-enter-from {
@@ -275,8 +312,8 @@ const stages = [
grid-template-columns: repeat(2, 1fr);
}
.stage-detail {
padding: 16px;
.detail-content {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="monolith-demo">
<div class="demo-header">
<h4>🏢 单体架构演示</h4>
<p>观察单体应用如何处理请求以及模块间的依赖关系</p>
<span class="icon">🏢</span>
<span class="title">单体架构演示</span>
<span class="subtitle">观察单体应用如何处理请求</span>
</div>
<div class="monolith-diagram">
@@ -47,13 +48,9 @@
<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 class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>所有模块在同一个进程中运行内存共享但一个模块崩溃可能导致整个进程挂掉雪崩效应
</div>
</div>
</template>
@@ -136,24 +133,35 @@ const reset = () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.monolith-diagram {
@@ -378,4 +386,23 @@ const reset = () => {
flex-wrap: wrap;
}
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="physical-server-demo">
<div class="demo-header">
<h4>🖥 物理服务器时代演示</h4>
<p>点击"发送请求"观察早期 CGI 服务器的处理瓶颈</p>
<span class="icon">🖥</span>
<span class="title">物理服务器时代演示</span>
<span class="subtitle">观察早期 CGI 服务器的处理瓶颈</span>
</div>
<div class="demo-stage">
@@ -85,27 +86,9 @@
</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 class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>进程级隔离带来了稳定性但也带来了巨大的性能开销
</div>
</div>
</template>
@@ -192,24 +175,35 @@ const sendRequest = async () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-stage {
@@ -472,4 +466,23 @@ const sendRequest = async () => {
height: 3px;
}
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="tech-stack-timeline-demo">
<div class="demo-header">
<h4>📚 技术栈演进时间线</h4>
<p>每个时代的主流技术栈</p>
<span class="icon">📚</span>
<span class="title">技术栈演进时间线</span>
<span class="subtitle">每个时代的主流技术栈</span>
</div>
<div class="timeline">
@@ -106,6 +107,8 @@ const eras = [
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
@@ -247,4 +250,23 @@ const eras = [
grid-template-columns: 1fr;
}
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -408,14 +408,15 @@ onUnmounted(() => {
<style scoped>
.animation-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.playback-controls {
@@ -426,13 +427,16 @@ onUnmounted(() => {
.play-btn,
.reset-btn {
padding: 10px 20px;
padding: 0.625rem 1.25rem;
border: none;
border-radius: 6px;
font-size: 14px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.play-btn {
@@ -442,7 +446,8 @@ onUnmounted(() => {
.play-btn:hover {
background: #27ae60;
transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.4);
}
.reset-btn {
@@ -452,28 +457,35 @@ onUnmounted(() => {
.reset-btn:hover {
background: #7f8c8d;
transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(149, 165, 166, 0.4);
}
.animation-selector {
margin-bottom: 15px;
margin-bottom: 1.25rem;
}
.animation-selector label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.animation-selector select {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
background: white;
padding: 0.5rem 0.75rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 0.875rem;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.animation-selector select:hover {
border-color: var(--vp-c-brand);
}
.parameters {
@@ -531,30 +543,35 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -562,54 +579,60 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -13,85 +13,89 @@
-->
<template>
<div class="canvas-basics-demo">
<div class="control-panel">
<div class="shape-selector">
<label>Shape / 形状</label>
<div class="button-group">
<button
v-for="shape in shapes"
:key="shape.value"
:class="{ active: currentShape === shape.value }"
@click="currentShape = shape.value"
>
{{ shape.label }}
</button>
</div>
</div>
<div class="parameters">
<div class="param-row">
<label>Fill Color / 填充颜色</label>
<input type="color" v-model="fillColor" />
</div>
<div class="param-row">
<label>Stroke Color / 描边颜色</label>
<input type="color" v-model="strokeColor" />
</div>
<div class="param-row">
<label>Stroke Width / 描边宽度: {{ strokeWidth }}px</label>
<input type="range" v-model.number="strokeWidth" min="1" max="20" />
</div>
<div class="param-row" v-if="currentShape === 'rect'">
<label>Size / 大小: {{ rectSize }}px</label>
<input type="range" v-model.number="rectSize" min="20" max="200" />
</div>
<div class="param-row" v-if="currentShape === 'circle'">
<label>Radius / 半径: {{ circleRadius }}px</label>
<input
type="range"
v-model.number="circleRadius"
min="10"
max="150"
/>
</div>
<div class="param-row" v-if="currentShape === 'line'">
<label>Line Length / 线条长度: {{ lineLength }}px</label>
<input type="range" v-model.number="lineLength" min="50" max="300" />
</div>
</div>
<button class="draw-btn" @click="draw">
<span class="icon">🎨</span>
Draw / 绘制
</button>
<button class="clear-btn" @click="clearCanvas">
<span class="icon">🗑</span>
Clear / 清除
</button>
<div class="demo-header">
<span class="icon">🎨</span>
<span class="title">Canvas 基础</span>
<span class="subtitle">用代码画图通俗说编程画板</span>
</div>
<div class="canvas-container">
<canvas ref="canvasRef" width="600" height="400"></canvas>
</div>
<div class="demo-content">
<div class="controls">
<div class="shape-selector">
<label>Shape / 形状</label>
<div class="button-group">
<button
v-for="shape in shapes"
:key="shape.value"
:class="{ active: currentShape === shape.value }"
@click="currentShape = shape.value"
>
{{ shape.label }}
</button>
</div>
</div>
<div class="code-display">
<h4>Code / 代码</h4>
<pre><code>{{ currentCode }}</code></pre>
<div class="parameters">
<div class="param-row">
<label>Fill Color / 填充颜色</label>
<input type="color" v-model="fillColor" />
</div>
<div class="param-row">
<label>Stroke Color / 描边颜色</label>
<input type="color" v-model="strokeColor" />
</div>
<div class="param-row">
<label>Stroke Width / 描边宽度: {{ strokeWidth }}px</label>
<input type="range" v-model.number="strokeWidth" min="1" max="20" />
</div>
<div class="param-row" v-if="currentShape === 'rect'">
<label>Size / 大小: {{ rectSize }}px</label>
<input type="range" v-model.number="rectSize" min="20" max="200" />
</div>
<div class="param-row" v-if="currentShape === 'circle'">
<label>Radius / 半径: {{ circleRadius }}px</label>
<input
type="range"
v-model.number="circleRadius"
min="10"
max="150"
/>
</div>
<div class="param-row" v-if="currentShape === 'line'">
<label>Line Length / 线条长度: {{ lineLength }}px</label>
<input type="range" v-model.number="lineLength" min="50" max="300" />
</div>
</div>
<button class="draw-btn" @click="draw">
<span class="icon">🎨</span>
Draw / 绘制
</button>
<button class="clear-btn" @click="clearCanvas">
<span class="icon">🗑</span>
Clear / 清除
</button>
</div>
<div class="canvas-container">
<canvas ref="canvasRef" width="600" height="400"></canvas>
</div>
<div class="code-display">
<h4>Code / 代码</h4>
<pre><code>{{ currentCode }}</code></pre>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>提示</strong>
Canvas
是一个位图画布所有绘制都是像素操作绘制后无法修改已有内容只能覆盖或清除重绘
</p>
<span class="icon">💡</span>
<strong>核心思想</strong>Canvas 是一个位图画布所有绘制都是像素操作绘制后无法修改已有内容只能覆盖或清除重绘
</div>
</div>
</template>
@@ -101,8 +105,8 @@ import { ref, computed, watch, onMounted } from 'vue'
const canvasRef = ref(null)
const currentShape = ref('rect')
const fillColor = ref('#3498db')
const strokeColor = ref('#2c3e50')
const fillColor = ref('#3b82f6')
const strokeColor = ref('#1e293b')
const strokeWidth = ref(2)
const rectSize = ref(100)
const circleRadius = ref(50)
@@ -231,145 +235,193 @@ onMounted(() => {
<style scoped>
.canvas-basics-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.5rem;
}
.demo-header .title {
font-weight: 700;
font-size: 1.125rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin-left: 0.75rem;
padding: 0.25rem 0.75rem;
background: var(--vp-c-brand);
color: white;
border-radius: 20px;
font-weight: 500;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
margin-bottom: 1rem;
}
.shape-selector {
margin-bottom: 15px;
margin-bottom: 1.25rem;
}
.shape-selector label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
margin-bottom: 0.625rem;
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.button-group {
display: flex;
gap: 8px;
gap: 0.625rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.625rem 1.25rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 15px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.param-row {
display: flex;
flex-direction: column;
gap: 6px;
gap: 0.25rem;
}
.param-row label {
font-size: 13px;
font-size: 0.75rem;
font-weight: 500;
color: #555;
color: var(--vp-c-text-1);
}
.param-row input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
}
.param-row input[type='color'] {
width: 100%;
height: 36px;
border: 1px solid #ddd;
height: 32px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
}
.draw-btn,
.clear-btn {
padding: 10px 20px;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
margin-right: 10px;
margin-right: 0.5rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.draw-btn {
background: #3498db;
background: var(--vp-c-brand);
color: white;
}
.draw-btn:hover {
background: #2980b9;
transform: translateY(-1px);
opacity: 0.9;
}
.clear-btn {
background: #e74c3c;
background: var(--vp-c-danger);
color: white;
}
.clear-btn:hover {
background: #c0392b;
transform: translateY(-1px);
opacity: 0.9;
}
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
background: white;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
max-width: 100%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -377,31 +429,39 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 1rem 1.25rem;
border-radius: 12px;
font-size: 0.875rem;
color: #92400e;
border-left: 4px solid #f59e0b;
display: flex;
gap: 0.5rem;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.625rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
.info-box strong {
color: #78350f;
}
</style>
@@ -272,117 +272,140 @@ onMounted(() => {
<style scoped>
.coordinate-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.toggle-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 15px;
gap: 1rem;
margin-bottom: 1.25rem;
}
.toggle-option {
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
cursor: pointer;
font-size: 14px;
font-size: 0.875rem;
padding: 0.5rem 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
transition: all 0.2s;
}
.toggle-option:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.toggle-option input[type='checkbox'] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--vp-c-brand);
}
.info-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
padding: 12px;
background: white;
border-radius: 6px;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-size: 0.875rem;
}
.info-item .label {
font-weight: 600;
color: #555;
color: var(--vp-c-text-2);
}
.info-item .value {
font-family: 'Courier New', monospace;
color: #2c3e50;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-weight: 600;
}
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -390,31 +413,33 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -491,14 +491,15 @@ onMounted(() => {
<style scoped>
.event-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.mode-selector {
@@ -514,53 +515,60 @@ onMounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.instructions {
margin-bottom: 15px;
padding: 12px;
background: white;
border-radius: 6px;
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.instructions h4 {
margin: 0 0 8px 0;
color: #2c3e50;
font-size: 14px;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.instructions ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.instructions li {
margin-bottom: 6px;
color: #555;
font-size: 13px;
margin-bottom: 0.375rem;
color: var(--vp-c-text-2);
font-size: 0.813rem;
line-height: 1.5;
}
.event-log {
@@ -635,36 +643,41 @@ onMounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
outline: none;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
canvas:focus {
border-color: #3498db;
border-color: var(--vp-c-brand);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -672,32 +685,36 @@ canvas:focus {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -378,14 +378,15 @@ onUnmounted(() => {
<style scoped>
.particle-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.effect-selector {
@@ -401,29 +402,33 @@ onUnmounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
@@ -499,31 +504,36 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -531,54 +541,60 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -553,14 +553,15 @@ onUnmounted(() => {
<style scoped>
.performance-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.test-selector {
@@ -576,29 +577,33 @@ onUnmounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
@@ -721,28 +726,34 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.comparison {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.comparison h4 {
margin: 0 0 15px 0;
color: #2c3e50;
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.comparison-table {
@@ -756,29 +767,37 @@ canvas {
.comparison-table th,
.comparison-table td {
padding: 10px;
padding: 0.625rem;
text-align: left;
border-bottom: 1px solid #e0e0e0;
border-bottom: 1px solid var(--vp-c-divider);
}
.comparison-table th {
background: #f8f9fa;
background: var(--vp-c-bg-soft);
font-weight: 600;
color: #2c3e50;
color: var(--vp-c-text-1);
font-size: 0.813rem;
}
.comparison-table td {
font-size: 0.813rem;
color: var(--vp-c-text-2);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -786,32 +805,36 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="access-key-management-demo">
<div class="demo-header">
<h4>访问密钥AK/SK生命周期管理</h4>
<p class="intro-text">模拟 AK/SK 的创建使用和轮换流程</p>
<span class="icon">🔑</span>
<span class="title">访问密钥管理</span>
<span class="subtitle">理解 AK/SK 生命周期和轮换流程</span>
</div>
<div class="demo-content">
@@ -88,7 +89,8 @@
</div>
<div class="info-box">
<strong>💡 安全提示</strong>访问密钥泄露是云安全事件的主要原因之一建议优先使用 IAM 角色替代访问密钥如果必须使用请务必定期轮换
<span class="icon">💡</span>
<strong>核心思想</strong>访问密钥泄露是云安全事件的主要原因之一建议优先使用 IAM 角色替代访问密钥如果必须使用请务必定期轮换
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="best-practices-demo">
<div class="demo-header">
<h4>云账号权限管理最佳实践清单</h4>
<p class="intro-text">点击查看详细的实施指南和代码示例</p>
<span class="icon"></span>
<span class="title">权限管理最佳实践</span>
<span class="subtitle">理解云账号安全管理的核心原则</span>
</div>
<div class="demo-content">
@@ -52,7 +53,8 @@
</div>
<div class="info-box">
<strong>💡 实施建议</strong>按照优先级从 P0 开始逐步实施最佳实践每个改进都能显著提升账号安全性不要试图一次性完成所有改进
<span class="icon">💡</span>
<strong>核心思想</strong>按照优先级从 P0 开始逐步实施最佳实践每个改进都能显著提升账号安全性不要试图一次性完成所有改进
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="cross-account-access-demo">
<div class="demo-header">
<h4>跨账号访问流程演示</h4>
<p class="intro-text">角色扮演AssumeRole获取临时凭证</p>
<span class="icon">🔗</span>
<span class="title">跨账号访问</span>
<span class="subtitle">理解跨账号访问的 AssumeRole 机制</span>
</div>
<div class="demo-content">
@@ -65,7 +66,8 @@ s3_client = boto3.client(
</div>
<div class="info-box">
<strong>💡 跨账号访问优势</strong>通过角色扮演实现跨账号访问无需在每个账号创建 IAM 用户临时凭证自动过期更安全更易管理
<span class="icon">💡</span>
<strong>核心思想</strong>通过角色扮演实现跨账号访问无需在每个账号创建 IAM 用户临时凭证自动过期更安全更易管理
</div>
</div>
</template>
@@ -1,6 +1,13 @@
<template>
<div class="iam-structure">
<div class="structure-layers">
<div class="demo-header">
<span class="icon">🏗</span>
<span class="title">IAM 五大核心概念</span>
<span class="subtitle">云上权限管理的基础构件</span>
</div>
<div class="demo-content">
<div class="structure-layers">
<div
v-for="(layer, index) in layers"
:key="index"
@@ -14,21 +21,27 @@
<div class="layer-desc">{{ layer.shortDesc }}</div>
</div>
</div>
</div>
<div v-if="selectedLayerData" class="layer-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
<span class="detail-name">{{ selectedLayerData.name }}</span>
</div>
<div class="detail-desc">{{ selectedLayerData.description }}</div>
<div class="detail-examples">
<div class="example-title">示例</div>
<ul>
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
{{ example }}
</li>
</ul>
<div v-if="selectedLayerData" class="layer-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
<span class="detail-name">{{ selectedLayerData.name }}</span>
</div>
<div class="detail-desc">{{ selectedLayerData.description }}</div>
<div class="detail-examples">
<div class="example-title">示例</div>
<ul>
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
{{ example }}
</li>
</ul>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>IAM 就像公司的门禁系统根账号是老板拥有所有钥匙用户是员工有特定权限角色是临时访客证有时效策略是"谁能进哪些门"的规则
</div>
</div>
</div>
@@ -113,9 +126,39 @@ function selectLayer(index) {
.iam-structure {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.structure-layers {
@@ -221,4 +264,23 @@ function selectLayer(index) {
.detail-examples li:last-child {
margin-bottom: 0;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="iam-ram-comparison-demo">
<div class="demo-header">
<h4>AWS IAM vs 阿里云 RAM 对比</h4>
<p class="intro-text">点击各个模块查看详细对比</p>
<span class="icon">🔐</span>
<span class="title">IAM vs RAM 对比</span>
<span class="subtitle">理解不同云厂商的权限管理服务</span>
</div>
<div class="demo-content">
@@ -80,7 +81,8 @@
</div>
<div class="info-box">
<strong>💡 提示</strong>IAM RAM 的核心概念基本一致只是术语和实现细节略有不同掌握一个平台后可以快速迁移到另一个平台
<span class="icon">💡</span>
<strong>核心思想</strong>IAM RAM 的核心概念基本一致只是术语和实现细节略有不同掌握一个平台后可以快速迁移到另一个平台
</div>
</div>
</template>
@@ -163,31 +165,24 @@ function selectFeature(platform, index) {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-weight: 800;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.intro-text {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.demo-content {
margin-bottom: 1rem;
}
.demo-content { margin-bottom: 0.75rem; }
.comparison-container {
display: grid;
@@ -214,7 +209,7 @@ function selectFeature(platform, index) {
}
.platform-header.ram {
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
background: var(--vp-c-bg-soft);
}
.platform-header .logo {
@@ -234,9 +229,7 @@ function selectFeature(platform, index) {
color: var(--vp-c-text-2);
}
.features-list {
padding: 0.75rem;
}
.features-list { padding: 0.75rem; }
.feature-item {
display: flex;
@@ -258,9 +251,7 @@ function selectFeature(platform, index) {
transform: translateX(4px);
}
.feature-icon {
font-size: 1.2rem;
}
.feature-icon { font-size: 1.2rem; }
.feature-content {
display: flex;
@@ -289,13 +280,11 @@ function selectFeature(platform, index) {
padding: 1rem;
}
.detail-card {
text-align: center;
}
.detail-card { text-align: center; }
.detail-card h6 {
margin: 0 0 1rem 0;
font-size: 1.1rem;
font-size: 1rem;
color: var(--vp-c-brand-1);
}
@@ -324,18 +313,13 @@ function selectFeature(platform, index) {
color: var(--vp-c-text-1);
}
.aws-detail .label {
color: var(--vp-c-brand-1);
}
.ram-detail .label {
color: var(--vp-c-brand-delta);
}
.aws-detail .label { color: var(--vp-c-brand-1); }
.ram-detail .label { color: var(--vp-c-brand-delta); }
.aws-detail p,
.ram-detail p {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
font-size: 0.75rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
@@ -347,7 +331,7 @@ function selectFeature(platform, index) {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.65rem;
font-size: 0.6rem;
word-break: break-all;
color: var(--vp-c-text-2);
}
@@ -363,19 +347,17 @@ function selectFeature(platform, index) {
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
color: var(--vp-c-text-1);
}
.info-box .icon { flex-shrink: 0; }
@media (max-width: 1024px) {
.comparison-container {
@@ -383,16 +365,8 @@ function selectFeature(platform, index) {
gap: 1rem;
}
.comparison-details {
order: -1;
}
.comparison-row {
flex-direction: column;
}
.vs-divider {
padding: 0.5rem 0;
}
.comparison-details { order: -1; }
.comparison-row { flex-direction: column; }
.vs-divider { padding: 0.5rem 0; }
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="identity-provider-demo">
<div class="demo-header">
<h4>身份提供商(IdP)集成流程</h4>
<p class="intro-text">点击步骤查看 SSO 单点登录流程</p>
<span class="icon">🔐</span>
<span class="title">身份提供商集成</span>
<span class="subtitle">理解企业 SSO 单点登录流程</span>
</div>
<div class="demo-content">
@@ -38,7 +39,8 @@
</div>
<div class="info-box">
<strong>💡 SSO 优势</strong>通过企业 IdP 统一管理用户身份避免在每个云平台单独创建账号提高安全性和管理效率
<span class="icon">💡</span>
<strong>核心思想</strong>通过企业 IdP 统一管理用户身份避免在每个云平台单独创建账号提高安全性和管理效率
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="mfa-security-demo">
<div class="demo-header">
<h4>MFA 多因素认证模拟</h4>
<p class="intro-text">体验 MFA 因素认证流程</p>
<span class="icon">🔐</span>
<span class="title">因素认证</span>
<span class="subtitle">理解 MFA 双因素认证流程</span>
</div>
<div class="demo-content">
@@ -50,7 +51,8 @@
</div>
<div class="info-box">
<strong>💡 MFA 安全价值</strong>启用 MFA 可降低 99.9% 的账号被盗风险即使密码泄露攻击者没有你的 MFA 设备也无法登录
<span class="icon">💡</span>
<strong>核心思想</strong>启用 MFA 可降低 99.9% 的账号被盗风险即使密码泄露攻击者没有你的 MFA 设备也无法登录
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="permission-hierarchy-demo">
<div class="demo-header">
<h4>权限层级结构</h4>
<p class="intro-text">点击层级查看详细权限范围</p>
<span class="icon">🏛</span>
<span class="title">权限层级结构</span>
<span class="subtitle">理解不同权限级别的范围差异</span>
</div>
<div class="demo-content">
@@ -61,7 +62,8 @@
</div>
<div class="info-box">
<strong>💡 最小权限原则</strong>始终授予用户完成工作所需的最小权限从低权限开始根据实际需求逐步提升而不是一开始就授予高权限
<span class="icon">💡</span>
<strong>核心思想</strong>最小权限原则始终授予用户完成工作所需的最小权限从低权限开始根据实际需求逐步提升而不是一开始就授予高权限
</div>
</div>
</template>
@@ -1,5 +1,11 @@
<template>
<div class="policy-editor-demo">
<div class="demo-header">
<span class="icon">📋</span>
<span class="title">策略编辑器</span>
<span class="subtitle">理解 IAM 策略的 JSON 结构</span>
</div>
<div class="editor-layout">
<div class="editor-panel">
<div class="panel-title">策略编辑器</div>
@@ -42,6 +48,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>策略由 EffectActionResourceCondition 四个核心元素组成理解这四个元素的作用是编写 IAM 策略的基础
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="role-policy-demo">
<div class="demo-header">
<h4>角色与策略关系可视化</h4>
<p class="intro-text">拖动查看角色如何关联多个策略</p>
<span class="icon">🎭</span>
<span class="title">角色与策略</span>
<span class="subtitle">理解角色如何关联多个策略</span>
</div>
<div class="demo-content">
@@ -80,7 +81,8 @@
</div>
<div class="info-box">
<strong>💡 策略叠加</strong>一个角色可以附加多个策略最终的权限是所有策略的叠加结果Deny 策略优先级高于 Allow
<span class="icon">💡</span>
<strong>核心思想</strong>策略叠加一个角色可以附加多个策略最终的权限是所有策略的叠加结果Deny 策略优先级高于 Allow
</div>
</div>
</template>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="access-analytics-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">📊</span>
<span class="title">访问分析</span>
<span class="subtitle">理解 CDN 访问统计和日志分析</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
访问分析演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>通过日志分析可以了解谁在何时访问了什么资源帮助发现异常访问模式和安全事件
</div>
</div>
</template>
@@ -20,31 +26,58 @@ const description = ref('展示CDN和对象存储的访问统计分析,包括
</script>
<style scoped>
.demo-container {
.access-analytics-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon"></span>
<span class="title">{{ title }}</span>
<span class="subtitle">{{ description }}</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存策略演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>缓存策略平衡命中率和新鲜度TTL 设置太短会导致频繁回源太长会导致内容过期
</div>
</div>
</template>
@@ -4,9 +4,10 @@
-->
<template>
<div class="cdn-acceleration-demo">
<div class="header">
<div class="title">CDN 加速原理</div>
<div class="subtitle">边缘节点源站与回源的协同工作</div>
<div class="demo-header">
<span class="icon">🌐</span>
<span class="title">CDN 加速原理</span>
<span class="subtitle">边缘节点源站与回源的协同工作</span>
</div>
<div class="cdn-architecture">
@@ -64,7 +65,7 @@
</div>
<div class="stat">
<span class="stat-label">命中</span>
<span class="stat-value" :style="{ color: node.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
<span class="stat-value" :style="{ color: node.hitRate > 80 ? 'var(--vp-c-brand-1)' : 'var(--vp-c-brand)' }">
{{ node.hitRate }}%
</span>
</div>
@@ -135,25 +136,30 @@
<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-value" :style="{ color: 'var(--vp-c-brand-1)' }">{{ 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-value" :style="{ color: 'var(--vp-c-brand-delta)' }">{{ stats.cacheMiss }}</div>
<div class="stat-label">缓存未命中</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? 'var(--vp-c-brand-1)' : 'var(--vp-c-brand)' }">
{{ 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-value" :style="{ color: 'var(--vp-c-brand)' }">{{ stats.avgResponseTime }}ms</div>
<div class="stat-label">平均响应</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>CDN就像在全球开了分店用户访问最近的分店拿资源不用都跑总店来速度自然快
</div>
</div>
</template>
@@ -285,21 +291,32 @@ const resetDemo = () => {
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1.5rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.cdn-architecture {
@@ -338,27 +355,27 @@ const resetDemo = () => {
}
.layer-status.hit {
background: #dcfce7;
color: #166534;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.layer-status.miss {
background: #fee2e2;
color: #991b1b;
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
color: var(--vp-c-brand-delta);
}
.layer-status.active {
background: #dbeafe;
color: #1e40af;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
/* 用户层 */
.users-map {
position: relative;
height: 120px;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid #bae6fd;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
}
@@ -396,18 +413,18 @@ const resetDemo = () => {
display: flex;
align-items: center;
justify-content: center;
background: white;
background: var(--vp-c-bg);
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-label {
font-size: 0.65rem;
font-weight: 600;
color: #0369a1;
color: var(--vp-c-brand-1);
margin-top: 0.25rem;
white-space: nowrap;
background: rgba(255, 255, 255, 0.9);
background: var(--vp-c-bg);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
@@ -511,8 +528,8 @@ const resetDemo = () => {
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
}
@@ -528,12 +545,12 @@ const resetDemo = () => {
.server-name {
font-weight: 600;
font-size: 0.9rem;
color: #92400e;
color: var(--vp-c-text-1);
}
.server-address {
font-size: 0.75rem;
color: #b45309;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
@@ -543,14 +560,14 @@ const resetDemo = () => {
gap: 0.4rem;
font-size: 0.75rem;
font-weight: 600;
color: #15803d;
color: var(--vp-c-brand-1);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
background: var(--vp-c-brand-1);
animation: statusPulse 2s infinite;
}
@@ -560,8 +577,8 @@ const resetDemo = () => {
}
.back-to-source-flow {
background: #fef2f2;
border: 1px solid #fecaca;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin-top: 0.5rem;
@@ -571,7 +588,7 @@ const resetDemo = () => {
text-align: center;
font-size: 0.9rem;
font-weight: 600;
color: #dc2626;
color: var(--vp-c-brand-delta);
margin-bottom: 0.5rem;
}
@@ -589,11 +606,11 @@ const resetDemo = () => {
.flow-step {
font-size: 0.75rem;
color: #991b1b;
background: white;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
padding: 0.4rem 0.6rem;
border-radius: 4px;
border-left: 3px solid #dc2626;
border-left: 3px solid var(--vp-c-brand);
}
/* 控制区 */
@@ -639,13 +656,13 @@ const resetDemo = () => {
}
.control-btn.reset {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
background: rgba(var(--vp-c-brand-delta-rgb), 0.1);
border-color: var(--vp-c-brand-delta);
color: var(--vp-c-brand-delta);
}
.control-btn.reset:hover {
background: #fee2e2;
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
}
/* 统计面板 */
@@ -693,4 +710,23 @@ const resetDemo = () => {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,50 +1,83 @@
<template>
<div class="demo-container">
<div class="https-optimization-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🔒</span>
<span class="title">HTTPS 优化</span>
<span class="subtitle">理解 CDN HTTPS 协议和证书管理</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
HTTPS优化演示组件占位符 - 待实现具体交互
HTTPS 优化演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>HTTPS 通过 TLS/SSL 加密数据传输防止中间人攻击和数据泄露是现代 Web 应用的安全基础
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('HTTPS优化演示')
const title = ref('HTTPS 优化演示')
const description = ref('展示CDN的HTTPS优化技术,包括TLS握手优化、证书管理、HSTS等')
</script>
<style scoped>
.demo-container {
.https-optimization-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,12 +1,9 @@
<!--
ObjectStorageDemo.vue
对象存储架构演示 - 展示桶对象元数据的核心概念
-->
<template>
<div class="object-storage-demo">
<div class="header">
<div class="title">对象存储架构</div>
<div class="subtitle">理解 BucketObject Metadata 的关系</div>
<div class="demo-header">
<span class="icon">🗄</span>
<span class="title">对象存储架构</span>
<span class="subtitle">理解 BucketObject Metadata 的关系</span>
</div>
<div class="storage-architecture">
@@ -113,38 +110,9 @@
</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 class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>对象存储采用三层架构Account账户 Bucket Object对象每个对象都附带丰富的元数据用于检索和管理理解这个层次结构是掌握对象存储的第一步
</div>
</div>
</template>
@@ -262,69 +230,67 @@ const getFileIcon = (type) => {
.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);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1.5rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.storage-architecture {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
}
.account-layer {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
padding: 1rem;
border-radius: 10px;
background: var(--vp-c-brand-soft);
padding: 0.75rem;
border-radius: 8px;
text-align: center;
border: 2px solid #6366f1;
border: 2px solid var(--vp-c-brand);
}
.account-icon {
font-size: 2rem;
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.account-name {
font-weight: 600;
font-size: 0.95rem;
color: #4338ca;
font-size: 0.9rem;
color: var(--vp-c-brand-1);
margin-bottom: 0.25rem;
}
.account-desc {
font-size: 0.75rem;
color: #6366f1;
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.connector {
text-align: center;
color: var(--vp-c-text-3);
font-size: 1.25rem;
font-size: 1rem;
}
.buckets-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
}
.section-title {
@@ -332,13 +298,13 @@ const getFileIcon = (type) => {
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.section-desc {
font-size: 0.75rem;
font-size: 0.65rem;
font-weight: normal;
color: var(--vp-c-text-2);
margin-left: auto;
@@ -346,17 +312,17 @@ const getFileIcon = (type) => {
.buckets-row {
display: flex;
gap: 0.75rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.bucket-card {
flex: 1;
min-width: 140px;
min-width: 120px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
@@ -370,29 +336,26 @@ const getFileIcon = (type) => {
.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);
box-shadow: 0 0 3px var(--vp-c-brand-dimm);
}
.bucket-icon {
font-size: 1.75rem;
margin-bottom: 0.25rem;
}
.bucket-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
.bucket-name {
font-weight: 600;
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-1);
word-break: break-all;
}
.bucket-meta {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.bucket-size {
font-size: 0.75rem;
font-size: 0.7rem;
color: var(--vp-c-brand);
font-weight: 600;
margin-top: 0.25rem;
@@ -401,30 +364,30 @@ const getFileIcon = (type) => {
.objects-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
border-radius: 8px;
padding: 0.75rem;
min-height: 120px;
}
.objects-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.4rem;
}
.object-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.object-item:hover {
background: var(--vp-c-bg-mute);
background: var(--vp-c-bg-alt);
}
.object-item.selected {
@@ -432,9 +395,7 @@ const getFileIcon = (type) => {
border: 1px solid var(--vp-c-brand);
}
.object-icon {
font-size: 1.25rem;
}
.object-icon { font-size: 1rem; }
.object-info {
flex: 1;
@@ -442,7 +403,7 @@ const getFileIcon = (type) => {
}
.object-key {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-1);
white-space: nowrap;
@@ -451,33 +412,35 @@ const getFileIcon = (type) => {
}
.object-meta {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
}
.object-arrow {
color: var(--vp-c-text-3);
font-size: 0.8rem;
}
.objects-placeholder {
.objects-placeholder,
.metadata-placeholder {
text-align: center;
padding: 2rem;
padding: 1.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.8rem;
}
.metadata-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
border-radius: 8px;
padding: 0.75rem;
min-height: 120px;
}
.metadata-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
gap: 0.75rem;
}
@media (max-width: 768px) {
@@ -488,30 +451,30 @@ const getFileIcon = (type) => {
.metadata-section {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
}
.metadata-section-title {
font-weight: 600;
font-size: 0.85rem;
font-size: 0.75rem;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
margin-bottom: 0.4rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.metadata-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
gap: 0.3rem;
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.1rem;
font-size: 0.75rem;
font-size: 0.7rem;
}
.metadata-key {
@@ -525,66 +488,16 @@ const getFileIcon = (type) => {
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;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.summary-icon {
font-size: 1.5rem;
}
.summary-text {
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
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;
}
.info-box .icon { flex-shrink: 0; }
</style>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="traffic-scheduling-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🚦</span>
<span class="title">流量调度</span>
<span class="subtitle">理解 CDN 智能调度和负载均衡</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
流量调度演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>智能调度通过就近访问负载均衡和故障切换实现全球加速和高可用性
</div>
</div>
</template>
@@ -20,31 +26,58 @@ const description = ref('展示CDN的智能流量调度机制,包括负载均
</script>
<style scoped>
.demo-container {
.traffic-scheduling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,9 +4,10 @@
-->
<template>
<div class="upload-process-demo">
<div class="header">
<div class="title">文件上传流程</div>
<div class="subtitle">直传 vs 分片上传 vs 断点续传</div>
<div class="demo-header">
<span class="icon">📤</span>
<span class="title">文件上传流程</span>
<span class="subtitle">理解直传分片断点续传三种方式</span>
</div>
<!-- 上传方式选择 -->
@@ -179,6 +180,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>大文件分片上传提高可靠性网络中断可以从断点续传避免重复上传整个文件
</div>
</div>
</template>
@@ -4,100 +4,62 @@
-->
<template>
<div class="evolution-timeline">
<div class="timeline-header">
<span class="header-icon">🚀</span>
<span class="header-title">前端开发演进时间线</span>
<span class="header-subtitle">"贴海报""搭乐高" 20 年变迁</span>
<div class="demo-header">
<span class="icon">🚀</span>
<span class="title">前端演进时间线</span>
<span class="subtitle">"贴海报""搭乐高"20年变迁</span>
</div>
<!-- 时间线 -->
<div class="timeline-container">
<div
v-for="(era, index) in eras"
:key="era.id"
class="era-item"
:class="{ active: activeEra === era.id }"
@click="activeEra = activeEra === era.id ? null : era.id"
>
<div class="era-marker">
<div class="era-dot">{{ era.emoji }}</div>
<div v-if="index < eras.length - 1" class="era-line"></div>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-year">{{ era.year }}</span>
<span class="era-name">{{ era.name }}</span>
<div class="demo-content">
<!-- 时间线 -->
<div class="timeline-container">
<div
v-for="(era, index) in eras"
:key="era.id"
class="era-item"
:class="{ active: activeEra === era.id }"
@click="activeEra = activeEra === era.id ? null : era.id"
>
<div class="era-marker">
<div class="era-dot">{{ era.emoji }}</div>
<div v-if="index < eras.length - 1" class="era-line"></div>
</div>
<div class="era-brief">{{ era.brief }}</div>
<Transition name="expand">
<div v-if="activeEra === era.id" class="era-detail">
<div class="detail-section">
<div class="section-title">🔑 关键技术</div>
<div class="tech-tags">
<span
v-for="tech in era.technologies"
:key="tech"
class="tech-tag"
>{{ tech }}</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">💪 优点</div>
<div class="benefit-list">
<div
v-for="benefit in era.pros"
:key="benefit"
class="benefit-item"
>
<span class="check-icon"></span>
<span>{{ benefit }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-title"> 缺点</div>
<div class="problem-list">
<div
v-for="problem in era.cons"
:key="problem"
class="problem-item"
>
<span class="warn-icon">!</span>
<span>{{ problem }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="era.metaphor">
<div class="section-title">💡 生活比喻</div>
<div class="metaphor-box">{{ era.metaphor }}</div>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-year">{{ era.year }}</span>
<span class="era-name">{{ era.name }}</span>
</div>
</Transition>
<div class="era-brief">{{ era.brief }}</div>
<Transition name="expand">
<div v-if="activeEra === era.id" class="era-detail">
<div class="detail-section">
<div class="section-title">🔑 关键技术</div>
<div class="tech-tags">
<span
v-for="tech in era.technologies.slice(0, 5)"
:key="tech"
class="tech-tag"
>{{ tech }}</span>
</div>
</div>
<div class="detail-section" v-if="era.metaphor">
<div class="section-title">💡 生活比喻</div>
<div class="metaphor-box">{{ era.metaphor }}</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
</div>
<!-- 提示 -->
<div class="timeline-hint">
<span>👆</span>
<span>点击任意时代查看详细信息</span>
</div>
<!-- 核心要点 -->
<div class="key-takeaway">
<span class="takeaway-icon">🎯</span>
<div class="takeaway-content">
<strong>核心思想</strong>
前端技术的演进本质是为了解决两个问题
<strong>提升开发效率</strong>从手动到自动化
<strong>支撑更复杂的应用</strong>从简单页面到桌面级应用
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>前端技术的演进本质是为了解决两个问题提升开发效率从手动到自动化和支撑更复杂的应用从简单页面到桌面级应用
</div>
</div>
</template>
@@ -168,37 +130,39 @@ const eras = [
<style scoped>
.evolution-timeline {
border: 2px solid #e0e0e0;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.timeline-header {
text-align: center;
margin-bottom: 32px;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.header-icon {
font-size: 48px;
display: block;
margin-bottom: 12px;
.demo-header .icon {
font-size: 1.25rem;
}
.header-title {
display: block;
font-size: 24px;
.demo-header .title {
font-weight: bold;
color: #333;
margin-bottom: 8px;
font-size: 1rem;
}
.header-subtitle {
display: block;
font-size: 14px;
color: #666;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
/* 时间线容器 */
@@ -208,8 +172,8 @@ const eras = [
.era-item {
display: flex;
gap: 20px;
margin-bottom: 24px;
gap: 1rem;
margin-bottom: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
@@ -231,89 +195,88 @@ const eras = [
}
.era-dot {
width: 56px;
height: 56px;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
font-size: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1;
transition: all 0.3s ease;
}
.era-item:hover .era-dot {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.era-line {
width: 4px;
flex: 1;
background: linear-gradient(180deg, #667eea, #e0e0e0);
background: var(--vp-c-divider);
margin-top: 8px;
min-height: 40px;
min-height: 30px;
}
/* 内容区域 */
.era-content {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.era-item:hover .era-content {
border-color: #667eea;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
border-color: var(--vp-c-brand);
}
.era-item.active .era-content {
border-color: #667eea;
background: linear-gradient(135deg, #f8f9ff, #ffffff);
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.era-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.era-year {
padding: 4px 12px;
background: linear-gradient(135deg, #667eea, #764ba2);
padding: 2px 10px;
background: var(--vp-c-brand);
color: white;
border-radius: 12px;
font-size: 12px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: bold;
}
.era-name {
font-size: 18px;
font-size: 1rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.era-brief {
font-size: 14px;
color: #666;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 详情展开 */
.era-detail {
margin-top: 16px;
padding-top: 16px;
border-top: 2px dashed #e0e0e0;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 2px dashed var(--vp-c-divider);
}
.detail-section {
margin-bottom: 16px;
margin-bottom: 0.75rem;
}
.detail-section:last-child {
@@ -321,124 +284,36 @@ const eras = [
}
.section-title {
font-size: 13px;
font-size: 0.8rem;
font-weight: bold;
color: #667eea;
margin-bottom: 8px;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
}
/* 技术标签 */
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 0.5rem;
}
.tech-tag {
padding: 4px 12px;
background: #f0f4ff;
color: #667eea;
border-radius: 12px;
font-size: 12px;
padding: 2px 10px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-brand);
border-radius: 8px;
font-size: 0.75rem;
font-weight: 500;
}
/* 优点列表 */
.benefit-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.benefit-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #16a34a;
}
.check-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #dcfce7;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}
/* 缺点列表 */
.problem-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.problem-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #dc2626;
}
.warn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #fecaca;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}
/* 比喻框 */
.metaphor-box {
background: linear-gradient(135deg, #fff7ed, #ffedd5);
border-left: 4px solid #f97316;
padding: 12px;
border-radius: 8px;
font-size: 13px;
color: #9a3412;
line-height: 1.6;
}
/* 提示 */
.timeline-hint {
text-align: center;
font-size: 13px;
color: #666;
margin: 16px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* 核心要点 */
.key-takeaway {
display: flex;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #dcfce7, #d1fae5);
border-radius: 12px;
border-left: 4px solid #16a34a;
}
.takeaway-icon {
font-size: 24px;
flex-shrink: 0;
}
.takeaway-content {
flex: 1;
font-size: 14px;
color: #14532d;
background: var(--vp-c-bg-alt);
border-left: 4px solid var(--vp-c-brand);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@@ -457,28 +332,25 @@ const eras = [
.expand-enter-to,
.expand-leave-from {
max-height: 1000px;
max-height: 600px;
opacity: 1;
}
/* 响应式 */
@media (max-width: 768px) {
.era-item {
flex-direction: column;
gap: 12px;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.era-marker {
flex-direction: row;
gap: 12px;
}
.info-box .icon {
flex-shrink: 0;
}
.era-line {
width: 100%;
height: 4px;
min-height: 0;
margin-top: 0;
margin-left: 8px;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,19 +1,19 @@
<!--
ImperativeVsDeclarativeDemo.vue
命令式 vs 声明式编程对比演示
用途
通过并排的交互式计数器直观展示 ImperativejQuery DeclarativeVue
在代码量和心智负担上的差异
交互功能
- 两个可交互的计数器
- 切换展示背后的代码实现
- 高亮显示 jQuery 需要手动更新的多个 DOM 节点 vs Vue 的自动绑定
ImperativeVsDeclarativeDemo.vue - 命令式 vs 声明式编程对比
"画画的两种方式"来解释 jQuery vs Vue/React 的区别
-->
<template>
<div class="imperative-declarative-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">🎨</span>
<span class="title">编程范式对比</span>
<span class="subtitle">告诉"怎么做" vs 告诉"要什么"</span>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 视图切换 -->
<div class="toggle-group">
<button
v-for="view in views"
@@ -24,14 +24,13 @@
{{ view.label }}
</button>
</div>
</div>
<div class="comparison-container">
<!-- Imperative Side (jQuery) -->
<div class="side imperative-side">
<div class="side-header">
<span class="badge imperative">jQuery / Imperative</span>
<h4>"Tell me HOW"</h4>
<span class="badge imperative">jQuery / 命令式</span>
<span class="sub-label">通俗说法: 告诉怎么做</span>
</div>
<div class="demo-area">
@@ -112,8 +111,8 @@
<!-- Declarative Side (Vue) -->
<div class="side declarative-side">
<div class="side-header">
<span class="badge declarative">Vue / Declarative</span>
<h4>"Tell me WHAT"</h4>
<span class="badge declarative">Vue / 声明式</span>
<span class="sub-label">通俗说法: 告诉要什么</span>
</div>
<div class="demo-area">
@@ -188,11 +187,20 @@
</div>
</div>
<!-- 底部控制 -->
<div class="demo-controls">
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
</button>
</div>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>
命令式编程需要一步步告诉浏览器"怎么做"声明式编程只需告诉浏览器"要什么"框架会自动处理细节
</div>
</div>
</template>
@@ -224,13 +232,40 @@ function updateJq(change) {
.imperative-declarative-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 标题区 */
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.toggle-group {
@@ -275,9 +310,10 @@ function updateJq(change) {
.side-header {
text-align: center;
margin-bottom: 1rem;
}
.badge {
.side-header .badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
@@ -285,14 +321,11 @@ function updateJq(change) {
font-weight: 600;
}
.badge.imperative {
background-color: rgba(7, 105, 173, 0.2);
color: #0769ad;
}
.badge.declarative {
background-color: rgba(66, 184, 131, 0.2);
color: #2c8a5e;
.side-header .sub-label {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.side-header h4 {
@@ -356,7 +389,7 @@ function updateJq(change) {
}
.status-text.warning {
color: #f87171;
color: var(--vp-c-warning);
font-weight: 600;
}
@@ -406,8 +439,8 @@ function updateJq(change) {
}
.imperative-code {
background-color: #1e1e2e;
color: #a6accd;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
.imperative-code code {
@@ -415,8 +448,8 @@ function updateJq(change) {
}
.declarative-code {
background-color: #1e1e2e;
color: #a6accd;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
.declarative-code code {
@@ -433,7 +466,7 @@ function updateJq(change) {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #0769ad, #42b883);
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
@@ -460,23 +493,43 @@ function updateJq(change) {
}
.pain-point {
background-color: rgba(248, 113, 113, 0.1);
color: #dc2626;
background: var(--vp-c-bg-alt);
color: var(--vp-c-danger);
}
.benefit {
background-color: rgba(74, 222, 128, 0.1);
color: #16a34a;
background: var(--vp-c-bg-alt);
color: var(--vp-c-success);
}
.demo-controls {
display: flex;
justify-content: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
margin-top: 0.75rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
@@ -1,152 +1,130 @@
<!--
JQueryVsStateDemo.vue - 餐厅账本对比
JQueryVsStateDemo.vue - 前端开发模式对比
"手工记账 vs 智能管家"的比喻来解释 jQuery vs Vue/React
-->
<template>
<div class="restaurant-demo">
<!-- 故事引入 -->
<div class="story-intro">
<div class="story-icon">👨🍳📒🤖</div>
<h3 class="story-title">老张的餐厅账本</h3>
<p class="story-desc">
老张开了家餐厅每天要点菜做菜算账有两种记账方式<br>
<strong>传统方式老张手工记</strong>jQuery 模式 vs <strong>智能方式请个管家</strong>Vue/React 模式<br>
看看哪种更轻松
</p>
<div class="jquery-vs-state-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">🔄</span>
<span class="title">前端开发模式</span>
<span class="subtitle">手动操作DOM vs 状态管理</span>
</div>
<!-- 模式选择 -->
<div class="mode-tabs">
<button
class="tab-btn"
:class="{ active: mode === 'manual' }"
@click="mode = 'manual'"
>
<span class="tab-icon"></span>
<span class="tab-text">手工记账</span>
<span class="tab-sub">jQuery 方式</span>
</button>
<button
class="tab-btn"
:class="{ active: mode === 'smart' }"
@click="mode = 'smart'"
>
<span class="tab-icon">🤖</span>
<span class="tab-text">智能管家</span>
<span class="tab-sub">Vue/React 方式</span>
</button>
</div>
<!-- 对比展示区 -->
<div class="comparison-showcase">
<!-- 左侧场景描述 -->
<div class="scenario-panel">
<div class="scenario-header">
<span class="scenario-icon">{{ mode === 'manual' ? '👨‍🍳' : '🤖' }}</span>
<span class="scenario-title">{{ mode === 'manual' ? '老张手工记账' : '智能管家记账' }}</span>
</div>
<div class="scenario-content">
<div class="step-list">
<div
v-for="(step, index) in currentSteps"
:key="index"
class="step-item"
:class="{ active: index === currentStep }"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-text">{{ step }}</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 模式选择 -->
<div class="mode-tabs">
<button
class="tab-btn"
:class="{ active: mode === 'manual' }"
@click="mode = 'manual'"
>
<span class="tab-icon"></span>
<span class="tab-text">手工记账</span>
<span class="tab-sub">通俗说法: jQuery</span>
</button>
<button
class="tab-btn"
:class="{ active: mode === 'smart' }"
@click="mode = 'smart'"
>
<span class="tab-icon">🤖</span>
<span class="tab-text">智能管家</span>
<span class="tab-sub">通俗说法: Vue/React</span>
</button>
</div>
<!-- 右侧账本展示 -->
<div class="ledger-panel">
<div class="ledger-header">
<span class="ledger-icon">📒</span>
<span class="ledger-title">今日账本</span>
<span class="ledger-status" :class="mode">{{ ledgerStatus }}</span>
</div>
<!-- 对比展示 -->
<div class="comparison-showcase">
<!-- 左侧场景描述 -->
<div class="scenario-panel">
<div class="scenario-header">
<span class="scenario-icon">{{ mode === 'manual' ? '👨‍🍳' : '🤖' }}</span>
<span class="scenario-title">{{ mode === 'manual' ? '手工记账' : '智能管家' }}</span>
</div>
<div class="ledger-content">
<!-- 订单列表 -->
<div class="order-list">
<div
v-for="order in orders"
:key="order.id"
class="order-item"
:class="{ completed: order.completed }"
>
<div class="order-info">
<span class="order-name">{{ order.name }}</span>
<span class="order-price">¥{{ order.price }}</span>
</div>
<div class="order-status">
{{ order.completed ? '✓' : '○' }}
<div class="scenario-content">
<div class="step-list">
<div
v-for="(step, index) in currentSteps"
:key="index"
class="step-item"
:class="{ active: index === currentStep }"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-text">{{ step }}</div>
</div>
</div>
</div>
</div>
<!-- 总计 -->
<div class="total-section">
<div class="total-row">
<span>菜品数量</span>
<span class="total-value">{{ completedCount }}/{{ orders.length }} </span>
<!-- 右侧账本展示 -->
<div class="ledger-panel">
<div class="ledger-header">
<span class="ledger-icon">📒</span>
<span class="ledger-title">今日账本</span>
<span class="ledger-status" :class="mode">{{ ledgerStatus }}</span>
</div>
<div class="ledger-content">
<!-- 订单列表 -->
<div class="order-list">
<div
v-for="order in orders"
:key="order.id"
class="order-item"
:class="{ completed: order.completed }"
>
<div class="order-info">
<span class="order-name">{{ order.name }}</span>
<span class="order-price">¥{{ order.price }}</span>
</div>
<div class="order-status">
{{ order.completed ? '✓' : '○' }}
</div>
</div>
</div>
<div class="total-row total-final">
<span>今日营收</span>
<span class="total-amount">¥{{ totalRevenue }}</span>
<!-- 总计 -->
<div class="total-section">
<div class="total-row">
<span>菜品数量</span>
<span class="total-value">{{ completedCount }}/{{ orders.length }} </span>
</div>
<div class="total-row total-final">
<span>今日营收</span>
<span class="total-amount">¥{{ totalRevenue }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button
class="btn btn-primary"
@click="processOrder"
:disabled="isProcessing || allCompleted"
>
{{ isProcessing ? '处理中...' : allCompleted ? '今日完成!' : '下一道菜' }}
</button>
<button
class="btn btn-secondary"
@click="resetDemo"
>
重新开始
</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button
class="btn btn-primary"
@click="processOrder"
:disabled="isProcessing || allCompleted"
>
{{ isProcessing ? '处理中...' : allCompleted ? '今日完成!' : '下一道菜' }}
</button>
<button
class="btn btn-secondary"
@click="resetDemo"
>
重新开始
</button>
</div>
<!-- 优缺点对比 -->
<div class="comparison-table">
<div class="table-header">
<div class="table-title">💡 两种方式对比</div>
</div>
<div class="table-content">
<div class="comparison-row header">
<div class="col-feature">特点</div>
<div class="col-manual">手工记账 (jQuery)</div>
<div class="col-smart">智能管家 (Vue/React)</div>
</div>
<div class="comparison-row">
<div class="col-feature">工作方式</div>
<div class="col-manual">手动改每一处</div>
<div class="col-smart">改数据界面自动变</div>
</div>
<div class="comparison-row">
<div class="col-feature">容易出错</div>
<div class="col-manual">容易漏改某处</div>
<div class="col-smart">自动同步不易错</div>
</div>
<div class="comparison-row">
<div class="col-feature">适合场景</div>
<div class="col-manual">简单页面</div>
<div class="col-smart">复杂交互应用</div>
</div>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>
<span v-if="mode === 'manual'">jQuery需要手动查找和修改DOM,就像手工记账,容易出错</span>
<span v-else>Vue/React通过状态自动更新界面,就像智能管家,改数据界面自动变</span>
</div>
</div>
</template>
@@ -237,53 +215,53 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.restaurant-demo {
border: 2px solid #e8e8e8;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
.jquery-vs-state-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 故事引入 */
.story-intro {
text-align: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, #ffecb3);
border-radius: 16px;
border: 2px dashed #ffc107;
/* 标题区 */
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.story-icon {
font-size: 48px;
margin-bottom: 12px;
.demo-header .icon {
font-size: 1.25rem;
}
.story-title {
font-size: 24px;
.demo-header .title {
font-weight: bold;
color: #e65100;
margin: 0 0 8px 0;
font-size: 1rem;
}
.story-desc {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
/* 主内容区 */
.demo-content {
margin-bottom: 0.75rem;
}
/* 模式选项卡 */
.mode-tabs {
display: flex;
gap: 12px;
margin-bottom: 24px;
background: white;
padding: 8px;
border-radius: 12px;
border: 2px solid #e0e0e0;
gap: 0.75rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.tab-btn {
@@ -291,35 +269,36 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 16px;
gap: 0.25rem;
padding: 0.75rem;
border: none;
border-radius: 8px;
border-radius: 6px;
background: transparent;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
color: var(--vp-c-text-1);
}
.tab-btn:hover {
background: #f5f5f5;
background: var(--vp-c-bg-alt);
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.tab-icon {
font-size: 32px;
font-size: 1.5rem;
}
.tab-text {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
}
.tab-sub {
font-size: 12px;
font-size: 0.75rem;
opacity: 0.8;
}
@@ -327,8 +306,8 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.comparison-showcase {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
@@ -339,67 +318,67 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
/* 场景面板 */
.scenario-panel {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
}
.scenario-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #ffecb3, #ffe082);
border-bottom: 2px solid #e0e0e0;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 2px solid var(--vp-c-divider);
}
.scenario-icon {
font-size: 28px;
font-size: 1.5rem;
}
.scenario-title {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.scenario-content {
padding: 16px;
padding: 1rem;
}
.step-list {
display: flex;
flex-direction: column;
gap: 8px;
gap: 0.5rem;
}
.step-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s ease;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
transition: all 0.2s;
}
.step-item.active {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
transform: translateX(8px);
transform: translateX(4px);
}
.step-number {
width: 28px;
height: 28px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: white;
color: #333;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 50%;
font-size: 12px;
font-size: 0.75rem;
font-weight: bold;
}
@@ -409,142 +388,147 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
.step-text {
font-size: 14px;
font-size: 0.85rem;
flex: 1;
}
/* 账本面板 */
.ledger-panel {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
}
.ledger-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #c8e6c9, #a5d6a7);
border-bottom: 2px solid #e0e0e0;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 2px solid var(--vp-c-divider);
}
.ledger-icon {
font-size: 28px;
font-size: 1.5rem;
}
.ledger-title {
flex: 1;
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.ledger-status {
font-size: 12px;
padding: 4px 12px;
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
border-radius: 12px;
background: white;
color: #666;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.ledger-status.manual {
background: #fff3e0;
color: #e65100;
background: var(--vp-c-warning);
color: white;
}
.ledger-status.smart {
background: var(--vp-c-success);
color: white;
}
.ledger-content {
padding: 16px;
padding: 1rem;
}
.order-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
gap: 0.5rem;
margin-bottom: 1rem;
}
.order-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s ease;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
transition: all 0.2s;
}
.order-item.completed {
background: #e8f5e9;
border-left: 4px solid #4caf50;
background: var(--vp-c-success);
border-left: 4px solid var(--vp-c-brand);
opacity: 0.3;
}
.order-info {
display: flex;
flex-direction: column;
gap: 4px;
gap: 0.25rem;
}
.order-name {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.order-price {
font-size: 13px;
color: #e65100;
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: bold;
}
.order-status {
font-size: 18px;
font-size: 1rem;
}
.total-section {
border-top: 2px dashed #e0e0e0;
padding-top: 12px;
border-top: 2px dashed var(--vp-c-divider);
padding-top: 0.75rem;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
color: #666;
padding: 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.total-row.total-final {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
border-top: 2px solid #e0e0e0;
margin-top: 8px;
padding-top: 12px;
color: var(--vp-c-text-1);
border-top: 2px solid var(--vp-c-divider);
margin-top: 0.5rem;
padding-top: 0.75rem;
}
.total-amount {
color: #4caf50;
font-size: 20px;
color: var(--vp-c-success);
font-size: 1.1rem;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 24px;
gap: 0.75rem;
}
.btn {
padding: 12px 24px;
padding: 0.5rem 1rem;
border: none;
border-radius: 8px;
font-size: 16px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
@@ -558,91 +542,31 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
/* 对比表格 */
.comparison-table {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
overflow: hidden;
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.table-header {
padding: 16px;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-bottom: 2px solid #e0e0e0;
.info-box .icon {
flex-shrink: 0;
}
.table-title {
font-size: 16px;
font-weight: bold;
color: #1565c0;
}
.table-content {
padding: 0;
}
.comparison-row {
display: grid;
grid-template-columns: 1.2fr 1.4fr 1.4fr;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.comparison-row:last-child {
border-bottom: none;
}
.comparison-row.header {
background: #f5f5f5;
font-weight: bold;
color: #333;
}
.col-feature {
color: #666;
}
.col-manual {
color: #e65100;
}
.col-smart {
color: #4caf50;
}
.comparison-row.header .col-manual,
.comparison-row.header .col-smart {
color: #333;
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-showcase {
grid-template-columns: 1fr;
}
.comparison-row {
grid-template-columns: 1fr;
gap: 8px;
}
.comparison-row.header {
display: none;
}
.mode-tabs {
flex-direction: column;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,16 +4,24 @@
-->
<template>
<div class="routing-demo">
<!-- 故事引入 -->
<div class="story-box">
<div class="story-emoji">📖📄</div>
<h4 class="story-title">小明看书记</h4>
<p class="story-text">
小明喜欢看书有两种看书方式<br>
<strong>MPA 方式像翻书</strong>每翻一页都要换一本书 <strong>SPA 方式像换纸</strong>在同一本书里换内容
</p>
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">📖</span>
<span class="title">路由模式对比</span>
<span class="subtitle">MPA 多页应用 vs SPA 单页应用</span>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 故事引入 -->
<div class="story-box">
<p class="story-text">
<strong>通俗说法</strong>小明喜欢看书有两种看书方式<br>
<strong>MPA 方式像翻书</strong>每翻一页都要换一本书<br>
<strong>SPA 方式像换纸</strong>在同一本书里换内容
</p>
</div>
<!-- 模式选择 -->
<div class="mode-selector">
<div
@@ -23,7 +31,7 @@
>
<div class="mode-icon">📚</div>
<div class="mode-name">MPA 多页应用</div>
<div class="mode-sub">像翻书每次都换一本</div>
<div class="mode-sub">通俗说法: 像翻书</div>
<div class="mode-desc">每点一次链接浏览器向服务器要新页面</div>
</div>
@@ -36,7 +44,7 @@
>
<div class="mode-icon">📄</div>
<div class="mode-name">SPA 单页应用</div>
<div class="mode-sub">像换纸同一本书换内容</div>
<div class="mode-sub">通俗说法: 像换纸</div>
<div class="mode-desc">只加载一次后续只切换内容</div>
</div>
</div>
@@ -185,14 +193,13 @@
</div>
<!-- 核心要点 -->
<div class="key-takeaway">
<div class="takeaway-icon">🎯</div>
<div class="takeaway-content">
<strong>核心差异</strong>
<strong>MPA</strong> 每次切换都要"整页刷新"翻书适合内容为主的网站
<strong>SPA</strong> 只加载一次后续"局部更新"像换纸适合交互复杂的应用
关键是<strong>状态会不会丢</strong>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>
<strong>MPA</strong> 每次切换都要"整页刷新"像翻书适合内容为主的网站
<strong>SPA</strong> 只加载一次后续"局部更新"换纸适合交互复杂的应用
关键是<strong>状态会不会丢</strong>
</div>
</div>
</div>
</template>
@@ -251,7 +258,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
<style scoped>
.routing-demo {
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
@@ -303,7 +310,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
min-width: 200px;
max-width: 280px;
background: white;
border: 3px solid #e0e0e0;
border: 3px solid var(--vp-c-divider);
border-radius: 16px;
padding: 20px;
text-align: center;
@@ -356,7 +363,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.demo-area {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
padding: 20px;
margin-bottom: 24px;
}
@@ -493,7 +500,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
/* 阅读区 */
.reading-paper {
background: white;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
min-height: 200px;
@@ -524,7 +531,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.state-test {
margin-top: 16px;
padding-top: 16px;
border-top: 2px dashed #e0e0e0;
border-top: 2px dashed var(--vp-c-divider);
}
.test-label {
@@ -537,7 +544,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.test-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 13px;
box-sizing: border-box;
@@ -572,7 +579,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.nav-btn {
padding: 8px 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: white;
cursor: pointer;
@@ -632,7 +639,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.comparison-table {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
margin-bottom: 20px;
}
@@ -655,7 +662,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
grid-template-columns: 1fr 1.5fr 1.5fr;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
border-bottom: 1px solid var(--vp-c-divider);
}
.comparison-row:last-child {
@@ -1,141 +1,140 @@
<!--
SliceRequestDemo.vue - 搬家快递大作战
"搬家打包"的比喻来解释 HTTP 请求优化切图 vs 雪碧图
SliceRequestDemo.vue - HTTP请求优化对比
"搬家"的比喻来解释雪碧图 vs 切片请求
-->
<template>
<div class="moving-game">
<!-- 故事引入 -->
<div class="story-box">
<div class="story-emoji">📦🚚🏠</div>
<h4 class="story-title">小明搬家记</h4>
<p class="story-text">
小明要搬 6 箱书到新房子有两种搬家方式<br>
<strong>A 方案一箱一箱搬</strong>切图模式 vs <strong>B 方案一次性打包运走</strong>雪碧图模式<br>
看看哪种更省时间
</p>
<div class="slice-request-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">HTTP请求优化</span>
<span class="subtitle">雪碧图 vs 独立请求</span>
</div>
<!-- 模式选择 -->
<div class="mode-selector">
<div
class="mode-card"
:class="{ active: mode === 'separate' }"
@click="mode = 'separate'"
>
<div class="mode-icon">🛵</div>
<div class="mode-name">A 方案一箱一趟</div>
<div class="mode-desc">小面包车一次拉一箱</div>
<div class="mode-detail">需要 6 趟运输</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 故事引入 -->
<div class="story-box">
<p class="story-text">
<strong>通俗说法</strong>就像搬家<br>
<strong>切图模式</strong>一箱一箱搬需要6趟6次HTTP请求<br>
<strong>雪碧图模式</strong>打包一次性运走只需1趟1次HTTP请求
</p>
</div>
<div class="vs-divider">VS</div>
<div
class="mode-card"
:class="{ active: mode === 'packed' }"
@click="mode = 'packed'"
>
<div class="mode-icon">🚚</div>
<div class="mode-name">B 方案打包一车拉</div>
<div class="mode-desc">大卡车6箱一次运走</div>
<div class="mode-detail">只需 1 趟运输</div>
</div>
</div>
<!-- 动画演示区 -->
<div class="animation-area">
<!-- 起点 -->
<div class="location start">
<div class="location-icon">🏠</div>
<div class="location-label">旧家</div>
<div class="boxes-remaining">
剩余箱子: <span class="count">{{ remainingBoxes }}</span>
</div>
</div>
<!-- 道路 -->
<div class="road">
<div class="road-line"></div>
<!-- 运输车辆 -->
<!-- 模式选择 -->
<div class="mode-selector">
<div
v-for="vehicle in vehicles"
:key="vehicle.id"
class="vehicle"
:class="{ 'moving': vehicle.isMoving }"
:style="{ left: vehicle.position + '%' }"
class="mode-card"
:class="{ active: mode === 'separate' }"
@click="mode = 'separate'"
>
<div class="vehicle-body">
{{ mode === 'separate' ? '🛵' : '🚚' }}
<div class="mode-icon">🛵</div>
<div class="mode-name">切图模式</div>
<div class="mode-desc">通俗说法: 一箱一趟</div>
<div class="mode-detail">需要 6 趟运输</div>
</div>
<div class="vs-divider">VS</div>
<div
class="mode-card"
:class="{ active: mode === 'packed' }"
@click="mode = 'packed'"
>
<div class="mode-icon">🚚</div>
<div class="mode-name">雪碧图模式</div>
<div class="mode-desc">通俗说法: 打包一车拉</div>
<div class="mode-detail">只需 1 趟运输</div>
</div>
</div>
<!-- 动画演示区 -->
<div class="animation-area">
<!-- 起点 -->
<div class="location start">
<div class="location-icon">🏠</div>
<div class="location-label">旧家</div>
<div class="boxes-remaining">
剩余箱子: <span class="count">{{ remainingBoxes }}</span>
</div>
<div class="vehicle-cargo" v-if="vehicle.cargo > 0">
{{ mode === 'separate' ? '📦' : '📦×' + vehicle.cargo }}
</div>
<!-- 道路 -->
<div class="road">
<div class="road-line"></div>
<!-- 运输车辆 -->
<div
v-for="vehicle in vehicles"
:key="vehicle.id"
class="vehicle"
:class="{ 'moving': vehicle.isMoving }"
:style="{ left: vehicle.position + '%' }"
>
<div class="vehicle-body">
{{ mode === 'separate' ? '🛵' : '🚚' }}
</div>
<div class="vehicle-cargo" v-if="vehicle.cargo > 0">
{{ mode === 'separate' ? '📦' : '📦×' + vehicle.cargo }}
</div>
</div>
</div>
<!-- 终点 -->
<div class="location end">
<div class="location-icon">🏡</div>
<div class="location-label">新家</div>
<div class="boxes-delivered">
已送达: <span class="count">{{ deliveredBoxes }}</span>/6
</div>
</div>
</div>
<!-- 终点 -->
<div class="location end">
<div class="location-icon">🏡</div>
<div class="location-label">新家</div>
<div class="boxes-delivered">
已送达: <span class="count">{{ deliveredBoxes }}</span>/6
<!-- 统计面板 -->
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">运输趟数</div>
<div class="stat-value" :class="{ 'good': trips <= 2, 'bad': trips > 2 }">
{{ trips }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ totalTime.toFixed(1) }} </div>
</div>
<div class="stat-item">
<div class="stat-label">效率评分</div>
<div class="stat-value" :class="efficiencyClass">
{{ efficiency }}
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button
class="btn btn-primary"
@click="startSimulation"
:disabled="isRunning"
>
{{ isRunning ? '运输中...' : '开始搬家' }}
</button>
<button
class="btn btn-secondary"
@click="resetSimulation"
>
重置
</button>
</div>
</div>
<!-- 统计面板 -->
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">运输趟数</div>
<div class="stat-value" :class="{ 'good': trips <= 2, 'bad': trips > 2 }">
{{ trips }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ totalTime.toFixed(1) }} </div>
</div>
<div class="stat-item">
<div class="stat-label">效率评分</div>
<div class="stat-value" :class="efficiencyClass">
{{ efficiency }}
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button
class="btn btn-primary"
@click="startSimulation"
:disabled="isRunning"
>
{{ isRunning ? '运输中...' : '开始搬家' }}
</button>
<button
class="btn btn-secondary"
@click="resetSimulation"
>
重置
</button>
</div>
<!-- 知识点总结 -->
<div class="knowledge-box">
<div class="knowledge-title">💡 核心原理</div>
<div class="knowledge-content">
<p v-if="mode === 'separate'">
<strong>切图模式分开请求</strong>就像一箱一箱搬每次只拉一件货
浏览器要发起 6 HTTP 请求每次都要建立连接传输数据
<span class="highlight-bad">效率低耗时长</span>
</p>
<p v-else>
<strong>雪碧图模式合并请求</strong>就像用大卡车一次性拉走所有箱子
浏览器只需 1 HTTP 请求就能获取所有图片
<span class="highlight-good">大幅减少连接开销速度更快</span>
</p>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>
<span v-if="mode === 'separate'">切图模式每次只拉一件货,需要6次HTTP请求,效率低</span>
<span v-else>雪碧图模式打包一次性运走,只需1次HTTP请求,大幅减少连接开销</span>
</div>
</div>
</template>
@@ -254,40 +253,56 @@ const resetStats = () => {
</script>
<style scoped>
.moving-game {
border: 2px solid #e8e8e8;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
.slice-request-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 标题区 */
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
/* 主内容区 */
.demo-content {
margin-bottom: 0.75rem;
}
/* 故事框 */
.story-box {
text-align: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, #ffecb3);
border-radius: 16px;
border: 2px dashed #ffc107;
}
.story-emoji {
font-size: 48px;
margin-bottom: 8px;
}
.story-title {
font-size: 20px;
font-weight: bold;
color: #8b4513;
margin: 0 0 8px 0;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.story-text {
font-size: 14px;
color: #666;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin: 0;
line-height: 1.6;
}
@@ -297,67 +312,67 @@ const resetStats = () => {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.mode-card {
background: white;
border: 3px solid #e0e0e0;
border-radius: 16px;
padding: 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
transition: all 0.2s;
min-width: 160px;
flex: 1;
max-width: 280px;
max-width: 220px;
}
.mode-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.mode-card.active {
border-color: #4caf50;
background: #e8f5e9;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.mode-icon {
font-size: 48px;
margin-bottom: 12px;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.mode-name {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
margin-bottom: 8px;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.mode-desc {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.mode-detail {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #e65100;
padding: 4px 12px;
background: #fff3e0;
color: var(--vp-c-brand);
padding: 0.25rem 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 12px;
display: inline-block;
}
.vs-divider {
font-size: 24px;
font-size: 1.25rem;
font-weight: bold;
color: #999;
padding: 0 8px;
color: var(--vp-c-text-3);
padding: 0 0.5rem;
}
/* 动画演示区 */
@@ -365,52 +380,52 @@ const resetStats = () => {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
gap: 1rem;
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.location {
text-align: center;
min-width: 100px;
min-width: 80px;
}
.location-icon {
font-size: 40px;
margin-bottom: 8px;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.location-label {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #333;
margin-bottom: 8px;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.boxes-remaining,
.boxes-delivered {
font-size: 12px;
color: #666;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.count {
font-weight: bold;
color: #e65100;
font-size: 16px;
color: var(--vp-c-brand);
font-size: 0.9rem;
}
.road {
flex: 1;
position: relative;
height: 80px;
background: linear-gradient(to bottom, #e8eaf6 0%, #c5cae9 100%);
border-radius: 8px;
height: 60px;
background: var(--vp-c-bg-alt);
border-radius: 6px;
overflow: hidden;
}
@@ -422,8 +437,8 @@ const resetStats = () => {
height: 4px;
background: repeating-linear-gradient(
90deg,
#7986cb 0px,
#7986cb 20px,
var(--vp-c-brand) 0px,
var(--vp-c-brand) 20px,
transparent 20px,
transparent 40px
);
@@ -441,89 +456,86 @@ const resetStats = () => {
}
.vehicle-body {
font-size: 32px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
font-size: 1.5rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.vehicle-cargo {
font-size: 12px;
background: white;
padding: 2px 6px;
border-radius: 8px;
margin-top: 2px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
font-size: 0.75rem;
background: var(--vp-c-bg);
padding: 0.125rem 0.375rem;
border-radius: 6px;
margin-top: 0.125rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-weight: bold;
color: #e65100;
color: var(--vp-c-brand);
}
/* 统计面板 */
.stats-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
gap: 1rem;
margin-bottom: 1rem;
}
.stat-item {
background: white;
border-radius: 12px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
text-align: center;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
}
.stat-label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 24px;
font-size: 1.25rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.stat-value.good {
color: #4caf50;
color: var(--vp-c-success);
}
.stat-value.bad {
color: #f44336;
color: var(--vp-c-danger);
}
.stat-value.excellent {
color: #2196f3;
}
.stat-value.good {
color: #4caf50;
color: var(--vp-c-brand);
}
.stat-value.poor {
color: #ff9800;
color: var(--vp-c-warning);
}
/* 控制按钮 */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.btn {
padding: 12px 24px;
padding: 0.5rem 1rem;
border: none;
border-radius: 8px;
font-size: 16px;
border-radius: 6px;
font-size: 0.9rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:disabled {
@@ -532,48 +544,32 @@ const resetStats = () => {
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
/* 知识点总结 */
.knowledge-box {
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
border-radius: 12px;
padding: 20px;
border-left: 4px solid #2196f3;
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.knowledge-title {
font-size: 16px;
font-weight: bold;
color: #1565c0;
margin-bottom: 12px;
.info-box .icon {
flex-shrink: 0;
}
.knowledge-content {
font-size: 14px;
line-height: 1.6;
color: #333;
}
.knowledge-content p {
margin: 0;
}
.highlight-good {
color: #4caf50;
font-weight: bold;
}
.highlight-bad {
color: #f44336;
font-weight: bold;
.info-box strong {
color: var(--vp-c-text-1);
}
/* 响应式 */
@@ -588,7 +584,7 @@ const resetStats = () => {
.animation-area {
flex-direction: column;
gap: 12px;
gap: 0.75rem;
}
.road {
@@ -1,6 +1,13 @@
<template>
<div class="three-areas-demo">
<div class="scene">
<div class="demo-header">
<span class="icon">📂</span>
<span class="title">Git 三区概念</span>
<span class="subtitle">工作区 暂存区 仓库</span>
</div>
<div class="demo-content">
<div class="scene">
<!-- 1. Working Directory (Desk) -->
<div class="zone working">
<div class="zone-header">
@@ -121,6 +128,7 @@
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="block">
@@ -132,6 +140,11 @@
<pre class="mono"><code>{{ statusText }}</code></pre>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>Git 的三区就像餐厅工作区是餐桌随便放暂存区是备菜盘准备上菜仓库是菜单永久记录
</div>
</div>
</template>
@@ -239,11 +252,40 @@ const commitFiles = () => {
.three-areas-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow-x: auto;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.scene {
@@ -641,4 +683,23 @@ const commitFiles = () => {
grid-template-columns: 1fr;
}
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,134 +1,136 @@
<!--
GitWorkflowDemo.vue
Git 工作流演示 - 简洁
Git 基础工作流演示 - 寄快递
用途展示 Git 的基本工作流程
交互初始化提交创建分支合并
展示 Git 的基本工作流程修改 暂存 提交
高度控制紧凑布局确保在 600px
-->
<template>
<div class="git-workflow-demo">
<!-- 控制面板 -->
<div class="control-panel">
<button
@click="initRepo"
:disabled="inited || mergePending"
class="action-btn"
>
🎯 初始化仓库
</button>
<button
@click="makeCommit"
:disabled="!inited || mergePending"
class="action-btn"
>
提交
</button>
<button
@click="createBranch"
:disabled="!inited || hasBranch || mergePending"
class="action-btn"
>
🌿 创建分支
</button>
<button
@click="prepareMerge"
:disabled="!hasBranch || mergePending"
class="action-btn"
>
🔀 准备合并
</button>
<button @click="finishMerge" :disabled="!mergePending" class="action-btn">
完成合并
</button>
<button @click="reset" class="action-btn secondary">🔄 重置</button>
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">Git 工作流</span>
<span class="subtitle">修改 暂存 提交三步走</span>
</div>
<!-- 提交历史可视化 -->
<div class="visualization">
<div class="graph-container">
<svg viewBox="0 0 400 150" class="git-graph">
<!-- 主分支线 -->
<line
x1="50"
y1="60"
x2="350"
y2="60"
stroke="#3b82f6"
stroke-width="3"
/>
<div class="demo-content">
<!-- 文件状态区域 -->
<div class="file-area">
<div class="area-header">
<span class="area-icon">📝</span>
<span class="area-name">工作区</span>
<span class="area-desc">你正在改的文件</span>
</div>
<div class="file-list">
<div
v-for="file in files"
:key="file.name"
class="file-item"
:class="{
'modified': file.status === 'modified',
'staged': file.status === 'staged',
'committed': file.status === 'committed'
}"
>
<span class="file-icon">{{ getIcon(file.status) }}</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-status">{{ getStatusText(file.status) }}</span>
</div>
</div>
</div>
<!-- 分支线 -->
<line
v-if="hasBranch"
x1="150"
y1="60"
x2="150"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<line
v-if="hasBranch"
x1="150"
y1="100"
x2="300"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<!-- 箭头 -->
<div class="arrow-group" v-if="!allCommitted">
<div class="arrow" :class="{ active: hasStaged }"></div>
<div class="arrow-label" v-if="hasStaged">git add</div>
</div>
<!-- 合并线 -->
<path
v-if="mergePending"
d="M 300 100 Q 320 80, 320 60"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
stroke-dasharray="5,5"
/>
<!-- 暂存区 -->
<div class="stage-area">
<div class="area-header">
<span class="area-icon">📋</span>
<span class="area-name">暂存区</span>
<span class="area-desc">准备打包的文件</span>
</div>
<div class="file-list">
<div
v-for="file in stagedFiles"
:key="file.name"
class="file-item staged"
>
<span class="file-icon">📌</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-status">待提交</span>
</div>
<div v-if="stagedFiles.length === 0" class="empty-tip">
暂无文件
</div>
</div>
</div>
<!-- 提交节点 -->
<circle
v-for="(commit, i) in mainCommits"
:key="'main-' + i"
:cx="80 + i * 60"
cy="60"
r="10"
fill="#3b82f6"
/>
<circle
v-for="(commit, i) in branchCommits"
:key="'branch-' + i"
:cx="200 + i * 60"
cy="100"
r="10"
fill="#10b981"
/>
</svg>
<!-- 箭头 -->
<div class="arrow-group" v-if="hasStaged">
<div class="arrow active"></div>
<div class="arrow-label">git commit</div>
</div>
<!-- 仓库区 -->
<div class="repo-area">
<div class="area-header">
<span class="area-icon">🏪</span>
<span class="area-name">仓库</span>
<span class="area-desc">已保存的版本</span>
</div>
<div class="commit-list">
<div
v-for="(commit, i) in commits"
:key="i"
class="commit-item"
>
<span class="commit-icon"></span>
<span class="commit-msg">{{ commit.msg }}</span>
</div>
<div v-if="commits.length === 0" class="empty-tip">
暂无提交
</div>
</div>
</div>
</div>
<!-- 状态信息 -->
<div class="status-panel">
<div class="status-item">
<span class="label">提交数:</span>
<span class="value">{{ mainCommits.length }}</span>
</div>
<div class="status-item">
<span class="label">分支:</span>
<span class="value">{{ hasBranch ? '2' : '1' }}</span>
</div>
<div class="status-item">
<span class="label">状态:</span>
<span class="value">{{ status }}</span>
</div>
<!-- 操作按钮 -->
<div class="action-panel">
<button
@click="modifyFile"
class="action-btn"
:disabled="allModified"
>
修改文件
</button>
<button
@click="stageFiles"
class="action-btn"
:disabled="!hasModified || allStaged"
>
📌 暂存修改
</button>
<button
@click="commitFiles"
class="action-btn"
:disabled="!hasStaged"
>
提交版本
</button>
<button
@click="reset"
class="action-btn secondary"
>
🔄 重置
</button>
</div>
<!-- 说明 -->
<div class="info-box">
<p>
<strong>💡 工作流程:</strong> 初始化 提交 创建分支 开发 合并
</p>
<span class="icon">💡</span>
<strong>核心思想</strong>工作区修改 暂存区挑选 仓库永久保存
</div>
</div>
</template>
@@ -136,56 +138,92 @@
<script setup>
import { ref, computed } from 'vue'
const inited = ref(false)
const hasBranch = ref(false)
const mergePending = ref(false)
const mainCommits = ref([])
const branchCommits = ref([])
const files = ref([
{ name: 'index.html', status: 'unmodified' },
{ name: 'app.js', status: 'unmodified' },
{ name: 'style.css', status: 'unmodified' }
])
const status = computed(() => {
if (mergePending.value) return '准备合并:检查改动/解决冲突后再完成合并'
if (hasBranch) return '分支已创建'
if (inited) return '已初始化'
return '未初始化'
})
const commits = ref([])
const initRepo = () => {
inited.value = true
mainCommits.value = [{ hash: 'abc123' }]
}
const hasModified = computed(() =>
files.value.some(f => f.status === 'modified')
)
const makeCommit = () => {
if (inited.value) {
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
const hasStaged = computed(() =>
files.value.some(f => f.status === 'staged')
)
const allCommitted = computed(() =>
files.value.every(f => f.status === 'committed')
)
const allModified = computed(() =>
files.value.every(f => f.status === 'modified')
)
const allStaged = computed(() =>
files.value.every(f => f.status === 'staged' || f.status === 'committed')
)
const stagedFiles = computed(() =>
files.value.filter(f => f.status === 'staged')
)
const getIcon = (status) => {
switch (status) {
case 'modified': return '📝'
case 'staged': return '📌'
case 'committed': return '✅'
default: return '📄'
}
}
const createBranch = () => {
if (inited.value && !hasBranch.value) {
hasBranch.value = true
branchCommits.value = [{ hash: 'def456' }]
const getStatusText = (status) => {
switch (status) {
case 'modified': return '已修改'
case 'staged': return '已暂存'
case 'committed': return '已提交'
default: return '未修改'
}
}
const prepareMerge = () => {
if (!hasBranch.value) return
mergePending.value = true
const modifyFile = () => {
const unmodified = files.value.filter(f => f.status === 'unmodified' || f.status === 'committed')
if (unmodified.length > 0) {
const file = unmodified[0]
file.status = 'modified'
}
}
const finishMerge = () => {
if (!mergePending.value) return
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
hasBranch.value = false
branchCommits.value = []
mergePending.value = false
const stageFiles = () => {
files.value.forEach(f => {
if (f.status === 'modified') {
f.status = 'staged'
}
})
}
const commitFiles = () => {
const staged = files.value.filter(f => f.status === 'staged')
if (staged.length > 0) {
files.value.forEach(f => {
if (f.status === 'staged') {
f.status = 'committed'
}
})
commits.value.push({
msg: `提交了 ${staged.length} 个文件`,
files: staged.map(f => f.name)
})
}
}
const reset = () => {
inited.value = false
hasBranch.value = false
mergePending.value = false
mainCommits.value = []
branchCommits.value = []
files.value.forEach(f => {
f.status = 'unmodified'
})
commits.value = []
}
</script>
@@ -193,94 +231,230 @@ const reset = () => {
.git-workflow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 550px;
overflow-y: auto;
}
.control-panel {
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.area-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.area-icon {
font-size: 1.1rem;
}
.area-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.area-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-left: 0.5rem;
}
.file-area,
.stage-area,
.repo-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.file-list,
.commit-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 60px;
}
.file-item,
.commit-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.85rem;
}
.file-item.modified {
background: var(--vp-c-bg-alt);
border-left: 3px solid var(--vp-c-warning);
}
.file-item.staged {
background: var(--vp-c-brand-soft);
border-left: 3px solid var(--vp-c-brand);
}
.file-item.committed {
background: var(--vp-c-bg-alt);
border-left: 3px solid var(--vp-c-success);
opacity: 0.7;
}
.file-icon {
font-size: 1rem;
}
.file-name {
flex: 1;
color: var(--vp-c-text-1);
}
.file-status {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.commit-item {
font-size: 0.8rem;
}
.commit-icon {
font-size: 0.9rem;
}
.commit-msg {
flex: 1;
color: var(--vp-c-text-2);
}
.empty-tip {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
padding: 0.5rem;
}
.arrow-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0;
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.arrow.active {
color: var(--vp-c-brand);
transform: scale(1.2);
}
.arrow-label {
font-size: 0.7rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.action-panel {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
padding: 0.5rem 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex: 1;
min-width: 100px;
}
.action-btn:hover:not(:disabled) {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
opacity: 0.9;
transform: translateY(-1px);
}
.action-btn:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
border-color: var(--vp-c-divider);
color: var(--vp-c-text-2);
background: var(--vp-c-divider);
}
.action-btn.secondary {
border-color: var(--vp-c-divider);
}
.visualization {
margin: 1.5rem 0;
}
.graph-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
background: transparent;
border: 1px solid var(--vp-c-divider);
}
.git-graph {
width: 100%;
height: auto;
}
.status-panel {
display: flex;
gap: 2rem;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.status-item {
display: flex;
gap: 0.5rem;
}
.status-item .label {
color: var(--vp-c-text-2);
}
.status-item .value {
font-weight: 600;
.action-btn.secondary:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box p {
margin: 0;
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
line-height: 1.6;
}
</style>
@@ -4,9 +4,10 @@
-->
<template>
<div class="coupling-demo">
<div class="header">
<div class="title">系统解耦从紧耦合到松耦合</div>
<div class="subtitle">观察同步调用与异步消息的区别</div>
<div class="demo-header">
<span class="icon">🔗</span>
<span class="title">系统解耦</span>
<span class="subtitle">从紧耦合到松耦合</span>
</div>
<div class="mode-switch">
@@ -26,7 +27,7 @@
</button>
</div>
<div class="demo-container">
<div class="demo-content">
<!-- 紧耦合模式 -->
<div v-if="!useAsync" class="synchronous-mode">
<div class="scenario">
@@ -65,7 +66,7 @@
<div class="problem-list">
<div class="problem-item">
<span class="icon"></span>
<span><strong>依赖性强</strong>通知服务宕机订单创建失败</span>
<span><strong>依赖性强</strong>通知服务宕机,订单创建失败</span>
</div>
<div class="problem-item">
<span class="icon"></span>
@@ -111,7 +112,7 @@
<div class="consumer-box" :class="{ failed: consumerFailed }">
<div class="consumer-name">短信服务</div>
<div class="consumer-status">
{{ consumerFailed ? '离线不影响订单' : '运行中' }}
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
</div>
</div>
<div class="consumer-box">
@@ -122,10 +123,6 @@
<div class="consumer-name">积分服务</div>
<div class="consumer-status">运行中</div>
</div>
<div class="consumer-box new">
<div class="consumer-name">数据分析</div>
<div class="consumer-status">新增 </div>
</div>
</div>
</div>
@@ -137,7 +134,7 @@
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>响应快</strong>订单服务只耗时 50ms发送消息</span
><strong>响应快</strong>订单服务只耗时 50ms(发送消息)</span
>
</div>
<div class="benefit-item">
@@ -153,41 +150,9 @@
</div>
</div>
<div class="comparison-summary">
<div class="summary-title">📊 对比总结</div>
<div class="summary-table">
<table>
<thead>
<tr>
<th>维度</th>
<th>紧耦合 (同步)</th>
<th>松耦合 (异步)</th>
</tr>
</thead>
<tbody>
<tr>
<td>服务依赖</td>
<td class="bad">强依赖一个挂全挂</td>
<td class="good">弱依赖独立运行</td>
</tr>
<tr>
<td>响应时间</td>
<td class="bad">1200ms串行执行</td>
<td class="good">50ms只发消息</td>
</tr>
<tr>
<td>扩展性</td>
<td class="bad">修改订单服务代码</td>
<td class="good">增加新消费者即可</td>
</tr>
<tr>
<td>可用性</td>
<td class="bad">90%任一服务故障</td>
<td class="good">99.9%独立故障域</td>
</tr>
</tbody>
</table>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>同步调用强依赖响应慢;异步消息解耦响应快易扩展
</div>
</div>
</template>
@@ -203,7 +168,7 @@ const messageInQueue = ref(false)
const syncCalls = ref([
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
{ id: 3, service: '调用通知服务', active: false, status: '失败订单回滚' }
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
])
const testSyncCall = () => {
@@ -232,42 +197,52 @@ const testAsyncCall = () => {
<style scoped>
.coupling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.mode-switch {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.mode-btn {
flex: 1;
padding: 0.75rem 1rem;
padding: 0.5rem 0.75rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
@@ -277,18 +252,18 @@ const testAsyncCall = () => {
.mode-btn.active {
background: var(--vp-c-brand);
color: #fff;
color: white;
border-color: var(--vp-c-brand);
}
.demo-container {
margin-bottom: 1.5rem;
.demo-content {
margin-bottom: 0.75rem;
}
.scenario-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
margin-bottom: 0.75rem;
text-align: center;
}
@@ -296,55 +271,55 @@ const testAsyncCall = () => {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 10px;
margin-bottom: 1rem;
border-radius: 8px;
margin-bottom: 0.75rem;
}
.service-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
min-width: 180px;
min-width: 140px;
transition: all 0.3s;
}
.service-box.failed {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
border-color: var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.service-name {
font-weight: 600;
font-size: 0.95rem;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.service-desc {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.error-msg {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #ef4444;
background: var(--vp-c-danger);
color: white;
border-radius: 4px;
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
}
.arrows {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
width: 100%;
max-width: 300px;
max-width: 250px;
}
.sync-call {
@@ -357,7 +332,7 @@ const testAsyncCall = () => {
}
.sync-call.active {
background: rgba(239, 68, 68, 0.1);
background: var(--vp-c-danger-soft);
}
.call-line {
@@ -367,96 +342,91 @@ const testAsyncCall = () => {
}
.sync-call.active .call-line {
background: #ef4444;
background: var(--vp-c-danger);
}
.call-label {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
flex: 1;
}
.call-status {
font-size: 0.75rem;
color: #ef4444;
font-size: 0.7rem;
color: var(--vp-c-danger);
font-weight: 600;
}
.mq-bridge {
display: flex;
align-items: center;
gap: 1rem;
gap: 0.75rem;
}
.mq-box {
background: rgba(59, 130, 246, 0.1);
background: var(--vp-c-brand-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
min-width: 140px;
min-width: 120px;
}
.mq-icon {
font-size: 2rem;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.mq-label {
font-weight: 600;
font-size: 0.9rem;
font-size: 0.85rem;
}
.msg-indicator {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #dcfce7;
color: #166534;
background: var(--vp-c-success);
color: white;
border-radius: 4px;
font-size: 0.75rem;
font-size: 0.7rem;
font-weight: 600;
}
.flow-arrow {
font-size: 1.5rem;
font-size: 1.25rem;
color: var(--vp-c-brand);
}
.consumers-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 0.5rem;
width: 100%;
max-width: 500px;
max-width: 400px;
}
.consumer-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
text-align: center;
transition: all 0.3s;
}
.consumer-box.failed {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.consumer-box.new {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
border-color: var(--vp-c-warning);
background: var(--vp-c-warning-soft);
}
.consumer-name {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.consumer-status {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
}
@@ -464,8 +434,8 @@ const testAsyncCall = () => {
.benefit-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.problem-item,
@@ -473,92 +443,66 @@ const testAsyncCall = () => {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.5;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
line-height: 1.4;
}
.problem-item {
background: rgba(239, 68, 68, 0.1);
background: var(--vp-c-danger-soft);
}
.benefit-item {
background: rgba(34, 197, 94, 0.1);
background: var(--vp-c-success-soft);
}
.icon {
font-size: 1.2rem;
font-size: 1rem;
flex-shrink: 0;
}
.test-btn {
width: 100%;
padding: 0.75rem;
padding: 0.5rem;
border: none;
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
.test-btn.fail {
background: #ef4444;
background: var(--vp-c-danger);
color: white;
}
.test-btn.fail:hover {
background: #dc2626;
opacity: 0.9;
}
.test-btn.success {
background: #22c55e;
background: var(--vp-c-success);
color: white;
}
.test-btn.success:hover {
background: #16a34a;
opacity: 0.9;
}
.comparison-summary {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.summary-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.summary-table {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
th {
background: var(--vp-c-bg-soft);
font-weight: 600;
}
.bad {
color: #ef4444;
}
.good {
color: #16a34a;
.info-box .icon {
flex-shrink: 0;
}
</style>
@@ -4,133 +4,132 @@
-->
<template>
<div class="dlq-demo">
<div class="header">
<div class="title">死信队列消息的"急救站"</div>
<div class="subtitle">处理无法消费的消息避免阻塞队列</div>
<div class="demo-header">
<span class="icon">🚑</span>
<span class="title">死信队列</span>
<span class="subtitle">消息的"急救站" - 处理失败消息</span>
</div>
<div class="controls">
<div class="control">
<label>失败率</label>
<input v-model="failureRate" type="range" min="0" max="100" step="10" />
<input v-model.number="failureRate" type="range" min="0" max="100" step="10" />
<span class="value">{{ failureRate }}%</span>
</div>
<div class="control">
<label>最大重试次数</label>
<input v-model="maxRetries" type="range" min="1" max="5" step="1" />
<label>最大重试</label>
<input v-model.number="maxRetries" type="range" min="1" max="5" step="1" />
<span class="value">{{ maxRetries }}</span>
</div>
</div>
<div class="flow-container">
<div class="main-queue-section">
<div class="section-title">📦 主队列</div>
<div class="queue-box main-queue">
<div class="queue-header">
<span>正常消息队列</span>
<span class="count">{{ mainQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in mainQueue"
:key="msg.id"
class="message-item"
:class="{ processing: msg.processing }"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-retries" v-if="msg.retries > 0">
重试: {{ msg.retries }}/{{ maxRetries }}
<div class="demo-content">
<div class="flow-container">
<div class="main-queue-section">
<div class="section-title">📦 主队列</div>
<div class="queue-box main-queue">
<div class="queue-header">
<span>正常消息队列</span>
<span class="count">{{ mainQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in mainQueue.slice(0, 3)"
:key="msg.id"
class="message-item"
:class="{ processing: msg.processing }"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-retries" v-if="msg.retries > 0">
重试: {{ msg.retries }}/{{ maxRetries }}
</div>
</div>
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
<div v-else-if="mainQueue.length > 3" class="more">
还有 {{ mainQueue.length - 3 }} ...
</div>
</div>
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
</div>
<button class="add-btn" @click="addMessage" :disabled="processing">
+ 添加消息
</button>
</div>
<button class="add-btn" @click="addMessage" :disabled="processing">
+ 添加消息
</button>
</div>
<div class="processing-section">
<div class="section-title"> 消费处理</div>
<div class="processor-box">
<div class="processor-icon" :class="{ active: processing }">
{{ processing ? '⚙️' : '💤' }}
</div>
<div class="processor-status">
{{ processing ? '处理中...' : '空闲' }}
</div>
<div v-if="currentMessage" class="current-msg">
处理: #{{ currentMessage.id }}
</div>
<div v-if="lastResult" class="last-result" :class="lastResult.type">
{{ lastResult.message }}
</div>
</div>
</div>
<div class="dlq-section">
<div class="section-title"> 死信队列</div>
<div class="queue-box dead-letter">
<div class="queue-header">
<span>失败消息</span>
<span class="count">{{ deadLetterQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in deadLetterQueue"
:key="msg.id"
class="message-item failed"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-error">{{ msg.error }}</div>
<div class="processing-section">
<div class="section-title"> 消费处理</div>
<div class="processor-box">
<div class="processor-icon" :class="{ active: processing }">
{{ processing ? '⚙️' : '💤' }}
</div>
<div v-if="deadLetterQueue.length === 0" class="empty">
无失败消息
<div class="processor-status">
{{ processing ? '处理中...' : '空闲' }}
</div>
<div v-if="currentMessage" class="current-msg">
处理: #{{ currentMessage.id }}
</div>
<div v-if="lastResult" class="last-result" :class="lastResult.type">
{{ lastResult.message }}
</div>
</div>
</div>
<button
class="retry-btn"
@click="retryDeadLetters"
:disabled="deadLetterQueue.length === 0"
>
🔄 重试死信
</button>
<div class="dlq-section">
<div class="section-title"> 死信队列</div>
<div class="queue-box dead-letter">
<div class="queue-header">
<span>失败消息</span>
<span class="count">{{ deadLetterQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in deadLetterQueue.slice(0, 2)"
:key="msg.id"
class="message-item failed"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-error">{{ msg.error }}</div>
</div>
<div v-if="deadLetterQueue.length === 0" class="empty">
无失败消息
</div>
<div v-else-if="deadLetterQueue.length > 2" class="more">
还有 {{ deadLetterQueue.length - 2 }} ...
</div>
</div>
</div>
<button
class="retry-btn"
@click="retryDeadLetters"
:disabled="deadLetterQueue.length === 0"
>
🔄 重试死信
</button>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">总消息数</div>
<div class="stat-value">{{ totalMessages }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">成功处理</div>
<div class="stat-value">{{ successCount }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">进入死信</div>
<div class="stat-value">{{ deadLetterCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">成功率</div>
<div class="stat-value">{{ successRate }}%</div>
</div>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">总消息数</div>
<div class="stat-value">{{ totalMessages }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">成功处理</div>
<div class="stat-value">{{ successCount }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">进入死信</div>
<div class="stat-value">{{ deadLetterCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">成功率</div>
<div class="stat-value">{{ successRate }}%</div>
</div>
</div>
<div class="explanation">
<div class="exp-title">💡 死信队列的作用</div>
<div class="exp-content">
<div class="exp-item">
<strong>1. 隔离异常消息</strong>失败消息不会阻塞正常消息的处理
</div>
<div class="exp-item">
<strong>2. 保留失败记录</strong>可以后续人工介入或自动重试
</div>
<div class="exp-item">
<strong>3. 系统保护</strong>避免因持续失败导致消费者崩溃
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>失败消息进入死信队列,避免阻塞正常消息,可后续人工介入或自动重试
</div>
</div>
</template>
@@ -174,7 +173,7 @@ const processNext = () => {
return
}
let msg = mainQueue[0]
let msg = mainQueue.value[0]
msg.processing = true
processing.value = true
currentMessage.value = msg
@@ -188,7 +187,7 @@ const processNext = () => {
msg.processing = false
if (msg.retries >= maxRetries.value) {
//
// ,
mainQueue.value.shift()
deadLetterQueue.value.push({
id: msg.id,
@@ -202,7 +201,7 @@ const processNext = () => {
//
lastResult.value = {
type: 'warning',
message: `⚠️ 消息 #${msg.id} 处理失败重试 ${msg.retries}/${maxRetries.value}`
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
}
}
@@ -262,39 +261,48 @@ addMessage = addMessageWithAutoProcess
.dlq-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);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
font-size: 0.85rem;
}
.control input[type='range'] {
@@ -307,25 +315,29 @@ addMessage = addMessageWithAutoProcess
text-align: right;
}
.demo-content {
margin-bottom: 0.75rem;
}
.flow-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.section-title {
font-size: 0.85rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.75rem;
margin-bottom: 0.5rem;
}
.queue-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
border-radius: 8px;
overflow: hidden;
}
@@ -334,7 +346,7 @@ addMessage = addMessageWithAutoProcess
}
.queue-box.dead-letter {
border-color: #ef4444;
border-color: var(--vp-c-danger);
}
.queue-header {
@@ -343,12 +355,12 @@ addMessage = addMessageWithAutoProcess
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
}
.message-list {
max-height: 200px;
max-height: 150px;
overflow-y: auto;
padding: 0.5rem;
}
@@ -361,17 +373,17 @@ addMessage = addMessageWithAutoProcess
background: var(--vp-c-bg-soft);
border-radius: 6px;
margin-bottom: 0.4rem;
font-size: 0.8rem;
font-size: 0.75rem;
}
.message-item.processing {
border: 1px solid #f59e0b;
background: rgba(245, 158, 11, 0.1);
border: 1px solid var(--vp-c-warning);
background: var(--vp-c-warning-soft);
}
.message-item.failed {
border: 1px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.msg-id {
@@ -379,32 +391,32 @@ addMessage = addMessageWithAutoProcess
}
.msg-retries {
font-size: 0.7rem;
color: #f59e0b;
font-size: 0.65rem;
color: var(--vp-c-warning);
}
.msg-error {
font-size: 0.7rem;
color: #ef4444;
font-size: 0.65rem;
color: var(--vp-c-danger);
}
.empty {
.empty, .more {
text-align: center;
padding: 1.5rem;
padding: 1rem 0.5rem;
color: var(--vp-c-text-3);
font-size: 0.85rem;
font-size: 0.75rem;
}
.add-btn,
.retry-btn {
width: 100%;
padding: 0.6rem;
padding: 0.5rem;
border: none;
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
margin-top: 0.75rem;
font-size: 0.8rem;
margin-top: 0.5rem;
transition: all 0.2s;
}
@@ -414,8 +426,7 @@ addMessage = addMessageWithAutoProcess
}
.add-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
opacity: 0.9;
}
.add-btn:disabled {
@@ -424,21 +435,21 @@ addMessage = addMessageWithAutoProcess
}
.retry-btn {
background: #f59e0b;
background: var(--vp-c-warning);
color: white;
}
.retry-btn:hover:not(:disabled) {
background: #d97706;
opacity: 0.8;
}
.processor-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.5rem;
border-radius: 8px;
padding: 1rem;
text-align: center;
min-height: 200px;
min-height: 150px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -446,8 +457,8 @@ addMessage = addMessageWithAutoProcess
}
.processor-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.processor-icon.active {
@@ -455,97 +466,87 @@ addMessage = addMessageWithAutoProcess
}
.processor-status {
font-size: 0.9rem;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.current-msg {
font-size: 0.85rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.last-result {
font-size: 0.8rem;
font-size: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
margin-top: 0.5rem;
}
.last-result.success {
background: #dcfce7;
color: #166534;
background: var(--vp-c-success);
color: white;
}
.last-result.warning {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
background: var(--vp-c-warning-soft);
color: var(--vp-c-warning-dark);
}
.last-result.error {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
background: var(--vp-c-danger-soft);
color: var(--vp-c-danger-dark);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.stat-card.success {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.05);
border-color: var(--vp-c-success);
background: var(--vp-c-success-soft);
}
.stat-card.warning {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.05);
border-color: var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.stat-label {
font-size: 0.8rem;
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.stat-value {
font-size: 1.3rem;
font-size: 1.1rem;
font-weight: 700;
}
.explanation {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.exp-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.exp-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.exp-item {
font-size: 0.9rem;
line-height: 1.5;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
@keyframes spin {
@@ -0,0 +1,522 @@
<!--
DecouplingDemo.vue
系统解耦演示 - 同步 vs 异步对比
-->
<template>
<div class="decoupling-demo">
<div class="demo-header">
<span class="icon">🔗</span>
<span class="title">系统解耦演示</span>
<span class="subtitle">从紧耦合到松耦合的演进</span>
</div>
<div class="mode-switch">
<button
class="mode-btn"
:class="{ active: !useAsync }"
@click="useAsync = false"
>
🔗 紧耦合 (同步)
</button>
<button
class="mode-btn"
:class="{ active: useAsync }"
@click="useAsync = true"
>
🔓 松耦合 (异步)
</button>
</div>
<div class="demo-content">
<!-- 紧耦合模式 -->
<div v-if="!useAsync" class="synchronous-mode">
<div class="scenario">
<div class="scenario-title"> 紧耦合的致命问题</div>
<div class="flow-diagram">
<div class="service-box order">
<div class="service-name">订单服务</div>
<div class="service-desc">创建订单</div>
</div>
<div class="arrows">
<div
v-for="call in syncCalls"
:key="call.id"
class="sync-call"
:class="{ active: call.active }"
>
<div class="call-line"></div>
<div class="call-label">{{ call.service }}</div>
<div v-if="call.active" class="call-status">
{{ call.status }}
</div>
</div>
</div>
<div
class="service-box notification"
:class="{ failed: notificationFailed }"
>
<div class="service-name">通知服务</div>
<div class="service-desc">发送短信/邮件</div>
<div v-if="notificationFailed" class="error-msg">
服务宕机
</div>
</div>
</div>
<div class="problem-list">
<div class="problem-item">
<span class="icon"></span>
<span
><strong>依赖性强</strong>通知服务宕机,订单创建失败</span
>
</div>
<div class="problem-item">
<span class="icon"></span>
<span
><strong>响应慢</strong>总耗时 = 300ms + 500ms + 400ms =
1200ms</span
>
</div>
<div class="problem-item">
<span class="icon"></span>
<span
><strong>扩展难</strong>增加新服务需要修改订单代码</span
>
</div>
</div>
<button class="test-btn fail" @click="testSyncCall">
模拟通知服务故障
</button>
</div>
</div>
<!-- 松耦合模式 -->
<div v-else class="asynchronous-mode">
<div class="scenario">
<div class="scenario-title"> 松耦合的核心优势</div>
<div class="flow-diagram">
<div class="service-box order">
<div class="service-name">订单服务</div>
<div class="service-desc">创建订单 + 发送消息</div>
</div>
<div class="mq-bridge">
<div class="mq-box">
<div class="mq-icon">📨</div>
<div class="mq-label">消息队列</div>
<div v-if="messageInQueue" class="msg-indicator">
消息已发送
</div>
</div>
<div class="flow-arrow"></div>
</div>
<div class="consumers-group">
<div class="consumer-box" :class="{ failed: consumerFailed }">
<div class="consumer-name">短信服务</div>
<div class="consumer-status">
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
</div>
</div>
<div class="consumer-box">
<div class="consumer-name">邮件服务</div>
<div class="consumer-status">运行中</div>
</div>
<div class="consumer-box">
<div class="consumer-name">积分服务</div>
<div class="consumer-status">运行中</div>
</div>
</div>
</div>
<div class="benefit-list">
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>独立运行</strong>通知服务宕机不影响订单创建</span
>
</div>
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>响应快</strong>订单服务只耗时 50ms(发送消息)</span
>
</div>
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>易扩展</strong>增加新消费者无需修改订单代码</span
>
</div>
</div>
<button class="test-btn success" @click="testAsyncCall">
发送订单消息
</button>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>同步调用强依赖响应慢;异步消息解耦响应快易扩展
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const useAsync = ref(false)
const notificationFailed = ref(false)
const consumerFailed = ref(false)
const messageInQueue = ref(false)
const syncCalls = ref([
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
{
id: 3,
service: '调用通知服务',
active: false,
status: '失败!订单回滚'
}
])
const testSyncCall = () => {
notificationFailed.value = true
syncCalls.value.forEach((call, index) => {
setTimeout(() => {
call.active = true
if (index === syncCalls.value.length - 1) {
setTimeout(() => {
call.active = false
}, 2000)
}
}, index * 800)
})
}
const testAsyncCall = () => {
messageInQueue.value = true
setTimeout(() => {
messageInQueue.value = false
}, 2000)
}
</script>
<style scoped>
.decoupling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.mode-switch {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.mode-btn {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.demo-content {
margin-bottom: 16px;
}
.scenario-title {
font-weight: 600;
font-size: 16px;
margin-bottom: 16px;
text-align: center;
}
.flow-diagram {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
margin-bottom: 16px;
}
.service-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 16px;
text-align: center;
min-width: 160px;
transition: all 0.3s;
}
.service-box.failed {
border-color: var(--vp-c-danger);
background: rgba(239, 68, 68, 0.1);
}
.service-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 6px;
}
.service-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
.error-msg {
margin-top: 10px;
padding: 8px 12px;
background: var(--vp-c-danger);
color: white;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
.arrows {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 280px;
}
.sync-call {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.sync-call.active {
background: rgba(239, 68, 68, 0.1);
}
.call-line {
width: 2px;
height: 24px;
background: var(--vp-c-divider);
}
.sync-call.active .call-line {
background: var(--vp-c-danger);
}
.call-label {
font-size: 13px;
color: var(--vp-c-text-2);
flex: 1;
}
.call-status {
font-size: 12px;
color: var(--vp-c-danger);
font-weight: 600;
}
.mq-bridge {
display: flex;
align-items: center;
gap: 16px;
}
.mq-box {
background: rgba(59, 130, 246, 0.1);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 16px;
text-align: center;
min-width: 140px;
}
.mq-icon {
font-size: 32px;
margin-bottom: 8px;
}
.mq-label {
font-weight: 600;
font-size: 15px;
}
.msg-indicator {
margin-top: 10px;
padding: 8px 12px;
background: var(--vp-c-success);
color: white;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
}
.flow-arrow {
font-size: 24px;
color: var(--vp-c-brand);
}
.consumers-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
width: 100%;
max-width: 450px;
}
.consumer-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 12px;
text-align: center;
transition: all 0.3s;
}
.consumer-box.failed {
border-color: var(--vp-c-warning);
background: rgba(245, 158, 11, 0.1);
}
.consumer-name {
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
.consumer-status {
font-size: 12px;
color: var(--vp-c-text-2);
}
.problem-list,
.benefit-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.problem-item,
.benefit-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
}
.problem-item {
background: rgba(239, 68, 68, 0.1);
}
.benefit-item {
background: rgba(34, 197, 94, 0.1);
}
.icon {
font-size: 18px;
flex-shrink: 0;
}
.test-btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.test-btn.fail {
background: var(--vp-c-danger);
color: white;
}
.test-btn.fail:hover {
opacity: 0.9;
}
.test-btn.success {
background: var(--vp-c-success);
color: white;
}
.test-btn.success:hover {
opacity: 0.9;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
margin-top: 16px;
display: flex;
gap: 8px;
}
.info-box .icon {
flex-shrink: 0;
}
</style>
@@ -1,50 +1,881 @@
<!--
IdempotenceDemo.vue
幂等性演示 - 重复消费处理
-->
<template>
<div class="demo-container">
<div class="idempotence-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🔄</span>
<span class="title">幂等性演示</span>
<span class="subtitle">保证重复消费不会产生副作用</span>
</div>
<div class="scenario-switch">
<button
class="scenario-btn"
:class="{ active: scenario === 'transfer' }"
@click="scenario = 'transfer'"
>
💰 银行转账
</button>
<button
class="scenario-btn"
:class="{ active: scenario === 'elevator' }"
@click="scenario = 'elevator'"
>
🛗 电梯按钮
</button>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
幂等性演示组件占位符 - 待实现具体交互
</el-alert>
<!-- 银行转账场景 -->
<div v-if="scenario === 'transfer'" class="transfer-scenario">
<div class="scenario-header">
<div class="title"> 非幂等操作: 银行转账</div>
<div class="subtitle">重复消费会导致多次扣款</div>
</div>
<div class="account-system">
<div class="account-card sender">
<div class="account-name">发送方</div>
<div class="account-balance">
余额: ¥<span class="balance-amount">{{ senderBalance }}</span>
</div>
</div>
<div class="transfer-flow">
<div class="flow-animation" :class="{ active: isTransferring }">
<div class="money-icon">💰</div>
<div class="flow-label">转账 ¥100</div>
</div>
<div class="retry-info" v-if="retryCount > 0">
<div class="retry-badge">重试 {{ retryCount }} </div>
</div>
</div>
<div class="account-card receiver">
<div class="account-name">接收方</div>
<div class="account-balance">
余额: ¥<span class="balance-amount">{{ receiverBalance }}</span>
</div>
</div>
</div>
<div class="control-panel">
<div class="control-row">
<div class="control-item">
<label>幂等性保护</label>
<div class="toggle-switch">
<button
class="toggle-btn"
:class="{ active: useIdempotence }"
@click="useIdempotence = !useIdempotence"
>
<span class="toggle-slider"></span>
</button>
<span class="toggle-label">{{ useIdempotence ? '已启用' : '未启用' }}</span>
</div>
</div>
<button
class="action-btn"
@click="simulateTransfer"
:disabled="isTransferring"
>
{{ isTransferring ? '处理中...' : '模拟重复消费' }}
</button>
</div>
<div class="idempotence-info" v-if="useIdempotence">
<div class="info-item">
<span class="info-icon">🔑</span>
<span class="info-text">每笔交易有唯一ID,重复请求被自动过滤</span>
</div>
</div>
</div>
<div class="result-log">
<div class="log-header">处理日志</div>
<div class="log-list">
<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-message">{{ log.message }}</span>
</div>
<div v-if="logs.length === 0" class="log-empty">
暂无日志,点击按钮开始模拟
</div>
</div>
</div>
<div class="comparison-box">
<div class="comparison-item bad">
<div class="comp-header"> 无幂等保护</div>
<div class="comp-body">
<div class="comp-result">扣款 ¥{{ (retryCount + 1) * 100 }}</div>
<div class="comp-desc">重复消费造成多次扣款</div>
</div>
</div>
<div class="comparison-item good">
<div class="comp-header"> 有幂等保护</div>
<div class="comp-body">
<div class="comp-result">扣款 ¥100</div>
<div class="comp-desc">重复请求被过滤,只扣一次</div>
</div>
</div>
</div>
</div>
<!-- 电梯按钮场景 -->
<div v-else class="elevator-scenario">
<div class="scenario-header">
<div class="title"> 天然幂等操作: 电梯按钮</div>
<div class="subtitle">无论按多少次,电梯只响应一次</div>
</div>
<div class="elevator-system">
<div class="elevator-panel">
<div class="panel-title">电梯按钮面板</div>
<div class="button-grid">
<button
v-for="floor in floors"
:key="floor"
class="floor-btn"
:class="{ active: selectedFloor === floor }"
@click="pressFloor(floor)"
>
{{ floor }}F
</button>
</div>
<div class="press-count">
<span class="count-label">按钮按了</span>
<span class="count-value">{{ pressCount }}</span>
<span class="count-label"></span>
</div>
</div>
<div class="elevator-shaft">
<div class="floor-marks">
<div
v-for="floor in floors"
:key="floor"
class="floor-mark"
:class="{ current: elevatorFloor === floor }"
>
<span class="floor-num">{{ floor }}F</span>
</div>
</div>
<div class="elevator-car" :style="{ bottom: elevatorPosition }">
<div class="car-icon">🛗</div>
</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>快速连按3次</label>
<button class="action-btn" @click="pressMultipleTimes">
🚀 连续点击
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
虽然按了{{ pressCount }},但电梯只响应一次请求
</div>
</div>
<div class="explanation-box">
<div class="explanation-title">为什么电梯按钮是幂等的?</div>
<div class="explanation-list">
<div class="explanation-item">
<span class="icon"></span>
<span>状态只切换一次: 停靠 已选中</span>
</div>
<div class="explanation-item">
<span class="icon"></span>
<span>重复请求不改变目标楼层</span>
</div>
<div class="explanation-item">
<span class="icon"></span>
<span>无需额外的幂等性保护机制</span>
</div>
</div>
</div>
</div>
</div>
<div class="principle-box">
<div class="principle-icon">🎯</div>
<div class="principle-content">
<strong>幂等性核心原则:</strong>
{{ scenario === 'transfer'
? '为每条消息生成唯一ID,处理前检查是否已处理,避免重复操作'
: '设计操作时确保重复执行和执行一次的效果相同' }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('幂等性演示')
const description = ref('展示消息消费中的幂等性问题,以及如何通过幂等性设计保证消息处理的正确性')
//
const scenario = ref('transfer')
//
const senderBalance = ref(1000)
const receiverBalance = ref(500)
const isTransferring = ref(false)
const useIdempotence = ref(false)
const retryCount = ref(0)
const logs = ref([])
const addLog = (message, type = '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.unshift({ time, message, type })
}
const simulateTransfer = () => {
if (isTransferring.value) return
isTransferring.value = true
retryCount.value = 0
logs.value = []
const originalSenderBalance = senderBalance.value
const originalReceiverBalance = receiverBalance.value
addLog('收到转账请求: ¥100', 'info')
//
const processTransfer = (attempt) => {
return new Promise((resolve) => {
setTimeout(() => {
retryCount.value = attempt
if (useIdempotence.value) {
if (attempt === 0) {
senderBalance.value = originalSenderBalance - 100
receiverBalance.value = originalReceiverBalance + 100
addLog(`${attempt + 1}次处理: 成功转账 ¥100`, 'success')
addLog('幂等性检查: 唯一ID已记录,后续请求被过滤', 'info')
} else {
addLog(`${attempt + 1}次处理: 重复请求,已忽略`, 'warning')
}
} else {
senderBalance.value -= 100
receiverBalance.value += 100
addLog(`${attempt + 1}次处理: 转账 ¥100`, attempt === 0 ? 'success' : 'error')
}
if (attempt < 2) {
setTimeout(() => processTransfer(attempt + 1), 1000)
} else {
setTimeout(() => {
isTransferring.value = false
}, 500)
}
resolve()
}, 1000)
})
}
processTransfer(0)
}
//
const floors = [1, 2, 3, 4, 5]
const selectedFloor = ref(null)
const elevatorFloor = ref(1)
const pressCount = ref(0)
const elevatorPosition = computed(() => {
return ((elevatorFloor.value - 1) / 4) * 100 + '%'
})
const pressFloor = (floor) => {
pressCount.value++
selectedFloor.value = floor
setTimeout(() => {
elevatorFloor.value = floor
}, 500)
}
const pressMultipleTimes = () => {
const targetFloor = Math.floor(Math.random() * 5) + 1
let count = 0
const interval = setInterval(() => {
pressFloor(targetFloor)
count++
if (count >= 3) {
clearInterval(interval)
}
}, 200)
}
</script>
<style scoped>
.demo-container {
.idempotence-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.scenario-switch {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.scenario-btn {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.scenario-btn:hover {
border-color: var(--vp-c-brand);
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.scenario-header {
text-align: center;
margin-bottom: 20px;
}
.scenario-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.scenario-header .subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
.account-system {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
margin-bottom: 20px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.account-card {
flex: 1;
max-width: 200px;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
text-align: center;
}
.account-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 10px;
}
.account-balance {
font-size: 14px;
color: var(--vp-c-text-2);
}
.balance-amount {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-brand);
}
.transfer-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.flow-animation {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
border-radius: 8px;
transition: all 0.3s;
}
.flow-animation.active {
background: rgba(59, 130, 246, 0.1);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.money-icon {
font-size: 32px;
}
.flow-label {
font-weight: 600;
font-size: 13px;
}
.retry-info {
display: flex;
justify-content: center;
}
.retry-badge {
padding: 4px 10px;
background: var(--vp-c-warning);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
.control-panel {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.control-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.control-item {
display: flex;
align-items: center;
gap: 12px;
}
.control-item label {
font-weight: 600;
font-size: 14px;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-btn {
position: relative;
width: 48px;
height: 26px;
background: var(--vp-c-divider);
border: none;
border-radius: 13px;
cursor: pointer;
padding: 0;
transition: all 0.3s;
}
.toggle-btn.active {
background: var(--vp-c-brand);
}
.toggle-slider {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: all 0.3s;
}
.toggle-btn.active .toggle-slider {
left: 25px;
}
.toggle-label {
font-size: 13px;
font-weight: 600;
}
.action-btn {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.idempotence-info {
margin-top: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
border-radius: 6px;
font-size: 13px;
}
.info-icon {
font-size: 16px;
}
.info-text {
flex: 1;
}
.result-log {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.log-header {
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
}
.log-list {
max-height: 200px;
overflow-y: auto;
}
.log-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
border-radius: 4px;
font-size: 12px;
margin-bottom: 6px;
}
.log-item.success {
background: rgba(34, 197, 94, 0.1);
}
.log-item.error {
background: rgba(239, 68, 68, 0.1);
}
.log-item.warning {
background: rgba(245, 158, 11, 0.1);
}
.log-time {
color: var(--vp-c-text-3);
font-family: monospace;
}
.log-message {
flex: 1;
}
.log-empty {
text-align: center;
padding: 20px;
color: var(--vp-c-text-3);
font-size: 13px;
}
.comparison-box {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.comparison-item {
padding: 16px;
border-radius: 8px;
text-align: center;
}
.comparison-item.bad {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.comparison-item.good {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.comp-header {
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
}
.comp-result {
font-weight: 700;
font-size: 18px;
margin-bottom: 6px;
}
.comp-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.elevator-system {
display: flex;
gap: 24px;
margin-bottom: 20px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.elevator-panel {
flex: 1;
max-width: 250px;
}
.panel-title {
font-weight: 600;
font-size: 14px;
text-align: center;
margin-bottom: 16px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.floor-btn {
padding: 16px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.floor-btn:hover {
border-color: var(--vp-c-brand);
}
.floor-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.press-count {
text-align: center;
font-size: 13px;
color: var(--vp-c-text-2);
}
.count-label {
font-size: 12px;
}
.count-value {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-brand);
}
.elevator-shaft {
flex: 1;
position: relative;
height: 300px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 20px;
}
.floor-marks {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.floor-mark {
display: flex;
align-items: center;
gap: 8px;
}
.floor-num {
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-2);
}
.floor-mark.current .floor-num {
color: var(--vp-c-brand);
font-weight: 700;
}
.elevator-car {
position: absolute;
left: 50%;
transform: translateX(-50%);
transition: bottom 0.5s ease;
}
.car-icon {
font-size: 32px;
animation: bounce 0.5s ease;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.info-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.explanation-box {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
}
.explanation-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 12px;
}
.explanation-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.explanation-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.6;
}
.explanation-item .icon {
flex-shrink: 0;
}
.principle-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-1);
margin-top: 16px;
}
.principle-icon {
font-size: 24px;
}
.principle-content {
flex: 1;
}
</style>
@@ -1,13 +1,207 @@
<!--
ReliabilityDemo.vue
消息可靠性演示 - 三道防线
-->
<template>
<div class="demo-container">
<div class="reliability-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🛡</span>
<span class="title">消息可靠性演示</span>
<span class="subtitle">三道防线保证消息不丢失</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
消息可靠性演示组件占位符 - 待实现具体交互
</el-alert>
<div class="defense-system">
<!-- 防线1: 生产者确认 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line1">防线 1</div>
<div class="defense-title">生产者确认 (Producer ACK)</div>
</div>
<div class="defense-content">
<div class="flow-diagram">
<div class="component producer">
<div class="comp-icon">📤</div>
<div class="comp-label">生产者</div>
<div class="comp-desc">发送消息</div>
</div>
<div class="message-flow">
<div class="msg-item" :class="{ active: step === 1 }">
<div class="msg-icon">📨</div>
<div class="msg-label">消息</div>
<div v-if="step === 1" class="msg-status">
{{ ackStatus }}
</div>
</div>
<div class="ack-item" :class="{ active: step === 2 }">
<div class="ack-icon"></div>
<div class="ack-label">ACK确认</div>
<div v-if="step === 2" class="ack-status">
{{ ackMessage }}
</div>
</div>
</div>
<div class="component broker">
<div class="comp-icon">📦</div>
<div class="comp-label">Broker</div>
<div class="comp-desc">接收并存储</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>发送消息</label>
<button class="action-btn" @click="sendWithAck" :disabled="step > 0">
发送并等待确认
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
如果没收到ACK,生产者会重试或记录本地日志
</div>
</div>
</div>
</div>
<!-- 防线2: Broker持久化 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line2">防线 2</div>
<div class="defense-title">Broker持久化</div>
</div>
<div class="defense-content">
<div class="storage-diagram">
<div class="storage-container">
<div class="storage-option" :class="{ active: storageType === 'memory' }">
<div class="option-icon"></div>
<div class="option-label">内存存储</div>
<div class="option-desc">速度快,但重启丢失</div>
<div class="option-risk"> 高风险</div>
</div>
<div class="vs-divider">vs</div>
<div class="storage-option recommended" :class="{ active: storageType === 'disk' }">
<div class="option-icon">💾</div>
<div class="option-label">磁盘存储</div>
<div class="option-desc">落盘保证不丢失</div>
<div class="option-risk"> 推荐</div>
</div>
</div>
<div class="replication-info">
<div class="replication-title">
<span class="icon">🔄</span>
多副本同步
</div>
<div class="replication-detail">
消息同步到3个节点,即使1个节点宕机也不丢数据
</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>存储方式</label>
<div class="btn-group">
<button
class="toggle-btn"
:class="{ active: storageType === 'memory' }"
@click="storageType = 'memory'"
>
内存
</button>
<button
class="toggle-btn"
:class="{ active: storageType === 'disk' }"
@click="storageType = 'disk'"
>
磁盘
</button>
</div>
</div>
<div class="info-text" :class="{ warning: storageType === 'memory' }">
<span class="info-icon">{{ storageType === 'disk' ? '✅' : '⚠️' }}</span>
{{ storageType === 'disk' ? '消息已落盘,安全可靠' : '消息仅在内存,重启丢失' }}
</div>
</div>
</div>
</div>
<!-- 防线3: 消费者确认 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line3">防线 3</div>
<div class="defense-title">消费者确认 (Consumer ACK)</div>
</div>
<div class="defense-content">
<div class="consumer-flow">
<div class="flow-step" :class="{ active: consumerStep >= 1 }">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">拉取消息</div>
<div class="step-desc">从Broker获取消息</div>
</div>
</div>
<div class="flow-arrow" :class="{ active: consumerStep >= 1 }"></div>
<div class="flow-step" :class="{ active: consumerStep >= 2 }">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">处理消息</div>
<div class="step-desc">执行业务逻辑</div>
</div>
</div>
<div class="flow-arrow" :class="{ active: consumerStep >= 2 }"></div>
<div class="flow-step" :class="{ active: consumerStep >= 3 }">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">手动ACK</div>
<div class="step-desc">确认处理完成</div>
</div>
</div>
</div>
<div class="ack-comparison">
<div class="ack-option">
<div class="ack-type">自动 ACK</div>
<div class="ack-desc">高效但可能丢消息</div>
<div class="ack-risk"> 不推荐</div>
</div>
<div class="ack-option recommended">
<div class="ack-type">手动 ACK</div>
<div class="ack-desc">可靠,处理完才确认</div>
<div class="ack-risk"> 推荐</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>模拟消费</label>
<button class="action-btn" @click="simulateConsume" :disabled="consumerStep > 0">
开始消费流程
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
如果处理失败,不发送ACK,Broker会重新投递
</div>
</div>
</div>
</div>
</div>
<div class="summary-box">
<div class="summary-icon">🎯</div>
<div class="summary-content">
<strong>三道防线,缺一不可</strong>生产者确认 Broker持久化 消费者确认
</div>
</div>
</div>
</template>
@@ -15,36 +209,500 @@
<script setup>
import { ref } from 'vue'
const title = ref('消息可靠性演示')
const description = ref('展示消息队列如何保证消息的可靠传输,包括消息确认、持久化、重试机制等')
// 线1:
const step = ref(0)
const ackStatus = ref('')
const ackMessage = ref('')
// 线2:
const storageType = ref('disk')
// 线3:
const consumerStep = ref(0)
const sendWithAck = () => {
step.value = 1
ackStatus.value = '发送中...'
setTimeout(() => {
step.value = 2
ackStatus.value = '已发送'
ackMessage.value = '收到ACK,消息安全'
setTimeout(() => {
step.value = 0
ackStatus.value = ''
ackMessage.value = ''
}, 3000)
}, 1500)
}
const simulateConsume = () => {
consumerStep.value = 1
setTimeout(() => {
consumerStep.value = 2
setTimeout(() => {
consumerStep.value = 3
setTimeout(() => {
consumerStep.value = 0
}, 3000)
}, 1500)
}, 1500)
}
</script>
<style scoped>
.demo-container {
.reliability-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.defense-system {
display: flex;
flex-direction: column;
gap: 24px;
}
.defense-line {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.defense-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.defense-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 700;
color: white;
}
.defense-badge.line1 {
background: #3b82f6
}
.defense-badge.line2 {
background: #f59e0b
}
.defense-badge.line3 {
background: #22c55e
}
.defense-title {
font-weight: 600;
font-size: 15px;
color: var(--vp-c-text-1);
}
.defense-content {
padding: 20px;
}
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 20px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.component {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
min-width: 120px;
}
.comp-icon {
font-size: 32px;
}
.comp-label {
font-weight: 600;
font-size: 14px;
}
.comp-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.demo-content {
.message-flow {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.msg-item,
.ack-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.msg-item.active {
background: rgba(59, 130, 246, 0.1);
}
.ack-item.active {
background: rgba(34, 197, 94, 0.1);
}
.msg-icon,
.ack-icon {
font-size: 24px;
}
.msg-label,
.ack-label {
font-size: 12px;
font-weight: 600;
}
.msg-status,
.ack-status {
font-size: 11px;
color: var(--vp-c-text-2);
}
.storage-diagram {
margin-bottom: 20px;
}
.storage-container {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 16px;
}
.storage-option {
flex: 1;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
text-align: center;
transition: all 0.3s;
}
.storage-option.active {
border-color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.05);
}
.storage-option.recommended {
border-color: var(--vp-c-success);
}
.storage-option.recommended.active {
background: rgba(34, 197, 94, 0.05);
}
.option-icon {
font-size: 36px;
margin-bottom: 10px;
}
.option-label {
font-weight: 600;
font-size: 15px;
margin-bottom: 6px;
}
.option-desc {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.option-risk {
font-size: 12px;
font-weight: 600;
}
.vs-divider {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-2);
}
.replication-info {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.replication-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
}
.replication-icon {
font-size: 20px;
}
.replication-detail {
font-size: 13px;
color: var(--vp-c-text-2);
}
.consumer-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
min-width: 100px;
transition: all 0.3s;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.05);
}
.step-num {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.step-title {
font-weight: 600;
font-size: 13px;
}
.step-desc {
font-size: 11px;
color: var(--vp-c-text-2);
}
.flow-arrow {
font-size: 24px;
color: var(--vp-c-divider);
transition: all 0.3s;
}
.flow-arrow.active {
color: var(--vp-c-brand);
}
.ack-comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.ack-option {
padding: 16px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
text-align: center;
}
.ack-option.recommended {
border-color: var(--vp-c-success);
background: rgba(34, 197, 94, 0.05);
}
.ack-type {
font-weight: 600;
font-size: 15px;
margin-bottom: 8px;
}
.ack-desc {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.ack-risk {
font-size: 12px;
font-weight: 600;
}
.control-panel {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.control-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.control-item label {
font-weight: 600;
font-size: 14px;
}
.action-btn {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-group {
display: flex;
gap: 8px;
}
.toggle-btn {
padding: 8px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.toggle-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.info-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.info-text.warning {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.info-icon {
font-size: 16px;
}
.summary-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.summary-icon {
font-size: 24px;
}
.summary-content {
flex: 1;
}
</style>
@@ -1,50 +1,51 @@
<!--
BigFrontendScopeDemo.vue
前端 vs 大前端跨端范围演示
-->
<template>
<div class="bigfe-demo">
<div class="header">
<div class="title">前端 vs 大前端到底前端都包含什么</div>
<div class="subtitle">点一下不同立刻看到它跑在哪里怎么发布</div>
<div class="demo-header">
<span class="icon">🌐</span>
<span class="title">前端 vs 大前端</span>
<span class="subtitle">了解不同平台的运行环境和技术栈</span>
</div>
<div class="platforms">
<button
v-for="p in platforms"
:key="p.key"
class="platform"
:class="{ active: current === p.key }"
@click="current = p.key"
>
<span class="icon">{{ p.icon }}</span>
<span>{{ p.label }}</span>
</button>
</div>
<div class="demo-content">
<div class="platforms">
<button
v-for="p in platforms"
:key="p.key"
class="platform"
:class="{ active: current === p.key }"
@click="current = p.key"
>
<span class="icon">{{ p.icon }}</span>
<span>{{ p.label }}</span>
</button>
</div>
<div class="cards">
<div class="card">
<div class="label">运行环境</div>
<div class="value">{{ currentData.runtime }}</div>
<div class="cards">
<div class="card">
<div class="label">运行环境</div>
<div class="value">{{ currentData.runtime }}</div>
</div>
<div class="card">
<div class="label">主要技术</div>
<div class="value">{{ currentData.stack }}</div>
</div>
<div class="card">
<div class="label">发布方式</div>
<div class="value">{{ currentData.release }}</div>
</div>
</div>
<div class="card">
<div class="label">主要技术</div>
<div class="value">{{ currentData.stack }}</div>
</div>
<div class="card">
<div class="label">发布方式</div>
<div class="value">{{ currentData.release }}</div>
<div class="skills">
<div class="skills-title">哪些能力是"共通的"</div>
<div class="tags">
<span v-for="t in commonSkills.slice(0, 6)" :key="t" class="tag">{{ t }}</span>
</div>
</div>
</div>
<div class="skills">
<div class="skills-title">哪些能力是共通的</div>
<div class="tags">
<span v-for="t in commonSkills" :key="t" class="tag">{{ t }}</span>
</div>
<div class="skills-note">
大前端的核心不是会更多框架而是<strong>用同一套工程能力把体验交付到不同平台</strong>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>大前端不是"会更多框架"而是用同一套工程能力把体验交付到不同平台
</div>
</div>
</template>
@@ -53,11 +54,11 @@
import { ref, computed } from 'vue'
const platforms = [
{ key: 'web', label: 'Web 网站', icon: '🌐' },
{ key: 'h5', label: 'H5 活动页', icon: '📱' },
{ key: 'web', label: 'Web网站', icon: '🌐' },
{ key: 'h5', label: 'H5活动页', icon: '📱' },
{ key: 'miniapp', label: '小程序', icon: '🧩' },
{ key: 'native', label: 'App(原生)', icon: '📲' },
{ key: 'cross', label: '跨端 App', icon: '🧱' },
{ key: 'native', label: '原生App', icon: '📲' },
{ key: 'cross', label: '跨端App', icon: '🧱' },
{ key: 'desktop', label: '桌面应用', icon: '🖥️' }
]
@@ -112,32 +113,45 @@ const commonSkills = [
<style scoped>
.bigfe-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.platforms {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0 1rem;
margin-bottom: 1rem;
}
.platform {
@@ -150,22 +164,25 @@ const commonSkills = [
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: all 0.2s;
}
.platform:hover {
background: var(--vp-c-bg-soft);
}
.platform.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.icon {
font-size: 1rem;
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
font-weight: 600;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.card {
@@ -185,17 +202,19 @@ const commonSkills = [
font-size: 0.95rem;
font-weight: 600;
line-height: 1.35;
color: var(--vp-c-text-1);
}
.skills {
margin-top: 1rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 1rem;
padding-top: 0.75rem;
}
.skills-title {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.tags {
@@ -208,14 +227,26 @@ const commonSkills = [
font-size: 0.8rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
border: 1px solid rgba(34, 197, 94, 0.2);
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.skills-note {
margin-top: 0.75rem;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,6 +4,12 @@
-->
<template>
<div class="box-demo">
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">CSS 盒模型</span>
<span class="subtitle">理解元素尺寸的构成通俗说盒子的四层包装</span>
</div>
<div class="controls">
<div class="control-item">
<div class="control-header">
@@ -67,6 +73,11 @@
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>每个元素都是一个"盒子"从内到外依次是内容区 内边距 边框 外边距
</div>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
@@ -99,13 +110,37 @@ const total = computed(
<style scoped>
.box-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 24px;
margin: 24px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 24px;
gap: 1rem;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.controls {
@@ -310,4 +345,22 @@ input[type='range'] {
.line {
white-space: pre;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,73 +1,86 @@
<!--
CssFlexbox.vue
Flex 速学三个按钮控制方向/对齐/换行实时看盒子怎么排
Flex 布局速学三个按钮控制方向/对齐/换行实时看盒子怎么排
-->
<template>
<div class="flex-demo">
<div class="controls">
<div class="control-item">
<div class="control-header">
<label>主轴方向 (flex-direction)</label>
<div class="demo-header">
<span class="icon">📐</span>
<span class="title">Flex 布局</span>
<span class="subtitle">一行代码搞定排列对齐通俗说自动排版</span>
</div>
<div class="demo-content">
<div class="controls">
<div class="control-item">
<div class="control-header">
<label>主轴方向 (flex-direction)</label>
</div>
<div class="chips">
<button
v-for="d in directions"
:key="d.id"
:class="['chip', { active: dir === d.id }]"
@click="dir = d.id"
>
{{ d.label }}
</button>
</div>
</div>
<div class="chips">
<button
v-for="d in directions"
:key="d.id"
:class="['chip', { active: dir === d.id }]"
@click="dir = d.id"
>
{{ d.label }}
</button>
<div class="control-item">
<div class="control-header">
<label>主轴对齐 (justify-content)</label>
</div>
<div class="chips">
<button
v-for="j in justifies"
:key="j.id"
:class="['chip', { active: justify === j.id }]"
@click="justify = j.id"
>
{{ j.label }}
</button>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button
v-for="w in wraps"
:key="w.id"
:class="['chip', { active: wrap === w.id }]"
@click="wrap = w.id"
>
{{ w.label }}
</button>
</div>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>主轴对齐 (justify-content)</label>
</div>
<div class="chips">
<button
v-for="j in justifies"
:key="j.id"
:class="['chip', { active: justify === j.id }]"
@click="justify = j.id"
>
{{ j.label }}
</button>
<div class="canvas-container">
<div class="canvas" :style="boxStyle">
<div v-for="n in 6" :key="n" class="item">{{ n }}</div>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button
v-for="w in wraps"
:key="w.id"
:class="['chip', { active: wrap === w.id }]"
@click="wrap = w.id"
>
{{ w.label }}
</button>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line">display: flex;</div>
<div class="line">flex-direction: {{ dir }};</div>
<div class="line">justify-content: {{ justify }};</div>
<div class="line">flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
</div>
</div>
<div class="canvas-container">
<div class="canvas" :style="boxStyle">
<div v-for="n in 8" :key="n" class="item">{{ n }}</div>
</div>
</div>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line">display: flex;</div>
<div class="line">flex-direction: {{ dir }};</div>
<div class="line">justify-content: {{ justify }};</div>
<div class="line">flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>Flex 让元素自动排列不用手动计算位置就像书架上的书会自动对齐
</div>
</div>
</template>
@@ -99,7 +112,7 @@ const boxStyle = computed(() => ({
justifyContent: justify.value,
flexWrap: wrap.value,
gap: '12px',
minHeight: '200px',
minHeight: '180px',
padding: '16px'
}))
</script>
@@ -107,50 +120,76 @@ const boxStyle = computed(() => ({
<style scoped>
.flex-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 24px;
margin: 24px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
flex-direction: column;
gap: 24px;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.control-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 12px;
gap: 0.5rem;
}
.control-header label {
font-weight: 700;
font-weight: 600;
color: var(--vp-c-text-1);
font-size: 13px;
font-size: 0.8rem;
}
.chips {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.chip {
padding: 6px 12px;
padding: 0.25rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
cursor: pointer;
font-size: 13px;
font-size: 0.8rem;
transition: all 0.2s;
}
@@ -161,77 +200,60 @@ const boxStyle = computed(() => ({
.chip.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
background: var(--vp-c-brand-soft);
font-weight: 600;
}
.canvas-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 4px; /* Tiny padding for the inner canvas */
border-radius: 8px;
padding: 0.25rem;
margin-bottom: 1rem;
}
.canvas {
background: var(--vp-c-bg);
border-radius: 8px;
/* border: 1px dashed var(--vp-c-divider); */
border-radius: 6px;
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
background-size: 20px 20px;
}
.item {
width: 60px;
height: 60px;
border-radius: 8px;
background: linear-gradient(135deg, #0ea5e9, #10b981);
width: 50px;
height: 50px;
border-radius: 6px;
background: var(--vp-c-brand);
color: #fff;
font-weight: 800;
font-weight: 700;
display: grid;
place-items: center;
font-size: 18px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 16px;
transition: all 0.3s;
flex-shrink: 0;
}
.val {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-brand);
font-weight: 600;
font-size: 13px;
}
input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
margin-top: 8px;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
border-radius: 8px;
padding: 0.75rem;
}
.code-title {
font-weight: 700;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.code-content {
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 6px;
padding: 0.75rem;
font-family: var(--vp-font-family-mono);
font-size: 13px;
font-size: 0.75rem;
overflow-x: auto;
line-height: 1.6;
}
@@ -239,4 +261,22 @@ input[type='range'] {
.line {
white-space: pre;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,35 +1,45 @@
<!--
DomManipulator.vue
DOM 速体验输入标题+切换高亮类直观看到文本和样式变化
DOM 操作速体验输入标题+切换高亮类直观看到文本和样式变化
-->
<template>
<div class="dom-demo">
<div class="controls">
<div class="field">
<label>改个标题</label>
<input v-model="title" placeholder="输入新标题" />
</div>
<div class="field checkbox">
<label
><input type="checkbox" v-model="highlight" /> 高亮模式
(class="highlight")</label
>
</div>
<div class="demo-header">
<span class="icon">🎯</span>
<span class="title">DOM 操作</span>
<span class="subtitle">网页内容的动态修改通俗说用代码改页面</span>
</div>
<div class="card" :class="{ highlight }">
<h2 id="hero">{{ title }}</h2>
<p id="desc">这里是段落说明勾选高亮看看变化</p>
<button @click="toggleText">{{ buttonText }}</button>
</div>
<div class="demo-content">
<div class="controls">
<div class="field">
<label>改个标题</label>
<input v-model="title" placeholder="输入新标题" />
</div>
<div class="field checkbox">
<label><input type="checkbox" v-model="highlight" /> 高亮模式 (class="highlight")</label>
</div>
</div>
<pre class="code"><code>// JS
<div class="card" :class="{ highlight }">
<h2 id="hero">{{ title }}</h2>
<p id="desc">这里是段落说明勾选高亮看看变化</p>
<button @click="toggleText">{{ buttonText }}</button>
</div>
<pre class="code"><code>// JS
const titleEl = document.getElementById('hero')
titleEl.textContent = '{{ title }}'
// JS class
const card = document.querySelector('.card')
card.classList.toggle('highlight', {{ highlight }})</code></pre>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>DOM 是网页的"乐高积木"JavaScript 可以随时添加删除修改这些积木
</div>
</div>
</template>
@@ -48,73 +58,154 @@ const toggleText = () => {
<style scoped>
.dom-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.field {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 10px;
border-radius: 8px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 6px;
gap: 0.5rem;
}
.field label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.checkbox {
flex-direction: row;
align-items: center;
gap: 8px;
gap: 0.5rem;
}
input[type='text'],
input[type='text'] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
input[type='text']:focus {
outline: none;
border-color: var(--vp-c-brand);
}
input[type='checkbox'] {
accent-color: var(--vp-c-brand);
}
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
transition: all 0.2s;
margin-bottom: 1rem;
}
.card.highlight {
border-color: #f59e0b;
box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2);
background: #fff7ed;
border-color: var(--vp-c-warning);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background: var(--vp-c-bg-soft);
}
.card h2 {
margin: 0 0 8px 0;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
}
.card p {
margin: 0 0 12px 0;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.card button {
background: var(--vp-c-brand);
color: #fff;
color: white;
border: none;
border-radius: 8px;
padding: 8px 12px;
border-radius: 6px;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.85rem;
transition: opacity 0.2s;
}
.card button:hover {
opacity: 0.9;
}
.code {
background: #0b1221;
color: #e5e7eb;
border-radius: 10px;
padding: 12px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border-radius: 8px;
padding: 0.75rem;
font-family: var(--vp-font-family-mono);
font-size: 13px;
font-size: 0.75rem;
overflow-x: auto;
line-height: 1.6;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,70 +1,83 @@
<template>
<div class="imperative-declarative-demo">
<div class="demo-grid">
<!-- Imperative (jQuery Style) -->
<div class="panel imperative">
<div class="panel-header">
<span class="badge yellow">Imperative (命令式)</span>
<span class="sub-text">jQuery Style</span>
</div>
<div class="code-preview">
<code>
// DOM<br />
$('#count').text(val);<br />
if (val > 5) $('#msg').show();
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span id="imp-count-display">{{ impCount }}</span>
<div v-show="impShowMsg" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="impIncrement" class="btn">Step 1: Value++</button>
<button @click="impUpdateText" class="btn" :disabled="!impChanged">
Step 2: Update Text
</button>
<button
@click="impCheckState"
class="btn"
:disabled="!impTextUpdated"
>
Step 3: Check Logic
</button>
</div>
<div class="status-log">{{ impStatus }}</div>
</div>
</div>
<div class="demo-header">
<span class="icon">🔄</span>
<span class="title">命令式 vs 声明式</span>
<span class="subtitle">两种编程思维的对比通俗说手动操作 vs 自动响应</span>
</div>
<!-- Declarative (Vue Style) -->
<div class="panel declarative">
<div class="panel-header">
<span class="badge green">Declarative (声明式)</span>
<span class="sub-text">Vue/React Style</span>
</div>
<div class="code-preview">
<code>
// <br />
{{ '{' + '{ count }' + '}' }}<br />
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span>{{ decCount }}</span>
<div v-if="decCount > 5" class="warning-msg"> Count is high!</div>
<div class="demo-content">
<div class="demo-grid">
<!-- Imperative (jQuery Style) -->
<div class="panel imperative">
<div class="panel-header">
<span class="badge yellow">命令式 (Imperative)</span>
<span class="sub-text">jQuery Style - 手动操作</span>
</div>
<div class="actions">
<button @click="decIncrement" class="btn primary">
Value++ (Auto Render)
</button>
<div class="code-preview">
<code>
// DOM<br />
$('#count').text(val);<br />
if (val > 5) $('#msg').show();
</code>
</div>
<div class="status-log">
Framework handles DOM updates automatically.
<div class="interactive-area">
<div class="output-box">
Count: <span id="imp-count-display">{{ impCount }}</span>
<div v-show="impShowMsg" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="impIncrement" class="btn">Step 1: Value++</button>
<button @click="impUpdateText" class="btn" :disabled="!impChanged">
Step 2: Update Text
</button>
<button
@click="impCheckState"
class="btn"
:disabled="!impTextUpdated"
>
Step 3: Check Logic
</button>
</div>
<div class="status-log">{{ impStatus }}</div>
</div>
</div>
<!-- Declarative (Vue Style) -->
<div class="panel declarative">
<div class="panel-header">
<span class="badge green">声明式 (Declarative)</span>
<span class="sub-text">Vue/React Style - 自动响应</span>
</div>
<div class="code-preview">
<code v-pre>
//
{{ count }}
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span>{{ decCount }}</span>
<div v-if="decCount > 5" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="decIncrement" class="btn primary">
Value++ (Auto Render)
</button>
</div>
<div class="status-log">
Framework handles DOM updates automatically.
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>命令式像"手把手教电脑怎么做"声明式像"告诉电脑要什么,它自己搞定"
</div>
</div>
</template>
@@ -119,12 +132,40 @@ const decIncrement = () => {
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
gap: 1rem;
}
@media (max-width: 640px) {
@@ -143,7 +184,7 @@ const decIncrement = () => {
}
.panel-header {
padding: 0.8rem;
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
@@ -152,48 +193,51 @@ const decIncrement = () => {
}
.badge {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: bold;
padding: 2px 6px;
padding: 2px 8px;
border-radius: 4px;
color: white;
}
.badge.yellow {
background: #f59e0b;
background: var(--vp-c-warning);
}
.badge.green {
background: #10b981;
background: var(--vp-c-success);
}
.sub-text {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.code-preview {
background: #1e1e2e;
padding: 0.8rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
font-family: monospace;
font-size: 0.8rem;
color: #a6accd;
height: 80px;
font-size: 0.75rem;
color: var(--vp-c-text-1);
height: 70px;
overflow: hidden;
}
.interactive-area {
padding: 1rem;
padding: 0.75rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.75rem;
}
.output-box {
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
text-align: center;
font-weight: bold;
min-height: 80px;
min-height: 70px;
display: flex;
flex-direction: column;
align-items: center;
@@ -201,10 +245,9 @@ const decIncrement = () => {
}
.warning-msg {
color: #ef4444;
color: var(--vp-c-danger);
margin-top: 0.5rem;
font-size: 0.9rem;
animation: popIn 0.3s;
font-size: 0.85rem;
}
.actions {
@@ -217,42 +260,53 @@ const decIncrement = () => {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: white;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
font-size: 0.75rem;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
background: #f3f4f6;
background: var(--vp-c-bg-soft);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn.primary {
background: #3b82f6;
background: var(--vp-c-brand);
color: white;
border: none;
}
.btn.primary:hover {
background: #2563eb;
opacity: 0.9;
}
.status-log {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-size: 0.7rem;
color: var(--vp-c-text-2);
font-style: italic;
min-height: 1.2em;
}
@keyframes popIn {
0% {
transform: scale(0.8);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -303,6 +303,8 @@ const handleBtnClick = () => {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 600px;
overflow-y: auto;
}
/* New Config Panel Styles */
+2
View File
@@ -222,6 +222,7 @@ import MessageQueueComponentsDemo from './components/appendix/queue-design/Messa
import PointToPointVsPubSubDemo from './components/appendix/queue-design/PointToPointVsPubSubDemo.vue'
import MessageQueueComparisonDemo from './components/appendix/queue-design/MessageQueueComparisonDemo.vue'
import CouplingDemo from './components/appendix/queue-design/CouplingDemo.vue'
import DecouplingDemo from './components/appendix/queue-design/DecouplingDemo.vue'
import PubSubDemo from './components/appendix/queue-design/PubSubDemo.vue'
import DeadLetterQueueDemo from './components/appendix/queue-design/DeadLetterQueueDemo.vue'
import DelayedMessageDemo from './components/appendix/queue-design/DelayedMessageDemo.vue'
@@ -693,6 +694,7 @@ export default {
app.component('PointToPointVsPubSubDemo', PointToPointVsPubSubDemo)
app.component('MessageQueueComparisonDemo', MessageQueueComparisonDemo)
app.component('CouplingDemo', CouplingDemo)
app.component('DecouplingDemo', DecouplingDemo)
app.component('PubSubDemo', PubSubDemo)
app.component('DeadLetterQueueDemo', DeadLetterQueueDemo)
app.component('DelayedMessageDemo', DelayedMessageDemo)