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:
@@ -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>
|
||||
|
||||
+47
-12
@@ -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);
|
||||
|
||||
+46
-10
@@ -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>
|
||||
|
||||
+99
-62
@@ -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>
|
||||
|
||||
+44
-31
@@ -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>
|
||||
|
||||
+24
-2
@@ -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>策略由 Effect、Action、Resource、Condition 四个核心元素组成,理解这四个元素的作用是编写 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>
|
||||
|
||||
+48
-15
@@ -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>
|
||||
|
||||
|
||||
+80
-44
@@ -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>
|
||||
|
||||
+50
-17
@@ -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>
|
||||
|
||||
+80
-167
@@ -1,12 +1,9 @@
|
||||
<!--
|
||||
ObjectStorageDemo.vue
|
||||
对象存储架构演示 - 展示桶、对象、元数据的核心概念
|
||||
-->
|
||||
<template>
|
||||
<div class="object-storage-demo">
|
||||
<div class="header">
|
||||
<div class="title">对象存储架构</div>
|
||||
<div class="subtitle">理解 Bucket、Object 和 Metadata 的关系</div>
|
||||
<div class="demo-header">
|
||||
<span class="icon">🗄️</span>
|
||||
<span class="title">对象存储架构</span>
|
||||
<span class="subtitle">理解 Bucket、Object 和 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 Policy、ACL、STS 临时凭证多层权限</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>
|
||||
|
||||
+48
-15
@@ -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>
|
||||
|
||||
|
||||
+132
-260
@@ -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>
|
||||
|
||||
+93
-40
@@ -1,19 +1,19 @@
|
||||
<!--
|
||||
ImperativeVsDeclarativeDemo.vue
|
||||
命令式 vs 声明式编程对比演示
|
||||
|
||||
用途:
|
||||
通过并排的交互式计数器,直观展示 Imperative(jQuery)和 Declarative(Vue)
|
||||
在代码量和心智负担上的差异。
|
||||
|
||||
交互功能:
|
||||
- 两个可交互的计数器。
|
||||
- 切换展示背后的代码实现。
|
||||
- 高亮显示 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;
|
||||
|
||||
+247
-323
@@ -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 {
|
||||
|
||||
+261
-265
@@ -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>
|
||||
|
||||
+147
-93
@@ -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 />
|
||||
<div v-if="count > 5">...</div>
|
||||
</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 }}
|
||||
<div v-if="count > 5">...</div>
|
||||
</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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user