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