feat: save current work to dev branch

This commit is contained in:
sanbuphy
2026-01-15 20:10:19 +08:00
parent c9e7ece75d
commit c8567ce23f
76 changed files with 28352 additions and 6 deletions
@@ -0,0 +1,403 @@
<template>
<div class="css-box-model">
<div class="model-container">
<div class="box-display">
<div
class="margin-box"
:style="{
padding: margin + 'px',
background: '#f3f4f6',
display: 'inline-block'
}"
>
<div
class="border-box"
:style="{
padding: borderWidth + 'px',
borderStyle: borderStyle,
borderColor: borderColor,
background: '#e5e7eb',
display: 'inline-block'
}"
>
<div
class="padding-box"
:style="{
padding: padding + 'px',
background: '#d1d5db',
display: 'inline-block'
}"
>
<div
class="content-box"
:style="{
width: width + 'px',
height: height + 'px',
background: contentColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '14px',
fontWeight: 'bold'
}"
>
{{ width }} × {{ height }}
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label>内容宽度 (Width)</label>
<input
type="range"
v-model.number="width"
min="50"
max="200"
class="slider"
/>
<span class="value">{{ width }}px</span>
</div>
<div class="control-group">
<label>内容高度 (Height)</label>
<input
type="range"
v-model.number="height"
min="50"
max="200"
class="slider"
/>
<span class="value">{{ height }}px</span>
</div>
<div class="control-group">
<label>内边距 (Padding)</label>
<input
type="range"
v-model.number="padding"
min="0"
max="50"
class="slider"
/>
<span class="value">{{ padding }}px</span>
</div>
<div class="control-group">
<label>边框宽度 (Border)</label>
<input
type="range"
v-model.number="borderWidth"
min="0"
max="20"
class="slider"
/>
<span class="value">{{ borderWidth }}px</span>
</div>
<div class="control-group">
<label>边框样式 (Style)</label>
<select v-model="borderStyle" class="select">
<option value="solid">solid (实线)</option>
<option value="dashed">dashed (虚线)</option>
<option value="dotted">dotted (点线)</option>
<option value="double">double (双线)</option>
</select>
</div>
<div class="control-group">
<label>外边距 (Margin)</label>
<input
type="range"
v-model.number="margin"
min="0"
max="50"
class="slider"
/>
<span class="value">{{ margin }}px</span>
</div>
<div class="control-group">
<label>内容颜色</label>
<input
type="color"
v-model="contentColor"
class="color-picker"
/>
</div>
<div class="control-group">
<label>边框颜色</label>
<input
type="color"
v-model="borderColor"
class="color-picker"
/>
</div>
</div>
<div class="dimensions">
<div class="dimension-item">
<span class="label">总宽度:</span>
<span class="value">{{ totalWidth }}px</span>
</div>
<div class="dimension-item">
<span class="label">总高度:</span>
<span class="value">{{ totalHeight }}px</span>
</div>
<div class="calculation">
总宽度 = {{ margin }} + {{ borderWidth }} + {{ padding }} + {{ width }} + {{ padding }} + {{ borderWidth }} + {{ margin }}
</div>
</div>
</div>
<div class="code-output">
<div class="code-title">💻 实时 CSS 代码</div>
<pre><code>.box {
/* 内容尺寸 */
width: {{ width }}px;
height: {{ height }}px;
/* 内边距 */
padding: {{ padding }}px;
/* 边框 */
border: {{ borderWidth }}px {{ borderStyle }} {{ borderColor }};
/* 外边距 */
margin: {{ margin }}px;
/* 内容背景色 */
background-color: {{ contentColor }};
}
/* 总尺寸计算 */
/* 总宽度: {{ totalWidth }}px */
/* 总高度: {{ totalHeight }}px */</code></pre>
</div>
<div class="explanation">
<div class="exp-title">📦 CSS 盒模型说明</div>
<div class="exp-content">
<strong>Content (内容)</strong>元素的实际内容通过 width height 设置
<br><br>
<strong>Padding (内边距)</strong>内容和边框之间的空间属于元素内部
<br><br>
<strong>Border (边框)</strong>包裹内容的边界线
<br><br>
<strong>Margin (外边距)</strong>元素外部的空间用于分隔其他元素
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const width = ref(100)
const height = ref(100)
const padding = ref(20)
const borderWidth = ref(5)
const borderStyle = ref('solid')
const margin = ref(20)
const contentColor = ref('#3b82f6')
const borderColor = ref('#1e40af')
const totalWidth = computed(() => {
return margin * 2 + borderWidth * 2 + padding * 2 + width
})
const totalHeight = computed(() => {
return margin * 2 + borderWidth * 2 + padding * 2 + height
})
</script>
<style scoped>
.css-box-model {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.model-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.box-display {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: repeating-conic-gradient(#f9fafb 0% 25%, #fff 0% 50%) 50% / 20px 20px;
border-radius: 8px;
margin-bottom: 20px;
padding: 20px;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.select {
padding: 8px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
cursor: pointer;
}
.color-picker {
width: 100%;
height: 40px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
}
.dimensions {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.dimension-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
}
.dimension-item .label {
color: var(--vp-c-text-2);
font-weight: 600;
}
.dimension-item .value {
color: var(--vp-c-brand);
font-family: monospace;
}
.calculation {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.code-output {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
.explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.exp-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.exp-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,369 @@
<template>
<div class="css-flexbox">
<div class="preview-container">
<div class="flex-container" :style="flexContainerStyle">
<div
v-for="(item, index) in items"
:key="index"
class="flex-item"
:style="{ flex: item.flex, minWidth: item.minWidth + 'px' }"
>
Item {{ index + 1 }}
</div>
</div>
</div>
<div class="controls">
<div class="control-section">
<div class="section-title">容器属性</div>
<div class="control-group">
<label>flex-direction (方向)</label>
<div class="button-group">
<button
v-for="dir in ['row', 'column', 'row-reverse', 'column-reverse']"
:key="dir"
class="control-btn"
:class="{ active: flexDirection === dir }"
@click="flexDirection = dir"
>
{{ dir }}
</button>
</div>
</div>
<div class="control-group">
<label>justify-content (主轴对齐)</label>
<div class="button-group">
<button
v-for="align in ['flex-start', 'center', 'flex-end', 'space-between', 'space-around', 'space-evenly']"
:key="align"
class="control-btn"
:class="{ active: justifyContent === align }"
@click="justifyContent = align"
>
{{ align }}
</button>
</div>
</div>
<div class="control-group">
<label>align-items (交叉轴对齐)</label>
<div class="button-group">
<button
v-for="align in ['stretch', 'flex-start', 'center', 'flex-end']"
:key="align"
class="control-btn"
:class="{ active: alignItems === align }"
@click="alignItems = align"
>
{{ align }}
</button>
</div>
</div>
<div class="control-group">
<label>flex-wrap (换行)</label>
<div class="button-group">
<button
class="control-btn"
:class="{ active: flexWrap === 'nowrap' }"
@click="flexWrap = 'nowrap'"
>
nowrap
</button>
<button
class="control-btn"
:class="{ active: flexWrap === 'wrap' }"
@click="flexWrap = 'wrap'"
>
wrap
</button>
<button
class="control-btn"
:class="{ active: flexWrap === 'wrap-reverse' }"
@click="flexWrap = 'wrap-reverse'"
>
wrap-reverse
</button>
</div>
</div>
<div class="control-group">
<label>gap (间距)</label>
<input
type="range"
v-model.number="gap"
min="0"
max="30"
class="slider"
/>
<span class="value">{{ gap }}px</span>
</div>
</div>
<div class="control-section">
<div class="section-title">项目属性</div>
<div class="control-group">
<label>Item 1 flex-grow</label>
<input
type="range"
v-model.number="items[0].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[0].flex }}</span>
</div>
<div class="control-group">
<label>Item 2 flex-grow</label>
<input
type="range"
v-model.number="items[1].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[1].flex }}</span>
</div>
<div class="control-group">
<label>Item 3 flex-grow</label>
<input
type="range"
v-model.number="items[2].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[2].flex }}</span>
</div>
</div>
</div>
<div class="code-output">
<div class="code-title">生成的 CSS 代码</div>
<pre><code>.container {
display: flex;
flex-direction: {{ flexDirection }};
justify-content: {{ justifyContent }};
align-items: {{ alignItems }};
flex-wrap: {{ flexWrap }};
gap: {{ gap }}px;
}
.item {
flex: {{ items[0].flex }}; /* 第一个项目的值 */
}</code></pre>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const flexDirection = ref('row')
const justifyContent = ref('flex-start')
const alignItems = ref('stretch')
const flexWrap = ref('nowrap')
const gap = ref(0)
const items = ref([
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 }
])
const flexContainerStyle = computed(() => ({
display: 'flex',
flexDirection: flexDirection.value,
justifyContent: justifyContent.value,
alignItems: alignItems.value,
flexWrap: flexWrap.value,
gap: gap.value + 'px',
minHeight: '200px',
background: '#f3f4f6',
borderRadius: '8px',
padding: '10px'
}))
</script>
<style scoped>
.css-flexbox {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.preview-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.flex-container {
width: 100%;
min-height: 200px;
}
.flex-item {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-align: center;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.flex-item:nth-child(2) {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.flex-item:nth-child(3) {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.control-btn {
padding: 6px 12px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.control-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
margin-left: 8px;
}
.code-output {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
</style>
@@ -0,0 +1,460 @@
<template>
<div class="deployment-architecture">
<div class="architecture-view">
<div class="view-selector">
<button
v-for="(view, index) in views"
:key="index"
class="view-btn"
:class="{ active: currentView === index }"
@click="currentView = index"
>
{{ view.name }}
</button>
</div>
<div class="architecture-diagram">
<!-- 基础架构 -->
<div v-if="currentView === 0" class="basic-architecture">
<div class="user-node">
<div class="node-icon">👤</div>
<div class="node-label">用户</div>
</div>
<div class="arrow-down"></div>
<div class="domain-node">
<div class="node-icon">🌐</div>
<div class="node-label">域名</div>
<div class="node-desc">example.com</div>
</div>
<div class="arrow-down"> DNS 解析</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器</div>
<div class="node-desc">IP: 1.2.3.4</div>
</div>
<div class="arrow-down"></div>
<div class="web-node">
<div class="node-icon">🌍</div>
<div class="node-label">Web 应用</div>
</div>
</div>
<!-- CDN 架构 -->
<div v-if="currentView === 1" class="cdn-architecture">
<div class="user-nodes">
<div class="user-node china">
<div class="node-icon">🇨🇳</div>
<div class="node-label">中国用户</div>
</div>
<div class="user-node usa">
<div class="node-icon">🇺🇸</div>
<div class="node-label">美国用户</div>
</div>
</div>
<div class="arrow-group">
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
<div class="cdn-nodes">
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN 北京节点</div>
</div>
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN 纽约节点</div>
</div>
</div>
<div class="arrow-down"> 缓存未命中</div>
<div class="origin-node">
<div class="node-icon">🖥</div>
<div class="node-label">源服务器</div>
</div>
</div>
<!-- 负载均衡 -->
<div v-if="currentView === 2" class="loadbalancer-architecture">
<div class="user-node">
<div class="node-icon">👥</div>
<div class="node-label">用户请求</div>
</div>
<div class="arrow-down"></div>
<div class="lb-node">
<div class="node-icon"></div>
<div class="node-label">负载均衡器</div>
</div>
<div class="arrow-group">
<div class="arrow-1"></div>
<div class="arrow-2"></div>
<div class="arrow-3"></div>
</div>
<div class="server-nodes">
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 1</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 2</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 3</div>
</div>
</div>
</div>
<!-- 完整架构 -->
<div v-if="currentView === 3" class="full-architecture">
<div class="user-nodes">
<div class="user-node">
<div class="node-icon">👤</div>
<div class="node-label">用户</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="dns-node">
<div class="node-icon">🔍</div>
<div class="node-label">DNS</div>
</div>
<div class="arrow-down"></div>
<div class="cdn-lb-row">
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN</div>
</div>
<div class="lb-node">
<div class="node-icon"></div>
<div class="node-label">LB</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="server-cluster">
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">Web 1</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">Web 2</div>
</div>
<div class="server-node">
<div class="node-icon">💾</div>
<div class="node-label">Database</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-cards">
<div class="info-card" v-if="currentView === 0">
<div class="card-title">🌐 域名 (Domain)</div>
<div class="card-content">
<strong>什么是域名</strong>
<br>域名是网站的地址 example.com便于记忆和访问
<br><br>
<strong>域名注册</strong>
<br> 注册商GoDaddyNamecheap阿里云
<br> 选择后缀.com.cn.org.io
<br> 价格$10-50/
</div>
</div>
<div class="info-card" v-if="currentView === 1">
<div class="card-title">📡 CDN (内容分发网络)</div>
<div class="card-content">
<strong>什么是 CDN</strong>
<br>将内容缓存到全球各地的节点用户就近访问
<br><br>
<strong>优势</strong>
<br> 加速访问就近获取内容
<br> 减轻负载减少源站压力
<br> 提高可用性节点故障自动切换
<br><br>
<strong>常见 CDN</strong>
<br> CloudflareAWS CloudFront阿里云 CDN
</div>
</div>
<div class="info-card" v-if="currentView === 2">
<div class="card-title"> 负载均衡 (Load Balancer)</div>
<div class="card-content">
<strong>什么是负载均衡</strong>
<br>将请求分发到多台服务器提高并发能力
<br><br>
<strong>负载均衡算法</strong>
<br> 轮询 (Round Robin)
<br> 最少连接 (Least Connections)
<br> IP 哈希 (IP Hash)
<br><br>
<strong>常见工具</strong>
<br> NginxHAProxyAWS ELB
</div>
</div>
<div class="info-card" v-if="currentView === 3">
<div class="card-title">🏗 完整部署架构</div>
<div class="card-content">
<strong>现代 Web 应用架构</strong>
<br><br>
1. 用户通过域名访问
<br>2. DNS 解析到 CDN 或负载均衡器
<br>3. CDN 缓存静态资源
<br>4. 负载均衡器分发请求
<br>5. Web 服务器处理动态请求
<br>6. 数据库存储持久化数据
<br><br>
<strong>监控和运维</strong>
<br> 日志收集性能监控自动备份
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentView = ref(0)
const views = [
{ name: '基础架构' },
{ name: 'CDN 加速' },
{ name: '负载均衡' },
{ name: '完整架构' }
]
</script>
<style scoped>
.deployment-architecture {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.architecture-view {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.view-selector {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
flex-wrap: wrap;
}
.view-btn {
padding: 10px 20px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.view-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.architecture-diagram {
min-height: 300px;
}
.node-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.node-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.node-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.user-node,
.domain-node,
.server-node,
.web-node,
.cdn-node,
.lb-node,
.dns-node,
.origin-node {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 15px;
text-align: center;
margin: 0 auto;
max-width: 200px;
}
.arrow-down {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 10px 0;
}
.basic-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.cdn-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.user-nodes {
display: flex;
gap: 30px;
justify-content: center;
}
.user-node.china {
background: #ffebee;
border-color: #f44336;
}
.user-node.usa {
background: #e3f2fd;
border-color: #2196f3;
}
.arrow-group {
display: flex;
gap: 20px;
font-size: 2rem;
color: var(--vp-c-text-3);
}
.cdn-nodes {
display: flex;
gap: 20px;
}
.cdn-node {
background: #e8f5e9;
border-color: #4caf50;
}
.loadbalancer-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.lb-node {
background: #fff3e0;
border-color: #ff9800;
}
.server-nodes {
display: flex;
gap: 15px;
}
.full-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.cdn-lb-row {
display: flex;
gap: 20px;
}
.server-cluster {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.info-cards {
display: grid;
gap: 15px;
}
.info-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.card-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.card-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
@media (max-width: 768px) {
.user-nodes,
.cdn-nodes,
.server-nodes,
.cdn-lb-row,
.server-cluster {
flex-direction: column;
align-items: center;
}
}
</style>
@@ -0,0 +1,429 @@
<template>
<div class="dns-lookup-demo">
<div class="domain-input">
<label>输入域名</label>
<input
type="text"
v-model="domain"
placeholder="例如: www.google.com"
class="input-field"
@keyup.enter="startLookup"
/>
<button class="lookup-btn" @click="startLookup">
🔍 开始解析
</button>
</div>
<div class="lookup-process" v-if="isLooking">
<div class="process-title">DNS 解析过程</div>
<div class="step-list">
<div
v-for="(step, index) in steps"
:key="index"
class="step-item"
:class="{
active: currentStep === index,
completed: currentStep > index
}"
>
<div class="step-icon">
{{ currentStep > index ? '✓' : index + 1 }}
</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep === index" class="step-animation">
{{ step.animation }}
</div>
</div>
<div class="step-arrow" v-if="index < steps.length - 1">
</div>
</div>
</div>
</div>
<div class="result-box" v-if="completed">
<div class="result-title"> 解析完成</div>
<div class="result-content">
<div class="result-item">
<span class="label">域名:</span>
<span class="value">{{ domain }}</span>
</div>
<div class="result-item">
<span class="label">IP 地址:</span>
<span class="value">{{ resolvedIP }}</span>
</div>
<div class="result-item">
<span class="label">解析时间:</span>
<span class="value">{{ lookupTime }}ms</span>
</div>
</div>
<button class="reset-btn" @click="reset">
🔄 重新解析
</button>
</div>
<div class="info-box">
<div class="info-title">💡 DNS 知识点</div>
<div class="info-content">
<div class="info-item">
<strong>什么是 DNS</strong>
<br>
DNS域名系统就像互联网的电话簿将易记的域名 google.com转换为计算机能识别的 IP 地址 142.250.185.238
</div>
<div class="info-item">
<strong>为什么需要 DNS</strong>
<br>
IP 地址难记142.250.185.238 vs google.com
<br>
IP 可能变化服务器迁移时 IP 会变域名不变
<br>
负载均衡一个域名可以对应多个 IP
</div>
<div class="info-item">
<strong>DNS 解析的层次</strong>
<br>
1 浏览器缓存最近访问过的域名
<br>
2 系统缓存操作系统的 DNS 缓存
<br>
3 路由器缓存本地路由器的缓存
<br>
4 ISP DNS网络服务商的 DNS 服务器
<br>
5 根域名服务器最高层级的 DNS
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const domain = ref('www.google.com')
const isLooking = ref(false)
const currentStep = ref(-1)
const completed = ref(false)
const resolvedIP = ref('')
const lookupTime = ref(0)
const steps = [
{
title: '检查浏览器缓存',
desc: '查看最近是否访问过该域名',
animation: '🔍 正在搜索浏览器缓存...'
},
{
title: '检查系统缓存',
desc: '查看操作系统的 DNS 缓存',
animation: '💻 正在查询系统 DNS 缓存...'
},
{
title: '查询路由器 DNS',
desc: '向本地路由器发送 DNS 查询',
animation: '📡 正在向路由器发送查询...'
},
{
title: '查询 ISP DNS 服务器',
desc: '向网络服务商的 DNS 服务器查询',
animation: '🌐 正在联系 ISP DNS 服务器...'
},
{
title: '查询根域名服务器',
desc: '从 . 根服务器开始递归查询',
animation: '🔝 正在查询根域名服务器...'
},
{
title: '获取 IP 地址',
desc: '成功解析到 IP 地址',
animation: '✅ 找到 IP 地址!'
}
]
const ipAddresses = {
'www.google.com': '142.250.185.238',
'www.baidu.com': '110.242.68.4',
'www.github.com': '140.82.112.3',
'default': '93.184.216.34'
}
const startLookup = () => {
isLooking.value = true
completed.value = false
currentStep.value = -1
const startTime = Date.now()
// 模拟 DNS 查询过程
let stepIndex = 0
const interval = setInterval(() => {
if (stepIndex < steps.length) {
currentStep.value = stepIndex
stepIndex++
} else {
clearInterval(interval)
const endTime = Date.now()
lookupTime.value = endTime - startTime
resolvedIP.value = ipAddresses[domain.value.toLowerCase()] || ipAddresses['default']
completed.value = true
}
}, 800)
}
const reset = () => {
isLooking.value = false
currentStep.value = -1
completed.value = false
}
</script>
<style scoped>
.dns-lookup-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.domain-input {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.domain-input label {
width: 100%;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.input-field {
flex: 1;
min-width: 200px;
padding: 12px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.input-field:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.lookup-btn {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.lookup-btn:hover {
background: var(--vp-c-brand-dark);
}
.lookup-process {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.process-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 20px;
text-align: center;
}
.step-list {
display: flex;
flex-direction: column;
gap: 0;
}
.step-item {
display: flex;
align-items: flex-start;
gap: 15px;
position: relative;
opacity: 0.3;
transition: opacity 0.3s;
}
.step-item.active {
opacity: 1;
}
.step-item.completed {
opacity: 0.7;
}
.step-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-divider);
color: var(--vp-c-text-3);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-item.active .step-icon {
background: var(--vp-c-brand);
color: white;
}
.step-item.completed .step-icon {
background: #22c55e;
color: white;
}
.step-content {
flex: 1;
padding-top: 5px;
}
.step-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.step-animation {
font-size: 0.8rem;
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 8px;
border-radius: 4px;
font-family: monospace;
}
.step-arrow {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: calc(100% - 20px);
background: var(--vp-c-divider);
}
.step-item.completed .step-arrow {
background: #22c55e;
}
.result-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #22c55e;
}
.result-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.result-content {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.result-item {
display: flex;
justify-content: space-between;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.result-item .label {
font-size: 0.85rem;
color: var(--vp-c-text-3);
font-weight: 600;
}
.result-item .value {
font-size: 0.9rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
}
.reset-btn {
width: 100%;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.reset-btn:hover {
background: var(--vp-c-brand-dark);
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.info-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,408 @@
<template>
<div class="dom-manipulator">
<div class="preview-area">
<div class="preview-title">实时预览</div>
<div class="preview-box" ref="previewBox">
<div id="target-element" :style="elementStyle">
{{ text }}
</div>
</div>
</div>
<div class="controls">
<div class="control-section">
<div class="section-title">📝 文本内容操作</div>
<div class="control-group">
<label>修改文本</label>
<input
type="text"
v-model="text"
class="text-input"
placeholder="输入文本..."
/>
</div>
<div class="button-group">
<button class="action-btn" @click="changeText">
修改内容
</button>
<button class="action-btn" @click="appendText">
追加内容
</button>
<button class="action-btn" @click="clearText">
清空内容
</button>
</div>
</div>
<div class="control-section">
<div class="section-title">🎨 样式操作</div>
<div class="control-group">
<label>背景颜色</label>
<input
type="color"
v-model="backgroundColor"
class="color-picker"
/>
</div>
<div class="control-group">
<label>文字颜色</label>
<input
type="color"
v-model="color"
class="color-picker"
/>
</div>
<div class="control-group">
<label>字体大小 ({{ fontSize }}px)</label>
<input
type="range"
v-model.number="fontSize"
min="12"
max="48"
class="slider"
/>
</div>
<div class="control-group">
<label>内边距 ({{ padding }}px)</label>
<input
type="range"
v-model.number="padding"
min="0"
max="50"
class="slider"
/>
</div>
<div class="control-group">
<label>圆角 ({{ borderRadius }}px)</label>
<input
type="range"
v-model.number="borderRadius"
min="0"
max="50"
class="slider"
/>
</div>
<div class="button-group">
<button class="action-btn" @click="toggleHidden">
{{ isHidden ? '显示' : '隐藏' }}
</button>
<button class="action-btn" @click="resetStyles">
重置样式
</button>
</div>
</div>
<div class="control-section">
<div class="section-title">📊 属性操作</div>
<div class="property-list">
<div class="property-item">
<span class="prop-label">元素 ID:</span>
<span class="prop-value">target-element</span>
</div>
<div class="property-item">
<span class="prop-label">类名:</span>
<span class="prop-value">{{ className }}</span>
</div>
<div class="property-item">
<span class="prop-label">可见性:</span>
<span class="prop-value">{{ isHidden ? '隐藏' : '可见' }}</span>
</div>
<div class="property-item">
<span class="prop-label">文本长度:</span>
<span class="prop-value">{{ text.length }} 字符</span>
</div>
</div>
</div>
</div>
<div class="code-display">
<div class="code-title">💻 等效的 JavaScript 代码</div>
<pre><code>// 获取元素
const element = document.getElementById('target-element');
// 修改文本内容
element.textContent = '{{ text }}';
// 修改样式
element.style.backgroundColor = '{{ backgroundColor }}';
element.style.color = '{{ color }}';
element.style.fontSize = '{{ fontSize }}px';
element.style.padding = '{{ padding }}px';
element.style.borderRadius = '{{ borderRadius }}px';
// 显示/隐藏
element.style.display = '{{ isHidden ? 'none' : 'block' }}';</code></pre>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const text = ref('Hello DOM!')
const backgroundColor = ref('#3b82f6')
const color = ref('#ffffff')
const fontSize = ref(24)
const padding = ref(20)
const borderRadius = ref(8)
const isHidden = ref(false)
const className = ref('demo-element')
const elementStyle = computed(() => ({
backgroundColor: backgroundColor.value,
color: color.value,
fontSize: fontSize.value + 'px',
padding: padding.value + 'px',
borderRadius: borderRadius.value + 'px',
display: isHidden.value ? 'none' : 'block',
transition: 'all 0.3s ease',
fontWeight: 'bold',
textAlign: 'center',
minHeight: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}))
const changeText = () => {
const newTexts = [
'Hello World!',
'DOM 很有趣!',
'JavaScript 强大!',
'继续学习!',
'你真棒!'
]
text.value = newTexts[Math.floor(Math.random() * newTexts.length)]
}
const appendText = () => {
text.value += ' 👋'
}
const clearText = () => {
text.value = ''
}
const toggleHidden = () => {
isHidden.value = !isHidden.value
}
const resetStyles = () => {
backgroundColor.value = '#3b82f6'
color.value = '#ffffff'
fontSize.value = 24
padding.value = 20
borderRadius.value = 8
isHidden.value = false
}
</script>
<style scoped>
.dom-manipulator {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.preview-area {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.preview-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.preview-box {
min-height: 200px;
background: repeating-conic-gradient(#f9fafb 0% 25%, #fff 0% 50%) 50% / 20px 20px;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.text-input {
width: 100%;
padding: 10px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.color-picker {
width: 100%;
height: 40px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.action-btn {
padding: 8px 16px;
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-bg-soft);
color: var(--vp-c-brand);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--vp-c-brand);
color: white;
}
.property-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.property-item {
display: flex;
justify-content: space-between;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.85rem;
}
.prop-label {
color: var(--vp-c-text-3);
font-weight: 600;
}
.prop-value {
color: var(--vp-c-brand);
font-family: monospace;
}
.code-display {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
</style>
@@ -0,0 +1,360 @@
<template>
<div class="git-workflow">
<div class="control-panel">
<button class="action-btn" @click="init" :disabled="inited">
📁 初始化仓库
</button>
<button class="action-btn" @click="commit" :disabled="!inited">
提交 (Commit)
</button>
<button class="action-btn" @click="createBranch" :disabled="!inited || branches.length >= 3">
🌿 创建分支
</button>
<button class="action-btn" @click="merge" :disabled="!inited || branches.length < 2">
🔀 合并分支
</button>
<button class="action-btn danger" @click="reset">
🔄 重置
</button>
</div>
<div class="visualization">
<div class="branch-lines">
<svg class="git-graph" viewBox="0 0 400 200">
<!-- Main branch line -->
<line x1="50" y1="50" x2="350" y2="50" stroke="#e34c26" stroke-width="3" />
<!-- Feature branch line -->
<line
v-if="branches.length > 1"
x1="150"
y1="50"
x2="350"
y2="50"
stroke="#264de4"
stroke-width="3"
:style="{ transform: `translateY(${branches.length > 1 ? 50 : 0}px)` }"
/>
<!-- Commits on main branch -->
<circle v-for="(commit, index) in mainBranchCommits" :key="'main-' + index"
cx="80 + index * 60"
cy="50"
r="12"
:fill="commit.merged ? '#9ca3af' : '#e34c26'"
stroke="white"
stroke-width="2"
/>
<!-- Commits on feature branch -->
<circle v-for="(commit, index) in featureBranchCommits" :key="'feat-' + index"
v-if="branches.length > 1"
cx="140 + (index + 1) * 60"
cy="100"
r="12"
fill="#264de4"
stroke="white"
stroke-width="2"
/>
<!-- Merge arrow -->
<path v-if="showMergeArrow"
d="M 320 100 Q 340 75, 320 50"
stroke="#22c55e"
stroke-width="2"
fill="none"
stroke-dasharray="5,5"
/>
</svg>
</div>
<div class="commit-list">
<div class="section-title">提交历史</div>
<div class="commits">
<div v-for="(commit, index) in allCommits" :key="index" class="commit-item">
<div class="commit-hash">{{ commit.hash }}</div>
<div class="commit-message">{{ commit.message }}</div>
<div class="commit-branch">{{ commit.branch }}</div>
</div>
<div v-if="allCommits.length === 0" class="no-commits">
暂无提交点击"初始化仓库"开始
</div>
</div>
</div>
</div>
<div class="info-panel">
<div class="info-title">💡 Git 核心概念</div>
<div class="info-content">
<div class="concept-item">
<strong>📁 工作区 (Working Directory)</strong>你实际操作的文件
</div>
<div class="concept-item">
<strong>📦 暂存区 (Staging Area)</strong>准备提交的文件
</div>
<div class="concept-item">
<strong>📚 仓库 (Repository)</strong>保存提交历史的地方
</div>
<div class="concept-item">
<strong>🌿 分支 (Branch)</strong>独立的开发线互不干扰
</div>
<div class="concept-item">
<strong>🔀 合并 (Merge)</strong>将分支的改动整合到一起
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inited = ref(false)
const commitCount = ref(0)
const branches = ref(['main'])
const currentBranch = ref('main')
const commits = ref([])
const showMergeArrow = ref(false)
const mainBranchCommits = computed(() => {
return commits.value.filter(c => c.branch === 'main')
})
const featureBranchCommits = computed(() => {
return commits.value.filter(c => c.branch === 'feature')
})
const allCommits = computed(() => {
return [...commits.value].reverse()
})
const generateHash = () => {
return Math.random().toString(16).substr(2, 7)
}
const messages = [
'初始化项目',
'添加基础功能',
'修复 bug',
'更新文档',
'优化性能',
'添加新特性',
'重构代码',
'改进样式'
]
const init = () => {
inited.value = true
commitCount.value = 0
branches.value = ['main']
commits.value = []
}
const commit = () => {
commitCount.value++
const message = messages[(commitCount.value - 1) % messages.length]
commits.value.push({
hash: generateHash(),
message: `${message} #${commitCount.value}`,
branch: currentBranch.value,
merged: false
})
}
const createBranch = () => {
if (branches.value.length < 3) {
const newBranch = 'feature'
branches.value.push(newBranch)
currentBranch.value = newBranch
}
}
const merge = () => {
if (branches.value.length >= 2) {
showMergeArrow.value = true
setTimeout(() => {
// Mark feature commits as merged
commits.value.forEach(c => {
if (c.branch === 'feature') {
c.merged = true
}
})
// Create merge commit
commits.value.push({
hash: generateHash(),
message: '合并分支 feature → main',
branch: 'main',
merged: false
})
branches.value = ['main']
currentBranch.value = 'main'
showMergeArrow.value = false
}, 1000)
}
}
const reset = () => {
inited.value = false
commitCount.value = 0
branches.value = ['main']
currentBranch.value = 'main'
commits.value = []
showMergeArrow.value = false
}
</script>
<style scoped>
.git-workflow {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 25px;
}
.action-btn {
padding: 10px 18px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.danger:hover {
border-color: #ef4444;
color: #ef4444;
}
.visualization {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.visualization {
grid-template-columns: 1fr;
}
}
.branch-lines {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.git-graph {
width: 100%;
height: 150px;
}
.commit-list {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--vp-c-divider);
}
.commits {
max-height: 150px;
overflow-y: auto;
}
.commit-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
margin-bottom: 6px;
background: var(--vp-c-bg-soft);
font-size: 0.85rem;
}
.commit-hash {
font-family: monospace;
color: var(--vp-c-brand);
font-weight: 600;
min-width: 70px;
}
.commit-message {
flex: 1;
color: var(--vp-c-text-1);
}
.commit-branch {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 10px;
background: var(--vp-c-brand);
color: white;
}
.no-commits {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.85rem;
padding: 20px;
}
.info-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.concept-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
padding-left: 10px;
}
</style>
@@ -0,0 +1,325 @@
<template>
<div class="network-layers">
<div class="layers-stack">
<div
v-for="(layer, index) in layers"
:key="layer.name"
class="layer-card"
:class="{ active: selectedLayer === index }"
@click="selectedLayer = index"
>
<div class="layer-number">{{ index + 1 }}</div>
<div class="layer-content">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-english">{{ layer.english }}</div>
<div class="layer-protocols">{{ layer.protocols }}</div>
</div>
<div class="layer-icon">{{ layer.icon }}</div>
</div>
</div>
<div class="layer-detail" v-if="selectedLayer !== null">
<div class="detail-title">{{ layers[selectedLayer].name }}</div>
<div class="detail-desc">{{ layers[selectedLayer].description }}</div>
<div class="detail-functions">
<div class="function-title">主要功能</div>
<div class="function-list">
<div v-for="(func, index) in layers[selectedLayer].functions" :key="index" class="function-item">
{{ func }}
</div>
</div>
</div>
<div class="detail-examples">
<div class="example-title">常见设备</div>
<div class="example-list">
<div v-for="(device, index) in layers[selectedLayer].devices" :key="index" class="example-item">
📡 {{ device }}
</div>
</div>
</div>
</div>
<div class="data-flow">
<div class="flow-title">数据封装过程发送</div>
<div class="flow-steps">
<div class="flow-step" v-for="(step, index) in 5" :key="index">
<div class="step-label">{{ layers[4 - index].name }}</div>
<div class="step-box">
<span class="box-label">{{ layers[4 - index].dataUnit }}</span>
</div>
<div class="step-arrow" v-if="index < 4"> 添加头部</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedLayer = ref(0)
const layers = [
{
name: '应用层',
english: 'Application Layer',
protocols: 'HTTP, HTTPS, FTP, SMTP, DNS, SSH',
icon: '📱',
dataUnit: '数据',
description: '直接为用户的应用程序(如浏览器、邮件客户端)提供网络服务接口。',
functions: [
'为应用程序提供网络接口',
'定义应用程序间通信的协议',
'处理数据格式和加密',
'用户认证和授权'
],
devices: ['网关', '防火墙', '代理服务器']
},
{
name: '传输层',
english: 'Transport Layer',
protocols: 'TCP, UDP',
icon: '🚚',
dataUnit: '段/数据报',
description: '负责端到端的通信,确保数据可靠地从源端传输到目的端。',
functions: [
'分段和重组数据',
'端口号寻址(进程间通信)',
'流量控制和拥塞控制',
'错误检测和纠正(TCP'
],
devices: ['防火墙', '负载均衡器']
},
{
name: '网络层',
english: 'Network Layer',
protocols: 'IP, ICMP, IGMP, ARP',
icon: '🌐',
dataUnit: '包',
description: '负责数据包的路由选择,通过网络将数据从源主机传输到目的主机。',
functions: [
'逻辑寻址(IP 地址)',
'路由选择和转发',
'分组交换',
'拥塞控制'
],
devices: ['路由器', '三层交换机']
},
{
name: '数据链路层',
english: 'Data Link Layer',
protocols: 'Ethernet, Wi-Fi, PPP',
icon: '🔗',
dataUnit: '帧',
description: '负责在直连的两个节点间传输数据,处理物理层的错误。',
functions: [
'物理地址寻址(MAC 地址)',
'帧的封装和解封装',
'错误检测(CRC',
'流量控制',
'介质访问控制(MAC'
],
devices: ['交换机', '网桥', '网卡']
},
{
name: '物理层',
english: 'Physical Layer',
protocols: 'Ethernet PHY, Wi-Fi Radio, USB',
icon: '⚡',
dataUnit: '比特',
description: '负责在物理介质上传输原始的比特流(0 和 1)。',
functions: [
'定义物理设备标准',
'传输介质规范',
'比特传输和同步',
'电气特性和机械特性'
],
devices: ['中继器', '集线器', '网线', '光纤']
}
]
</script>
<style scoped>
.network-layers {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.layers-stack {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 25px;
}
.layer-card {
display: flex;
align-items: center;
gap: 15px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
}
.layer-card:hover {
border-color: var(--vp-c-brand);
transform: translateX(5px);
}
.layer-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.layer-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
}
.layer-content {
flex: 1;
}
.layer-name {
font-size: 1.1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.layer-english {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.layer-protocols {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.layer-icon {
font-size: 2rem;
}
.layer-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 4px solid var(--vp-c-brand);
}
.detail-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.detail-desc {
font-size: 0.95rem;
color: var(--vp-c-text-2);
line-height: 1.8;
margin-bottom: 20px;
}
.detail-functions,
.detail-examples {
margin-bottom: 15px;
}
.function-title,
.example-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.function-list,
.example-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.function-item,
.example-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
padding-left: 10px;
}
.data-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.flow-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
text-align: center;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 10px;
}
.flow-step {
display: flex;
align-items: center;
gap: 15px;
}
.step-label {
width: 100px;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: right;
}
.step-box {
flex: 1;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 6px;
padding: 10px;
text-align: center;
position: relative;
}
.box-label {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: 600;
}
.step-arrow {
width: 100px;
font-size: 0.75rem;
color: var(--vp-c-text-3);
text-align: center;
}
</style>
@@ -0,0 +1,675 @@
<template>
<div class="network-troubleshooting">
<div class="problem-selector">
<div class="selector-title">选择问题类型</div>
<div class="problem-list">
<button
v-for="(problem, index) in problems"
:key="index"
class="problem-btn"
:class="{ active: selectedProblem === index }"
@click="selectProblem(index)"
>
<span class="problem-icon">{{ problem.icon }}</span>
<span class="problem-text">{{ problem.name }}</span>
</button>
</div>
</div>
<div class="solution-panel" v-if="selectedProblem !== null">
<div class="solution-header">
<div class="solution-title">{{ problems[selectedProblem].name }}</div>
<div class="solution-desc">{{ problems[selectedProblem].description }}</div>
</div>
<div class="solution-steps">
<div class="steps-title">🔧 解决步骤</div>
<div class="steps-list">
<div
v-for="(step, index) in problems[selectedProblem].steps"
:key="index"
class="step-item"
:class="{ completed: completedSteps.has(index) }"
@click="toggleStep(index)"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-action">{{ step.action }}</div>
<div class="step-command" v-if="step.command">
<code>{{ step.command }}</code>
</div>
<div class="step-explanation">{{ step.explanation }}</div>
</div>
<div class="step-check">
{{ completedSteps.has(index) ? '✓' : '○' }}
</div>
</div>
</div>
</div>
<div class="related-tools">
<div class="tools-title">🛠 相关工具</div>
<div class="tools-list">
<div
v-for="(tool, index) in problems[selectedProblem].tools"
:key="index"
class="tool-item"
>
<div class="tool-name">{{ tool.name }}</div>
<div class="tool-usage">{{ tool.usage }}</div>
</div>
</div>
</div>
</div>
<div class="common-commands">
<div class="commands-title">📋 常用诊断命令</div>
<div class="commands-grid">
<div class="command-card" v-for="(cmd, index) in commands" :key="index">
<div class="command-name">{{ cmd.name }}</div>
<div class="command-syntax">{{ cmd.syntax }}</div>
<div class="command-desc">{{ cmd.description }}</div>
</div>
</div>
</div>
<div class="troubleshooting-tips">
<div class="tips-title">💡 故障排查技巧</div>
<div class="tips-list">
<div class="tip-item">
<div class="tip-number">1</div>
<div class="tip-content">
<strong>从底层到顶层</strong>
<br>物理层 链路层 网络层 传输层 应用层
</div>
</div>
<div class="tip-item">
<div class="tip-number">2</div>
<div class="tip-content">
<strong>分层排查</strong>
<br>先确定问题发生在哪一层再针对性解决
</div>
</div>
<div class="tip-item">
<div class="tip-number">3</div>
<div class="tip-content">
<strong>二分法定位</strong>
<br> ping 本机 ping 网关 ping 外网 ping 域名
</div>
</div>
<div class="tip-item">
<div class="tip-number">4</div>
<div class="tip-content">
<strong>查看日志</strong>
<br>系统日志应用日志防火墙日志记录关键信息
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedProblem = ref(0)
const completedSteps = ref(new Set())
const problems = [
{
icon: '🌐',
name: '无法访问网页',
description: '浏览器无法打开网站,显示连接错误',
steps: [
{
action: '检查网络连接',
command: 'ping 8.8.8.8',
explanation: '测试是否能够连接到互联网(8.8.8.8 是 Google DNS'
},
{
action: '检查 DNS 解析',
command: 'nslookup google.com',
explanation: '测试域名是否能正确解析为 IP 地址'
},
{
action: '清除 DNS 缓存',
command: 'ipconfig /flushdns (Windows)',
explanation: '清除本地 DNS 缓存,可能解决 DNS 污染或过期问题'
},
{
action: '检查代理设置',
command: '查看浏览器代理设置',
explanation: '确认没有配置错误的代理服务器'
},
{
action: '测试其他网站',
command: '尝试访问不同网站',
explanation: '确定是单个网站问题还是全局网络问题'
}
],
tools: [
{ name: 'ping', usage: '测试网络连通性' },
{ name: 'nslookup', usage: '查询 DNS 记录' },
{ name: 'traceroute', usage: '追踪网络路由' }
]
},
{
icon: '📶',
name: 'Wi-Fi 连接问题',
description: 'Wi-Fi 信号弱、频繁断开或无法连接',
steps: [
{
action: '检查 Wi-Fi 开关',
command: '检查物理开关或系统设置',
explanation: '确认 Wi-Fi 功能已开启'
},
{
action: '重启网络设备',
command: '重启路由器和光猫',
explanation: '电源重启可以解决大部分临时故障'
},
{
action: '忘记网络重新连接',
command: '删除 Wi-Fi 配置后重新输入密码',
explanation: '清除错误的配置信息'
},
{
action: '更新网卡驱动',
command: '设备管理器 → 网络适配器 → 更新驱动',
explanation: '过时的驱动可能导致兼容性问题'
},
{
action: '更改 DNS 服务器',
command: '设置为 8.8.8.8 或 114.114.114.114',
explanation: 'ISP 的 DNS 可能不稳定'
}
],
tools: [
{ name: 'wifi-menu (macOS)', usage: '查看 Wi-Fi 信息' },
{ name: 'netsh wlan (Windows)', usage: '管理无线网络' },
{ name: 'iwconfig (Linux)', usage: '配置无线接口' }
]
},
{
icon: '🐌',
name: '网速很慢',
description: '网络连接正常但速度很慢',
steps: [
{
action: '测试实际带宽',
command: '访问 speedtest.net',
explanation: '测试当前网络的上传和下载速度'
},
{
action: '检查网络占用',
command: 'netstat -an | grep ESTABLISHED',
explanation: '查看是否有大量连接占用带宽'
},
{
action: '关闭后台应用',
command: '检查下载、更新、云同步等',
explanation: '后台应用可能占用大量带宽'
},
{
action: '更换信道',
command: '路由器管理后台 → 无线设置',
explanation: '拥挤的信道会严重影响 Wi-Fi 速度'
},
{
action: '联系 ISP',
command: '检查运营商是否有故障或限速',
explanation: '可能是运营商线路问题'
}
],
tools: [
{ name: 'speedtest-cli', usage: '命令行测速' },
{ name: 'nethogs', usage: '查看进程流量' },
{ name: 'iftop', usage: '实时监控带宽' }
]
},
{
icon: '⏱️',
name: '延迟很高',
description: '网络响应慢,游戏卡顿',
steps: [
{
action: '测试 ping 值',
command: 'ping -c 100 google.com',
explanation: '发送 100 个包,统计平均延迟和丢包率'
},
{
action: '追踪路由',
command: 'traceroute google.com',
explanation: '查看哪一跳延迟过高'
},
{
action: '检查本地网络',
command: 'ping 局域网其他设备',
explanation: '排除本地网络问题'
},
{
action: '使用有线连接',
command: '插入网线测试',
explanation: 'Wi-Fi 可能不稳定或有干扰'
},
{
action: '检查 QoS 设置',
command: '路由器 QoS 配置',
explanation: '可能被其他设备或应用占用优先级'
}
],
tools: [
{ name: 'ping', usage: '测试延迟和丢包' },
{ name: 'traceroute', usage: '追踪路由路径' },
{ name: 'mtr', usage: '结合 ping 和 traceroute' }
]
},
{
icon: '🔌',
name: '端口无法访问',
description: '服务正常运行但外部无法访问',
steps: [
{
action: '检查服务监听',
command: 'netstat -tuln | grep :80',
explanation: '确认服务正在监听正确的端口'
},
{
action: '检查防火墙',
command: 'iptables -L (Linux) 或 firewall-cmd (CentOS)',
explanation: '防火墙可能阻止了端口'
},
{
action: '测试本地访问',
command: 'curl http://localhost:8080',
explanation: '确认服务本身运行正常'
},
{
action: '检查云服务商安全组',
command: '控制台 → 安全组规则',
explanation: '云服务器需要额外配置安全组'
},
{
action: '检查端口占用',
command: 'lsof -i :8080',
explanation: '确认端口没有被其他程序占用'
}
],
tools: [
{ name: 'netstat', usage: '查看网络连接' },
{ name: 'telnet', usage: '测试端口连通性' },
{ name: 'nmap', usage: '端口扫描工具' }
]
}
]
const commands = [
{
name: 'ping',
syntax: 'ping [host]',
description: '测试到目标主机的连通性和延迟'
},
{
name: 'traceroute',
syntax: 'traceroute [host]',
description: '显示数据包到达目标的路由路径'
},
{
name: 'nslookup',
syntax: 'nslookup [domain]',
description: '查询域名的 DNS 记录'
},
{
name: 'netstat',
syntax: 'netstat -tuln',
description: '显示网络连接和监听端口'
},
{
name: 'curl',
syntax: 'curl -v [url]',
description: '测试 HTTP 请求并查看详细信息'
},
{
name: 'tcpdump',
syntax: 'tcpdump -i eth0',
description: '抓取网络数据包进行分析'
}
]
const selectProblem = (index) => {
selectedProblem.value = index
completedSteps.value = new Set()
}
const toggleStep = (index) => {
if (completedSteps.value.has(index)) {
completedSteps.value.delete(index)
} else {
completedSteps.value.add(index)
}
}
</script>
<style scoped>
.network-troubleshooting {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.problem-selector {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.selector-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.problem-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.problem-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
cursor: pointer;
transition: all 0.2s;
}
.problem-btn:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.problem-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
}
.problem-btn.active .problem-text {
color: white;
}
.problem-icon {
font-size: 1.5rem;
}
.problem-text {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.solution-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.solution-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid var(--vp-c-divider);
}
.solution-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.solution-desc {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.solution-steps {
margin-bottom: 25px;
}
.steps-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.steps-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-item {
display: flex;
gap: 15px;
padding: 15px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.2s;
}
.step-item:hover {
border-left-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.step-item.completed {
border-left-color: #22c55e;
opacity: 0.7;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-action {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.step-command {
margin-bottom: 6px;
}
.step-command code {
background: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.step-explanation {
font-size: 0.85rem;
color: var(--vp-c-text-3);
line-height: 1.6;
}
.step-check {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
.step-item.completed .step-check {
border-color: #22c55e;
color: #22c55e;
}
.related-tools {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.tools-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.tools-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tool-item {
background: var(--vp-c-bg);
padding: 10px 15px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.tool-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
margin-bottom: 4px;
}
.tool-usage {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.common-commands {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.commands-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.commands-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.command-card {
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.command-name {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-brand);
font-family: monospace;
margin-bottom: 6px;
}
.command-syntax {
font-size: 0.8rem;
color: var(--vp-c-text-2);
font-family: monospace;
margin-bottom: 6px;
}
.command-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
line-height: 1.5;
}
.troubleshooting-tips {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.tips-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.tip-item {
display: flex;
gap: 15px;
}
.tip-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.tip-content {
flex: 1;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,527 @@
<template>
<div class="subnet-calculator">
<div class="calculator-input">
<div class="input-group">
<label class="input-label">IP 地址</label>
<input
v-model="ipAddress"
type="text"
placeholder="例如: 192.168.1.0"
class="ip-input"
/>
</div>
<div class="input-group">
<label class="input-label">子网掩码</label>
<select v-model="cidr" class="cidr-select">
<option v-for="n in 32" :key="n" :value="n">/{{ n }}</option>
</select>
</div>
<button class="calculate-btn" @click="calculate">计算</button>
</div>
<div class="results" v-if="results">
<div class="result-section">
<div class="section-title">基本信息</div>
<div class="result-grid">
<div class="result-item">
<div class="result-label">网络地址</div>
<div class="result-value">{{ results.network }}</div>
</div>
<div class="result-item">
<div class="result-label">广播地址</div>
<div class="result-value">{{ results.broadcast }}</div>
</div>
<div class="result-item">
<div class="result-label">子网掩码</div>
<div class="result-value">{{ results.mask }}</div>
</div>
<div class="result-item">
<div class="result-label">可用主机数</div>
<div class="result-value">{{ results.hosts }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">IP 范围</div>
<div class="range-display">
<div class="range-item">
<div class="range-label">起始 IP</div>
<div class="range-value">{{ results.firstHost }}</div>
</div>
<div class="range-arrow"></div>
<div class="range-item">
<div class="range-label">结束 IP</div>
<div class="range-value">{{ results.lastHost }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">二进制表示</div>
<div class="binary-display">
<div class="binary-row">
<div class="binary-label">IP 地址</div>
<div class="binary-value">{{ results.binaryIp }}</div>
</div>
<div class="binary-row">
<div class="binary-label">子网掩码</div>
<div class="binary-value">{{ results.binaryMask }}</div>
</div>
<div class="binary-row">
<div class="binary-label">网络地址</div>
<div class="binary-value">{{ results.binaryNetwork }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">子网类型</div>
<div class="subnet-info">
<div class="info-tag" :class="getSubnetClass(cidr)">
{{ getSubnetType(cidr) }}
</div>
<div class="info-desc">{{ getSubnetDescription(cidr) }}</div>
</div>
</div>
</div>
<div class="example-presets">
<div class="presets-title">常见子网示例</div>
<div class="presets-grid">
<button
v-for="(preset, index) in presets"
:key="index"
class="preset-btn"
@click="applyPreset(preset)"
>
{{ preset.name }}
</button>
</div>
</div>
<div class="info-box">
<div class="info-title">💡 子网划分知识点</div>
<div class="info-content">
<div class="info-item">
<strong>什么是子网</strong>
将一个大网络分割成更小的网络提高地址利用率和网络性能
</div>
<div class="info-item">
<strong>CIDR 表示法</strong>
/24 24 8
</div>
<div class="info-item">
<strong>常用子网掩码</strong>
<br>
/8 = 255.0.0.0 (A )
<br>
/16 = 255.255.0.0 (B )
<br>
/24 = 255.255.255.0 (C )
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const ipAddress = ref('192.168.1.0')
const cidr = ref(24)
const results = ref(null)
const presets = [
{ name: '小型网络 /24', ip: '192.168.1.0', cidr: 24 },
{ name: '家庭网络 /26', ip: '192.168.1.0', cidr: 26 },
{ name: '大型网络 /16', ip: '192.168.0.0', cidr: 16 },
{ name: '超大型网络 /8', ip: '10.0.0.0', cidr: 8 }
]
const calculate = () => {
const ip = ipAddress.value.split('.').map(Number)
const mask = cidr.value
//
const maskBits = Array(32).fill(0).map((_, i) => (i < mask ? 1 : 0))
const maskBytes = []
for (let i = 0; i < 4; i++) {
maskBytes.push(
maskBits.slice(i * 8, (i + 1) * 8).reduce((acc, bit) => acc * 2 + bit, 0)
)
}
//
const networkBytes = ip.map((byte, i) => byte & maskBytes[i])
// 广
const hostBits = 32 - mask
const broadcastBytes = [...networkBytes]
if (hostBits <= 8) {
broadcastBytes[3] |= (1 << hostBits) - 1
} else if (hostBits <= 16) {
broadcastBytes[2] |= ((1 << (hostBits - 8)) - 1)
broadcastBytes[3] = 255
} else if (hostBits <= 24) {
broadcastBytes[1] |= ((1 << (hostBits - 16)) - 1)
broadcastBytes[2] = 255
broadcastBytes[3] = 255
} else {
broadcastBytes[0] |= ((1 << (hostBits - 24)) - 1)
broadcastBytes[1] = 255
broadcastBytes[2] = 255
broadcastBytes[3] = 255
}
//
const firstHost = [...broadcastBytes]
firstHost[3] = networkBytes[3] + 1
const lastHost = [...broadcastBytes]
lastHost[3] = broadcastBytes[3] - 1
//
const hosts = Math.pow(2, hostBits) - 2
//
const toBinary = (bytes) =>
bytes.map((b) => b.toString(2).padStart(8, '0')).join('.')
results.value = {
network: networkBytes.join('.'),
broadcast: broadcastBytes.join('.'),
mask: maskBytes.join('.'),
hosts: hosts > 0 ? hosts : 0,
firstHost: firstHost.join('.'),
lastHost: lastHost.join('.'),
binaryIp: toBinary(ip),
binaryMask: toBinary(maskBytes),
binaryNetwork: toBinary(networkBytes)
}
}
const applyPreset = (preset) => {
ipAddress.value = preset.ip
cidr.value = preset.cidr
calculate()
}
const getSubnetType = (mask) => {
if (mask <= 8) return 'A 类网络'
if (mask <= 16) return 'B 类网络'
if (mask <= 24) return 'C 类网络'
return '小型子网'
}
const getSubnetClass = (mask) => {
if (mask <= 8) return 'class-a'
if (mask <= 16) return 'class-b'
if (mask <= 24) return 'class-c'
return 'class-small'
}
const getSubnetDescription = (mask) => {
if (mask <= 8) return '超大型网络,适合互联网服务提供商'
if (mask <= 16) return '大型网络,适合公司或机构'
if (mask <= 24) return '标准网络,适合小型企业或家庭'
return '小型子网,适合特定部门或用途'
}
//
calculate()
</script>
<style scoped>
.subnet-calculator {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.calculator-input {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
}
.input-group {
flex: 1;
min-width: 200px;
}
.input-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
display: block;
}
.ip-input,
.cidr-select {
width: 100%;
padding: 10px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.ip-input:focus,
.cidr-select:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.calculate-btn {
padding: 10px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.calculate-btn:hover {
background: var(--vp-c-brand-dark);
}
.results {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 25px;
}
.result-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.section-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.result-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
@media (max-width: 768px) {
.result-grid {
grid-template-columns: 1fr;
}
}
.result-item {
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
}
.result-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 5px;
}
.result-value {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
}
.range-display {
display: flex;
align-items: center;
gap: 15px;
}
.range-item {
flex: 1;
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
text-align: center;
}
.range-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 5px;
}
.range-value {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
}
.range-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.binary-display {
display: flex;
flex-direction: column;
gap: 10px;
}
.binary-row {
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 15px;
}
.binary-label {
width: 100px;
font-size: 0.85rem;
color: var(--vp-c-text-3);
font-weight: 600;
}
.binary-value {
flex: 1;
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-brand);
word-break: break-all;
}
.subnet-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.info-tag {
display: inline-block;
padding: 6px 16px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.info-tag.class-a {
background: #fee2e2;
color: #dc2626;
}
.info-tag.class-b {
background: #fef3c7;
color: #d97706;
}
.info-tag.class-c {
background: #dbeafe;
color: #2563eb;
}
.info-tag.class-small {
background: #d1fae5;
color: #059669;
}
.info-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.example-presets {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.presets-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.presets-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 768px) {
.presets-grid {
grid-template-columns: 1fr;
}
}
.preset-btn {
padding: 10px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.preset-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,387 @@
<template>
<div class="tcp-handshake-demo">
<div class="participants">
<div class="participant client">
<div class="participant-icon">💻</div>
<div class="participant-name">客户端</div>
<div class="participant-ip">192.168.1.100</div>
</div>
<div class="connection-area">
<div class="connection-line" :class="{ active: step >= 1 }"></div>
<div class="packets">
<div
v-for="(packet, index) in packets"
:key="index"
class="packet"
:class="{
active: step === index + 1,
sent: step > index + 1
}"
>
<div class="packet-content">{{ packet.content }}</div>
<div class="packet-direction">{{ packet.direction }}</div>
</div>
</div>
</div>
<div class="participant server">
<div class="participant-icon">🖥</div>
<div class="participant-name">服务器</div>
<div class="participant-ip">93.184.216.34</div>
</div>
</div>
<div class="controls">
<button
class="control-btn"
@click="startHandshake"
:disabled="handshaking || step === 3"
>
{{ step === 3 ? '✅ 握手完成' : handshaking ? '🔄 握手中...' : '🤝 开始三次握手' }}
</button>
<button class="control-btn reset" @click="reset" v-if="step === 3">
🔄 重新演示
</button>
</div>
<div class="step-explanation">
<div class="explanation-title">当前步骤说明</div>
<div class="explanation-content" v-if="step === 0">
点击"开始三次握手"按钮观察客户端和服务器如何建立可靠连接
</div>
<div class="explanation-content" v-else-if="step === 1">
<strong>第一步SYN同步请求</strong>
<br><br>
客户端发送一个 SYN 包给服务器告诉服务器"我想和你建立连接"
<br>
客户端会生成一个随机序列号seq=x这个号码很重要后续的数据传输都要用它来保证数据不丢失不重复
</div>
<div class="explanation-content" v-else-if="step === 2">
<strong>第二步SYN-ACK同步确认</strong>
<br><br>
服务器收到客户端的 SYN 请求后
<br>1. 生成自己的随机序列号seq=y
<br>2. 把客户端的序列号加 1ack=x+1表示"我收到了你的请求"
<br>3. 发送 SYN-ACK 包给客户端
</div>
<div class="explanation-content" v-else-if="step === 3">
<strong>第三步ACK确认</strong>
<br><br>
客户端收到服务器的 SYN-ACK
<br>1. 把服务器的序列号加 1ack=y+1表示"我也收到了你的确认"
<br>2. 发送 ACK 包给服务器
<br><br>
<strong>🎉 连接建立成功</strong>双方现在可以开始传输数据了
</div>
</div>
<div class="why-three">
<div class="why-title">🤔 为什么需要三次握手</div>
<div class="why-content">
<div class="why-item">
<strong>1. 确认双方都能正常收发数据</strong>
<br>
第一次握手证明客户端能发送
<br>
第二次握手证明服务器能接收和发送
<br>
第三次握手证明客户端能接收
</div>
<div class="why-item">
<strong>2. 防止已失效的连接请求突然传到服务器</strong>
<br>
如果只有两次握手客户端发送的第一个连接请求在网络中滞留
等到连接释放后才到达服务器服务器会误以为是新的连接请求
浪费资源三次握手可以避免这个问题
</div>
<div class="why-item">
<strong>3. 同步双方的初始序列号</strong>
<br>
双方需要协商一个起始序列号用于后续的数据传输和确认
</div>
</div>
</div>
<div class="analogy">
<div class="analogy-title">💡 生活中的类比</div>
<div class="analogy-content">
想象你在打电话给朋友
<br><br>
<strong></strong>"喂?你能听到我说话吗?" SYN
<br>
<strong>朋友</strong>"能听到,你能听到我吗?" SYN-ACK
<br>
<strong></strong>"我也能听到!" ACK
<br><br>
现在双方确认都能听到对方可以开始正常通话了
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const step = ref(0)
const handshaking = ref(false)
const packets = [
{
content: 'SYN seq=x',
direction: '客户端 → 服务器'
},
{
content: 'SYN-ACK seq=y, ack=x+1',
direction: '服务器 → 客户端'
},
{
content: 'ACK ack=y+1',
direction: '客户端 → 服务器'
}
]
const startHandshake = () => {
if (handshaking.value || step.value === 3) return
handshaking.value = true
step.value = 0
setTimeout(() => {
step.value = 1
setTimeout(() => {
step.value = 2
setTimeout(() => {
step.value = 3
handshaking.value = false
}, 1500)
}, 1500)
}, 500)
}
const reset = () => {
step.value = 0
handshaking.value = false
}
</script>
<style scoped>
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.participants {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
gap: 20px;
}
.participant {
flex: 1;
text-align: center;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.participant.client {
border-color: #3b82f6;
}
.participant.server {
border-color: #ef4444;
}
.participant-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.participant-name {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 5px;
}
.participant-ip {
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.connection-area {
flex: 1;
position: relative;
padding: 20px;
}
.connection-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
transition: all 0.3s;
}
.connection-line.active {
background: linear-gradient(90deg, #3b82f6, #ef4444);
}
.packets {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
}
.packet {
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
opacity: 0.3;
transform: scale(0.9);
transition: all 0.3s;
}
.packet.active {
opacity: 1;
transform: scale(1.05);
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.packet.sent {
opacity: 0.6;
}
.packet-content {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
margin-bottom: 4px;
}
.packet-direction {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
}
.control-btn {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
}
.control-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.control-btn.reset {
background: #22c55e;
}
.control-btn.reset:hover {
background: #16a34a;
}
.step-explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 4px solid var(--vp-c-brand);
}
.explanation-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.explanation-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
.why-three {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.why-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.why-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.why-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.analogy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid #f59e0b;
}
.analogy-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.analogy-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,509 @@
<template>
<div class="tcp-udp-comparison">
<div class="comparison-grid">
<div class="protocol-card tcp">
<div class="protocol-header">
<div class="protocol-icon">🔒</div>
<div class="protocol-title">TCP</div>
<div class="protocol-subtitle">传输控制协议</div>
</div>
<div class="protocol-features">
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">可靠传输</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">面向连接</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">流量控制</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">拥塞控制</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">速度较慢</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">开销较大</div>
</div>
</div>
<div class="protocol-example">
<div class="example-title">应用场景</div>
<div class="example-tags">
<span class="tag">网页浏览</span>
<span class="tag">文件传输</span>
<span class="tag">邮件发送</span>
</div>
</div>
<div class="handshake-demo">
<div class="demo-title">三次握手</div>
<div class="handshake-steps">
<div class="step" :class="{ active: tcpStep >= 1 }">
<div class="step-arrow"></div>
<div class="step-text">SYN</div>
</div>
<div class="step" :class="{ active: tcpStep >= 2 }">
<div class="step-arrow"></div>
<div class="step-text">SYN-ACK</div>
</div>
<div class="step" :class="{ active: tcpStep >= 3 }">
<div class="step-arrow"></div>
<div class="step-text">ACK</div>
</div>
</div>
<button class="demo-btn" @click="startTcpHandshake">
{{ tcpStep === 0 ? '演示握手' : '重新演示' }}
</button>
</div>
</div>
<div class="protocol-card udp">
<div class="protocol-header">
<div class="protocol-icon"></div>
<div class="protocol-title">UDP</div>
<div class="protocol-subtitle">用户数据报协议</div>
</div>
<div class="protocol-features">
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">快速传输</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">开销小</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">无连接</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">支持多播</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">不可靠</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">可能丢包</div>
</div>
</div>
<div class="protocol-example">
<div class="example-title">应用场景</div>
<div class="example-tags">
<span class="tag">视频直播</span>
<span class="tag">在线游戏</span>
<span class="tag">语音通话</span>
</div>
</div>
<div class="handshake-demo">
<div class="demo-title">直接发送</div>
<div class="handshake-steps">
<div class="step direct">
<div class="step-arrow"></div>
<div class="step-text">直接发送数据</div>
</div>
</div>
<button class="demo-btn" @click="sendUdpData">
{{ udpSent ? '再发一次' : '发送数据' }}
</button>
</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody>
<tr>
<td>连接</td>
<td>面向连接</td>
<td>无连接</td>
</tr>
<tr>
<td>可靠性</td>
<td>可靠确认重传</td>
<td>不可靠尽最大努力</td>
</tr>
<tr>
<td>速度</td>
<td>较慢</td>
<td>很快</td>
</tr>
<tr>
<td>开销</td>
<td>20字节头部</td>
<td>8字节头部</td>
</tr>
<tr>
<td>流量控制</td>
<td>滑动窗口</td>
<td></td>
</tr>
<tr>
<td>应用</td>
<td>HTTP, FTP, SMTP, SSH</td>
<td>DNS, DHCP, 视频流</td>
</tr>
</tbody>
</table>
</div>
<div class="real-world-example">
<div class="example-title">🎬 实际应用示例</div>
<div class="scenario-grid">
<div class="scenario">
<div class="scenario-icon">📺</div>
<div class="scenario-name">视频直播</div>
<div class="scenario-desc">
使用 <strong>UDP</strong>因为
<br> 丢几帧没关系关键是实时
<br> 重传会造成延迟和卡顿
</div>
</div>
<div class="scenario">
<div class="scenario-icon">🌐</div>
<div class="scenario-name">网页浏览</div>
<div class="scenario-desc">
使用 <strong>TCP</strong>因为
<br> 内容必须完整准确
<br> 丢失任何数据都不可接受
</div>
</div>
<div class="scenario">
<div class="scenario-icon">🎮</div>
<div class="scenario-name">在线游戏</div>
<div class="scenario-desc">
使用 <strong>UDP</strong>因为
<br> 响应速度比准确更重要
<br> 实时同步玩家位置
</div>
</div>
<div class="scenario">
<div class="scenario-icon">📧</div>
<div class="scenario-name">邮件发送</div>
<div class="scenario-desc">
使用 <strong>TCP</strong>因为
<br> 邮件内容不能丢失
<br> 可靠性是第一要务
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tcpStep = ref(0)
const udpSent = ref(false)
const startTcpHandshake = () => {
tcpStep.value = 0
setTimeout(() => tcpStep.value = 1, 500)
setTimeout(() => tcpStep.value = 2, 1200)
setTimeout(() => tcpStep.value = 3, 1900)
setTimeout(() => {
tcpStep.value = 0
}, 4000)
}
const sendUdpData = () => {
udpSent.value = true
setTimeout(() => {
udpSent.value = false
}, 1000)
}
</script>
<style scoped>
.tcp-udp-comparison {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
.protocol-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.protocol-card.tcp {
border-color: #e34c26;
}
.protocol-card.udp {
border-color: #264de4;
}
.protocol-header {
text-align: center;
margin-bottom: 20px;
}
.protocol-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.protocol-title {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 5px;
}
.protocol-subtitle {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.protocol-features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.feature-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
}
.feature-item.good {
border-left: 3px solid #22c55e;
}
.feature-item.bad {
border-left: 3px solid #ef4444;
}
.feature-icon {
font-weight: bold;
font-size: 1rem;
}
.feature-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.protocol-example {
margin-bottom: 20px;
}
.example-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.example-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 4px 12px;
background: var(--vp-c-brand);
color: white;
border-radius: 12px;
font-size: 0.75rem;
}
.handshake-demo {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.demo-title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
text-align: center;
}
.handshake-steps {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.step {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
opacity: 0.3;
transition: opacity 0.3s;
}
.step.active {
opacity: 1;
background: var(--vp-c-bg);
}
.step.direct {
opacity: 1;
background: var(--vp-c-bg);
}
.step-arrow {
font-size: 1.5rem;
color: var(--vp-c-brand);
}
.step-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.demo-btn {
width: 100%;
padding: 8px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.demo-btn:hover {
background: var(--vp-c-brand-dark);
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
th {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
}
td {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
tr:last-child td {
border-bottom: none;
}
.real-world-example {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.example-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
@media (max-width: 768px) {
.scenario-grid {
grid-template-columns: 1fr;
}
}
.scenario {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.scenario-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.scenario-name {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.scenario-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
</style>
@@ -0,0 +1,335 @@
<template>
<div class="url-to-browser">
<div class="url-input-section">
<div class="url-bar">
<div class="lock-icon">🔒</div>
<div class="url-text">https://www.example.com/page</div>
<button class="go-button" @click="startProcess">Go</button>
</div>
</div>
<div class="process-flow">
<div
v-for="(step, index) in steps"
:key="step.id"
class="flow-step"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<div class="step-connector" v-if="index > 0"></div>
<div class="step-circle">
<div class="step-number">{{ index + 1 }}</div>
</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep === index" class="step-detail">
{{ step.detail }}
</div>
</div>
</div>
</div>
<div class="timeline">
<div class="timeline-bar">
<div class="timeline-fill" :style="{ width: progress + '%' }"></div>
</div>
<div class="timeline-label">{{ Math.round(progress / 10) }} / 10 </div>
</div>
<div class="info-box">
<div class="info-title">💡 知识点</div>
<div class="info-content">
<strong>DNS (域名系统)</strong>将域名转换为 IP 地址就像电话簿将姓名转换为电话号码
<br><br>
<strong>TCP 三次握手</strong>确保客户端和服务器都准备好通信
<br><br>
<strong>HTTP/HTTPS</strong>应用层协议定义了请求和响应的格式
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(-1)
const progress = ref(0)
const steps = [
{
id: 1,
title: 'URL 解析',
desc: '解析地址',
detail: '浏览器检查 URL 格式,提取协议(https)、域名(www.example.com)、路径(/page)'
},
{
id: 2,
title: 'DNS 查询',
desc: '查找 IP 地址',
detail: '查询 DNS 服务器:www.example.com → 93.184.216.34'
},
{
id: 3,
title: 'TCP 连接',
desc: '建立连接',
detail: '三次握手:SYN → SYN-ACK → ACK,建立可靠连接'
},
{
id: 4,
title: 'TLS 握手',
desc: '加密协商',
detail: '协商加密算法,交换证书,建立安全通道(HTTPS)'
},
{
id: 5,
title: '发送请求',
desc: 'HTTP GET',
detail: '发送:GET /page HTTP/1.1\nHost: www.example.com'
},
{
id: 6,
title: '服务器处理',
desc: '生成响应',
detail: '服务器接收请求,处理逻辑,查询数据库,生成 HTML'
},
{
id: 7,
title: '接收响应',
desc: 'HTTP 200 OK',
detail: '接收:HTML + CSS + JS 资源,状态码 200 表示成功'
},
{
id: 8,
title: '解析 DOM',
desc: '构建页面结构',
detail: '解析 HTML,构建 DOM 树,解析 CSS 构建 CSSOM 树'
},
{
id: 9,
title: '执行 JS',
desc: '添加交互',
detail: '执行 JavaScript,处理事件,动态修改页面'
},
{
id: 10,
title: '渲染完成',
desc: '页面显示',
detail: 'DOM + CSSOM → Render Tree → Layout → Paint → 显示页面'
}
]
const startProcess = () => {
currentStep.value = -1
progress.value = 0
let stepIndex = 0
const interval = setInterval(() => {
if (stepIndex < steps.length) {
currentStep.value = stepIndex
progress.value = ((stepIndex + 1) / steps.length) * 100
stepIndex++
} else {
clearInterval(interval)
setTimeout(() => {
currentStep.value = -1
progress.value = 0
}, 3000)
}
}, 1000)
}
</script>
<style scoped>
.url-to-browser {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.url-input-section {
margin-bottom: 25px;
}
.url-bar {
display: flex;
align-items: center;
gap: 10px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
padding: 8px 15px;
}
.lock-icon {
font-size: 1rem;
}
.url-text {
flex: 1;
font-family: monospace;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.go-button {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 6px 18px;
border-radius: 15px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.go-button:hover {
background: var(--vp-c-brand-dark);
}
.process-flow {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 25px;
}
.flow-step {
display: flex;
align-items: flex-start;
gap: 15px;
position: relative;
opacity: 0.4;
transition: opacity 0.3s;
}
.flow-step.active {
opacity: 1;
}
.flow-step.completed {
opacity: 0.7;
}
.step-connector {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: calc(100% - 20px);
background: var(--vp-c-divider);
}
.flow-step.completed .step-connector {
background: var(--vp-c-brand);
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
flex-shrink: 0;
z-index: 1;
}
.flow-step.active .step-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.flow-step.completed .step-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.step-number {
font-weight: bold;
font-size: 0.9rem;
}
.step-content {
flex: 1;
padding-top: 5px;
padding-bottom: 15px;
}
.step-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.step-detail {
font-size: 0.8rem;
color: var(--vp-c-brand);
background: var(--vp-c-bg);
padding: 10px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
white-space: pre-line;
line-height: 1.6;
}
.timeline {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.timeline-bar {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.timeline-fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.5s ease;
}
.timeline-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
text-align: center;
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.info-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,289 @@
<template>
<div class="web-tech-triad">
<div class="triad-container">
<!-- HTML -->
<div class="tech-card html">
<div class="tech-icon">🏗</div>
<div class="tech-title">HTML</div>
<div class="tech-subtitle">结构层</div>
<div class="tech-desc">网页的骨架</div>
<div class="code-example">
<div class="code-header">&lt;结构&gt;</div>
<div class="code-content">
&lt;h1&gt;标题&lt;/h1&gt;<br>
&lt;p&gt;段落&lt;/p&gt;
</div>
</div>
<div class="tech-role">
<div class="role-item"> 定义内容</div>
<div class="role-item"> 组织结构</div>
</div>
</div>
<!-- CSS -->
<div class="tech-card css">
<div class="tech-icon">🎨</div>
<div class="tech-title">CSS</div>
<div class="tech-subtitle">表现层</div>
<div class="tech-desc">网页的化妆师</div>
<div class="code-example">
<div class="code-header">&lt;样式&gt;</div>
<div class="code-content">
color: red;<br>
font-size: 16px;
</div>
</div>
<div class="tech-role">
<div class="role-item"> 控制外观</div>
<div class="role-item"> 响应布局</div>
</div>
</div>
<!-- JavaScript -->
<div class="tech-card js">
<div class="tech-icon"></div>
<div class="tech-title">JavaScript</div>
<div class="tech-subtitle">行为层</div>
<div class="tech-desc">网页的灵魂</div>
<div class="code-example">
<div class="code-header">&lt;交互&gt;</div>
<div class="code-content">
onclick="..."<br>
addEventListener()
</div>
</div>
<div class="tech-role">
<div class="role-item"> 处理事件</div>
<div class="role-item"> 动态交互</div>
</div>
</div>
</div>
<div class="collaboration">
<div class="collab-title">🤝 三者如何协作</div>
<div class="collab-demo">
<div class="collab-step">
<div class="step-number">1</div>
<div class="step-content">
<span class="step-tech">HTML</span> 搭建骨架
</div>
</div>
<div class="collab-arrow"></div>
<div class="collab-step">
<div class="step-number">2</div>
<div class="step-content">
<span class="step-tech">CSS</span> 美化外观
</div>
</div>
<div class="collab-arrow"></div>
<div class="collab-step">
<div class="step-number">3</div>
<div class="step-content">
<span class="step-tech">JS</span> 添加交互
</div>
</div>
</div>
</div>
<div class="analogy">
<div class="analogy-title">💡 生动比喻</div>
<div class="analogy-content">
建网站就像<strong>盖房子</strong>
<br><br>
🏗 <strong>HTML</strong> = 房屋结构屋顶门窗
<br>
🎨 <strong>CSS</strong> = 室内装修颜色家具装饰
<br>
<strong>JavaScript</strong> = 智能家居灯光控制自动化
</div>
</div>
</div>
</template>
<style scoped>
.web-tech-triad {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.triad-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.triad-container {
grid-template-columns: 1fr;
}
}
.tech-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
text-align: center;
}
.tech-card.html {
border-color: #e34c26;
}
.tech-card.css {
border-color: #264de4;
}
.tech-card.js {
border-color: #f7df1e;
}
.tech-icon {
font-size: 2.5rem;
margin-bottom: 10px;
}
.tech-title {
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 5px;
}
.tech-card.html .tech-title {
color: #e34c26;
}
.tech-card.css .tech-title {
color: #264de4;
}
.tech-card.js .tech-title {
color: #f7df1e;
}
.tech-subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.tech-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 15px;
}
.code-example {
background: #000;
border-radius: 6px;
padding: 10px;
margin-bottom: 15px;
text-align: left;
}
.code-header {
font-size: 0.7rem;
color: #a1a1aa;
margin-bottom: 6px;
font-family: monospace;
}
.code-content {
font-size: 0.75rem;
color: #22c55e;
font-family: monospace;
line-height: 1.6;
}
.tech-role {
display: flex;
flex-direction: column;
gap: 6px;
}
.role-item {
font-size: 0.8rem;
color: var(--vp-c-text-2);
text-align: left;
}
.collaboration {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.collab-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
text-align: center;
}
.collab-demo {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.collab-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.step-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.step-tech {
font-weight: bold;
}
.collab-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.analogy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.analogy-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.analogy-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>