feat: comprehensive documentation and demo updates

- Update READMEs and docs across multiple languages
- Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics
- Add new appendix sections for Database and IDE intros
- Update VitePress config, theme, and utility scripts
- Clean up unused assets and components
This commit is contained in:
sanbuphy
2026-01-16 19:10:21 +08:00
parent c8567ce23f
commit 73f4788d7e
150 changed files with 19530 additions and 13401 deletions
@@ -0,0 +1,307 @@
<template>
<div class="browser-rendering-demo">
<div class="control-bar">
<div class="step-indicator">Step: {{ currentStep + 1 }} / 4</div>
<div class="steps-nav">
<button
v-for="(step, index) in steps"
:key="index"
:class="{ active: currentStep === index }"
@click="currentStep = index"
>
{{ step.label }}
</button>
</div>
</div>
<div class="workspace">
<!-- Left: Source Code -->
<div class="source-panel">
<div class="panel-label">HTML / CSS</div>
<div class="code-block">
<div class="line">&lt;div id="app"&gt;</div>
<div class="line indent">&lt;h1&gt;Hello&lt;/h1&gt;</div>
<div class="line indent">&lt;p&gt;World&lt;/p&gt;</div>
<div class="line">&lt;/div&gt;</div>
<div class="line mt-2">h1 { color: red; }</div>
</div>
</div>
<!-- Arrow -->
<div class="arrow"></div>
<!-- Right: Visualization -->
<div class="viz-panel">
<div class="panel-label">{{ steps[currentStep].title }}</div>
<transition name="fade" mode="out-in">
<!-- Step 1: DOM Tree -->
<div v-if="currentStep === 0" class="tree-viz">
<div class="node root">Document</div>
<div class="tree-lines">
<div class="line-v"></div>
</div>
<div class="node element">html</div>
<div class="tree-lines">
<div class="line-v"></div>
</div>
<div class="node element">body</div>
<div class="tree-children">
<div class="node element active">div#app</div>
<div class="tree-children row">
<div class="node element">h1</div>
<div class="node element">p</div>
</div>
</div>
</div>
<!-- Step 2: Render Tree -->
<div v-else-if="currentStep === 1" class="tree-viz render-tree">
<div class="node render-obj">RenderBlock (div)</div>
<div class="tree-children row">
<div class="node render-obj red">
RenderText (h1) <br /><small>color: red</small>
</div>
<div class="node render-obj">RenderText (p)</div>
</div>
</div>
<!-- Step 3: Layout -->
<div v-else-if="currentStep === 2" class="layout-viz">
<div class="layout-box root">
<span class="dims">100% x 100%</span>
<div class="layout-box container">
<span class="dims">div: 100% x auto</span>
<div class="layout-box item h1">h1: 100% x 32px (x:0, y:0)</div>
<div class="layout-box item p">p: 100% x 16px (x:0, y:32)</div>
</div>
</div>
</div>
<!-- Step 4: Paint -->
<div v-else-if="currentStep === 3" class="paint-viz">
<div class="browser-window">
<div class="painted-content">
<h1 style="color: red; margin: 0">Hello</h1>
<p style="margin: 0">World</p>
</div>
<div class="paint-layers">
<div class="layer-item">Layer 1: Background</div>
<div class="layer-item">Layer 2: Text</div>
</div>
</div>
</div>
</transition>
</div>
</div>
<div class="info-footer">
<p>{{ steps[currentStep].desc }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{
label: '1. DOM',
title: 'DOM Tree Construction',
desc: '浏览器解析 HTML 标记,构建 DOM (文档对象模型) 树。每个标签成为一个节点。'
},
{
label: '2. Render Tree',
title: 'Render Tree Construction',
desc: '结合 DOM 和 CSSOM,生成渲染树。只有可见元素会被包含(display: none 的元素会被排除)。'
},
{
label: '3. Layout',
title: 'Layout (Reflow)',
desc: '计算每个节点在屏幕上的确切位置和大小。这一步也叫"回流"。'
},
{
label: '4. Paint',
title: 'Painting & Composite',
desc: '将各个节点绘制到屏幕像素。现代浏览器会将不同部分绘制到不同图层,最后合成。'
}
]
</script>
<style scoped>
.browser-rendering-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.steps-nav {
display: flex;
gap: 0.5rem;
}
.steps-nav button {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
}
.steps-nav button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.workspace {
display: flex;
gap: 1rem;
align-items: center;
height: 300px;
}
.source-panel,
.viz-panel {
flex: 1;
height: 100%;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.panel-label {
font-weight: bold;
font-size: 0.8rem;
margin-bottom: 1rem;
color: var(--vp-c-text-2);
text-align: center;
}
.code-block {
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.line.indent {
padding-left: 1rem;
}
.line.mt-2 {
margin-top: 1rem;
}
.viz-panel {
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
/* Visualization Styles */
.node {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg-alt);
text-align: center;
font-size: 0.8rem;
margin: 0.2rem;
}
.node.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.node.root {
background: #e5e7eb;
color: #374151;
}
.tree-viz {
display: flex;
flex-direction: column;
align-items: center;
}
.tree-children.row {
display: flex;
gap: 1rem;
}
.render-obj.red {
border-color: red;
color: red;
}
.layout-box {
border: 1px dashed var(--vp-c-text-3);
padding: 1rem;
position: relative;
}
.layout-box .dims {
position: absolute;
top: 2px;
right: 2px;
font-size: 0.6rem;
color: var(--vp-c-text-3);
}
.layout-box.container {
border-color: var(--vp-c-brand);
margin: 0.5rem;
}
.layout-box.item {
border-style: solid;
margin-bottom: 0.5rem;
background: var(--vp-c-bg-soft);
}
.painted-content {
background: white;
padding: 1rem;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}
.paint-layers {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.info-footer {
margin-top: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.9rem;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -1,194 +1,80 @@
<!--
CssBoxModel.vue
盒模型速懂三根滑杆 + 颜色区分实时显示总宽高渲染顺序提示和 CSS 片段
-->
<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 class="box-demo">
<div class="controls">
<div class="control-item">
<div class="control-header">
<label>Padding (内边距)</label>
<span class="val">{{ padding }}px</span>
</div>
<input type="range" min="0" max="50" v-model.number="padding" />
</div>
<div class="control-item">
<div class="control-header">
<label>Border (边框)</label>
<span class="val">{{ border }}px</span>
</div>
<input type="range" min="0" max="30" v-model.number="border" />
</div>
<div class="control-item">
<div class="control-header">
<label>Margin (外边距)</label>
<span class="val">{{ margin }}px</span>
</div>
<input type="range" min="0" max="50" v-model.number="margin" />
</div>
</div>
<div class="stage-container">
<div class="stage-scroll">
<div class="layer margin" :style="{ padding: margin + 'px' }">
<span class="layer-label" v-if="margin >= 15">Margin</span>
<div class="layer border" :style="{ borderWidth: border + 'px' }">
<span class="layer-label" v-if="border >= 10">Border</span>
<div class="layer padding" :style="{ padding: padding + 'px' }">
<span class="layer-label" v-if="padding >= 15">Padding</span>
<div class="content">
<div class="content-inner">
内容区<br>
{{ contentW }} × {{ contentH }}
</div>
</div>
</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 class="meta">
<div class="meta-row main">
<span class="meta-label">总占用宽度</span>
<span class="meta-value">{{ total }}px</span>
</div>
<div class="dimensions">
<div class="dimension-item">
<span class="label">总宽度:</span>
<span class="value">{{ totalWidth }}px</span>
<div class="meta-detail">
<div class="detail-item">
<span class="detail-label">渲染顺序</span>
<span class="detail-text">内容 Padding Border Margin</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 class="detail-item">
<span class="detail-label">计算公式</span>
<span class="detail-text">Margin(×2) + Border(×2) + Padding(×2) + 内容宽</span>
</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 class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.box {</div>
<div class="line"> width: {{ contentW }}px;</div>
<div class="line"> height: {{ contentH }}px;</div>
<div class="line"> padding: {{ padding }}px;</div>
<div class="line"> border: {{ border }}px solid #0ea5e9;</div>
<div class="line"> margin: {{ margin }}px;</div>
<div class="line">}</div>
</div>
</div>
</div>
@@ -197,207 +83,212 @@
<script setup>
import { computed, ref } from 'vue'
const width = ref(100)
const height = ref(100)
const contentW = 120
const contentH = 80
const padding = ref(20)
const borderWidth = ref(5)
const borderStyle = ref('solid')
const border = ref(10)
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
})
const total = computed(() => margin.value * 2 + border.value * 2 + padding.value * 2 + contentW)
</script>
<style scoped>
.css-box-model {
.box-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
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;
padding: 24px;
margin: 24px 0;
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;
flex-direction: column;
gap: 24px;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-group {
.control-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px 16px;
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 {
.control-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
align-items: center;
font-size: 13px;
}
.dimension-item .label {
color: var(--vp-c-text-2);
label { font-weight: 700; color: var(--vp-c-text-1); }
.val { font-family: var(--vp-font-family-mono); color: var(--vp-c-brand); font-weight: 600; }
input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
}
.stage-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.stage-scroll {
overflow: auto;
max-width: 100%;
padding: 10px; /* space for scrollbar if needed */
}
.layer {
position: relative;
border-radius: 4px;
transition: all 0.2s ease-out;
display: flex;
align-items: center;
justify-content: center;
}
.layer-label {
position: absolute;
top: 2px;
left: 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
opacity: 0.7;
pointer-events: none;
}
/* Margin Layer */
.margin {
background-color: #f9fafb; /* gray-50 */
border: 1px dashed #d1d5db; /* gray-300 */
color: #6b7280;
}
.margin > .layer-label { color: #6b7280; }
/* Border Layer */
.border {
background-color: #e0f2fe; /* sky-100 */
border-style: solid;
border-color: #7dd3fc; /* sky-300 */
color: #0284c7;
}
.border > .layer-label { color: #0284c7; }
/* Padding Layer */
.padding {
background-color: #dbeafe; /* blue-100 */
border: 1px dashed #93c5fd; /* blue-300 */
color: #2563eb;
}
.padding > .layer-label { color: #2563eb; }
/* Content Box */
.content {
background: var(--vp-c-brand);
color: #fff;
border-radius: 4px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
width: 120px;
height: 80px;
flex-shrink: 0;
}
.content-inner {
text-align: center;
font-size: 13px;
line-height: 1.4;
}
.meta {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-alt);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.meta-row.main {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
.dimension-item .value {
color: var(--vp-c-brand);
font-family: monospace;
.meta-value { color: var(--vp-c-brand); }
.meta-detail {
display: flex;
flex-direction: column;
gap: 6px;
}
.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;
.detail-item {
display: flex;
gap: 12px;
font-size: 13px;
align-items: flex-start;
}
.code-output {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand);
.detail-label {
color: var(--vp-c-text-2);
min-width: 60px;
flex-shrink: 0;
}
.detail-text {
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-weight: 700;
margin-bottom: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
.code-content {
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 16px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
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;
.line {
white-space: pre;
}
</style>
@@ -1,165 +1,52 @@
<!--
CssFlexbox.vue
Flex 速学三个按钮控制方向/对齐/换行实时看盒子怎么排
-->
<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="flex-demo">
<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 class="control-item">
<div class="control-header">
<label>主轴方向 (flex-direction)</label>
</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 class="chips">
<button v-for="d in directions" :key="d.id" :class="['chip', { active: dir === d.id }]" @click="dir = d.id">{{ d.label }}</button>
</div>
</div>
<div class="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 class="control-item">
<div class="control-header">
<label>主轴对齐 (justify-content)</label>
</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 class="chips">
<button v-for="j in justifies" :key="j.id" :class="['chip', { active: justify === j.id }]" @click="justify = j.id">{{ j.label }}</button>
</div>
<div 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 class="control-item">
<div class="control-header">
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button v-for="w in wraps" :key="w.id" :class="['chip', { active: wrap === w.id }]" @click="wrap = w.id">{{ w.label }}</button>
</div>
</div>
</div>
<div class="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;
}
<div class="canvas-container">
<div class="canvas" :style="boxStyle">
<div v-for="n in 8" :key="n" class="item">{{ n }}</div>
</div>
</div>
.item {
flex: {{ items[0].flex }}; /* 第一个项目的值 */
}</code></pre>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line"> display: flex;</div>
<div class="line"> flex-direction: {{ dir }};</div>
<div class="line"> justify-content: {{ justify }};</div>
<div class="line"> flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
</div>
</div>
</template>
@@ -167,203 +54,166 @@
<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 directions = [
{ id: 'row', label: '水平 (row)' },
{ id: 'column', label: '垂直 (column)' }
]
const justifies = [
{ id: 'flex-start', label: '靠左/上' },
{ id: 'center', label: '居中' },
{ id: 'space-between', label: '两端对齐' }
]
const wraps = [
{ id: 'nowrap', label: '不换行' },
{ id: 'wrap', label: '可换行' }
]
const items = ref([
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 }
])
const dir = ref('row')
const justify = ref('flex-start')
const wrap = ref('nowrap')
const flexContainerStyle = computed(() => ({
const boxStyle = computed(() => ({
display: 'flex',
flexDirection: flexDirection.value,
justifyContent: justifyContent.value,
alignItems: alignItems.value,
flexWrap: flexWrap.value,
gap: gap.value + 'px',
flexDirection: dir.value,
justifyContent: justify.value,
flexWrap: wrap.value,
gap: '12px',
minHeight: '200px',
background: '#f3f4f6',
borderRadius: '8px',
padding: '10px'
padding: '16px'
}))
</script>
<style scoped>
.css-flexbox {
.flex-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
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;
padding: 24px;
margin: 24px 0;
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%);
flex-direction: column;
gap: 24px;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-section {
.control-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
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 {
padding: 16px;
display: flex;
flex-wrap: wrap;
gap: 6px;
flex-direction: column;
gap: 12px;
}
.control-btn {
.control-header label {
font-weight: 700;
color: var(--vp-c-text-1);
font-size: 13px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
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;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.control-btn:hover {
.chip:hover {
background: var(--vp-c-bg-soft);
}
.chip.active {
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;
background: var(--vp-c-brand-dimm);
font-weight: 600;
margin-left: 8px;
}
.code-output {
.canvas-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 4px; /* Tiny padding for the inner canvas */
}
.canvas {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
/* border: 1px dashed var(--vp-c-divider); */
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
background-size: 20px 20px;
}
.item {
width: 60px;
height: 60px;
border-radius: 8px;
background: linear-gradient(135deg, #0ea5e9, #10b981);
color: #fff;
font-weight: 800;
display: grid;
place-items: center;
font-size: 18px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
}
.val {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-brand);
font-weight: 600;
font-size: 13px;
}
input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
margin-top: 8px;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
font-weight: 700;
margin-bottom: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
.code-content {
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 16px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
.line {
white-space: pre;
}
</style>
@@ -172,12 +172,10 @@
<div class="card-title">🌐 域名 (Domain)</div>
<div class="card-content">
<strong>什么是域名</strong>
<br>域名是网站的地址 example.com便于记忆和访问
<br><br>
<br />域名是网站的地址 example.com便于记忆和访问 <br /><br />
<strong>域名注册</strong>
<br> 注册商GoDaddyNamecheap阿里云
<br> 选择后缀.com.cn.org.io
<br> 价格$10-50/
<br /> 注册商GoDaddyNamecheap阿里云 <br />
选择后缀.com.cn.org.io <br /> 价格$10-50/
</div>
</div>
@@ -185,15 +183,12 @@
<div class="card-title">📡 CDN (内容分发网络)</div>
<div class="card-content">
<strong>什么是 CDN</strong>
<br>将内容缓存到全球各地的节点用户就近访问
<br><br>
<br />将内容缓存到全球各地的节点用户就近访问 <br /><br />
<strong>优势</strong>
<br> 加速访问就近获取内容
<br> 减轻负载减少源站压力
<br> 提高可用性节点故障自动切换
<br><br>
<br /> 加速访问就近获取内容 <br /> 减轻负载减少源站压力 <br />
提高可用性节点故障自动切换 <br /><br />
<strong>常见 CDN</strong>
<br> CloudflareAWS CloudFront阿里云 CDN
<br /> CloudflareAWS CloudFront阿里云 CDN
</div>
</div>
@@ -201,15 +196,12 @@
<div class="card-title"> 负载均衡 (Load Balancer)</div>
<div class="card-content">
<strong>什么是负载均衡</strong>
<br>将请求分发到多台服务器提高并发能力
<br><br>
<br />将请求分发到多台服务器提高并发能力 <br /><br />
<strong>负载均衡算法</strong>
<br> 轮询 (Round Robin)
<br> 最少连接 (Least Connections)
<br> IP 哈希 (IP Hash)
<br><br>
<br /> 轮询 (Round Robin) <br /> 最少连接 (Least Connections)
<br /> IP 哈希 (IP Hash) <br /><br />
<strong>常见工具</strong>
<br> NginxHAProxyAWS ELB
<br /> NginxHAProxyAWS ELB
</div>
</div>
@@ -217,16 +209,13 @@
<div class="card-title">🏗 完整部署架构</div>
<div class="card-content">
<strong>现代 Web 应用架构</strong>
<br><br>
<br /><br />
1. 用户通过域名访问
<br>2. DNS 解析到 CDN 或负载均衡器
<br>3. CDN 缓存静态资源
<br>4. 负载均衡器分发请求
<br>5. Web 服务器处理动态请求
<br>6. 数据库存储持久化数据
<br><br>
<br />2. DNS 解析到 CDN 或负载均衡器 <br />3. CDN 缓存静态资源
<br />4. 负载均衡器分发请求 <br />5. Web 服务器处理动态请求 <br />6.
数据库存储持久化数据 <br /><br />
<strong>监控和运维</strong>
<br> 日志收集性能监控自动备份
<br /> 日志收集性能监控自动备份
</div>
</div>
</div>
@@ -1,429 +1,289 @@
<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">
🔍 开始解析
<div class="control-panel">
<div class="input-group">
<label>Domain</label>
<input
v-model="domain"
placeholder="www.example.com"
class="domain-input"
/>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" v-model="useCache" />
<span class="slider"></span>
<span class="label">Simulate Cache Hit</span>
</label>
</div>
<button @click="startLookup" :disabled="isLooking" class="lookup-btn">
{{ isLooking ? 'Resolving...' : 'Lookup' }}
</button>
</div>
<div class="lookup-process" v-if="isLooking">
<div class="process-title">DNS 解析过程</div>
<div class="viz-area">
<!-- Client -->
<div class="node client">
<span class="icon">💻</span>
<span>Browser</span>
</div>
<div class="step-list">
<!-- Servers -->
<div class="servers-container">
<div
v-for="(step, index) in steps"
:key="index"
class="step-item"
v-for="(server, index) in servers"
:key="server.name"
class="node server"
:class="{
active: currentStep === index,
completed: currentStep > index
active: currentServer === index,
success: completed && currentServer === 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 class="server-icon">{{ server.icon }}</div>
<div class="server-name">{{ server.name }}</div>
<div class="server-desc">{{ server.desc }}</div>
</div>
</div>
<!-- Packet Animation -->
<div v-if="packet.visible" class="packet" :style="packetStyle">
{{ packet.text }}
</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 class="log-panel" ref="logPanel">
<div v-for="(log, i) in logs" :key="i" class="log-entry">
<span class="time">{{ log.time }}ms</span>
<span class="msg">{{ log.message }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, nextTick } from 'vue'
const domain = ref('www.google.com')
const useCache = ref(false)
const isLooking = ref(false)
const currentStep = ref(-1)
const currentServer = ref(-1)
const completed = ref(false)
const resolvedIP = ref('')
const lookupTime = ref(0)
const logs = ref([])
const packet = ref({ visible: false, text: '?', x: 0, y: 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 servers = [
{ name: 'Root (.)', icon: '🌲', desc: 'Global Root' },
{ name: 'TLD (.com)', icon: '🏢', desc: 'Top Level' },
{ name: 'Authoritative', icon: '📝', desc: 'example.com' }
]
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 packetStyle = computed(() => ({
transform: `translate(${packet.value.x}px, ${packet.value.y}px)`,
opacity: packet.value.visible ? 1 : 0
}))
const addLog = (message) => {
logs.value.push({
time: Math.floor(performance.now() % 10000),
message
})
// Auto scroll
nextTick(() => {
const el = document.querySelector('.log-panel')
if (el) el.scrollTop = el.scrollHeight
})
}
const startLookup = () => {
const startLookup = async () => {
if (isLooking.value) return
isLooking.value = true
currentServer.value = -1
completed.value = false
currentStep.value = -1
const startTime = Date.now()
logs.value = []
// 模拟 DNS 查询过程
let stepIndex = 0
const interval = setInterval(() => {
if (stepIndex < steps.length) {
currentStep.value = stepIndex
stepIndex++
addLog(`Starting DNS lookup for ${domain.value}`)
if (useCache.value) {
await wait(500)
addLog('✅ Cache HIT! IP found in browser cache.')
completed.value = true
isLooking.value = false
return
}
// Recursive Query Simulation
for (let i = 0; i < servers.length; i++) {
const server = servers[i]
addLog(`Querying ${server.name}...`)
// Simulate network delay
currentServer.value = i
await wait(800)
if (i < servers.length - 1) {
addLog(`Received referral to ${servers[i + 1].name}`)
} else {
clearInterval(interval)
const endTime = Date.now()
lookupTime.value = endTime - startTime
resolvedIP.value = ipAddresses[domain.value.toLowerCase()] || ipAddresses['default']
completed.value = true
addLog(`✅ Resolved IP: 142.250.185.238`)
}
}, 800)
}
completed.value = true
isLooking.value = false
}
const reset = () => {
isLooking.value = false
currentStep.value = -1
completed.value = false
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
</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;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.domain-input {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
.control-panel {
display: flex;
gap: 10px;
align-items: flex-end;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
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 {
.input-group {
display: flex;
flex-direction: column;
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);
.domain-input {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.lookup-btn {
padding: 12px 24px;
padding: 0.5rem 1.5rem;
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;
cursor: pointer;
height: 40px;
align-self: flex-end;
}
.step-arrow {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: calc(100% - 20px);
background: var(--vp-c-divider);
.lookup-btn:disabled {
opacity: 0.5;
}
.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 {
.viz-area {
display: flex;
justify-content: space-between;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
align-items: center;
margin-bottom: 2rem;
position: relative;
min-height: 150px;
}
.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 {
.node {
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
padding: 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
transition: all 0.3s;
width: 100px;
}
.info-item {
font-size: 0.85rem;
.node.client {
border-color: #3b82f6;
}
.servers-container {
display: flex;
gap: 1rem;
}
.node.server.active {
border-color: #f59e0b;
transform: scale(1.1);
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
}
.node.server.success {
border-color: #10b981;
}
.server-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.server-name {
font-weight: bold;
font-size: 0.8rem;
}
.server-desc {
font-size: 0.7rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
.log-panel {
background: #1e1e1e;
color: #10b981;
padding: 1rem;
border-radius: 6px;
height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 0.8rem;
}
.log-entry {
margin-bottom: 0.3rem;
}
.log-entry .time {
color: #6b7280;
margin-right: 0.5rem;
}
/* Toggle Switch */
.toggle {
display: flex;
align-items: center;
cursor: pointer;
gap: 0.5rem;
}
.toggle input {
display: none;
}
.toggle .slider {
width: 40px;
height: 20px;
background: #ccc;
border-radius: 20px;
position: relative;
transition: 0.3s;
}
.toggle .slider:before {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: 0.3s;
}
.toggle input:checked + .slider {
background: var(--vp-c-brand);
}
.toggle input:checked + .slider:before {
transform: translateX(20px);
}
</style>
@@ -1,408 +1,59 @@
<!--
DomManipulator.vue
DOM 速体验输入标题+切换高亮类直观看到文本和样式变化
-->
<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="dom-demo">
<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 class="field">
<label>改个标题</label>
<input v-model="title" placeholder="输入新标题" />
</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 class="field checkbox">
<label><input type="checkbox" v-model="highlight" /> 高亮模式 (class="highlight")</label>
</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 class="card" :class="{ highlight }">
<h2 id="hero">{{ title }}</h2>
<p id="desc">这里是段落说明勾选高亮看看变化</p>
<button @click="toggleText">{{ buttonText }}</button>
</div>
<pre class="code"><code>// JS 改内容
const titleEl = document.getElementById('hero')
titleEl.textContent = '{{ title }}'
// JS 切 class
const card = document.querySelector('.card')
card.classList.toggle('highlight', {{ highlight }})</code></pre>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { 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 title = ref('欢迎来到我的网站')
const highlight = ref(false)
const buttonText = ref('点我试试')
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
const toggleText = () => {
buttonText.value = buttonText.value === '点我试试' ? '再点一次' : '点我试试'
}
</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;
}
.dom-demo { border: 1px solid var(--vp-c-divider); border-radius: 12px; background: var(--vp-c-bg-soft); padding: 16px; margin: 20px 0; display: flex; flex-direction: column; gap: 12px; }
.controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; }
.field { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 6px; }
.checkbox { flex-direction: row; align-items: center; gap: 8px; }
input[type='text'], input[type='checkbox'] { accent-color: var(--vp-c-brand); }
.preview-area {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.card { border: 1px solid var(--vp-c-divider); border-radius: 12px; padding: 16px; background: var(--vp-c-bg); transition: all 0.2s; }
.card.highlight { border-color: #f59e0b; box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2); background: #fff7ed; }
.card h2 { margin: 0 0 8px 0; }
.card p { margin: 0 0 12px 0; color: var(--vp-c-text-2); }
.card button { background: var(--vp-c-brand); color: #fff; border: none; border-radius: 8px; padding: 8px 12px; cursor: pointer; }
.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;
}
.code { background: #0b1221; color: #e5e7eb; border-radius: 10px; padding: 12px; font-family: var(--vp-font-family-mono); font-size: 13px; overflow-x: auto; }
</style>
@@ -1,360 +0,0 @@
<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,337 @@
<template>
<div class="http-exchange-demo">
<div class="demo-container">
<!-- Client Side -->
<div class="panel client-panel">
<div class="panel-header">
<span class="icon">💻</span> Client (Browser)
</div>
<div class="request-builder">
<div class="input-row">
<select v-model="method" class="method-select">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
<input
v-model="path"
class="path-input"
placeholder="/index.html"
/>
</div>
<div class="headers-section">
<div class="section-title">Headers</div>
<div v-for="(value, key) in headers" :key="key" class="header-row">
<span class="header-key">{{ key }}:</span>
<span class="header-value">{{ value }}</span>
</div>
</div>
<button
@click="sendRequest"
class="send-btn"
:disabled="isProcessing"
>
{{ isProcessing ? 'Sending...' : 'Send Request' }}
</button>
</div>
</div>
<!-- Network Visualization -->
<div class="network-space">
<div class="connection-line"></div>
<div
v-if="currentPacket"
class="packet"
:class="currentPacket.type"
:style="{ left: packetPosition + '%' }"
>
{{ currentPacket.label }}
</div>
</div>
<!-- Server Side -->
<div class="panel server-panel">
<div class="panel-header"><span class="icon">🖥</span> Server</div>
<div class="response-viewer" :class="{ empty: !response }">
<div v-if="response">
<div class="status-row" :class="statusClass">
{{ response.status }} {{ response.statusText }}
</div>
<div class="headers-section">
<div
v-for="(value, key) in response.headers"
:key="key"
class="header-row"
>
<span class="header-key">{{ key }}:</span>
<span class="header-value">{{ value }}</span>
</div>
</div>
<div class="body-preview">
{{ response.body }}
</div>
</div>
<div v-else class="placeholder">Waiting for request...</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const method = ref('GET')
const path = ref('/index.html')
const isProcessing = ref(false)
const packetPosition = ref(10)
const currentPacket = ref(null)
const response = ref(null)
const headers = ref({
Host: 'www.example.com',
'User-Agent': 'Mozilla/5.0',
Accept: 'text/html'
})
const responses = {
GET: {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'text/html',
Server: 'Nginx'
},
body: '<!DOCTYPE html>\n<html>\n <body>Hello World</body>\n</html>'
},
POST: {
status: 201,
statusText: 'Created',
headers: {
'Content-Type': 'application/json'
},
body: '{"success": true, "id": 123}'
}
}
const statusClass = computed(() => {
if (!response.value) return ''
const code = response.value.status
if (code >= 200 && code < 300) return 'success'
if (code >= 400) return 'error'
return ''
})
const sendRequest = () => {
if (isProcessing.value) return
isProcessing.value = true
response.value = null
// Animate Request
currentPacket.value = {
type: 'request',
label: `${method.value} ${path.value}`
}
animatePacket(10, 90, () => {
// Server Processing
setTimeout(() => {
// Prepare Response
const mockResponse = responses[method.value] || responses['GET']
// Animate Response
currentPacket.value = {
type: 'response',
label: `${mockResponse.status} ${mockResponse.statusText}`
}
animatePacket(90, 10, () => {
response.value = mockResponse
currentPacket.value = null
isProcessing.value = false
})
}, 800)
})
}
const animatePacket = (start, end, callback) => {
let pos = start
const step = (end - start) / 50
const interval = setInterval(() => {
pos += step
packetPosition.value = pos
if ((step > 0 && pos >= end) || (step < 0 && pos <= end)) {
clearInterval(interval)
callback()
}
}, 10)
}
</script>
<style scoped>
.http-exchange-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-container {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 1rem;
height: 300px;
}
.panel {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.panel-header {
font-weight: bold;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.network-space {
width: 20%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.connection-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
border-top: 1px dashed var(--vp-c-text-3);
}
.packet {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
padding: 0.3rem 0.6rem;
border-radius: 4px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.packet.request {
background: #3b82f6;
color: white;
}
.packet.response {
background: #10b981;
color: white;
}
.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.method-select {
padding: 0.3rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.path-input {
flex: 1;
padding: 0.3rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.headers-section {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 4px;
}
.header-row {
display: flex;
justify-content: space-between;
}
.send-btn {
width: 100%;
padding: 0.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.response-viewer {
flex: 1;
font-size: 0.8rem;
overflow-y: auto;
}
.response-viewer.empty {
display: flex;
align-items: center;
justify-content: center;
}
.placeholder {
color: var(--vp-c-text-3);
}
.status-row {
font-weight: bold;
margin-bottom: 0.5rem;
}
.status-row.success {
color: #10b981;
}
.status-row.error {
color: #ef4444;
}
.body-preview {
margin-top: 1rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
}
</style>
@@ -24,7 +24,11 @@
<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">
<div
v-for="(func, index) in layers[selectedLayer].functions"
:key="index"
class="function-item"
>
{{ func }}
</div>
</div>
@@ -32,7 +36,11 @@
<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">
<div
v-for="(device, index) in layers[selectedLayer].devices"
:key="index"
class="example-item"
>
📡 {{ device }}
</div>
</div>
@@ -66,7 +74,8 @@ const layers = [
protocols: 'HTTP, HTTPS, FTP, SMTP, DNS, SSH',
icon: '📱',
dataUnit: '数据',
description: '直接为用户的应用程序(如浏览器、邮件客户端)提供网络服务接口。',
description:
'直接为用户的应用程序(如浏览器、邮件客户端)提供网络服务接口。',
functions: [
'为应用程序提供网络接口',
'定义应用程序间通信的协议',
@@ -19,7 +19,9 @@
<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 class="solution-desc">
{{ problems[selectedProblem].description }}
</div>
</div>
<div class="solution-steps">
@@ -80,28 +82,29 @@
<div class="tip-number">1</div>
<div class="tip-content">
<strong>从底层到顶层</strong>
<br>物理层 链路层 网络层 传输层 应用层
<br />物理层 链路层 网络层 传输层 应用层
</div>
</div>
<div class="tip-item">
<div class="tip-number">2</div>
<div class="tip-content">
<strong>分层排查</strong>
<br>先确定问题发生在哪一层再针对性解决
<br />先确定问题发生在哪一层再针对性解决
</div>
</div>
<div class="tip-item">
<div class="tip-number">3</div>
<div class="tip-content">
<strong>二分法定位</strong>
<br> ping 本机 ping 网关 ping 外网 ping 域名
<br />
ping 本机 ping 网关 ping 外网 ping 域名
</div>
</div>
<div class="tip-item">
<div class="tip-number">4</div>
<div class="tip-content">
<strong>查看日志</strong>
<br>系统日志应用日志防火墙日志记录关键信息
<br />系统日志应用日志防火墙日志记录关键信息
</div>
</div>
</div>
@@ -0,0 +1,122 @@
<!--
SemanticTagsDemo.vue
语义标签速查点击标签名右侧展示用途是否块级/行内常见场景和示例 HTML
-->
<template>
<div class="semantic">
<div class="tags">
<button
v-for="t in tags"
:key="t.name"
:class="['tag-btn', { active: t.name === current.name }]"
@click="current = t"
>
{{ t.name }}
</button>
</div>
<div class="panel">
<div class="row"><span class="label">用途</span><span>{{ current.purpose }}</span></div>
<div class="row"><span class="label">类型</span><span>{{ current.display }}</span></div>
<div class="row"><span class="label">常见位置</span><span>{{ current.scene }}</span></div>
<div class="row code-title">示例</div>
<pre><code>{{ current.example }}</code></pre>
<div class="row code-title">渲染效果</div>
<div class="preview-box" v-html="current.example"></div>
<div class="row tip">小贴士{{ current.tip }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tags = [
{
name: '<header>',
purpose: '页面/区块的头部区域,通常放 Logo、导航',
display: '块级',
scene: '页面顶部、文章顶部',
example: `<header style="background:#eee; padding:10px;">\n <h1 style="margin:0;">我的网站</h1>\n <nav>...</nav>\n</header>`,
tip: '一个页面可有多个 header,只要是各自区块的开头都行'
},
{
name: '<nav>',
purpose: '导航链接区域',
display: '块级',
scene: '全站导航、面包屑、侧边栏',
example: `<nav style="background:#f4f4f4; padding:10px;">\n <a href="javascript:void(0)">首页</a> | <a href="javascript:void(0)">关于</a>\n</nav>`,
tip: '尽量只放导航链接,便于屏幕阅读器识别'
},
{
name: '<main>',
purpose: '文档主体,一个页面只能有一个',
display: '块级',
scene: '包裹主要内容区域',
example: `<main style="border:1px dashed #999; padding:10px;">\n <article>主要内容区域...</article>\n</main>`,
tip: '辅助技术可快速跳转到 main,提高可访问性'
},
{
name: '<section>',
purpose: '主题分组的区块',
display: '块级',
scene: '页面分段、文档章节',
example: `<section style="border-left:4px solid #007acc; padding-left:10px;">\n <h2 style="margin:0;">功能亮点</h2>\n <p>这里是功能介绍...</p>\n</section>`,
tip: '每个 section 里最好有标题(h2/h3'
},
{
name: '<article>',
purpose: '一篇可独立传播的内容',
display: '块级',
scene: '博客文章、论坛帖子、卡片',
example: `<article style="border:1px solid #ddd; padding:10px; border-radius:4px;">\n <h2 style="margin-top:0;">博客标题</h2>\n <p>正文内容...</p>\n</article>`,
tip: 'article 里可以再嵌套 section'
},
{
name: '<aside>',
purpose: '旁注/侧栏信息',
display: '块级',
scene: '侧边栏、提示框、相关链接',
example: `<aside style="background:#fff3cd; padding:10px;">\n <h3 style="margin-top:0;">相关阅读</h3>\n <ul style="margin-bottom:0;">\n <li>文章一</li>\n <li>文章二</li>\n </ul>\n</aside>`,
tip: '与主内容相关但非主体'
},
{
name: '<footer>',
purpose: '页面/区块的底部区域',
display: '块级',
scene: '版权、联系信息、链接',
example: `<footer style="background:#333; color:#fff; padding:10px; text-align:center;">\n <p style="margin:0;">© 2026 MySite</p>\n</footer>`,
tip: '页面可有多个 footer,对应不同区块'
},
{
name: '<figure>',
purpose: '插图+说明的容器',
display: '块级',
scene: '图片/代码片段/表格附说明',
example: `<figure style="border:1px solid #ccc; padding:5px; margin:0; display:inline-block;">\n <img src="https://placehold.co/150x100?text=Hero+Img" alt="示例" style="display:block;"/>\n <figcaption style="text-align:center; font-size:12px; color:#666;">图注文字</figcaption>\n</figure>`,
tip: '搭配 <figcaption> 提示内容说明'
}
]
const current = ref(tags[0])
</script>
<style scoped>
.semantic { border: 1px solid var(--vp-c-divider); border-radius: 12px; background: var(--vp-c-bg-soft); padding: 16px; margin: 20px 0; display: grid; grid-template-columns: 1fr 2fr; gap: 12px; }
@media (max-width: 720px) { .semantic { grid-template-columns: 1fr; } }
.tags { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.tag-btn { padding: 10px 12px; border-radius: 10px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); cursor: pointer; text-align: left; }
.tag-btn.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.panel { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; justify-content: space-between; gap: 8px; font-size: 14px; }
.label { color: var(--vp-c-text-2); font-weight: 700; }
.code-title { font-weight: 700; margin-top: 4px; }
pre { margin: 0; background: #0b1221; color: #e5e7eb; border-radius: 8px; padding: 10px; font-family: var(--vp-font-family-mono); font-size: 13px; white-space: pre-wrap; }
.preview-box {
border: 1px dashed var(--vp-c-divider);
padding: 16px;
border-radius: 8px;
background: var(--vp-c-bg);
}
.tip { color: var(--vp-c-text-2); font-size: 13px; }
</style>
@@ -115,11 +115,11 @@
</div>
<div class="info-item">
<strong>常用子网掩码</strong>
<br>
<br />
/8 = 255.0.0.0 (A 类网络)
<br>
<br />
/16 = 255.255.0.0 (B 类网络)
<br>
<br />
/24 = 255.255.255.0 (C 类网络)
</div>
</div>
@@ -146,7 +146,9 @@ const calculate = () => {
const mask = cidr.value
// 计算子网掩码
const maskBits = Array(32).fill(0).map((_, i) => (i < mask ? 1 : 0))
const maskBits = Array(32)
.fill(0)
.map((_, i) => (i < mask ? 1 : 0))
const maskBytes = []
for (let i = 0; i < 4; i++) {
maskBytes.push(
@@ -163,14 +165,14 @@ const calculate = () => {
if (hostBits <= 8) {
broadcastBytes[3] |= (1 << hostBits) - 1
} else if (hostBits <= 16) {
broadcastBytes[2] |= ((1 << (hostBits - 8)) - 1)
broadcastBytes[2] |= (1 << (hostBits - 8)) - 1
broadcastBytes[3] = 255
} else if (hostBits <= 24) {
broadcastBytes[1] |= ((1 << (hostBits - 16)) - 1)
broadcastBytes[1] |= (1 << (hostBits - 16)) - 1
broadcastBytes[2] = 255
broadcastBytes[3] = 255
} else {
broadcastBytes[0] |= ((1 << (hostBits - 24)) - 1)
broadcastBytes[0] |= (1 << (hostBits - 24)) - 1
broadcastBytes[1] = 255
broadcastBytes[2] = 255
broadcastBytes[3] = 255
@@ -1,122 +1,70 @@
<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 class="diagram">
<!-- Client Column -->
<div class="column client">
<div class="actor-icon">💻 Client</div>
<div class="state-label">{{ clientState }}</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
}"
<!-- Interaction Area -->
<div class="interaction-zone">
<!-- Step 1: SYN -->
<div class="packet-row" :class="{ active: step === 1, done: step > 1 }">
<button
@click="sendSyn"
:disabled="step !== 0"
class="packet-btn syn"
>
<div class="packet-content">{{ packet.content }}</div>
<div class="packet-direction">{{ packet.direction }}</div>
</div>
SYN (SEQ=x)
</button>
</div>
<!-- Step 2: SYN-ACK -->
<div
class="packet-row reverse"
:class="{ active: step === 2, done: step > 2 }"
>
<button
@click="sendSynAck"
:disabled="step !== 1"
class="packet-btn syn-ack"
>
SYN-ACK (ACK=x+1, SEQ=y)
</button>
</div>
<!-- Step 3: ACK -->
<div class="packet-row" :class="{ active: step === 3, done: step > 3 }">
<button
@click="sendAck"
:disabled="step !== 2"
class="packet-btn ack"
>
ACK (ACK=y+1)
</button>
</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>
<!-- Server Column -->
<div class="column server">
<div class="actor-icon">🖥 Server</div>
<div class="state-label">{{ serverState }}</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 class="status-message">
<p v-if="step === 0">点击 <strong>SYN</strong> 开始连接</p>
<p v-if="step === 1">
服务器收到了请求现在需要回复 <strong>SYN-ACK</strong>
</p>
<p v-if="step === 2">
客户端收到了确认最后发送 <strong>ACK</strong> 完成握手
</p>
<p v-if="step === 3" class="success">🎉 连接已建立 (ESTABLISHED)!</p>
</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>
<button v-if="step === 3" @click="reset" class="reset-btn">Reset</button>
</div>
</template>
@@ -124,44 +72,29 @@
import { ref } from 'vue'
const step = ref(0)
const handshaking = ref(false)
const clientState = ref('CLOSED')
const serverState = ref('LISTEN')
const packets = [
{
content: 'SYN seq=x',
direction: '客户端 → 服务器'
},
{
content: 'SYN-ACK seq=y, ack=x+1',
direction: '服务器 → 客户端'
},
{
content: 'ACK ack=y+1',
direction: '客户端 → 服务器'
}
]
const sendSyn = () => {
step.value = 1
clientState.value = 'SYN_SENT'
}
const startHandshake = () => {
if (handshaking.value || step.value === 3) return
const sendSynAck = () => {
step.value = 2
serverState.value = 'SYN_RCVD'
}
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 sendAck = () => {
step.value = 3
clientState.value = 'ESTABLISHED'
serverState.value = 'ESTABLISHED'
}
const reset = () => {
step.value = 0
handshaking.value = false
clientState.value = 'CLOSED'
serverState.value = 'LISTEN'
}
</script>
@@ -169,219 +102,127 @@ const reset = () => {
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
text-align: center;
}
.participants {
.diagram {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
gap: 20px;
margin-bottom: 2rem;
}
.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 {
.column {
width: 120px;
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
gap: 1rem;
}
.packet {
padding: 12px;
.actor-icon {
font-size: 1.2rem;
font-weight: bold;
}
.state-label {
padding: 0.5rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.interaction-zone {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
padding: 0 2rem;
}
.packet-row {
display: flex;
justify-content: flex-start;
opacity: 0.3;
transform: scale(0.9);
transition: all 0.3s;
}
.packet.active {
.packet-row.reverse {
justify-content: flex-end;
}
.packet-row.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-row.done {
opacity: 1;
}
.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;
.packet-btn {
padding: 0.5rem 1rem;
border-radius: 20px;
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-family: monospace;
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;
transition: all 0.2s;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
}
.analogy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid #f59e0b;
.packet-btn:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.analogy-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
.packet-btn.syn {
background: #3b82f6;
color: white;
}
.packet-btn.syn-ack {
background: #f59e0b;
color: white;
}
.packet-btn.ack {
background: #10b981;
color: white;
}
.analogy-content {
.packet-btn:disabled {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
cursor: not-allowed;
border-color: transparent;
}
.status-message {
height: 2rem;
margin-bottom: 1rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
.status-message .success {
color: #10b981;
font-weight: bold;
}
.reset-btn {
padding: 0.5rem 1.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
}
.reset-btn:hover {
background: var(--vp-c-bg-alt);
}
</style>
@@ -175,36 +175,32 @@
<div class="scenario-icon">📺</div>
<div class="scenario-name">视频直播</div>
<div class="scenario-desc">
使用 <strong>UDP</strong>因为
<br> 丢几帧没关系关键是实时
<br> 重传会造成延迟和卡顿
使用 <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> 丢失任何数据都不可接受
使用 <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> 实时同步玩家位置
使用 <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> 可靠性是第一要务
使用 <strong>TCP</strong>因为 <br /> 邮件内容不能丢失 <br />
可靠性是第一要务
</div>
</div>
</div>
@@ -220,9 +216,9 @@ 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 = 1), 500)
setTimeout(() => (tcpStep.value = 2), 1200)
setTimeout(() => (tcpStep.value = 3), 1900)
setTimeout(() => {
tcpStep.value = 0
}, 4000)
@@ -0,0 +1,247 @@
<template>
<div class="url-parser-demo">
<div class="control-panel">
<div class="input-group">
<label>输入 URL</label>
<input
v-model="inputUrl"
type="text"
placeholder="https://www.example.com:8080/path?query=1#fragment"
class="url-input"
/>
</div>
<div class="encoding-toggle">
<button @click="encodeUrl" class="action-btn">Encode</button>
<button @click="decodeUrl" class="action-btn">Decode</button>
</div>
</div>
<div class="visualization-area">
<div v-if="parsedUrl" class="url-parts">
<div
v-for="(part, key) in parts"
:key="key"
class="url-part"
:class="key"
@mouseover="highlightedPart = key"
@mouseleave="highlightedPart = null"
>
<div class="part-label">{{ labels[key] }}</div>
<div class="part-value">{{ part || '-' }}</div>
<div class="part-desc" v-if="highlightedPart === key">
{{ descriptions[key] }}
</div>
</div>
</div>
<div v-else class="error-message">无效的 URL 格式</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
URL (统一资源定位符)
是互联网资源的地址浏览器首先需要将它拆解成不同的部分才能知道要去哪里域名用什么方式协议找什么东西路径
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const inputUrl = ref(
'https://www.example.com:8080/search/results?q=vue&page=1#top'
)
const highlightedPart = ref(null)
const labels = {
protocol: '协议 (Protocol)',
host: '域名 (Host)',
port: '端口 (Port)',
pathname: '路径 (Path)',
search: '查询 (Query)',
hash: '锚点 (Fragment)'
}
const descriptions = {
protocol: '告诉浏览器使用什么方式连接(如 https 安全连接)',
host: '服务器的地址,需要通过 DNS 解析为 IP',
port: '服务器的门牌号(http默认80https默认443',
pathname: '资源在服务器上的具体位置',
search: '传递给服务器的额外参数',
hash: '页面内的定位标记,不会发送给服务器'
}
const parsedUrl = computed(() => {
try {
return new URL(inputUrl.value)
} catch (e) {
return null
}
})
const parts = computed(() => {
if (!parsedUrl.value) return {}
return {
protocol: parsedUrl.value.protocol.replace(':', ''),
host: parsedUrl.value.hostname,
port:
parsedUrl.value.port ||
(parsedUrl.value.protocol === 'https:' ? '443' : '80'),
pathname: parsedUrl.value.pathname,
search: parsedUrl.value.search,
hash: parsedUrl.value.hash
}
})
const encodeUrl = () => {
inputUrl.value = encodeURI(inputUrl.value)
}
const decodeUrl = () => {
inputUrl.value = decodeURI(inputUrl.value)
}
</script>
<style scoped>
.url-parser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
margin-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.url-input {
padding: 0.8rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: monospace;
width: 100%;
}
.encoding-toggle {
display: flex;
gap: 1rem;
}
.action-btn {
padding: 0.4rem 1rem;
border-radius: 4px;
background: var(--vp-c-brand);
color: white;
border: none;
cursor: pointer;
font-size: 0.9rem;
}
.action-btn:hover {
background: var(--vp-c-brand-dark);
}
.visualization-area {
margin-bottom: 1.5rem;
min-height: 200px;
}
.url-parts {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.url-part {
flex: 1;
min-width: 140px;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
position: relative;
transition: all 0.2s;
cursor: default;
}
.url-part:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--vp-c-brand);
}
.url-part.protocol {
border-left: 4px solid #ef4444;
}
.url-part.host {
border-left: 4px solid #3b82f6;
}
.url-part.port {
border-left: 4px solid #f59e0b;
}
.url-part.pathname {
border-left: 4px solid #10b981;
}
.url-part.search {
border-left: 4px solid #8b5cf6;
}
.url-part.hash {
border-left: 4px solid #ec4899;
}
.part-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.part-value {
font-size: 1rem;
font-weight: bold;
word-break: break-all;
}
.part-desc {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--vp-c-text-inverse-1);
color: var(--vp-c-text-inverse-2);
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
z-index: 10;
margin-top: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 1rem;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.error-message {
color: #ef4444;
text-align: center;
padding: 2rem;
}
</style>
@@ -1,50 +1,33 @@
<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 class="url-to-browser-demo">
<div class="stage-nav">
<button
v-for="(stage, index) in stages"
:key="index"
:class="{ active: currentStage === index }"
@click="currentStage = index"
>
<span class="stage-num">{{ index + 1 }}</span>
<span class="stage-name">{{ stage.name }}</span>
</button>
</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 class="stage-content">
<transition name="fade" mode="out-in">
<div :key="currentStage" class="stage-viz">
<div class="viz-icon">{{ stages[currentStage].icon }}</div>
<div class="viz-desc">
<h3>{{ stages[currentStage].title }}</h3>
<p>{{ stages[currentStage].desc }}</p>
</div>
<div class="viz-action">
<component
:is="stages[currentStage].component"
v-if="stages[currentStage].component"
/>
</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>
</transition>
</div>
</div>
</template>
@@ -52,284 +35,131 @@
<script setup>
import { ref } from 'vue'
const currentStep = ref(-1)
const progress = ref(0)
const currentStage = ref(0)
const steps = [
const stages = [
{
id: 1,
title: 'URL 解析',
desc: '解析地址',
detail: '浏览器检查 URL 格式,提取协议(https)、域名(www.example.com)、路径(/page)'
name: 'URL',
title: 'Parsing the Address',
desc: 'Browser breaks down the URL to understand protocol, domain, and path.',
icon: '🔍',
component: 'UrlParserDemo'
},
{
id: 2,
title: 'DNS 查询',
desc: '查找 IP 地址',
detail: '查询 DNS 服务器:www.example.com → 93.184.216.34'
name: 'DNS',
title: 'Finding the IP',
desc: 'Browser asks DNS servers to translate the domain name into an IP address.',
icon: '🌐',
component: 'DnsLookupDemo'
},
{
id: 3,
title: 'TCP 连接',
desc: '建立连接',
detail: '三次握手:SYN → SYN-ACK → ACK,建立可靠连接'
name: 'TCP',
title: 'Establishing Connection',
desc: 'Browser and Server perform a 3-way handshake to create a reliable connection.',
icon: '🤝',
component: 'TcpHandshakeDemo'
},
{
id: 4,
title: 'TLS 握手',
desc: '加密协商',
detail: '协商加密算法,交换证书,建立安全通道(HTTPS)'
name: 'HTTP',
title: 'Exchanging Data',
desc: 'Browser sends a request, and the server sends back the website content.',
icon: '📨',
component: 'HttpExchangeDemo'
},
{
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 → 显示页面'
name: 'Render',
title: 'Painting the Page',
desc: 'Browser parses HTML/CSS and paints pixels on your screen.',
icon: '🎨',
component: 'BrowserRenderingDemo'
}
]
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 {
.url-to-browser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 2rem 0;
}
.url-input-section {
margin-bottom: 25px;
}
.url-bar {
.stage-nav {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
margin-bottom: 2rem;
position: relative;
}
.stage-nav::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
z-index: 0;
}
.stage-nav button {
position: relative;
z-index: 1;
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);
padding: 0.5rem 1rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
flex-shrink: 0;
z-index: 1;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.flow-step.active .step-circle {
.stage-nav button.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
transform: scale(1.1);
}
.flow-step.completed .step-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.step-number {
.stage-num {
font-weight: bold;
font-size: 0.9rem;
font-family: monospace;
}
.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 {
.stage-content {
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);
padding: 2rem;
text-align: center;
min-height: 200px;
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
.viz-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.info-title {
font-size: 0.95rem;
font-weight: bold;
.viz-desc h3 {
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.info-content {
font-size: 0.85rem;
.viz-desc p {
color: var(--vp-c-text-2);
line-height: 1.8;
max-width: 500px;
margin: 0 auto;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -1,289 +1,332 @@
<!--
WebTechTriad.vue
三剑客轻交互同一段小页面切换 HTML/CSS/JS
目标让读者看到页面上的某一块就能立刻找到代码里的哪一行再用三步解释发生了什么
风格先玩后讲句子短
-->
<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 class="triad">
<div class="top">
<div>
<div class="title">先玩一下同一段页面切换层次</div>
<div class="subtitle">HTML 定骨架 CSS 换外观 JS 让它动起来</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 class="modes">
<button
v-for="m in modes"
:key="m.id"
:class="['mode', { active: current === m.id }]"
@click="current = m.id"
>
{{ m.label }}
</button>
</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 class="preview" :class="current">
<div class="hint">点一下标题/段落/按钮我会在下面的代码里高亮对应行</div>
<h1
class="hero"
:class="{ selected: selectedPart === 'h1' }"
@click="selectedPart = 'h1'"
>
<span class="badge"></span>
欢迎来到我的网站
</h1>
<p
class="desc"
:class="{ selected: selectedPart === 'p' }"
@click="selectedPart = 'p'"
>
<span class="badge"></span>
这是一段描述文字告诉用户这里能做什么
</p>
<button
class="cta"
:class="{ selected: selectedPart === 'btn' }"
@click="selectedPart = 'btn'; increment()"
>
<span class="badge"></span>
点我试试看 ({{ clicks }})
</button>
<div class="click-tip" v-if="current === 'js'">
现在再点一次按钮计数会变这是 JS 在改页面
</div>
</div>
<div class="code-block">
<div class="code-title">{{ codeTitle }}</div>
<div class="code-content">
<div
v-for="(line, i) in codeLines"
:key="i"
:class="['line', { hl: line.key === selectedPart }]"
>
{{ line.text }}
</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 class="explain">
<div class="card">
<div class="card-title">对照页面 代码</div>
<div class="map">
<button
v-for="row in mappingRows"
:key="row.key"
:class="['map-row', { active: selectedPart === row.key }]"
@click="selectedPart = row.key"
>
<span class="left">{{ row.left }}</span>
<span class="right">{{ row.right }}</span>
</button>
</div>
</div>
<div class="card">
<div class="card-title">发生了什么简单版</div>
<ol class="steps">
<li v-for="s in steps" :key="s">{{ s }}</li>
</ol>
</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 class="one-line">
<span class="one-line-title">一句话总结</span>
<span class="one-line-body">{{ oneLine }}</span>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const modes = [
{ id: 'html', label: '看骨架 (HTML)' },
{ id: 'css', label: '看外观 (CSS)' },
{ id: 'js', label: '看交互 (JS)' }
]
const current = ref('html')
const clicks = ref(0)
const selectedPart = ref('h1') // 'h1' | 'p' | 'btn'
const codeTitle = computed(() => {
if (current.value === 'html') return 'HTML 片段:告诉浏览器这是什么'
if (current.value === 'css') return 'CSS 片段:决定长什么样'
return 'JS 片段:让它动起来'
})
const codeLines = computed(() => {
if (current.value === 'html') {
return [
{ key: 'h1', text: '<h1>欢迎来到我的网站</h1>' },
{ key: 'p', text: '<p>这是一段描述文字...</p>' },
{ key: 'btn', text: '<button>点我试试看</button>' }
]
}
if (current.value === 'css') {
return [
{ key: 'h1', text: '.hero { color: #0ea5e9; font-size: 24px; }' },
{ key: 'p', text: '.desc { color: #111827; }' },
{ key: 'btn', text: '.cta { background: #0ea5e9; color: #fff; border-radius: 10px; }' }
]
}
return [
{ key: 'btn', text: "const btn = document.querySelector('button')" },
{ key: 'btn', text: 'let count = 0' },
{ key: 'btn', text: "btn.addEventListener('click', () => {" },
{ key: 'btn', text: ' count++' },
{ key: 'btn', text: " btn.textContent = '点我试试看 (' + count + ')'" },
{ key: 'btn', text: '})' }
]
})
const mappingRows = computed(() => {
if (current.value === 'html') {
return [
{ key: 'h1', left: '① 标题', right: '<h1>...</h1>' },
{ key: 'p', left: '② 段落', right: '<p>...</p>' },
{ key: 'btn', left: '③ 按钮', right: '<button>...</button>' }
]
}
if (current.value === 'css') {
return [
{ key: 'h1', left: '① 标题', right: '.hero { ... }' },
{ key: 'p', left: '② 段落', right: '.desc { ... }' },
{ key: 'btn', left: '③ 按钮', right: '.cta { ... }' }
]
}
return [
{ key: 'h1', left: '① 标题', right: '(此例未涉及)' },
{ key: 'p', left: '② 段落', right: '(此例未涉及)' },
{ key: 'btn', left: '③ 按钮', right: "addEventListener('click', ...)" }
]
})
const steps = computed(() => {
if (current.value === 'html') {
return [
'浏览器读到 HTML:知道页面上有“标题/段落/按钮”。',
'把它们先按默认规则摆出来(所以看起来很朴素)。',
'下一步才轮到 CSS 和 JS。'
]
}
if (current.value === 'css') {
return [
'浏览器先把 HTML 结构摆好。',
'再读取 CSS:给标题/段落/按钮套上颜色、字号、间距。',
'重新绘制外观:你看到页面“变好看”。'
]
}
return [
'页面先按 HTML+CSS 显示出来。',
'JS 给按钮装上“点击开关”(事件监听)。',
'你点击按钮时:JS 改按钮文字/计数,页面立即更新。'
]
})
const oneLine = computed(() => {
if (current.value === 'html') return '先把“有哪些东西、是什么东西”说清楚。'
if (current.value === 'css') return '在不改结构的前提下,把外观调到你想要的样子。'
return '把“点击/输入”等行为接上逻辑,让页面能互动。'
})
// Keep the demo behavior: only JS mode should increment on click.
// We implement it by watching mode and only allowing clicks to increment in JS mode.
const increment = () => {
if (current.value !== 'js') return
clicks.value++
}
</script>
<style scoped>
.web-tech-triad {
.triad {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.top { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.title { font-weight: 800; font-size: 16px; }
.subtitle { color: var(--vp-c-text-2); font-size: 13px; margin-top: 4px; }
.modes { display: flex; gap: 8px; flex-wrap: wrap; }
.mode {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.mode:hover { background: var(--vp-c-bg-soft); }
.mode.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.preview {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg);
display: flex;
flex-direction: column;
gap: 16px;
transition: all 0.2s;
}
.hint { color: var(--vp-c-text-2); font-size: 13px; margin-bottom: 8px; }
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
margin-right: 12px;
font-weight: 800;
font-size: 12px;
flex-shrink: 0;
}
.hero { margin: 0; cursor: pointer; display: flex; align-items: center; line-height: 1.4; }
.desc { margin: 0; color: var(--vp-c-text-2); cursor: pointer; display: flex; align-items: center; line-height: 1.5; }
.cta {
width: fit-content;
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 {
padding: 8px 16px;
cursor: pointer;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
text-align: center;
display: flex;
align-items: center;
font-size: 14px;
transition: all 0.2s;
}
.tech-card.html {
border-color: #e34c26;
.selected {
outline: 2px solid var(--vp-c-brand);
outline-offset: 4px;
border-radius: 4px;
}
.tech-card.css {
border-color: #264de4;
}
.click-tip { margin-top: 6px; color: var(--vp-c-text-2); font-size: 13px; }
.tech-card.js {
border-color: #f7df1e;
}
.preview.css .hero { color: #0ea5e9; }
.preview.css .desc { color: var(--vp-c-text-1); }
.preview.css .cta { background: #0ea5e9; color: #fff; border-color: #0ea5e9; box-shadow: 0 4px 12px rgba(14,165,233,0.25); }
.tech-icon {
font-size: 2.5rem;
margin-bottom: 10px;
}
.preview.js .cta { background: #22c55e; color: #fff; border-color: #22c55e; box-shadow: 0 4px 12px rgba(34,197,94,0.25); }
.preview.js { border-color: rgba(34, 197, 94, 0.4); }
.tech-title {
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 5px;
}
.code-block { background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); border-radius: 10px; padding: 16px; }
.code-title { font-weight: 700; margin-bottom: 8px; font-size: 13px; color: var(--vp-c-text-2); }
pre { margin: 0; background: #0b1221; color: #e5e7eb; border-radius: 8px; padding: 16px; font-family: var(--vp-font-family-mono); font-size: 13px; overflow-x: auto; line-height: 1.6; }
.line { min-height: 1.6em; }
.hl { background: rgba(34, 197, 94, 0.2); border-radius: 4px; display: block; width: 100%; }
.tech-card.html .tech-title {
color: #e34c26;
}
.explain { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.card { background: var(--vp-c-bg); border: 1px dashed var(--vp-c-divider); border-radius: 10px; padding: 10px; }
.card-title { font-weight: 700; margin-bottom: 4px; }
.card-body { color: var(--vp-c-text-2); font-size: 13px; line-height: 1.5; }
.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;
.map { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.map-row {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 10px;
margin-bottom: 15px;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
text-align: left;
}
.code-header {
font-size: 0.7rem;
color: #a1a1aa;
margin-bottom: 6px;
font-family: monospace;
.map-row.active {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.left { font-weight: 800; }
.right { color: var(--vp-c-text-2); }
.steps { margin: 8px 0 0 18px; color: var(--vp-c-text-2); line-height: 1.6; }
.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 {
.one-line {
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;
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
}
.one-line-title { font-weight: 800; }
.one-line-body { color: var(--vp-c-text-2); }
</style>