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:
@@ -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"><div id="app"></div>
|
||||
<div class="line indent"><h1>Hello</h1></div>
|
||||
<div class="line indent"><p>World</p></div>
|
||||
<div class="line"></div></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>• 注册商:GoDaddy、Namecheap、阿里云
|
||||
<br>• 选择后缀:.com、.cn、.org、.io
|
||||
<br>• 价格:$10-50/年
|
||||
<br />• 注册商:GoDaddy、Namecheap、阿里云 <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>• Cloudflare、AWS CloudFront、阿里云 CDN
|
||||
<br />• Cloudflare、AWS 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>• Nginx、HAProxy、AWS ELB
|
||||
<br />• Nginx、HAProxy、AWS 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. 把客户端的序列号加 1(ack=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. 把服务器的序列号加 1(ack=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默认80,https默认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"><结构></div>
|
||||
<div class="code-content">
|
||||
<h1>标题</h1><br>
|
||||
<p>段落</p>
|
||||
</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"><样式></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"><交互></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>
|
||||
|
||||
Reference in New Issue
Block a user